From fbbd070184a251d971ab89089b28d6e2560fcfe5 Mon Sep 17 00:00:00 2001 From: Luo Zhihao Date: Sun, 5 Apr 2026 14:35:42 +0800 Subject: [PATCH 01/16] Support ASTC HDR --- crates/bevy_image/src/image.rs | 20 ++++++++++-- crates/bevy_image/src/ktx2.rs | 56 ++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/crates/bevy_image/src/image.rs b/crates/bevy_image/src/image.rs index 456c61406f2b0..fa12477d0eff0 100644 --- a/crates/bevy_image/src/image.rs +++ b/crates/bevy_image/src/image.rs @@ -1484,6 +1484,9 @@ impl Image { format_description .required_features() .contains(Features::TEXTURE_COMPRESSION_ASTC) + || format_description + .required_features() + .contains(Features::TEXTURE_COMPRESSION_ASTC_HDR) || format_description .required_features() .contains(Features::TEXTURE_COMPRESSION_BC) @@ -2186,6 +2189,11 @@ bitflags::bitflags! { /// /// [ASTC Format Specification]: https://registry.khronos.org/DataFormat/specs/1.3/dataformat.1.3.html#ASTC const ASTC_LDR = 1 << 0; + /// Support for ASTC HDR textures. + /// + /// For more information see: + /// - [`Features::TEXTURE_COMPRESSION_ASTC_HDR`] + const ASTC_HDR = 1 << 1; /// Support for Block Compressed textures. /// /// For more information see: @@ -2197,7 +2205,7 @@ bitflags::bitflags! { /// [S3TC Format Specification]: https://registry.khronos.org/DataFormat/specs/1.3/dataformat.1.3.html#S3TC /// [RGTC Format Specification]: https://registry.khronos.org/DataFormat/specs/1.3/dataformat.1.3.html#RGTC /// [BPTC Format Specification]: https://registry.khronos.org/DataFormat/specs/1.3/dataformat.1.3.html#BPTC - const BC = 1 << 1; + const BC = 1 << 2; /// Support for Ericsson Texture Compression. /// /// For more information see: @@ -2205,7 +2213,7 @@ bitflags::bitflags! { /// - [ETC2 Format Specification] /// /// [ETC2 Format Specification]: https://registry.khronos.org/DataFormat/specs/1.3/dataformat.1.3.html#ETC2 - const ETC2 = 1 << 2; + const ETC2 = 1 << 3; } } @@ -2216,6 +2224,9 @@ impl CompressedImageFormats { if features.contains(Features::TEXTURE_COMPRESSION_ASTC) { supported_compressed_formats |= Self::ASTC_LDR; } + if features.contains(Features::TEXTURE_COMPRESSION_ASTC_HDR) { + supported_compressed_formats |= Self::ASTC_HDR; + } if features.contains(Features::TEXTURE_COMPRESSION_BC) { supported_compressed_formats |= Self::BC; } @@ -2255,6 +2266,11 @@ impl CompressedImageFormats { | TextureFormat::EacR11Snorm | TextureFormat::EacRg11Unorm | TextureFormat::EacRg11Snorm => self.contains(CompressedImageFormats::ETC2), + TextureFormat::Astc { channel, .. } + if matches!(channel, wgpu_types::AstcChannel::Hdr) => + { + self.contains(CompressedImageFormats::ASTC_HDR) + } TextureFormat::Astc { .. } => self.contains(CompressedImageFormats::ASTC_LDR), _ => true, } diff --git a/crates/bevy_image/src/ktx2.rs b/crates/bevy_image/src/ktx2.rs index 7491c5446851c..a8362434dda2c 100644 --- a/crates/bevy_image/src/ktx2.rs +++ b/crates/bevy_image/src/ktx2.rs @@ -1499,6 +1499,62 @@ pub fn ktx2_format_to_texture_format( }, } } + ktx2::Format::ASTC_4x4_SFLOAT_BLOCK => TextureFormat::Astc { + block: AstcBlock::B4x4, + channel: AstcChannel::Hdr, + }, + ktx2::Format::ASTC_5x4_SFLOAT_BLOCK => TextureFormat::Astc { + block: AstcBlock::B5x4, + channel: AstcChannel::Hdr, + }, + ktx2::Format::ASTC_5x5_SFLOAT_BLOCK => TextureFormat::Astc { + block: AstcBlock::B5x5, + channel: AstcChannel::Hdr, + }, + ktx2::Format::ASTC_6x5_SFLOAT_BLOCK => TextureFormat::Astc { + block: AstcBlock::B6x5, + channel: AstcChannel::Hdr, + }, + ktx2::Format::ASTC_6x6_SFLOAT_BLOCK => TextureFormat::Astc { + block: AstcBlock::B6x6, + channel: AstcChannel::Hdr, + }, + ktx2::Format::ASTC_8x5_SFLOAT_BLOCK => TextureFormat::Astc { + block: AstcBlock::B8x5, + channel: AstcChannel::Hdr, + }, + ktx2::Format::ASTC_8x6_SFLOAT_BLOCK => TextureFormat::Astc { + block: AstcBlock::B8x6, + channel: AstcChannel::Hdr, + }, + ktx2::Format::ASTC_8x8_SFLOAT_BLOCK => TextureFormat::Astc { + block: AstcBlock::B8x8, + channel: AstcChannel::Hdr, + }, + ktx2::Format::ASTC_10x5_SFLOAT_BLOCK => TextureFormat::Astc { + block: AstcBlock::B10x5, + channel: AstcChannel::Hdr, + }, + ktx2::Format::ASTC_10x6_SFLOAT_BLOCK => TextureFormat::Astc { + block: AstcBlock::B10x6, + channel: AstcChannel::Hdr, + }, + ktx2::Format::ASTC_10x8_SFLOAT_BLOCK => TextureFormat::Astc { + block: AstcBlock::B10x8, + channel: AstcChannel::Hdr, + }, + ktx2::Format::ASTC_10x10_SFLOAT_BLOCK => TextureFormat::Astc { + block: AstcBlock::B10x10, + channel: AstcChannel::Hdr, + }, + ktx2::Format::ASTC_12x10_SFLOAT_BLOCK => TextureFormat::Astc { + block: AstcBlock::B12x10, + channel: AstcChannel::Hdr, + }, + ktx2::Format::ASTC_12x12_SFLOAT_BLOCK => TextureFormat::Astc { + block: AstcBlock::B12x12, + channel: AstcChannel::Hdr, + }, _ => { return Err(TextureError::UnsupportedTextureFormat(format!( "{ktx2_format:?}" From 281553af7e0cc8664db42f1365128afd893cc6f6 Mon Sep 17 00:00:00 2001 From: Luo Zhihao Date: Sun, 5 Apr 2026 14:46:00 +0800 Subject: [PATCH 02/16] Remove basisu from bevy_image --- Cargo.toml | 6 - crates/bevy_image/Cargo.toml | 7 - crates/bevy_image/src/basis.rs | 169 ------------------ .../bevy_image/src/compressed_image_saver.rs | 86 --------- crates/bevy_image/src/image.rs | 45 +---- crates/bevy_image/src/image_loader.rs | 2 - crates/bevy_image/src/ktx2.rs | 160 +---------------- crates/bevy_image/src/lib.rs | 6 - crates/bevy_internal/Cargo.toml | 4 - 9 files changed, 7 insertions(+), 478 deletions(-) delete mode 100644 crates/bevy_image/src/basis.rs delete mode 100644 crates/bevy_image/src/compressed_image_saver.rs diff --git a/Cargo.toml b/Cargo.toml index 557a5ae8cc54c..83af4ad6a9999 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -428,12 +428,6 @@ trace_tracy_memory = ["bevy_internal/trace_tracy_memory"] # Tracing support trace = ["bevy_internal/trace", "dep:tracing"] -# Basis Universal compressed texture support -basis-universal = ["bevy_internal/basis-universal"] - -# Enables compressed KTX2 UASTC texture output on the asset processor -compressed_image_saver = ["bevy_internal/compressed_image_saver"] - # BMP image format support bmp = ["bevy_internal/bmp"] diff --git a/crates/bevy_image/Cargo.toml b/crates/bevy_image/Cargo.toml index f45a92afee3db..09d47904da027 100644 --- a/crates/bevy_image/Cargo.toml +++ b/crates/bevy_image/Cargo.toml @@ -16,7 +16,6 @@ default = ["bevy_reflect"] bevy_reflect = ["bevy_math/bevy_reflect"] # Image formats -basis-universal = ["dep:basis-universal"] bmp = ["image/bmp"] dds = ["ddsfile"] exr = ["image/exr"] @@ -45,9 +44,6 @@ zstd_rust = ["zstd", "dep:ruzstd"] # Binding to zstd C implementation (faster) zstd_c = ["zstd", "dep:zstd"] -# Enables compressed KTX2 UASTC texture output on the asset processor -compressed_image_saver = ["basis-universal"] - [dependencies] # bevy bevy_app = { path = "../bevy_app", version = "0.19.0-dev" } @@ -75,7 +71,6 @@ wgpu-types = { version = "29.0.1", default-features = false, features = [ ] } serde = { version = "1", features = ["derive"] } thiserror = { version = "2", default-features = false } -futures-lite = "2.0.1" guillotiere = "0.6.0" rectangle-pack = "0.4" ddsfile = { version = "0.5.2", optional = true } @@ -84,8 +79,6 @@ ktx2 = { version = "0.4.0", optional = true } flate2 = { version = "1.0.22", optional = true } zstd = { version = "0.13.3", optional = true } ruzstd = { version = "0.8.0", optional = true } -# For transcoding of UASTC/ETC1S universal formats, and for .basis file support -basis-universal = { version = "0.3.0", optional = true } tracing = { version = "0.1", default-features = false, features = ["std"] } half = { version = "2.4.1" } diff --git a/crates/bevy_image/src/basis.rs b/crates/bevy_image/src/basis.rs deleted file mode 100644 index c88edb1fdc7bc..0000000000000 --- a/crates/bevy_image/src/basis.rs +++ /dev/null @@ -1,169 +0,0 @@ -use basis_universal::{ - BasisTextureType, DecodeFlags, TranscodeParameters, Transcoder, TranscoderTextureFormat, -}; -use wgpu_types::{AstcBlock, AstcChannel, Extent3d, TextureDimension, TextureFormat}; - -use super::{CompressedImageFormats, Image, TextureError}; - -pub fn basis_buffer_to_image( - buffer: &[u8], - supported_compressed_formats: CompressedImageFormats, - is_srgb: bool, -) -> Result { - let mut transcoder = Transcoder::new(); - - #[cfg(debug_assertions)] - if !transcoder.validate_file_checksums(buffer, true) { - return Err(TextureError::InvalidData("Invalid checksum".to_string())); - } - if !transcoder.validate_header(buffer) { - return Err(TextureError::InvalidData("Invalid header".to_string())); - } - - let Some(image0_info) = transcoder.image_info(buffer, 0) else { - return Err(TextureError::InvalidData( - "Failed to get image info".to_string(), - )); - }; - - // First deal with transcoding to the desired format - // FIXME: Use external metadata to transcode to more appropriate formats for 1- or 2-component sources - let (transcode_format, texture_format) = - get_transcoded_formats(supported_compressed_formats, is_srgb); - let basis_texture_format = transcoder.basis_texture_format(buffer); - if !basis_texture_format.can_transcode_to_format(transcode_format) { - return Err(TextureError::UnsupportedTextureFormat(format!( - "{basis_texture_format:?} cannot be transcoded to {transcode_format:?}", - ))); - } - transcoder.prepare_transcoding(buffer).map_err(|_| { - TextureError::TranscodeError(format!( - "Failed to prepare for transcoding from {basis_texture_format:?}", - )) - })?; - let mut transcoded = Vec::new(); - - let image_count = transcoder.image_count(buffer); - let texture_type = transcoder.basis_texture_type(buffer); - if texture_type == BasisTextureType::TextureTypeCubemapArray && !image_count.is_multiple_of(6) { - return Err(TextureError::InvalidData(format!( - "Basis file with cube map array texture with non-modulo 6 number of images: {image_count}", - ))); - } - - let image0_mip_level_count = transcoder.image_level_count(buffer, 0); - for image_index in 0..image_count { - if let Some(image_info) = transcoder.image_info(buffer, image_index) - && texture_type == BasisTextureType::TextureType2D - && (image_info.m_orig_width != image0_info.m_orig_width - || image_info.m_orig_height != image0_info.m_orig_height) - { - return Err(TextureError::UnsupportedTextureFormat(format!( - "Basis file with multiple 2D textures with different sizes not supported. Image {} {}x{}, image 0 {}x{}", - image_index, - image_info.m_orig_width, - image_info.m_orig_height, - image0_info.m_orig_width, - image0_info.m_orig_height, - ))); - } - let mip_level_count = transcoder.image_level_count(buffer, image_index); - if mip_level_count != image0_mip_level_count { - return Err(TextureError::InvalidData(format!( - "Array or volume texture has inconsistent number of mip levels. Image {image_index} has {mip_level_count} but image 0 has {image0_mip_level_count}", - ))); - } - for level_index in 0..mip_level_count { - let mut data = transcoder - .transcode_image_level( - buffer, - transcode_format, - TranscodeParameters { - image_index, - level_index, - decode_flags: Some(DecodeFlags::HIGH_QUALITY), - ..Default::default() - }, - ) - .map_err(|error| { - TextureError::TranscodeError(format!( - "Failed to transcode mip level {level_index} from {basis_texture_format:?} to {transcode_format:?}: {error:?}", - )) - })?; - transcoded.append(&mut data); - } - } - - // Then prepare the Image - let mut image = Image::default(); - image.texture_descriptor.size = Extent3d { - width: image0_info.m_orig_width, - height: image0_info.m_orig_height, - depth_or_array_layers: image_count, - } - .physical_size(texture_format); - image.texture_descriptor.mip_level_count = image0_mip_level_count; - image.texture_descriptor.format = texture_format; - image.texture_descriptor.dimension = match texture_type { - BasisTextureType::TextureType2D - | BasisTextureType::TextureType2DArray - | BasisTextureType::TextureTypeCubemapArray => TextureDimension::D2, - BasisTextureType::TextureTypeVolume => TextureDimension::D3, - basis_texture_type => { - return Err(TextureError::UnsupportedTextureFormat(format!( - "{basis_texture_type:?}", - ))) - } - }; - image.data = Some(transcoded); - Ok(image) -} - -pub fn get_transcoded_formats( - supported_compressed_formats: CompressedImageFormats, - is_srgb: bool, -) -> (TranscoderTextureFormat, TextureFormat) { - // NOTE: UASTC can be losslessly transcoded to ASTC4x4 and ASTC uses the same - // space as BC7 (128-bits per 4x4 texel block) so prefer ASTC over BC for - // transcoding speed and quality. - if supported_compressed_formats.contains(CompressedImageFormats::ASTC_LDR) { - ( - TranscoderTextureFormat::ASTC_4x4_RGBA, - TextureFormat::Astc { - block: AstcBlock::B4x4, - channel: if is_srgb { - AstcChannel::UnormSrgb - } else { - AstcChannel::Unorm - }, - }, - ) - } else if supported_compressed_formats.contains(CompressedImageFormats::BC) { - ( - TranscoderTextureFormat::BC7_RGBA, - if is_srgb { - TextureFormat::Bc7RgbaUnormSrgb - } else { - TextureFormat::Bc7RgbaUnorm - }, - ) - } else if supported_compressed_formats.contains(CompressedImageFormats::ETC2) { - ( - TranscoderTextureFormat::ETC2_RGBA, - if is_srgb { - TextureFormat::Etc2Rgba8UnormSrgb - } else { - TextureFormat::Etc2Rgba8Unorm - }, - ) - } else { - ( - TranscoderTextureFormat::RGBA32, - if is_srgb { - TextureFormat::Rgba8UnormSrgb - } else { - TextureFormat::Rgba8Unorm - }, - ) - } -} diff --git a/crates/bevy_image/src/compressed_image_saver.rs b/crates/bevy_image/src/compressed_image_saver.rs deleted file mode 100644 index 6b6348a1a3c30..0000000000000 --- a/crates/bevy_image/src/compressed_image_saver.rs +++ /dev/null @@ -1,86 +0,0 @@ -use crate::{Image, ImageFormat, ImageFormatSetting, ImageLoader, ImageLoaderSettings}; - -use bevy_asset::{ - saver::{AssetSaver, SavedAsset}, - AssetPath, -}; -use bevy_reflect::TypePath; -use futures_lite::AsyncWriteExt; -use thiserror::Error; - -/// An [`AssetSaver`] that writes compressed basis universal (.ktx2) files. -#[derive(TypePath)] -pub struct CompressedImageSaver; - -/// Errors encountered when writing compressed images. -#[non_exhaustive] -#[derive(Debug, Error, TypePath)] -pub enum CompressedImageSaverError { - /// I/O error. - #[error(transparent)] - Io(#[from] std::io::Error), - /// Attempted to save an image with uninitialized data. - #[error("Cannot compress an uninitialized image")] - UninitializedImage, -} - -impl AssetSaver for CompressedImageSaver { - type Asset = Image; - - type Settings = (); - type OutputLoader = ImageLoader; - type Error = CompressedImageSaverError; - - async fn save( - &self, - writer: &mut bevy_asset::io::Writer, - image: SavedAsset<'_, '_, Self::Asset>, - _settings: &Self::Settings, - _asset_path: AssetPath<'_>, - ) -> Result { - let is_srgb = image.texture_descriptor.format.is_srgb(); - - let compressed_basis_data = { - let mut compressor_params = basis_universal::CompressorParams::new(); - compressor_params.set_basis_format(basis_universal::BasisTextureFormat::UASTC4x4); - compressor_params.set_generate_mipmaps(true); - let color_space = if is_srgb { - basis_universal::ColorSpace::Srgb - } else { - basis_universal::ColorSpace::Linear - }; - compressor_params.set_color_space(color_space); - compressor_params.set_uastc_quality_level(basis_universal::UASTC_QUALITY_DEFAULT); - - let mut source_image = compressor_params.source_image_mut(0); - let size = image.size(); - let Some(ref data) = image.data else { - return Err(CompressedImageSaverError::UninitializedImage); - }; - source_image.init(data, size.x, size.y, 4); - - let mut compressor = basis_universal::Compressor::new(4); - #[expect( - unsafe_code, - reason = "The basis-universal compressor cannot be interacted with except through unsafe functions" - )] - // SAFETY: the CompressorParams are "valid" to the best of our knowledge. The basis-universal - // library bindings note that invalid params might produce undefined behavior. - unsafe { - compressor.init(&compressor_params); - compressor.process().unwrap(); - } - compressor.basis_file().to_vec() - }; - - writer.write_all(&compressed_basis_data).await?; - Ok(ImageLoaderSettings { - format: ImageFormatSetting::Format(ImageFormat::Basis), - is_srgb, - sampler: image.sampler.clone(), - asset_usage: image.asset_usage, - texture_format: None, - array_layout: None, - }) - } -} diff --git a/crates/bevy_image/src/image.rs b/crates/bevy_image/src/image.rs index fa12477d0eff0..2dba7b050e84e 100644 --- a/crates/bevy_image/src/image.rs +++ b/crates/bevy_image/src/image.rs @@ -1,7 +1,5 @@ use crate::ImageLoader; -#[cfg(feature = "basis-universal")] -use super::basis::*; #[cfg(feature = "dds")] use super::dds::*; #[cfg(feature = "ktx2")] @@ -219,23 +217,6 @@ impl Plugin for ImagePlugin { .insert(&TRANSPARENT_IMAGE_HANDLE, Image::transparent()) .unwrap(); - #[cfg(feature = "compressed_image_saver")] - if let Some(processor) = app - .world() - .get_resource::() - { - processor.register_processor::, - crate::CompressedImageSaver, - >>(crate::CompressedImageSaver.into()); - processor.set_default_processor::, - crate::CompressedImageSaver, - >>("png"); - } - app.preregister_asset_loader::(ImageLoader::SUPPORTED_FILE_EXTENSIONS); } } @@ -243,9 +224,6 @@ impl Plugin for ImagePlugin { /// The format of an on-disk image asset. #[derive(Debug, Serialize, Deserialize, Copy, Clone)] pub enum ImageFormat { - /// An image in basis universal format. - #[cfg(feature = "basis-universal")] - Basis, /// An image in BMP format. #[cfg(feature = "bmp")] Bmp, @@ -309,8 +287,6 @@ impl ImageFormat { /// Gets the file extensions for a given format. pub const fn to_file_extensions(&self) -> &'static [&'static str] { match self { - #[cfg(feature = "basis-universal")] - ImageFormat::Basis => &["basis"], #[cfg(feature = "bmp")] ImageFormat::Bmp => &["bmp"], #[cfg(feature = "dds")] @@ -359,8 +335,6 @@ impl ImageFormat { /// If a format doesn't have any dedicated MIME types, this list will be empty. pub const fn to_mime_types(&self) -> &'static [&'static str] { match self { - #[cfg(feature = "basis-universal")] - ImageFormat::Basis => &["image/basis", "image/x-basis"], #[cfg(feature = "bmp")] ImageFormat::Bmp => &["image/bmp", "image/x-bmp"], #[cfg(feature = "dds")] @@ -423,7 +397,6 @@ impl ImageFormat { )] Some(match mime_type.to_ascii_lowercase().as_str() { // note: farbfeld does not have a MIME type - "image/basis" | "image/x-basis" => feature_gate!("basis-universal", Basis), "image/bmp" | "image/x-bmp" => feature_gate!("bmp", Bmp), "image/vnd-ms.dds" => feature_gate!("dds", Dds), "image/vnd.radiance" => feature_gate!("hdr", Hdr), @@ -458,7 +431,6 @@ impl ImageFormat { reason = "If all features listed below are disabled, then all arms will have a `return None`, keeping the surrounding `Some()` from being constructed." )] Some(match extension.to_ascii_lowercase().as_str() { - "basis" => feature_gate!("basis-universal", Basis), "bmp" => feature_gate!("bmp", Bmp), "dds" => feature_gate!("dds", Dds), "ff" | "farbfeld" => feature_gate!("ff", Farbfeld), @@ -517,8 +489,6 @@ impl ImageFormat { ImageFormat::Tiff => image::ImageFormat::Tiff, #[cfg(feature = "webp")] ImageFormat::WebP => image::ImageFormat::WebP, - #[cfg(feature = "basis-universal")] - ImageFormat::Basis => return None, #[cfg(feature = "ktx2")] ImageFormat::Ktx2 => return None, // FIXME: https://github.com/rust-lang/rust/issues/129031 @@ -1427,7 +1397,7 @@ impl Image { buffer: &[u8], image_type: ImageType, #[cfg_attr( - not(any(feature = "basis-universal", feature = "dds", feature = "ktx2")), + not(any(feature = "dds", feature = "ktx2")), expect(unused_variables, reason = "only used with certain features") )] supported_compressed_formats: CompressedImageFormats, @@ -1444,10 +1414,6 @@ impl Image { // cases. let mut image = match format { - #[cfg(feature = "basis-universal")] - ImageFormat::Basis => { - basis_buffer_to_image(buffer, supported_compressed_formats, is_srgb)? - } #[cfg(feature = "dds")] ImageFormat::Dds => dds_buffer_to_image(buffer, supported_compressed_formats, is_srgb)?, #[cfg(feature = "ktx2")] @@ -2266,11 +2232,10 @@ impl CompressedImageFormats { | TextureFormat::EacR11Snorm | TextureFormat::EacRg11Unorm | TextureFormat::EacRg11Snorm => self.contains(CompressedImageFormats::ETC2), - TextureFormat::Astc { channel, .. } - if matches!(channel, wgpu_types::AstcChannel::Hdr) => - { - self.contains(CompressedImageFormats::ASTC_HDR) - } + TextureFormat::Astc { + channel: wgpu_types::AstcChannel::Hdr, + .. + } => self.contains(CompressedImageFormats::ASTC_HDR), TextureFormat::Astc { .. } => self.contains(CompressedImageFormats::ASTC_LDR), _ => true, } diff --git a/crates/bevy_image/src/image_loader.rs b/crates/bevy_image/src/image_loader.rs index 79442a0a82e74..1221061a5fc68 100644 --- a/crates/bevy_image/src/image_loader.rs +++ b/crates/bevy_image/src/image_loader.rs @@ -18,8 +18,6 @@ pub struct ImageLoader { impl ImageLoader { /// Full list of supported formats. pub const SUPPORTED_FORMATS: &'static [ImageFormat] = &[ - #[cfg(feature = "basis-universal")] - ImageFormat::Basis, #[cfg(feature = "bmp")] ImageFormat::Bmp, #[cfg(feature = "dds")] diff --git a/crates/bevy_image/src/ktx2.rs b/crates/bevy_image/src/ktx2.rs index a8362434dda2c..c4e38c3cfe9bf 100644 --- a/crates/bevy_image/src/ktx2.rs +++ b/crates/bevy_image/src/ktx2.rs @@ -1,10 +1,6 @@ #[cfg(any(feature = "flate2", feature = "zstd_rust"))] use std::io::Read; -#[cfg(feature = "basis-universal")] -use basis_universal::{ - DecodeFlags, LowLevelUastcTranscoder, SliceParametersUastc, TranscoderBlockFormat, -}; use bevy_color::Srgba; use bevy_utils::default; #[cfg(any(feature = "flate2", feature = "zstd_rust", feature = "zstd_c"))] @@ -146,7 +142,8 @@ pub fn ktx2_buffer_to_image( TranscodeFormat::Rgb8 => { let mut rgba = vec![255u8; width as usize * height as usize * 4]; for (level, level_data) in levels.iter().enumerate() { - let n_pixels = (width as usize >> level).max(1) * (height as usize >> level).max(1); + let n_pixels = + (width as usize >> level).max(1) * (height as usize >> level).max(1); let mut offset = 0; for _layer in 0..layer_count { @@ -168,76 +165,6 @@ pub fn ktx2_buffer_to_image( TextureFormat::Rgba8Unorm } } - #[cfg(feature = "basis-universal")] - TranscodeFormat::Uastc(data_format) => { - let (transcode_block_format, texture_format) = - get_transcoded_formats(supported_compressed_formats, data_format, is_srgb); - let texture_format_info = texture_format; - let (block_width_pixels, block_height_pixels) = ( - texture_format_info.block_dimensions().0, - texture_format_info.block_dimensions().1, - ); - // Texture is not a depth or stencil format, it is possible to pass `None` and unwrap - let block_bytes = texture_format_info.block_copy_size(None).unwrap(); - - let transcoder = LowLevelUastcTranscoder::new(); - for (level, level_data) in levels.iter().enumerate() { - let (level_width, level_height) = ( - (width >> level as u32).max(1), - (height >> level as u32).max(1), - ); - let (num_blocks_x, num_blocks_y) = ( - level_width.div_ceil(block_width_pixels) .max(1), - level_height.div_ceil(block_height_pixels) .max(1), - ); - let level_bytes = (num_blocks_x * num_blocks_y * block_bytes) as usize; - - let mut offset = 0; - for _layer in 0..layer_count { - for _face in 0..face_count { - // NOTE: SliceParametersUastc does not implement Clone nor Copy so - // it has to be created per use - let slice_parameters = SliceParametersUastc { - num_blocks_x, - num_blocks_y, - has_alpha: false, - original_width: level_width, - original_height: level_height, - }; - transcoder - .transcode_slice( - &level_data[offset..(offset + level_bytes)], - slice_parameters, - DecodeFlags::HIGH_QUALITY, - transcode_block_format, - ) - .map(|mut transcoded_level| transcoded[level].append(&mut transcoded_level)) - .map_err(|error| { - TextureError::SuperDecompressionError(format!( - "Failed to transcode mip level {level} from UASTC to {transcode_block_format:?}: {error:?}", - )) - })?; - offset += level_bytes; - } - } - } - texture_format - } - // ETC1S is a subset of ETC1 which is a subset of ETC2 - // TODO: Implement transcoding - TranscodeFormat::Etc1s => { - let texture_format = if is_srgb { - TextureFormat::Etc2Rgb8UnormSrgb - } else { - TextureFormat::Etc2Rgb8Unorm - }; - if !supported_compressed_formats.supports(texture_format) { - return Err(error); - } - transcoded = levels.to_vec(); - texture_format - } - #[cfg(not(feature = "basis-universal"))] _ => return Err(error), }; levels = transcoded; @@ -304,89 +231,6 @@ pub fn ktx2_buffer_to_image( Ok(image) } -/// Determines an appropriate wgpu-compatible format based on compressed format support, and a -/// basis universal [`TextureChannelLayout`]. -#[cfg(feature = "basis-universal")] -pub fn get_transcoded_formats( - supported_compressed_formats: CompressedImageFormats, - data_format: TextureChannelLayout, - is_srgb: bool, -) -> (TranscoderBlockFormat, TextureFormat) { - match data_format { - TextureChannelLayout::Rrr => { - if supported_compressed_formats.contains(CompressedImageFormats::BC) { - (TranscoderBlockFormat::BC4, TextureFormat::Bc4RUnorm) - } else if supported_compressed_formats.contains(CompressedImageFormats::ETC2) { - ( - TranscoderBlockFormat::ETC2_EAC_R11, - TextureFormat::EacR11Unorm, - ) - } else { - (TranscoderBlockFormat::RGBA32, TextureFormat::R8Unorm) - } - } - TextureChannelLayout::Rrrg | TextureChannelLayout::Rg => { - if supported_compressed_formats.contains(CompressedImageFormats::BC) { - (TranscoderBlockFormat::BC5, TextureFormat::Bc5RgUnorm) - } else if supported_compressed_formats.contains(CompressedImageFormats::ETC2) { - ( - TranscoderBlockFormat::ETC2_EAC_RG11, - TextureFormat::EacRg11Unorm, - ) - } else { - (TranscoderBlockFormat::RGBA32, TextureFormat::Rg8Unorm) - } - } - // NOTE: Rgba16Float should be transcoded to BC6H/ASTC_HDR. Neither are supported by - // basis-universal, nor is ASTC_HDR supported by wgpu - TextureChannelLayout::Rgb | TextureChannelLayout::Rgba => { - // NOTE: UASTC can be losslessly transcoded to ASTC4x4 and ASTC uses the same - // space as BC7 (128-bits per 4x4 texel block) so prefer ASTC over BC for - // transcoding speed and quality. - if supported_compressed_formats.contains(CompressedImageFormats::ASTC_LDR) { - ( - TranscoderBlockFormat::ASTC_4x4, - TextureFormat::Astc { - block: AstcBlock::B4x4, - channel: if is_srgb { - AstcChannel::UnormSrgb - } else { - AstcChannel::Unorm - }, - }, - ) - } else if supported_compressed_formats.contains(CompressedImageFormats::BC) { - ( - TranscoderBlockFormat::BC7, - if is_srgb { - TextureFormat::Bc7RgbaUnormSrgb - } else { - TextureFormat::Bc7RgbaUnorm - }, - ) - } else if supported_compressed_formats.contains(CompressedImageFormats::ETC2) { - ( - TranscoderBlockFormat::ETC2_RGBA, - if is_srgb { - TextureFormat::Etc2Rgba8UnormSrgb - } else { - TextureFormat::Etc2Rgba8Unorm - }, - ) - } else { - ( - TranscoderBlockFormat::RGBA32, - if is_srgb { - TextureFormat::Rgba8UnormSrgb - } else { - TextureFormat::Rgba8Unorm - }, - ) - } - } - } -} - /// Reads the [`TextureFormat`] from a [`ktx2::Reader`]. /// /// # Errors diff --git a/crates/bevy_image/src/lib.rs b/crates/bevy_image/src/lib.rs index 8f47a238a852b..cefd36192d137 100644 --- a/crates/bevy_image/src/lib.rs +++ b/crates/bevy_image/src/lib.rs @@ -22,10 +22,6 @@ pub use self::image::*; mod serialized_image; #[cfg(feature = "serialize")] pub use self::serialized_image::*; -#[cfg(feature = "basis-universal")] -mod basis; -#[cfg(feature = "compressed_image_saver")] -mod compressed_image_saver; #[cfg(feature = "dds")] mod dds; mod dynamic_texture_atlas_builder; @@ -40,8 +36,6 @@ mod saver; mod texture_atlas; mod texture_atlas_builder; -#[cfg(feature = "compressed_image_saver")] -pub use compressed_image_saver::*; #[cfg(feature = "dds")] pub use dds::*; pub use dynamic_texture_atlas_builder::*; diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 8c3eb03b97513..e8b565b6df79c 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -29,9 +29,6 @@ detailed_trace = ["bevy_ecs/detailed_trace", "bevy_render?/detailed_trace"] sysinfo_plugin = ["bevy_diagnostic/sysinfo_plugin"] -# Enables compressed KTX2 UASTC texture output on the asset processor -compressed_image_saver = ["bevy_image/compressed_image_saver"] - # For ktx2 supercompression zlib = ["bevy_image/zlib"] zstd = ["bevy_image/zstd"] @@ -39,7 +36,6 @@ zstd_rust = ["bevy_image/zstd_rust"] zstd_c = ["bevy_image/zstd_c"] # Image format support (HDR and PNG enabled by default) -basis-universal = ["bevy_image/basis-universal"] bmp = ["bevy_image/bmp"] ff = ["bevy_image/ff"] gif = ["bevy_image/gif"] From 79057dda3dec6707bf8922ad1fd03160a98e9735 Mon Sep 17 00:00:00 2001 From: Luo Zhihao Date: Sun, 5 Apr 2026 16:52:34 +0800 Subject: [PATCH 03/16] bevy_basis_universal --- Cargo.toml | 3 + crates/bevy_basis_universal/Cargo.toml | 35 ++++ crates/bevy_basis_universal/LICENSE-APACHE | 176 ++++++++++++++++++ crates/bevy_basis_universal/LICENSE-MIT | 19 ++ crates/bevy_basis_universal/src/lib.rs | 137 ++++++++++++++ crates/bevy_basis_universal/src/loader.rs | 143 ++++++++++++++ crates/bevy_basis_universal/src/saver.rs | 84 +++++++++ crates/bevy_image/src/saver.rs | 2 +- crates/bevy_internal/Cargo.toml | 4 + crates/bevy_internal/src/lib.rs | 2 + crates/bevy_log/src/lib.rs | 4 +- docs/cargo_features.md | 4 +- .../large_scenes/mipmap_generator/README.md | 2 +- 13 files changed, 609 insertions(+), 6 deletions(-) create mode 100644 crates/bevy_basis_universal/Cargo.toml create mode 100644 crates/bevy_basis_universal/LICENSE-APACHE create mode 100644 crates/bevy_basis_universal/LICENSE-MIT create mode 100644 crates/bevy_basis_universal/src/lib.rs create mode 100644 crates/bevy_basis_universal/src/loader.rs create mode 100644 crates/bevy_basis_universal/src/saver.rs diff --git a/Cargo.toml b/Cargo.toml index 83af4ad6a9999..620ef5af1adfb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -428,6 +428,9 @@ trace_tracy_memory = ["bevy_internal/trace_tracy_memory"] # Tracing support trace = ["bevy_internal/trace", "dep:tracing"] +basis_universal = ["bevy_internal/basis_universal"] +basis_universal_saver = ["bevy_internal/basis_universal_saver"] + # BMP image format support bmp = ["bevy_internal/bmp"] diff --git a/crates/bevy_basis_universal/Cargo.toml b/crates/bevy_basis_universal/Cargo.toml new file mode 100644 index 0000000000000..cce4fde769d64 --- /dev/null +++ b/crates/bevy_basis_universal/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "bevy_basis_universal" +version = "0.19.0-dev" +edition = "2024" +description = "Provides basis universal texture loader and saver for Bevy Engine" +homepage = "https://bevy.org" +repository = "https://github.com/bevyengine/bevy" +license = "MIT OR Apache-2.0" +keywords = ["bevy"] + +[features] +saver = ["basisu_c_sys/encoder"] + +[dependencies] +basisu_c_sys = { version = "0.6.0", default-features = false, features = ["extra", "serde"] } +bevy_app = { version = "0.19.0-dev", path = "../bevy_app", default-features = false } +bevy_asset = { version = "0.19.0-dev", path = "../bevy_asset", default-features = false } +bevy_image = { version = "0.19.0-dev", path = "../bevy_image", default-features = false } +bevy_log = { version = "0.19.0-dev", path = "../bevy_log", default-features = false } +bevy_platform = { version = "0.19.0-dev", path = "../bevy_platform", default-features = false } +bevy_reflect = { version = "0.19.0-dev", path = "../bevy_reflect", default-features = false } +bevy_tasks = { version = "0.19.0-dev", path = "../bevy_tasks", default-features = false } +serde = { version = "1.0.228", default-features = false, features = ["derive"] } +thiserror = { version = "2.0.18", default-features = false } + +[lints] +workspace = true + +[package.metadata.docs.rs] +rustdoc-args = [ + "-Zunstable-options", + "--generate-link-to-definition", + "--generate-macro-expansion", +] +all-features = true diff --git a/crates/bevy_basis_universal/LICENSE-APACHE b/crates/bevy_basis_universal/LICENSE-APACHE new file mode 100644 index 0000000000000..d9a10c0d8e868 --- /dev/null +++ b/crates/bevy_basis_universal/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/crates/bevy_basis_universal/LICENSE-MIT b/crates/bevy_basis_universal/LICENSE-MIT new file mode 100644 index 0000000000000..9cf106272ac3b --- /dev/null +++ b/crates/bevy_basis_universal/LICENSE-MIT @@ -0,0 +1,19 @@ +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crates/bevy_basis_universal/src/lib.rs b/crates/bevy_basis_universal/src/lib.rs new file mode 100644 index 0000000000000..03f50c7b2e5d7 --- /dev/null +++ b/crates/bevy_basis_universal/src/lib.rs @@ -0,0 +1,137 @@ +#![cfg_attr(docsrs, feature(doc_cfg))] + +//! Provides loader and saver for Basis Universal KTX2 textures. +//! See [`loader`] and [`saver`] for more information. +//! +//! This uses [Basis Universal v2.1](https://github.com/BinomialLLC/basis_universal) C++ library. All basis universal formats are supported. + +pub mod loader; +#[cfg(feature = "saver")] +pub mod saver; + +use bevy_asset::AssetApp; +use bevy_image::{CompressedImageFormatSupport, CompressedImageFormats, ImageLoader}; +#[cfg(all( + target_arch = "wasm32", + target_vendor = "unknown", + target_os = "unknown", +))] +use bevy_platform::sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, +}; + +use bevy_app::{App, Plugin}; + +use crate::loader::BasisuLoader; +#[cfg(feature = "saver")] +use crate::saver::{BasisuProcessor, BasisuSaver}; + +/// Provides basis universal texture loader and saver. +pub struct BasisUniversalPlugin { + /// The file extensions handled by the basisu asset processor. + /// + /// Default is [`ImageLoader::SUPPORTED_FILE_EXTENSIONS`] except ktx2 and .dds. + pub processor_extensions: Vec, +} + +impl Default for BasisUniversalPlugin { + fn default() -> Self { + Self { + processor_extensions: ImageLoader::SUPPORTED_FILE_EXTENSIONS + .iter() + .filter(|s| !["ktx2", "dds"].contains(s)) + .map(ToString::to_string) + .collect(), + } + } +} + +#[cfg(all( + target_arch = "wasm32", + target_vendor = "unknown", + target_os = "unknown", +))] +#[derive(Resource, Clone, Deref)] +struct BasisuWasmReady(Arc); + +impl Plugin for BasisUniversalPlugin { + fn build(&self, app: &mut App) { + #[cfg(all( + target_arch = "wasm32", + target_vendor = "unknown", + target_os = "unknown", + ))] + { + let ready = BasisuWasmReady(Arc::new(AtomicUsize::new(0))); + let r = ready.clone(); + bevy_tasks::IoTaskPool::get() + .spawn_local(async move { + basisu_c_sys::extra::basisu_transcoder_init().await; + #[cfg(feature = "saver")] + basisu_c_sys::extra::basisu_encoder_init().await; + bevy::log::debug!("Basisu wasm initialized"); + r.store(1, Ordering::Release); + }) + .detach(); + app.insert_resource(ready); + } + #[cfg(not(all( + target_arch = "wasm32", + target_vendor = "unknown", + target_os = "unknown", + )))] + { + bevy_tasks::block_on(basisu_c_sys::extra::basisu_transcoder_init()); + #[cfg(feature = "saver")] + bevy_tasks::block_on(basisu_c_sys::extra::basisu_encoder_init()); + } + app.preregister_asset_loader::(&["basisu.ktx2"]); + + #[cfg(feature = "saver")] + { + if let Some(asset_processor) = app + .world() + .get_resource::() + { + asset_processor.register_processor::(BasisuSaver.into()); + for ext in &self.processor_extensions { + asset_processor.set_default_processor::(ext.as_str()); + } + } + } + } + + #[cfg(all( + target_arch = "wasm32", + target_vendor = "unknown", + target_os = "unknown", + ))] + fn ready(&self, app: &App) -> bool { + app.world() + .resource::() + .load(Ordering::Acquire) + != 0 + } + + fn finish(&self, app: &mut App) { + #[cfg(all( + target_arch = "wasm32", + target_vendor = "unknown", + target_os = "unknown", + ))] + app.world_mut().remove_resource::(); + + let supported_compressed_formats = if let Some(resource) = + app.world().get_resource::() + { + resource.0 + } else { + bevy_log::warn!("CompressedImageFormatSupport resource not found. It should either be initialized in finish() of \ + RenderPlugin, or manually if not using the RenderPlugin or the WGPU backend."); + CompressedImageFormats::NONE + }; + + app.register_asset_loader(BasisuLoader::new(supported_compressed_formats)); + } +} diff --git a/crates/bevy_basis_universal/src/loader.rs b/crates/bevy_basis_universal/src/loader.rs new file mode 100644 index 0000000000000..4b0b93033081c --- /dev/null +++ b/crates/bevy_basis_universal/src/loader.rs @@ -0,0 +1,143 @@ +//! Asset loader for Basis Universal KTX2 textures. +//! +//! The file extension must be `.basisu.ktx2` to use this loader. All basis universal compressed formats (ETC1S, UASTC, XUASTC) are supported. Zstd supercompression is always supported. No support for `.basis` files. +//! +//! Default transcode target selection: +//! +//! | BasisU formats | Target selection | +//! | ------------------------------ | -------------------------------------------------------------- | +//! | ETC1S | Etc2Rgba8/Etc2Rgb8/EacRg11/EacR11 > Bc7Rgba/Bc5Rg/Bc4R > Rgba8 | +//! | UASTC_LDR, ASTC_LDR, XUASTC_LDR| Astc > Bc7Rgba > Etc2Rgba8/Etc2Rgb8/EacRg11/EacR11 > Rgba8 | +//! | UASTC_HDR, ASTC_HDR | Astc > Bc6hRgbUfloat > Rgba16Float | + +use basisu_c_sys::extra::{BasisuTranscodeError, BasisuTranscoder, SupportedTextureCompression}; +use bevy_asset::{AssetLoader, RenderAssetUsages}; +use bevy_image::{CompressedImageFormats, Image, ImageSampler}; +use bevy_reflect::TypePath; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +/// Basis Universal texture loader. +#[derive(TypePath)] +pub struct BasisuLoader { + supported_compressed_formats: SupportedTextureCompression, +} + +impl BasisuLoader { + /// Create a basisu loader from the supported compressed formats. + pub fn new(supported_formats: CompressedImageFormats) -> Self { + let mut supported_compressed_formats = SupportedTextureCompression::empty(); + if supported_formats.contains(CompressedImageFormats::ASTC_LDR) { + supported_compressed_formats |= SupportedTextureCompression::ASTC_LDR; + } + if supported_formats.contains(CompressedImageFormats::ASTC_HDR) { + supported_compressed_formats |= SupportedTextureCompression::ASTC_HDR; + } + if supported_formats.contains(CompressedImageFormats::BC) { + supported_compressed_formats |= SupportedTextureCompression::BC; + } + if supported_formats.contains(CompressedImageFormats::ETC2) { + supported_compressed_formats |= SupportedTextureCompression::ETC2; + } + Self { + supported_compressed_formats, + } + } +} + +/// Settings for loading an [`Image`] using an [`BasisuLoader`]. +#[derive(Serialize, Deserialize, Default, Debug, Clone)] +pub struct BasisuLoaderSettings { + /// [`ImageSampler`] to use when rendering - this does + /// not affect the loading of the image data. + pub sampler: ImageSampler, + /// Where the asset will be used - see the docs on + /// [`RenderAssetUsages`] for details. + pub asset_usage: RenderAssetUsages, + /// Whether the texture should be created as sRGB format. + /// + /// If `None`, it will be determined by the KTX2 data format descriptor transfer function. + pub is_srgb: Option, + /// Forcibly transcode to a specific target format. If `None` the target format is selected automatically. + /// + /// It will fail to load if the target format is not supported by the device or it can't be transcoded by Basis Universal. + pub force_transcode_target: Option, +} + +/// An error when loading an image using [`BasisuLoader`]. +#[derive(Debug, Error)] +#[non_exhaustive] +pub enum BasisuLoaderError { + /// An error occurred while trying to load the image bytes. + #[error(transparent)] + Io(#[from] std::io::Error), + /// An error occurred while trying to transcode basisu textures. + #[error(transparent)] + TranscodeError(#[from] BasisuTranscodeError), +} + +impl AssetLoader for BasisuLoader { + type Asset = Image; + type Settings = BasisuLoaderSettings; + type Error = BasisuLoaderError; + + async fn load( + &self, + reader: &mut dyn bevy_asset::io::Reader, + settings: &Self::Settings, + _load_context: &mut bevy_asset::LoadContext<'_>, + ) -> Result { + let mut data = Vec::new(); + reader.read_to_end(&mut data).await?; + let src_bytes = data.len(); + + let _span = bevy_log::info_span!("transcoding basisu texture").entered(); + let time = if bevy_log::STATIC_MAX_LEVEL >= bevy_log::Level::DEBUG { + Some(bevy_platform::time::Instant::now()) + } else { + None + }; + let mut transcoder = BasisuTranscoder::new(); + let info = transcoder.prepare( + &data, + self.supported_compressed_formats, + basisu_c_sys::extra::ChannelType::Auto, + )?; + + let out_image = transcoder.transcode(settings.force_transcode_target, settings.is_srgb)?; + + if bevy_log::STATIC_MAX_LEVEL >= bevy_log::Level::DEBUG { + bevy_log::debug!( + "Transcoded a basisu texture {:?} -> {:?}, {:?}kb -> {:?}kb, preferred_target {:?}, extents {:?}, levels {:?}, view_dimension {:?}, in {:?}", + info.basis_format, + out_image.texture_descriptor.format, + src_bytes as f32 / 1000.0, + out_image.data.as_ref().unwrap().len() as f32 / 1000.0, + info.preferred_target, + out_image.texture_descriptor.size, + info.levels, + out_image + .texture_view_descriptor + .as_ref() + .unwrap() + .dimension + .unwrap(), + time.unwrap().elapsed(), + ); + } + + Ok(Image { + data: out_image.data, + data_order: out_image.data_order, + texture_descriptor: out_image.texture_descriptor, + texture_view_descriptor: out_image.texture_view_descriptor, + copy_on_resize: false, + sampler: settings.sampler.clone(), + asset_usage: settings.asset_usage, + }) + } + + fn extensions(&self) -> &[&str] { + &["basisu.ktx2"] + } +} diff --git a/crates/bevy_basis_universal/src/saver.rs b/crates/bevy_basis_universal/src/saver.rs new file mode 100644 index 0000000000000..6fcc36b16d8cc --- /dev/null +++ b/crates/bevy_basis_universal/src/saver.rs @@ -0,0 +1,84 @@ +//! Asset saver and processor for Basis Universal KTX2 textures. + +use bevy_asset::{ + processor::LoadTransformAndSave, saver::AssetSaver, transformer::IdentityAssetTransformer, + AsyncWriteExt, +}; +use bevy_image::{Image, ImageLoader}; +use bevy_reflect::TypePath; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +use basisu_c_sys::extra::{BasisuEncodeError, BasisuEncoder, BasisuEncoderParams}; + +use crate::loader::{BasisuLoader, BasisuLoaderSettings}; + +/// Basis universal asset processor. +pub type BasisuProcessor = + LoadTransformAndSave, BasisuSaver>; + +/// Basis universal texture saver. +#[derive(TypePath)] +pub struct BasisuSaver; + +/// Basis universal texture saver settings. +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct BasisuSaverSettings { + /// Basisu encoder params. + /// See the `BU_COMP_FLAGS_*` in [`basisu_c_sys`] if you want more controls, + /// like mipmap generation. + pub params: BasisuEncoderParams, +} + +impl Default for BasisuSaverSettings { + fn default() -> Self { + Self { + params: BasisuEncoderParams::new_with_srgb_defaults( + basisu_c_sys::BasisTextureFormat::XuastcLdr4x4, + ), + } + } +} + +/// An error when encoding an image using [`BasisuSaver`]. +#[derive(Debug, Error)] +#[non_exhaustive] +pub enum BasisuSaverError { + /// An error occurred while trying to load the bytes. + #[error(transparent)] + Io(#[from] std::io::Error), + /// An error occurred while trying to encode the image. + #[error(transparent)] + BasisuEncodeError(#[from] BasisuEncodeError), +} + +impl AssetSaver for BasisuSaver { + type Asset = Image; + type Settings = BasisuSaverSettings; + type OutputLoader = BasisuLoader; + type Error = BasisuSaverError; + + async fn save( + &self, + writer: &mut bevy_asset::io::Writer, + asset: bevy_asset::saver::SavedAsset<'_, '_, Self::Asset>, + settings: &Self::Settings, + _asset_path: bevy_asset::AssetPath<'_>, + ) -> Result<::Settings, Self::Error> { + let mut encoder = BasisuEncoder::new(); + encoder.set_image(basisu_c_sys::extra::SourceImage { + data: asset.data.as_deref().unwrap_or(&[]), + texture_descriptor: &asset.texture_descriptor, + texture_view_descriptor: &asset.texture_view_descriptor, + })?; + let result = encoder.compress(settings.params)?; + writer.write_all(&result).await?; + + Ok(BasisuLoaderSettings { + asset_usage: asset.asset_usage, + sampler: asset.sampler.clone(), + is_srgb: None, + force_transcode_target: None, + }) + } +} diff --git a/crates/bevy_image/src/saver.rs b/crates/bevy_image/src/saver.rs index e601326f0a3e3..8e5492d85536d 100644 --- a/crates/bevy_image/src/saver.rs +++ b/crates/bevy_image/src/saver.rs @@ -11,7 +11,7 @@ use crate::{Image, ImageFormat, ImageFormatSetting, ImageLoader, ImageLoaderSett /// [`AssetSaver`] for images that can be saved by the `image` crate. /// -/// Unlike `CompressedImageSaver`, this does not attempt to do any "texture optimization", like +/// This does not attempt to do any "texture optimization", like /// compression (though some file formats intrinsically perform some compression, e.g., JPEG). /// /// Some file formats do not support all texture formats (e.g., PNG does not support diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index e8b565b6df79c..d03e47af27913 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -52,6 +52,9 @@ exr = ["bevy_image/exr"] hdr = ["bevy_image/hdr"] ktx2 = ["bevy_image/ktx2"] +basis_universal = ["dep:bevy_basis_universal"] +basis_universal_saver = ["basis_universal", "bevy_basis_universal/saver"] + # Enable SPIR-V passthrough spirv_shader_passthrough = ["bevy_render/spirv_shader_passthrough"] @@ -522,6 +525,7 @@ bevy_material = { path = "../bevy_material", optional = true, version = "0.19.0- bevy_mesh = { path = "../bevy_mesh", optional = true, version = "0.19.0-dev" } bevy_camera = { path = "../bevy_camera", optional = true, version = "0.19.0-dev" } bevy_light = { path = "../bevy_light", optional = true, version = "0.19.0-dev" } +bevy_basis_universal = { path = "../bevy_basis_universal", optional = true, version = "0.19.0-dev" } bevy_input_focus = { path = "../bevy_input_focus", optional = true, version = "0.19.0-dev", default-features = false, features = [ "bevy_reflect", ] } diff --git a/crates/bevy_internal/src/lib.rs b/crates/bevy_internal/src/lib.rs index 858da645f1cf1..ee2cb044e564a 100644 --- a/crates/bevy_internal/src/lib.rs +++ b/crates/bevy_internal/src/lib.rs @@ -27,6 +27,8 @@ pub use bevy_app as app; pub use bevy_asset as asset; #[cfg(feature = "bevy_audio")] pub use bevy_audio as audio; +#[cfg(feature = "basis_universal")] +pub use bevy_basis_universal as basis_universal; #[cfg(feature = "bevy_camera")] pub use bevy_camera as camera; #[cfg(feature = "bevy_camera_controller")] diff --git a/crates/bevy_log/src/lib.rs b/crates/bevy_log/src/lib.rs index e084d42ced365..0b98bc4ed893d 100644 --- a/crates/bevy_log/src/lib.rs +++ b/crates/bevy_log/src/lib.rs @@ -47,8 +47,8 @@ pub mod prelude { pub use bevy_utils::once; pub use tracing::{ - self, debug, debug_span, error, error_span, event, info, info_span, trace, trace_span, warn, - warn_span, Level, + self, debug, debug_span, error, error_span, event, info, info_span, + level_filters::STATIC_MAX_LEVEL, trace, trace_span, warn, warn_span, Level, }; pub use tracing_subscriber; diff --git a/docs/cargo_features.md b/docs/cargo_features.md index 15be86fbe6bd9..f141bd40c79d4 100644 --- a/docs/cargo_features.md +++ b/docs/cargo_features.md @@ -65,7 +65,8 @@ This is the complete `bevy` cargo feature list, without "profiles" or "collectio |asset_processor|Enables the built-in asset processor for processed assets.| |async-io|Use async-io's implementation of block_on instead of futures-lite's implementation. This is preferred if your application uses async-io.| |async_executor|Uses `async-executor` as a task execution backend.| -|basis-universal|Basis Universal compressed texture support| +|basis_universal|Basis Universal compressed texture support| +|basis_universal_saver|Enables Basis Universal saver and asset processor| |bevy_animation|Provides animation functionality| |bevy_anti_alias|Provides various anti aliasing solutions| |bevy_asset|Provides asset functionality| @@ -110,7 +111,6 @@ This is the complete `bevy` cargo feature list, without "profiles" or "collectio |bevy_world_serialization|Provides ECS serialization functionality| |bluenoise_texture|Include spatio-temporal blue noise KTX2 file used by generated environment maps, Solari and atmosphere| |bmp|BMP image format support| -|compressed_image_saver|Enables compressed KTX2 UASTC texture output on the asset processor| |critical-section|`critical-section` provides the building blocks for synchronization primitives on all platforms, including `no_std`.| |custom_cursor|Enable winit custom cursor support| |dds|DDS compressed texture support| diff --git a/examples/large_scenes/mipmap_generator/README.md b/examples/large_scenes/mipmap_generator/README.md index 14e28c1221dd3..3c74b517b11e8 100644 --- a/examples/large_scenes/mipmap_generator/README.md +++ b/examples/large_scenes/mipmap_generator/README.md @@ -18,7 +18,7 @@ Test loading a gLTF, computing mips with texture compression, and caching compre Bevy supports a [variety of compressed image formats](https://docs.rs/bevy/latest/bevy/render/texture/enum.ImageFormat.html) that can also contain mipmaps. This plugin is intended for situations where the use of those formats is impractical (mostly prototyping/testing). With this plugin, mipmap generation happens slowly on the cpu. -Instead of using this plugin, consider using the new [CompressedImageSaver](https://bevyengine.org/news/bevy-0-12/#compressedimagesaver). +Instead of using this plugin, consider using the `BasisUniversalPlugin`'s asset processor that generates textures ahead of time. For generating compressed textures ahead of time also check out: From 72b14b2744c4e960102576ead4ddaac4a5bf2c1d Mon Sep 17 00:00:00 2001 From: Luo Zhihao Date: Sun, 5 Apr 2026 17:06:51 +0800 Subject: [PATCH 04/16] Fix wasm build --- crates/bevy_basis_universal/Cargo.toml | 1 + crates/bevy_basis_universal/src/lib.rs | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/bevy_basis_universal/Cargo.toml b/crates/bevy_basis_universal/Cargo.toml index cce4fde769d64..e5e1116a1d416 100644 --- a/crates/bevy_basis_universal/Cargo.toml +++ b/crates/bevy_basis_universal/Cargo.toml @@ -15,6 +15,7 @@ saver = ["basisu_c_sys/encoder"] basisu_c_sys = { version = "0.6.0", default-features = false, features = ["extra", "serde"] } bevy_app = { version = "0.19.0-dev", path = "../bevy_app", default-features = false } bevy_asset = { version = "0.19.0-dev", path = "../bevy_asset", default-features = false } +bevy_ecs = { version = "0.19.0-dev", path = "../bevy_ecs", default-features = false } bevy_image = { version = "0.19.0-dev", path = "../bevy_image", default-features = false } bevy_log = { version = "0.19.0-dev", path = "../bevy_log", default-features = false } bevy_platform = { version = "0.19.0-dev", path = "../bevy_platform", default-features = false } diff --git a/crates/bevy_basis_universal/src/lib.rs b/crates/bevy_basis_universal/src/lib.rs index 03f50c7b2e5d7..f0416f841347e 100644 --- a/crates/bevy_basis_universal/src/lib.rs +++ b/crates/bevy_basis_universal/src/lib.rs @@ -52,7 +52,7 @@ impl Default for BasisUniversalPlugin { target_vendor = "unknown", target_os = "unknown", ))] -#[derive(Resource, Clone, Deref)] +#[derive(bevy_ecs::resource::Resource, Clone)] struct BasisuWasmReady(Arc); impl Plugin for BasisUniversalPlugin { @@ -70,8 +70,8 @@ impl Plugin for BasisUniversalPlugin { basisu_c_sys::extra::basisu_transcoder_init().await; #[cfg(feature = "saver")] basisu_c_sys::extra::basisu_encoder_init().await; - bevy::log::debug!("Basisu wasm initialized"); - r.store(1, Ordering::Release); + bevy_log::debug!("Basisu wasm initialized"); + r.0.store(1, Ordering::Release); }) .detach(); app.insert_resource(ready); @@ -110,6 +110,7 @@ impl Plugin for BasisUniversalPlugin { fn ready(&self, app: &App) -> bool { app.world() .resource::() + .0 .load(Ordering::Acquire) != 0 } From 1b88dbad99b0638edc6cfa1facb6392c00730bf5 Mon Sep 17 00:00:00 2001 From: Luo Zhihao Date: Sun, 5 Apr 2026 17:17:38 +0800 Subject: [PATCH 05/16] Fix `imported_assets` on web --- .gitignore | 3 ++- examples/wasm/imported_assets | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) create mode 120000 examples/wasm/imported_assets diff --git a/.gitignore b/.gitignore index b5b92ed77cbeb..d200d47ca5dce 100644 --- a/.gitignore +++ b/.gitignore @@ -21,7 +21,8 @@ Cargo.lock # Bevy Assets assets/**/*.meta crates/bevy_asset/imported_assets -imported_assets +examples/asset/processing/imported_assets +/imported_assets .web-asset-cache examples/large_scenes/bistro/assets/* examples/large_scenes/caldera_hotel/assets/* diff --git a/examples/wasm/imported_assets b/examples/wasm/imported_assets new file mode 120000 index 0000000000000..b14502c5587e3 --- /dev/null +++ b/examples/wasm/imported_assets @@ -0,0 +1 @@ +../../imported_assets/ \ No newline at end of file From 8a09d0694029ba20ac7acc62a6d766205f80bf1a Mon Sep 17 00:00:00 2001 From: Luo Zhihao Date: Sun, 5 Apr 2026 17:33:34 +0800 Subject: [PATCH 06/16] taplo --- crates/bevy_basis_universal/Cargo.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/bevy_basis_universal/Cargo.toml b/crates/bevy_basis_universal/Cargo.toml index e5e1116a1d416..2121e2fc55157 100644 --- a/crates/bevy_basis_universal/Cargo.toml +++ b/crates/bevy_basis_universal/Cargo.toml @@ -12,7 +12,10 @@ keywords = ["bevy"] saver = ["basisu_c_sys/encoder"] [dependencies] -basisu_c_sys = { version = "0.6.0", default-features = false, features = ["extra", "serde"] } +basisu_c_sys = { version = "0.6.0", default-features = false, features = [ + "extra", + "serde", +] } bevy_app = { version = "0.19.0-dev", path = "../bevy_app", default-features = false } bevy_asset = { version = "0.19.0-dev", path = "../bevy_asset", default-features = false } bevy_ecs = { version = "0.19.0-dev", path = "../bevy_ecs", default-features = false } From d994d2f2657594f2085d25399b70386df985e99d Mon Sep 17 00:00:00 2001 From: Luo Zhihao Date: Sun, 5 Apr 2026 17:39:17 +0800 Subject: [PATCH 07/16] check-missing features --- Cargo.toml | 3 +++ docs/cargo_features.md | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 620ef5af1adfb..a2a132e2ccd5d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -428,7 +428,10 @@ trace_tracy_memory = ["bevy_internal/trace_tracy_memory"] # Tracing support trace = ["bevy_internal/trace", "dep:tracing"] +# Basis Universal compressed texture support basis_universal = ["bevy_internal/basis_universal"] + +# Basis Universal saver and asset processor basis_universal_saver = ["bevy_internal/basis_universal_saver"] # BMP image format support diff --git a/docs/cargo_features.md b/docs/cargo_features.md index f141bd40c79d4..155e22fbaf89b 100644 --- a/docs/cargo_features.md +++ b/docs/cargo_features.md @@ -66,7 +66,7 @@ This is the complete `bevy` cargo feature list, without "profiles" or "collectio |async-io|Use async-io's implementation of block_on instead of futures-lite's implementation. This is preferred if your application uses async-io.| |async_executor|Uses `async-executor` as a task execution backend.| |basis_universal|Basis Universal compressed texture support| -|basis_universal_saver|Enables Basis Universal saver and asset processor| +|basis_universal_saver|Basis Universal saver and asset processor| |bevy_animation|Provides animation functionality| |bevy_anti_alias|Provides various anti aliasing solutions| |bevy_asset|Provides asset functionality| From f64a95a97d5f832ec41b5deb15bfd2e6fb3515e3 Mon Sep 17 00:00:00 2001 From: Luo Zhihao Date: Mon, 6 Apr 2026 00:37:25 +0800 Subject: [PATCH 08/16] Remove TextureChannelLayout --- crates/bevy_basis_universal/src/loader.rs | 33 ++++++++++++----------- crates/bevy_image/src/image.rs | 21 --------------- crates/bevy_image/src/ktx2.rs | 24 +---------------- 3 files changed, 18 insertions(+), 60 deletions(-) diff --git a/crates/bevy_basis_universal/src/loader.rs b/crates/bevy_basis_universal/src/loader.rs index 4b0b93033081c..ffcc638dc3d32 100644 --- a/crates/bevy_basis_universal/src/loader.rs +++ b/crates/bevy_basis_universal/src/loader.rs @@ -108,22 +108,23 @@ impl AssetLoader for BasisuLoader { if bevy_log::STATIC_MAX_LEVEL >= bevy_log::Level::DEBUG { bevy_log::debug!( - "Transcoded a basisu texture {:?} -> {:?}, {:?}kb -> {:?}kb, preferred_target {:?}, extents {:?}, levels {:?}, view_dimension {:?}, in {:?}", - info.basis_format, - out_image.texture_descriptor.format, - src_bytes as f32 / 1000.0, - out_image.data.as_ref().unwrap().len() as f32 / 1000.0, - info.preferred_target, - out_image.texture_descriptor.size, - info.levels, - out_image - .texture_view_descriptor - .as_ref() - .unwrap() - .dimension - .unwrap(), - time.unwrap().elapsed(), - ); + "Transcoded a basisu texture {:?} -> {:?}, {:?}kb -> {:?}kb,\ + preferred_target {:?}, extents {:?}, levels {:?}, view_dimension {:?}, in {:?}", + info.basis_format, + out_image.texture_descriptor.format, + src_bytes as f32 / 1000.0, + out_image.data.as_ref().unwrap().len() as f32 / 1000.0, + info.preferred_target, + out_image.texture_descriptor.size, + info.levels, + out_image + .texture_view_descriptor + .as_ref() + .unwrap() + .dimension + .unwrap(), + time.unwrap().elapsed(), + ); } Ok(Image { diff --git a/crates/bevy_image/src/image.rs b/crates/bevy_image/src/image.rs index 2dba7b050e84e..4d8ba1871fd6e 100644 --- a/crates/bevy_image/src/image.rs +++ b/crates/bevy_image/src/image.rs @@ -1965,30 +1965,9 @@ impl Image { } } -/// A [UASTC] texture channel layout -/// -/// [UASTC]: https://github.com/BinomialLLC/basis_universal/wiki/UASTC-Texture-Specification/b624c07ad3c659e7b0f0badcb36e9a6b8820a99d -#[derive(Clone, Copy, Debug)] -pub enum TextureChannelLayout { - /// 3-color - Rgb, - /// 4-color - Rgba, - /// 1-color (R) extended to 3 (RRR) - Rrr, - /// 2-color (RG) extended to 4 (RRRG) - Rrrg, - /// 2-color - Rg, -} - /// Texture data need to be transcoded from this format for use with `wgpu`. #[derive(Clone, Copy, Debug)] pub enum TranscodeFormat { - /// Has to be transcoded from a compressed ETC1S texture. - Etc1s, - /// Has to be transcoded from a compressed UASTC texture. - Uastc(TextureChannelLayout), /// Has to be transcoded from `R8UnormSrgb` to `R8Unorm` for use with `wgpu`. R8UnormSrgb, /// Has to be transcoded from `Rg8UnormSrgb` to `R8G8Unorm` for use with `wgpu`. diff --git a/crates/bevy_image/src/ktx2.rs b/crates/bevy_image/src/ktx2.rs index c4e38c3cfe9bf..4f4e0fab67e91 100644 --- a/crates/bevy_image/src/ktx2.rs +++ b/crates/bevy_image/src/ktx2.rs @@ -14,7 +14,7 @@ use wgpu_types::{ TextureViewDimension, }; -use super::{CompressedImageFormats, Image, TextureChannelLayout, TextureError, TranscodeFormat}; +use super::{CompressedImageFormats, Image, TextureError, TranscodeFormat}; /// Converts KTX2 bytes to a bevy [`Image`] using the given compressed format support. /// @@ -165,7 +165,6 @@ pub fn ktx2_buffer_to_image( TextureFormat::Rgba8Unorm } } - _ => return Err(error), }; levels = transcoded; Ok(texture_format) @@ -1004,11 +1003,6 @@ pub fn ktx2_dfd_header_to_texture_format( AstcChannel::Unorm }, }, - Some(ColorModel::ETC1S) => { - return Err(TextureError::FormatRequiresTranscodingError( - TranscodeFormat::Etc1s, - )); - } Some(ColorModel::PVRTC) => { return Err(TextureError::UnsupportedTextureFormat( "PVRTC is not supported".to_string(), @@ -1019,22 +1013,6 @@ pub fn ktx2_dfd_header_to_texture_format( "PVRTC2 is not supported".to_string(), )); } - Some(ColorModel::UASTC) => { - return Err(TextureError::FormatRequiresTranscodingError( - TranscodeFormat::Uastc(match sample_information[0].channel_type { - 0 => TextureChannelLayout::Rgb, - 3 => TextureChannelLayout::Rgba, - 4 => TextureChannelLayout::Rrr, - 5 => TextureChannelLayout::Rrrg, - 6 => TextureChannelLayout::Rg, - channel_type => { - return Err(TextureError::UnsupportedTextureFormat(format!( - "Invalid KTX2 UASTC channel type: {channel_type}", - ))) - } - }), - )); - } None => { return Err(TextureError::UnsupportedTextureFormat( "Unspecified KTX2 color model".to_string(), From 3d7fe7daf0a60dd70ff57a54660fc177a6f25be4 Mon Sep 17 00:00:00 2001 From: Luo Zhihao Date: Mon, 6 Apr 2026 00:55:50 +0800 Subject: [PATCH 09/16] log --- crates/bevy_basis_universal/src/loader.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/bevy_basis_universal/src/loader.rs b/crates/bevy_basis_universal/src/loader.rs index ffcc638dc3d32..adce6526c0f12 100644 --- a/crates/bevy_basis_universal/src/loader.rs +++ b/crates/bevy_basis_universal/src/loader.rs @@ -85,13 +85,13 @@ impl AssetLoader for BasisuLoader { &self, reader: &mut dyn bevy_asset::io::Reader, settings: &Self::Settings, - _load_context: &mut bevy_asset::LoadContext<'_>, + load_context: &mut bevy_asset::LoadContext<'_>, ) -> Result { let mut data = Vec::new(); reader.read_to_end(&mut data).await?; let src_bytes = data.len(); - let _span = bevy_log::info_span!("transcoding basisu texture").entered(); + let _span = bevy_log::info_span!("Transcoding basisu texture").entered(); let time = if bevy_log::STATIC_MAX_LEVEL >= bevy_log::Level::DEBUG { Some(bevy_platform::time::Instant::now()) } else { @@ -108,8 +108,10 @@ impl AssetLoader for BasisuLoader { if bevy_log::STATIC_MAX_LEVEL >= bevy_log::Level::DEBUG { bevy_log::debug!( - "Transcoded a basisu texture {:?} -> {:?}, {:?}kb -> {:?}kb,\ - preferred_target {:?}, extents {:?}, levels {:?}, view_dimension {:?}, in {:?}", + "Transcoded basisu texture \"{}\", \ + {:?} -> {:?}, {}kb -> {}kb. \ + Preferred target: {:?}, extents: {:?}, level count: {}, view dimension: {:?} in {:?}", + load_context.path(), info.basis_format, out_image.texture_descriptor.format, src_bytes as f32 / 1000.0, From c868d1b968fa827300c40fdecb916c7991a0dcf7 Mon Sep 17 00:00:00 2001 From: Luo Zhihao Date: Tue, 7 Apr 2026 00:12:32 +0800 Subject: [PATCH 10/16] Merge bevy_basis_universal into bevy_image --- crates/bevy_basis_universal/Cargo.toml | 39 ---- crates/bevy_basis_universal/LICENSE-APACHE | 176 ---------------- crates/bevy_basis_universal/LICENSE-MIT | 19 -- crates/bevy_basis_universal/src/lib.rs | 138 ------------- crates/bevy_basis_universal/src/loader.rs | 146 ------------- crates/bevy_image/Cargo.toml | 5 + crates/bevy_image/src/basis_universal/mod.rs | 193 ++++++++++++++++++ .../src/basis_universal}/saver.rs | 27 ++- crates/bevy_image/src/image.rs | 30 +++ crates/bevy_image/src/ktx2.rs | 15 ++ crates/bevy_image/src/lib.rs | 4 + crates/bevy_internal/Cargo.toml | 5 +- crates/bevy_internal/src/lib.rs | 2 - crates/bevy_render/src/texture/mod.rs | 17 +- 14 files changed, 269 insertions(+), 547 deletions(-) delete mode 100644 crates/bevy_basis_universal/Cargo.toml delete mode 100644 crates/bevy_basis_universal/LICENSE-APACHE delete mode 100644 crates/bevy_basis_universal/LICENSE-MIT delete mode 100644 crates/bevy_basis_universal/src/lib.rs delete mode 100644 crates/bevy_basis_universal/src/loader.rs create mode 100644 crates/bevy_image/src/basis_universal/mod.rs rename crates/{bevy_basis_universal/src => bevy_image/src/basis_universal}/saver.rs (78%) diff --git a/crates/bevy_basis_universal/Cargo.toml b/crates/bevy_basis_universal/Cargo.toml deleted file mode 100644 index 2121e2fc55157..0000000000000 --- a/crates/bevy_basis_universal/Cargo.toml +++ /dev/null @@ -1,39 +0,0 @@ -[package] -name = "bevy_basis_universal" -version = "0.19.0-dev" -edition = "2024" -description = "Provides basis universal texture loader and saver for Bevy Engine" -homepage = "https://bevy.org" -repository = "https://github.com/bevyengine/bevy" -license = "MIT OR Apache-2.0" -keywords = ["bevy"] - -[features] -saver = ["basisu_c_sys/encoder"] - -[dependencies] -basisu_c_sys = { version = "0.6.0", default-features = false, features = [ - "extra", - "serde", -] } -bevy_app = { version = "0.19.0-dev", path = "../bevy_app", default-features = false } -bevy_asset = { version = "0.19.0-dev", path = "../bevy_asset", default-features = false } -bevy_ecs = { version = "0.19.0-dev", path = "../bevy_ecs", default-features = false } -bevy_image = { version = "0.19.0-dev", path = "../bevy_image", default-features = false } -bevy_log = { version = "0.19.0-dev", path = "../bevy_log", default-features = false } -bevy_platform = { version = "0.19.0-dev", path = "../bevy_platform", default-features = false } -bevy_reflect = { version = "0.19.0-dev", path = "../bevy_reflect", default-features = false } -bevy_tasks = { version = "0.19.0-dev", path = "../bevy_tasks", default-features = false } -serde = { version = "1.0.228", default-features = false, features = ["derive"] } -thiserror = { version = "2.0.18", default-features = false } - -[lints] -workspace = true - -[package.metadata.docs.rs] -rustdoc-args = [ - "-Zunstable-options", - "--generate-link-to-definition", - "--generate-macro-expansion", -] -all-features = true diff --git a/crates/bevy_basis_universal/LICENSE-APACHE b/crates/bevy_basis_universal/LICENSE-APACHE deleted file mode 100644 index d9a10c0d8e868..0000000000000 --- a/crates/bevy_basis_universal/LICENSE-APACHE +++ /dev/null @@ -1,176 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS diff --git a/crates/bevy_basis_universal/LICENSE-MIT b/crates/bevy_basis_universal/LICENSE-MIT deleted file mode 100644 index 9cf106272ac3b..0000000000000 --- a/crates/bevy_basis_universal/LICENSE-MIT +++ /dev/null @@ -1,19 +0,0 @@ -MIT License - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/crates/bevy_basis_universal/src/lib.rs b/crates/bevy_basis_universal/src/lib.rs deleted file mode 100644 index f0416f841347e..0000000000000 --- a/crates/bevy_basis_universal/src/lib.rs +++ /dev/null @@ -1,138 +0,0 @@ -#![cfg_attr(docsrs, feature(doc_cfg))] - -//! Provides loader and saver for Basis Universal KTX2 textures. -//! See [`loader`] and [`saver`] for more information. -//! -//! This uses [Basis Universal v2.1](https://github.com/BinomialLLC/basis_universal) C++ library. All basis universal formats are supported. - -pub mod loader; -#[cfg(feature = "saver")] -pub mod saver; - -use bevy_asset::AssetApp; -use bevy_image::{CompressedImageFormatSupport, CompressedImageFormats, ImageLoader}; -#[cfg(all( - target_arch = "wasm32", - target_vendor = "unknown", - target_os = "unknown", -))] -use bevy_platform::sync::{ - atomic::{AtomicUsize, Ordering}, - Arc, -}; - -use bevy_app::{App, Plugin}; - -use crate::loader::BasisuLoader; -#[cfg(feature = "saver")] -use crate::saver::{BasisuProcessor, BasisuSaver}; - -/// Provides basis universal texture loader and saver. -pub struct BasisUniversalPlugin { - /// The file extensions handled by the basisu asset processor. - /// - /// Default is [`ImageLoader::SUPPORTED_FILE_EXTENSIONS`] except ktx2 and .dds. - pub processor_extensions: Vec, -} - -impl Default for BasisUniversalPlugin { - fn default() -> Self { - Self { - processor_extensions: ImageLoader::SUPPORTED_FILE_EXTENSIONS - .iter() - .filter(|s| !["ktx2", "dds"].contains(s)) - .map(ToString::to_string) - .collect(), - } - } -} - -#[cfg(all( - target_arch = "wasm32", - target_vendor = "unknown", - target_os = "unknown", -))] -#[derive(bevy_ecs::resource::Resource, Clone)] -struct BasisuWasmReady(Arc); - -impl Plugin for BasisUniversalPlugin { - fn build(&self, app: &mut App) { - #[cfg(all( - target_arch = "wasm32", - target_vendor = "unknown", - target_os = "unknown", - ))] - { - let ready = BasisuWasmReady(Arc::new(AtomicUsize::new(0))); - let r = ready.clone(); - bevy_tasks::IoTaskPool::get() - .spawn_local(async move { - basisu_c_sys::extra::basisu_transcoder_init().await; - #[cfg(feature = "saver")] - basisu_c_sys::extra::basisu_encoder_init().await; - bevy_log::debug!("Basisu wasm initialized"); - r.0.store(1, Ordering::Release); - }) - .detach(); - app.insert_resource(ready); - } - #[cfg(not(all( - target_arch = "wasm32", - target_vendor = "unknown", - target_os = "unknown", - )))] - { - bevy_tasks::block_on(basisu_c_sys::extra::basisu_transcoder_init()); - #[cfg(feature = "saver")] - bevy_tasks::block_on(basisu_c_sys::extra::basisu_encoder_init()); - } - app.preregister_asset_loader::(&["basisu.ktx2"]); - - #[cfg(feature = "saver")] - { - if let Some(asset_processor) = app - .world() - .get_resource::() - { - asset_processor.register_processor::(BasisuSaver.into()); - for ext in &self.processor_extensions { - asset_processor.set_default_processor::(ext.as_str()); - } - } - } - } - - #[cfg(all( - target_arch = "wasm32", - target_vendor = "unknown", - target_os = "unknown", - ))] - fn ready(&self, app: &App) -> bool { - app.world() - .resource::() - .0 - .load(Ordering::Acquire) - != 0 - } - - fn finish(&self, app: &mut App) { - #[cfg(all( - target_arch = "wasm32", - target_vendor = "unknown", - target_os = "unknown", - ))] - app.world_mut().remove_resource::(); - - let supported_compressed_formats = if let Some(resource) = - app.world().get_resource::() - { - resource.0 - } else { - bevy_log::warn!("CompressedImageFormatSupport resource not found. It should either be initialized in finish() of \ - RenderPlugin, or manually if not using the RenderPlugin or the WGPU backend."); - CompressedImageFormats::NONE - }; - - app.register_asset_loader(BasisuLoader::new(supported_compressed_formats)); - } -} diff --git a/crates/bevy_basis_universal/src/loader.rs b/crates/bevy_basis_universal/src/loader.rs deleted file mode 100644 index adce6526c0f12..0000000000000 --- a/crates/bevy_basis_universal/src/loader.rs +++ /dev/null @@ -1,146 +0,0 @@ -//! Asset loader for Basis Universal KTX2 textures. -//! -//! The file extension must be `.basisu.ktx2` to use this loader. All basis universal compressed formats (ETC1S, UASTC, XUASTC) are supported. Zstd supercompression is always supported. No support for `.basis` files. -//! -//! Default transcode target selection: -//! -//! | BasisU formats | Target selection | -//! | ------------------------------ | -------------------------------------------------------------- | -//! | ETC1S | Etc2Rgba8/Etc2Rgb8/EacRg11/EacR11 > Bc7Rgba/Bc5Rg/Bc4R > Rgba8 | -//! | UASTC_LDR, ASTC_LDR, XUASTC_LDR| Astc > Bc7Rgba > Etc2Rgba8/Etc2Rgb8/EacRg11/EacR11 > Rgba8 | -//! | UASTC_HDR, ASTC_HDR | Astc > Bc6hRgbUfloat > Rgba16Float | - -use basisu_c_sys::extra::{BasisuTranscodeError, BasisuTranscoder, SupportedTextureCompression}; -use bevy_asset::{AssetLoader, RenderAssetUsages}; -use bevy_image::{CompressedImageFormats, Image, ImageSampler}; -use bevy_reflect::TypePath; -use serde::{Deserialize, Serialize}; -use thiserror::Error; - -/// Basis Universal texture loader. -#[derive(TypePath)] -pub struct BasisuLoader { - supported_compressed_formats: SupportedTextureCompression, -} - -impl BasisuLoader { - /// Create a basisu loader from the supported compressed formats. - pub fn new(supported_formats: CompressedImageFormats) -> Self { - let mut supported_compressed_formats = SupportedTextureCompression::empty(); - if supported_formats.contains(CompressedImageFormats::ASTC_LDR) { - supported_compressed_formats |= SupportedTextureCompression::ASTC_LDR; - } - if supported_formats.contains(CompressedImageFormats::ASTC_HDR) { - supported_compressed_formats |= SupportedTextureCompression::ASTC_HDR; - } - if supported_formats.contains(CompressedImageFormats::BC) { - supported_compressed_formats |= SupportedTextureCompression::BC; - } - if supported_formats.contains(CompressedImageFormats::ETC2) { - supported_compressed_formats |= SupportedTextureCompression::ETC2; - } - Self { - supported_compressed_formats, - } - } -} - -/// Settings for loading an [`Image`] using an [`BasisuLoader`]. -#[derive(Serialize, Deserialize, Default, Debug, Clone)] -pub struct BasisuLoaderSettings { - /// [`ImageSampler`] to use when rendering - this does - /// not affect the loading of the image data. - pub sampler: ImageSampler, - /// Where the asset will be used - see the docs on - /// [`RenderAssetUsages`] for details. - pub asset_usage: RenderAssetUsages, - /// Whether the texture should be created as sRGB format. - /// - /// If `None`, it will be determined by the KTX2 data format descriptor transfer function. - pub is_srgb: Option, - /// Forcibly transcode to a specific target format. If `None` the target format is selected automatically. - /// - /// It will fail to load if the target format is not supported by the device or it can't be transcoded by Basis Universal. - pub force_transcode_target: Option, -} - -/// An error when loading an image using [`BasisuLoader`]. -#[derive(Debug, Error)] -#[non_exhaustive] -pub enum BasisuLoaderError { - /// An error occurred while trying to load the image bytes. - #[error(transparent)] - Io(#[from] std::io::Error), - /// An error occurred while trying to transcode basisu textures. - #[error(transparent)] - TranscodeError(#[from] BasisuTranscodeError), -} - -impl AssetLoader for BasisuLoader { - type Asset = Image; - type Settings = BasisuLoaderSettings; - type Error = BasisuLoaderError; - - async fn load( - &self, - reader: &mut dyn bevy_asset::io::Reader, - settings: &Self::Settings, - load_context: &mut bevy_asset::LoadContext<'_>, - ) -> Result { - let mut data = Vec::new(); - reader.read_to_end(&mut data).await?; - let src_bytes = data.len(); - - let _span = bevy_log::info_span!("Transcoding basisu texture").entered(); - let time = if bevy_log::STATIC_MAX_LEVEL >= bevy_log::Level::DEBUG { - Some(bevy_platform::time::Instant::now()) - } else { - None - }; - let mut transcoder = BasisuTranscoder::new(); - let info = transcoder.prepare( - &data, - self.supported_compressed_formats, - basisu_c_sys::extra::ChannelType::Auto, - )?; - - let out_image = transcoder.transcode(settings.force_transcode_target, settings.is_srgb)?; - - if bevy_log::STATIC_MAX_LEVEL >= bevy_log::Level::DEBUG { - bevy_log::debug!( - "Transcoded basisu texture \"{}\", \ - {:?} -> {:?}, {}kb -> {}kb. \ - Preferred target: {:?}, extents: {:?}, level count: {}, view dimension: {:?} in {:?}", - load_context.path(), - info.basis_format, - out_image.texture_descriptor.format, - src_bytes as f32 / 1000.0, - out_image.data.as_ref().unwrap().len() as f32 / 1000.0, - info.preferred_target, - out_image.texture_descriptor.size, - info.levels, - out_image - .texture_view_descriptor - .as_ref() - .unwrap() - .dimension - .unwrap(), - time.unwrap().elapsed(), - ); - } - - Ok(Image { - data: out_image.data, - data_order: out_image.data_order, - texture_descriptor: out_image.texture_descriptor, - texture_view_descriptor: out_image.texture_view_descriptor, - copy_on_resize: false, - sampler: settings.sampler.clone(), - asset_usage: settings.asset_usage, - }) - } - - fn extensions(&self) -> &[&str] { - &["basisu.ktx2"] - } -} diff --git a/crates/bevy_image/Cargo.toml b/crates/bevy_image/Cargo.toml index 09d47904da027..166be9751a355 100644 --- a/crates/bevy_image/Cargo.toml +++ b/crates/bevy_image/Cargo.toml @@ -43,6 +43,8 @@ zstd = [] zstd_rust = ["zstd", "dep:ruzstd"] # Binding to zstd C implementation (faster) zstd_c = ["zstd", "dep:zstd"] +basis_universal = ["ktx2","dep:basisu_c_sys"] +basis_universal_saver = ["basis_universal", "basisu_c_sys/encoder"] [dependencies] # bevy @@ -81,6 +83,9 @@ zstd = { version = "0.13.3", optional = true } ruzstd = { version = "0.8.0", optional = true } tracing = { version = "0.1", default-features = false, features = ["std"] } half = { version = "2.4.1" } +basisu_c_sys = { version = "0.6.1", default-features = false, features = ["extra","serde"], optional = true } +bevy_log = { version = "0.19.0-dev", path = "../bevy_log", default-features = false } +bevy_tasks = { version = "0.19.0-dev", path = "../bevy_tasks", default-features = false } [dev-dependencies] bevy_ecs = { path = "../bevy_ecs", version = "0.19.0-dev" } diff --git a/crates/bevy_image/src/basis_universal/mod.rs b/crates/bevy_image/src/basis_universal/mod.rs new file mode 100644 index 0000000000000..f9ae59ac59274 --- /dev/null +++ b/crates/bevy_image/src/basis_universal/mod.rs @@ -0,0 +1,193 @@ +#[cfg(feature = "basis_universal_saver")] +mod saver; +#[cfg(feature = "basis_universal_saver")] +pub use saver::*; + +use crate::{CompressedImageFormats, Image, ImageLoader, TextureError}; +use basisu_c_sys::extra::{BasisuTranscoder, SupportedTextureCompression}; +use bevy_app::{App, Plugin}; +#[cfg(all( + target_arch = "wasm32", + target_vendor = "unknown", + target_os = "unknown", +))] +use bevy_platform::sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, +}; + +/// Converts and transcodes KTX2 Basis Universal bytes to a bevy [`Image`] using the given compressed format support. All basis universal compressed formats (ETC1S, UASTC, ASTC, XUASTC) are supported. Zstd supercompression is always supported. No support for `.basis` files. +/// +/// The current integrated basis universl version is 2.10 +/// +/// Default transcode target selection: +/// +/// | BasisU format | Target selection | +/// | ------------------------------ | -------------------------------------------------------------- | +/// | ETC1S | Bc7Rgba/Bc5Rg/Bc4R > Etc2Rgba8/Etc2Rgb8/EacRg11/EacR11 > Rgba8 | +/// | UASTC_LDR, ASTC_LDR, XUASTC_LDR| Astc > Bc7Rgba > Etc2Rgba8/Etc2Rgb8/EacRg11/EacR11 > Rgba8 | +/// | UASTC_HDR, ASTC_HDR | Astc > Bc6hRgbUfloat > Rgba16Float | +pub fn ktx2_basisu_buffer_to_image( + buffer: &[u8], + supported_compressed_formats: CompressedImageFormats, + is_srgb: bool, +) -> Result { + let src_bytes = buffer.len(); + + let _span = bevy_log::info_span!("Transcoding basisu texture").entered(); + let time = if bevy_log::STATIC_MAX_LEVEL >= bevy_log::Level::DEBUG { + Some(bevy_platform::time::Instant::now()) + } else { + None + }; + let mut compressions = SupportedTextureCompression::empty(); + if supported_compressed_formats.contains(CompressedImageFormats::ASTC_LDR) { + compressions |= SupportedTextureCompression::ASTC_LDR; + } + if supported_compressed_formats.contains(CompressedImageFormats::ASTC_HDR) { + compressions |= SupportedTextureCompression::ASTC_HDR; + } + if supported_compressed_formats.contains(CompressedImageFormats::BC) { + compressions |= SupportedTextureCompression::BC; + } + if supported_compressed_formats.contains(CompressedImageFormats::ETC2) { + compressions |= SupportedTextureCompression::ETC2; + } + let mut transcoder = BasisuTranscoder::new(); + let info = transcoder.prepare(buffer, compressions, basisu_c_sys::extra::ChannelType::Auto)?; + + let out_image = transcoder.transcode(None, Some(is_srgb))?; + + if bevy_log::STATIC_MAX_LEVEL >= bevy_log::Level::DEBUG { + bevy_log::debug!( + "Transcoded basisu texture, \ + {:?} -> {:?}, {}kb -> {}kb. \ + Preferred target: {:?}, extents: {:?}, level count: {}, view dimension: {:?} in {:?}", + info.basis_format, + out_image.texture_descriptor.format, + src_bytes as f32 / 1000.0, + out_image.data.as_ref().unwrap().len() as f32 / 1000.0, + info.preferred_target, + out_image.texture_descriptor.size, + info.levels, + out_image + .texture_view_descriptor + .as_ref() + .unwrap() + .dimension + .unwrap(), + time.unwrap().elapsed(), + ); + } + + Ok(Image { + data: out_image.data, + data_order: out_image.data_order, + texture_descriptor: out_image.texture_descriptor, + texture_view_descriptor: out_image.texture_view_descriptor, + ..Default::default() + }) +} + +/// Provides the necessary basis universal initialization. +/// Any bassiu encoding or transcoding will fail before this plugin is initialized. +pub struct BasisUniversalPlugin; + +/// Provides basis universal saver and asset processor +#[cfg(feature = "basis_universal_saver")] +pub struct BasisUniversalSaverPlugin { + /// The file extensions handled by the basisu asset processor. + /// + /// Default is [`ImageLoader::SUPPORTED_FILE_EXTENSIONS`] except ktx2 and .dds. + pub processor_extensions: Vec, +} + +impl Default for BasisUniversalSaverPlugin { + fn default() -> Self { + Self { + processor_extensions: ImageLoader::SUPPORTED_FILE_EXTENSIONS + .iter() + .filter(|s| !["ktx2", "dds"].contains(s)) + .map(ToString::to_string) + .collect(), + } + } +} + +impl Plugin for BasisUniversalSaverPlugin { + fn build(&self, app: &mut App) { + if let Some(asset_processor) = app + .world() + .get_resource::() + { + asset_processor.register_processor::(BasisuSaver.into()); + for ext in &self.processor_extensions { + asset_processor.set_default_processor::(ext.as_str()); + } + } + } +} + +#[cfg(all( + target_arch = "wasm32", + target_vendor = "unknown", + target_os = "unknown", +))] +#[derive(bevy_ecs::resource::Resource, Clone)] +struct BasisuWasmReady(Arc); + +impl Plugin for BasisUniversalPlugin { + fn build(&self, _app: &mut App) { + #[cfg(all( + target_arch = "wasm32", + target_vendor = "unknown", + target_os = "unknown", + ))] + { + let ready = BasisuWasmReady(Arc::new(AtomicUsize::new(0))); + let r = ready.clone(); + bevy_tasks::IoTaskPool::get() + .spawn_local(async move { + basisu_c_sys::extra::basisu_transcoder_init().await; + #[cfg(feature = "basis_universal_saver")] + basisu_c_sys::extra::basisu_encoder_init().await; + bevy_log::debug!("Basisu wasm initialized"); + r.0.store(1, Ordering::Release); + }) + .detach(); + _app.insert_resource(ready); + } + #[cfg(not(all( + target_arch = "wasm32", + target_vendor = "unknown", + target_os = "unknown", + )))] + { + bevy_tasks::block_on(basisu_c_sys::extra::basisu_transcoder_init()); + #[cfg(feature = "basis_universal_saver")] + bevy_tasks::block_on(basisu_c_sys::extra::basisu_encoder_init()); + } + } + + #[cfg(all( + target_arch = "wasm32", + target_vendor = "unknown", + target_os = "unknown", + ))] + fn ready(&self, app: &App) -> bool { + app.world() + .resource::() + .0 + .load(Ordering::Acquire) + != 0 + } + + fn finish(&self, _app: &mut App) { + #[cfg(all( + target_arch = "wasm32", + target_vendor = "unknown", + target_os = "unknown", + ))] + _app.world_mut().remove_resource::(); + } +} diff --git a/crates/bevy_basis_universal/src/saver.rs b/crates/bevy_image/src/basis_universal/saver.rs similarity index 78% rename from crates/bevy_basis_universal/src/saver.rs rename to crates/bevy_image/src/basis_universal/saver.rs index 6fcc36b16d8cc..8f9f728c2aabc 100644 --- a/crates/bevy_basis_universal/src/saver.rs +++ b/crates/bevy_image/src/basis_universal/saver.rs @@ -1,18 +1,15 @@ //! Asset saver and processor for Basis Universal KTX2 textures. - +use crate::{Image, ImageLoader, ImageLoaderSettings}; use bevy_asset::{ processor::LoadTransformAndSave, saver::AssetSaver, transformer::IdentityAssetTransformer, AsyncWriteExt, }; -use bevy_image::{Image, ImageLoader}; use bevy_reflect::TypePath; use serde::{Deserialize, Serialize}; use thiserror::Error; use basisu_c_sys::extra::{BasisuEncodeError, BasisuEncoder, BasisuEncoderParams}; -use crate::loader::{BasisuLoader, BasisuLoaderSettings}; - /// Basis universal asset processor. pub type BasisuProcessor = LoadTransformAndSave, BasisuSaver>; @@ -55,7 +52,7 @@ pub enum BasisuSaverError { impl AssetSaver for BasisuSaver { type Asset = Image; type Settings = BasisuSaverSettings; - type OutputLoader = BasisuLoader; + type OutputLoader = ImageLoader; type Error = BasisuSaverError; async fn save( @@ -65,6 +62,9 @@ impl AssetSaver for BasisuSaver { settings: &Self::Settings, _asset_path: bevy_asset::AssetPath<'_>, ) -> Result<::Settings, Self::Error> { + let _span = bevy_log::info_span!("Encoding basisu texture").entered(); + let time = bevy_platform::time::Instant::now(); + let mut encoder = BasisuEncoder::new(); encoder.set_image(basisu_c_sys::extra::SourceImage { data: asset.data.as_deref().unwrap_or(&[]), @@ -72,13 +72,24 @@ impl AssetSaver for BasisuSaver { texture_view_descriptor: &asset.texture_view_descriptor, })?; let result = encoder.compress(settings.params)?; + + bevy_log::debug!( + "Encoded basisu texture, {}kb -> {}kb in {:?}", + asset.data.as_deref().unwrap_or(&[]).len() as f32 / 1000.0, + result.len() as f32 / 1000.0, + time.elapsed(), + ); + drop(_span); + writer.write_all(&result).await?; - Ok(BasisuLoaderSettings { + Ok(ImageLoaderSettings { asset_usage: asset.asset_usage, sampler: asset.sampler.clone(), - is_srgb: None, - force_transcode_target: None, + array_layout: None, + is_srgb: true, + texture_format: None, + format: crate::ImageFormatSetting::Format(crate::ImageFormat::Ktx2), }) } } diff --git a/crates/bevy_image/src/image.rs b/crates/bevy_image/src/image.rs index 4d8ba1871fd6e..23c497a3938f5 100644 --- a/crates/bevy_image/src/image.rs +++ b/crates/bevy_image/src/image.rs @@ -1,9 +1,13 @@ +#[cfg(feature = "basis_universal")] +use crate::basis_universal::BasisUniversalPlugin; use crate::ImageLoader; #[cfg(feature = "dds")] use super::dds::*; #[cfg(feature = "ktx2")] use super::ktx2::*; +#[cfg(feature = "basis_universal")] +use basisu_c_sys::extra::BasisuTranscodeError; use bevy_app::{App, Plugin}; #[cfg(not(feature = "bevy_reflect"))] use bevy_reflect::TypePath; @@ -198,6 +202,9 @@ impl ImagePlugin { impl Plugin for ImagePlugin { fn build(&self, app: &mut App) { + #[cfg(feature = "basis_universal")] + app.add_plugins(BasisUniversalPlugin); + #[cfg(feature = "exr")] app.init_asset_loader::(); @@ -219,6 +226,25 @@ impl Plugin for ImagePlugin { app.preregister_asset_loader::(ImageLoader::SUPPORTED_FILE_EXTENSIONS); } + + fn finish(&self, app: &mut App) { + if !ImageLoader::SUPPORTED_FORMATS.is_empty() { + let supported_compressed_formats = if let Some(resource) = + app.world().get_resource::() + { + resource.0 + } else { + bevy_log::warn!( + "CompressedImageFormatSupport resource not found. \ + It should either be initialized in finish() of \ + RenderPlugin, or manually if not using the RenderPlugin or the WGPU backend." + ); + CompressedImageFormats::NONE + }; + + app.register_asset_loader(ImageLoader::new(supported_compressed_formats)); + } + } } /// The format of an on-disk image asset. @@ -2066,6 +2092,10 @@ pub enum TextureError { /// Only cubemaps with six faces are supported. #[error("only cubemaps with six faces are supported")] IncompleteCubemap, + /// Basis universal transcode error. + #[cfg(feature = "basis_universal")] + #[error(transparent)] + BasisuTranscodeError(#[from] BasisuTranscodeError), } /// The type of a raw image buffer. diff --git a/crates/bevy_image/src/ktx2.rs b/crates/bevy_image/src/ktx2.rs index 4f4e0fab67e91..07b9db0322dcb 100644 --- a/crates/bevy_image/src/ktx2.rs +++ b/crates/bevy_image/src/ktx2.rs @@ -30,6 +30,21 @@ pub fn ktx2_buffer_to_image( ) -> Result { let ktx2 = ktx2::Reader::new(buffer) .map_err(|err| TextureError::InvalidData(format!("Failed to parse ktx2 file: {err:?}")))?; + + #[cfg(feature = "basis_universal")] + for (key, value) in ktx2.key_value_data() { + // Recognize if the ktx2 file is basis universal format. + // We can't use `ColorModel` because basis universal can also be standard ASTC. + const BASISU_VERSION: &[u8] = b"Basis Universal 2."; + if key == "KTXwriter" && &value[..BASISU_VERSION.len()] == BASISU_VERSION { + return crate::ktx2_basisu_buffer_to_image( + buffer, + supported_compressed_formats, + is_srgb, + ); + } + } + let Header { pixel_width: width, pixel_height: height, diff --git a/crates/bevy_image/src/lib.rs b/crates/bevy_image/src/lib.rs index cefd36192d137..2c2978bef4a9f 100644 --- a/crates/bevy_image/src/lib.rs +++ b/crates/bevy_image/src/lib.rs @@ -22,6 +22,8 @@ pub use self::image::*; mod serialized_image; #[cfg(feature = "serialize")] pub use self::serialized_image::*; +#[cfg(feature = "basis_universal")] +mod basis_universal; #[cfg(feature = "dds")] mod dds; mod dynamic_texture_atlas_builder; @@ -36,6 +38,8 @@ mod saver; mod texture_atlas; mod texture_atlas_builder; +#[cfg(feature = "basis_universal")] +pub use basis_universal::*; #[cfg(feature = "dds")] pub use dds::*; pub use dynamic_texture_atlas_builder::*; diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index cc45c3e1a2cbd..a82404068378a 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -52,8 +52,8 @@ exr = ["bevy_image/exr"] hdr = ["bevy_image/hdr"] ktx2 = ["bevy_image/ktx2"] -basis_universal = ["dep:bevy_basis_universal"] -basis_universal_saver = ["basis_universal", "bevy_basis_universal/saver"] +basis_universal = ["bevy_image/basis_universal"] +basis_universal_saver = ["basis_universal", "bevy_image/basis_universal_saver"] # Enable SPIR-V passthrough spirv_shader_passthrough = ["bevy_render/spirv_shader_passthrough"] @@ -525,7 +525,6 @@ bevy_material = { path = "../bevy_material", optional = true, version = "0.19.0- bevy_mesh = { path = "../bevy_mesh", optional = true, version = "0.19.0-dev" } bevy_camera = { path = "../bevy_camera", optional = true, version = "0.19.0-dev" } bevy_light = { path = "../bevy_light", optional = true, version = "0.19.0-dev" } -bevy_basis_universal = { path = "../bevy_basis_universal", optional = true, version = "0.19.0-dev" } bevy_input_focus = { path = "../bevy_input_focus", optional = true, version = "0.19.0-dev", default-features = false, features = [ "bevy_reflect", ] } diff --git a/crates/bevy_internal/src/lib.rs b/crates/bevy_internal/src/lib.rs index 6a48f861a7474..c9ecefbc6889c 100644 --- a/crates/bevy_internal/src/lib.rs +++ b/crates/bevy_internal/src/lib.rs @@ -27,8 +27,6 @@ pub use bevy_app as app; pub use bevy_asset as asset; #[cfg(feature = "bevy_audio")] pub use bevy_audio as audio; -#[cfg(feature = "basis_universal")] -pub use bevy_basis_universal as basis_universal; #[cfg(feature = "bevy_camera")] pub use bevy_camera as camera; #[cfg(feature = "bevy_camera_controller")] diff --git a/crates/bevy_render/src/texture/mod.rs b/crates/bevy_render/src/texture/mod.rs index dc6b93da0c734..96821b0c391ee 100644 --- a/crates/bevy_render/src/texture/mod.rs +++ b/crates/bevy_render/src/texture/mod.rs @@ -5,7 +5,7 @@ mod texture_attachment; mod texture_cache; pub use crate::render_resource::DefaultImageSampler; -use bevy_image::{CompressedImageFormatSupport, CompressedImageFormats, ImageLoader, ImagePlugin}; +use bevy_image::ImagePlugin; pub use fallback_image::*; pub use gpu_image::*; pub use manual_texture_view::*; @@ -18,9 +18,7 @@ use crate::{ RenderStartup, RenderSystems, }; use bevy_app::{App, Plugin}; -use bevy_asset::AssetApp; use bevy_ecs::prelude::*; -use bevy_log::warn; #[derive(Default)] pub struct TexturePlugin; @@ -44,19 +42,6 @@ impl Plugin for TexturePlugin { } fn finish(&self, app: &mut App) { - if !ImageLoader::SUPPORTED_FORMATS.is_empty() { - let supported_compressed_formats = if let Some(resource) = - app.world().get_resource::() - { - resource.0 - } else { - warn!("CompressedImageFormatSupport resource not found. It should either be initialized in finish() of \ - RenderPlugin, or manually if not using the RenderPlugin or the WGPU backend."); - CompressedImageFormats::NONE - }; - - app.register_asset_loader(ImageLoader::new(supported_compressed_formats)); - } let default_sampler = app.get_added_plugins::()[0] .default_sampler .clone(); From fec923049bde52c3f7b5deae6c8a96e655013b18 Mon Sep 17 00:00:00 2001 From: Luo Zhihao Date: Tue, 7 Apr 2026 00:22:27 +0800 Subject: [PATCH 11/16] fmt --- crates/bevy_image/Cargo.toml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/bevy_image/Cargo.toml b/crates/bevy_image/Cargo.toml index 166be9751a355..e4ed6cb01eecf 100644 --- a/crates/bevy_image/Cargo.toml +++ b/crates/bevy_image/Cargo.toml @@ -43,7 +43,7 @@ zstd = [] zstd_rust = ["zstd", "dep:ruzstd"] # Binding to zstd C implementation (faster) zstd_c = ["zstd", "dep:zstd"] -basis_universal = ["ktx2","dep:basisu_c_sys"] +basis_universal = ["ktx2", "dep:basisu_c_sys"] basis_universal_saver = ["basis_universal", "basisu_c_sys/encoder"] [dependencies] @@ -83,7 +83,10 @@ zstd = { version = "0.13.3", optional = true } ruzstd = { version = "0.8.0", optional = true } tracing = { version = "0.1", default-features = false, features = ["std"] } half = { version = "2.4.1" } -basisu_c_sys = { version = "0.6.1", default-features = false, features = ["extra","serde"], optional = true } +basisu_c_sys = { version = "0.6.1", default-features = false, features = [ + "extra", + "serde", +], optional = true } bevy_log = { version = "0.19.0-dev", path = "../bevy_log", default-features = false } bevy_tasks = { version = "0.19.0-dev", path = "../bevy_tasks", default-features = false } From 5bacce69a0167ef5aa6bde6006ae65631eacfded Mon Sep 17 00:00:00 2001 From: Luo Zhihao Date: Tue, 7 Apr 2026 01:19:01 +0800 Subject: [PATCH 12/16] Fix feature gating --- crates/bevy_image/src/basis_universal/mod.rs | 37 +------------------ .../bevy_image/src/basis_universal/saver.rs | 37 ++++++++++++++++++- crates/bevy_image/src/image.rs | 2 +- crates/bevy_image/src/ktx2.rs | 8 ++++ 4 files changed, 46 insertions(+), 38 deletions(-) diff --git a/crates/bevy_image/src/basis_universal/mod.rs b/crates/bevy_image/src/basis_universal/mod.rs index f9ae59ac59274..6b89a1978e401 100644 --- a/crates/bevy_image/src/basis_universal/mod.rs +++ b/crates/bevy_image/src/basis_universal/mod.rs @@ -3,7 +3,7 @@ mod saver; #[cfg(feature = "basis_universal_saver")] pub use saver::*; -use crate::{CompressedImageFormats, Image, ImageLoader, TextureError}; +use crate::{CompressedImageFormats, Image, TextureError}; use basisu_c_sys::extra::{BasisuTranscoder, SupportedTextureCompression}; use bevy_app::{App, Plugin}; #[cfg(all( @@ -93,41 +93,6 @@ pub fn ktx2_basisu_buffer_to_image( /// Any bassiu encoding or transcoding will fail before this plugin is initialized. pub struct BasisUniversalPlugin; -/// Provides basis universal saver and asset processor -#[cfg(feature = "basis_universal_saver")] -pub struct BasisUniversalSaverPlugin { - /// The file extensions handled by the basisu asset processor. - /// - /// Default is [`ImageLoader::SUPPORTED_FILE_EXTENSIONS`] except ktx2 and .dds. - pub processor_extensions: Vec, -} - -impl Default for BasisUniversalSaverPlugin { - fn default() -> Self { - Self { - processor_extensions: ImageLoader::SUPPORTED_FILE_EXTENSIONS - .iter() - .filter(|s| !["ktx2", "dds"].contains(s)) - .map(ToString::to_string) - .collect(), - } - } -} - -impl Plugin for BasisUniversalSaverPlugin { - fn build(&self, app: &mut App) { - if let Some(asset_processor) = app - .world() - .get_resource::() - { - asset_processor.register_processor::(BasisuSaver.into()); - for ext in &self.processor_extensions { - asset_processor.set_default_processor::(ext.as_str()); - } - } - } -} - #[cfg(all( target_arch = "wasm32", target_vendor = "unknown", diff --git a/crates/bevy_image/src/basis_universal/saver.rs b/crates/bevy_image/src/basis_universal/saver.rs index 8f9f728c2aabc..7c4afdb3f1afe 100644 --- a/crates/bevy_image/src/basis_universal/saver.rs +++ b/crates/bevy_image/src/basis_universal/saver.rs @@ -1,5 +1,8 @@ //! Asset saver and processor for Basis Universal KTX2 textures. use crate::{Image, ImageLoader, ImageLoaderSettings}; +use basisu_c_sys::extra::BasisuEncoder; +pub use basisu_c_sys::extra::{BasisuEncodeError, BasisuEncoderParams}; +use bevy_app::{App, Plugin}; use bevy_asset::{ processor::LoadTransformAndSave, saver::AssetSaver, transformer::IdentityAssetTransformer, AsyncWriteExt, @@ -8,7 +11,39 @@ use bevy_reflect::TypePath; use serde::{Deserialize, Serialize}; use thiserror::Error; -use basisu_c_sys::extra::{BasisuEncodeError, BasisuEncoder, BasisuEncoderParams}; +/// Provides basis universal saver and asset processor +pub struct BasisUniversalSaverPlugin { + /// The file extensions handled by the basisu asset processor. + /// + /// Default is [`ImageLoader::SUPPORTED_FILE_EXTENSIONS`] except ktx2 and .dds. + pub processor_extensions: Vec, +} + +impl Default for BasisUniversalSaverPlugin { + fn default() -> Self { + Self { + processor_extensions: ImageLoader::SUPPORTED_FILE_EXTENSIONS + .iter() + .filter(|s| !["ktx2", "dds"].contains(s)) + .map(ToString::to_string) + .collect(), + } + } +} + +impl Plugin for BasisUniversalSaverPlugin { + fn build(&self, app: &mut App) { + if let Some(asset_processor) = app + .world() + .get_resource::() + { + asset_processor.register_processor::(BasisuSaver.into()); + for ext in &self.processor_extensions { + asset_processor.set_default_processor::(ext.as_str()); + } + } + } +} /// Basis universal asset processor. pub type BasisuProcessor = diff --git a/crates/bevy_image/src/image.rs b/crates/bevy_image/src/image.rs index 23c497a3938f5..cb50424226ae0 100644 --- a/crates/bevy_image/src/image.rs +++ b/crates/bevy_image/src/image.rs @@ -7,7 +7,7 @@ use super::dds::*; #[cfg(feature = "ktx2")] use super::ktx2::*; #[cfg(feature = "basis_universal")] -use basisu_c_sys::extra::BasisuTranscodeError; +pub use basisu_c_sys::extra::BasisuTranscodeError; use bevy_app::{App, Plugin}; #[cfg(not(feature = "bevy_reflect"))] use bevy_reflect::TypePath; diff --git a/crates/bevy_image/src/ktx2.rs b/crates/bevy_image/src/ktx2.rs index 07b9db0322dcb..c47d8af0b62be 100644 --- a/crates/bevy_image/src/ktx2.rs +++ b/crates/bevy_image/src/ktx2.rs @@ -61,6 +61,14 @@ pub fn ktx2_buffer_to_image( // Handle supercompression let mut levels: Vec>; + + #[cfg_attr( + not(any(feature = "flate2", feature = "zstd_rust", feature = "zstd_c")), + expect( + clippy::redundant_else, + reason = "else block is redundant when flate2, zstd_rust and zstd_c are all disabled" + ) + )] if let Some(supercompression_scheme) = supercompression_scheme { match supercompression_scheme { #[cfg(feature = "flate2")] From 0d94e7cc2eb5de49709e0cb44b7c8613cb342e8f Mon Sep 17 00:00:00 2001 From: Luo Zhihao Date: Tue, 7 Apr 2026 01:20:47 +0800 Subject: [PATCH 13/16] Rename to `BasisUniversalProcessorPlugin` --- crates/bevy_image/src/basis_universal/saver.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/bevy_image/src/basis_universal/saver.rs b/crates/bevy_image/src/basis_universal/saver.rs index 7c4afdb3f1afe..1d1857c39d198 100644 --- a/crates/bevy_image/src/basis_universal/saver.rs +++ b/crates/bevy_image/src/basis_universal/saver.rs @@ -11,15 +11,15 @@ use bevy_reflect::TypePath; use serde::{Deserialize, Serialize}; use thiserror::Error; -/// Provides basis universal saver and asset processor -pub struct BasisUniversalSaverPlugin { +/// Provides basis universal asset processor +pub struct BasisUniversalProcessorPlugin { /// The file extensions handled by the basisu asset processor. /// /// Default is [`ImageLoader::SUPPORTED_FILE_EXTENSIONS`] except ktx2 and .dds. pub processor_extensions: Vec, } -impl Default for BasisUniversalSaverPlugin { +impl Default for BasisUniversalProcessorPlugin { fn default() -> Self { Self { processor_extensions: ImageLoader::SUPPORTED_FILE_EXTENSIONS @@ -31,7 +31,7 @@ impl Default for BasisUniversalSaverPlugin { } } -impl Plugin for BasisUniversalSaverPlugin { +impl Plugin for BasisUniversalProcessorPlugin { fn build(&self, app: &mut App) { if let Some(asset_processor) = app .world() From 221a3ee004a05d554247659ef0b3e07f17f661a7 Mon Sep 17 00:00:00 2001 From: Luo Zhihao Date: Tue, 7 Apr 2026 01:26:45 +0800 Subject: [PATCH 14/16] add migration-guide --- .../basis_universal_improve.md | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 _release-content/migration-guides/basis_universal_improve.md diff --git a/_release-content/migration-guides/basis_universal_improve.md b/_release-content/migration-guides/basis_universal_improve.md new file mode 100644 index 0000000000000..9abf28336e614 --- /dev/null +++ b/_release-content/migration-guides/basis_universal_improve.md @@ -0,0 +1,37 @@ +--- +title: "Basis Universal update and improvement" +pull_requests: [23672] +--- + +Previously bevy used [basis-universal-rs](https://github.com/aclysma/basis-universal-rs) for basis universal support, including `.basis` and ktx2 UASTC texture +loading and `CompressedImageSaver`. However it doesn't support web and uses relatively outdated Basis Universal v1.16. + +Now bevy uses [`basisu_c_sys`](https://docs.rs/basisu_c_sys/latest/basisu_c_sys) which is basis universal v2.10 and supports all the basis universal formats (ETC1S, UASTC, ASTC and XUASTC) and `wasm32-unknown-unknown` on web. + +`ImageFormat::Basis` is removed. `CompressedImageSaver` is replaced by `BasisuSaver`/`BasisuProcessor` which is not added by `ImagePlugin` automatically. Also the `basis-universal` cargo feature is renamed to `basis_universal`, `compressed_image_saver` is replaced by `basis_universal_saver`. + +If you are using `.basis` files, it's recommanded to re-compress your textures to `.ktx2` format with basisu tool. Basis universal textures will be handled as `ImageFormat::Ktx2` if `basis_universal` feature is enabled. + +To use the `BasisuProcessor`, enable `basis_universal_saver` feature and add `BasisUniversalProcessorPlugin`: + +```rs +use bevy::image::BasisUniversalProcessorPlugin; +use bevy::prelude::*; + +fn main() { + App::new() + .add_plugins(( + DefaultPlugins + .set(bevy::log::LogPlugin { + filter: "bevy_image=debug,bevy_asset=debug,wgpu=warn".to_string(), + ..Default::default() + }) + .set(AssetPlugin { + mode: AssetMode::Processed, + ..Default::default() + }), + BasisUniversalProcessorPlugin::default(), + )) + .run(); +} +``` From e21575a0cd5996de2ccf104cb0a41957180e4ed3 Mon Sep 17 00:00:00 2001 From: Luo Zhihao Date: Tue, 7 Apr 2026 01:28:23 +0800 Subject: [PATCH 15/16] typo --- _release-content/migration-guides/basis_universal_improve.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_release-content/migration-guides/basis_universal_improve.md b/_release-content/migration-guides/basis_universal_improve.md index 9abf28336e614..543bec9c46826 100644 --- a/_release-content/migration-guides/basis_universal_improve.md +++ b/_release-content/migration-guides/basis_universal_improve.md @@ -10,7 +10,7 @@ Now bevy uses [`basisu_c_sys`](https://docs.rs/basisu_c_sys/latest/basisu_c_sys) `ImageFormat::Basis` is removed. `CompressedImageSaver` is replaced by `BasisuSaver`/`BasisuProcessor` which is not added by `ImagePlugin` automatically. Also the `basis-universal` cargo feature is renamed to `basis_universal`, `compressed_image_saver` is replaced by `basis_universal_saver`. -If you are using `.basis` files, it's recommanded to re-compress your textures to `.ktx2` format with basisu tool. Basis universal textures will be handled as `ImageFormat::Ktx2` if `basis_universal` feature is enabled. +If you are using `.basis` files, it's recommended to re-compress your textures to `.ktx2` format with basisu tool. Basis universal textures will be handled as `ImageFormat::Ktx2` if `basis_universal` feature is enabled. To use the `BasisuProcessor`, enable `basis_universal_saver` feature and add `BasisUniversalProcessorPlugin`: From ae5b91fd5a826331c3edd70de7331b8609dcc22b Mon Sep 17 00:00:00 2001 From: Luo Zhihao Date: Tue, 7 Apr 2026 01:56:34 +0800 Subject: [PATCH 16/16] add default_encoder_params to BasisUniversalProcessorPlugin --- .../bevy_image/src/basis_universal/saver.rs | 50 +++++++++++-------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/crates/bevy_image/src/basis_universal/saver.rs b/crates/bevy_image/src/basis_universal/saver.rs index 1d1857c39d198..01b1326e9065b 100644 --- a/crates/bevy_image/src/basis_universal/saver.rs +++ b/crates/bevy_image/src/basis_universal/saver.rs @@ -17,6 +17,10 @@ pub struct BasisUniversalProcessorPlugin { /// /// Default is [`ImageLoader::SUPPORTED_FILE_EXTENSIONS`] except ktx2 and .dds. pub processor_extensions: Vec, + /// Default basisu encoder params. + /// See the documents and `BU_COMP_FLAGS_*` in [`basisu_c_sys`] if you want more controls, + /// like mipmap generation. + pub default_encoder_params: BasisuEncoderParams, } impl Default for BasisUniversalProcessorPlugin { @@ -27,6 +31,9 @@ impl Default for BasisUniversalProcessorPlugin { .filter(|s| !["ktx2", "dds"].contains(s)) .map(ToString::to_string) .collect(), + default_encoder_params: BasisuEncoderParams::new_with_srgb_defaults( + basisu_c_sys::BasisTextureFormat::XuastcLdr4x4, + ), } } } @@ -37,7 +44,12 @@ impl Plugin for BasisUniversalProcessorPlugin { .world() .get_resource::() { - asset_processor.register_processor::(BasisuSaver.into()); + asset_processor.register_processor::( + BasisuSaver { + default_encoder_params: self.default_encoder_params, + } + .into(), + ); for ext in &self.processor_extensions { asset_processor.set_default_processor::(ext.as_str()); } @@ -51,25 +63,18 @@ pub type BasisuProcessor = /// Basis universal texture saver. #[derive(TypePath)] -pub struct BasisuSaver; - -/// Basis universal texture saver settings. -#[derive(Debug, Clone, Copy, Serialize, Deserialize)] -pub struct BasisuSaverSettings { - /// Basisu encoder params. - /// See the `BU_COMP_FLAGS_*` in [`basisu_c_sys`] if you want more controls, +pub struct BasisuSaver { + /// Default basisu encoder params. + /// See the documents and `BU_COMP_FLAGS_*` in [`basisu_c_sys`] if you want more controls, /// like mipmap generation. - pub params: BasisuEncoderParams, + pub default_encoder_params: BasisuEncoderParams, } -impl Default for BasisuSaverSettings { - fn default() -> Self { - Self { - params: BasisuEncoderParams::new_with_srgb_defaults( - basisu_c_sys::BasisTextureFormat::XuastcLdr4x4, - ), - } - } +/// Basis universal texture saver settings. +#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)] +pub struct BasisuSaverSettings { + /// Basisu encoder params. If it's None the [`BasisuSaver::default_encoder_params`] will be used. + pub encoder_params: Option, } /// An error when encoding an image using [`BasisuSaver`]. @@ -95,7 +100,7 @@ impl AssetSaver for BasisuSaver { writer: &mut bevy_asset::io::Writer, asset: bevy_asset::saver::SavedAsset<'_, '_, Self::Asset>, settings: &Self::Settings, - _asset_path: bevy_asset::AssetPath<'_>, + asset_path: bevy_asset::AssetPath<'_>, ) -> Result<::Settings, Self::Error> { let _span = bevy_log::info_span!("Encoding basisu texture").entered(); let time = bevy_platform::time::Instant::now(); @@ -106,10 +111,15 @@ impl AssetSaver for BasisuSaver { texture_descriptor: &asset.texture_descriptor, texture_view_descriptor: &asset.texture_view_descriptor, })?; - let result = encoder.compress(settings.params)?; + let result = encoder.compress( + settings + .encoder_params + .unwrap_or(self.default_encoder_params), + )?; bevy_log::debug!( - "Encoded basisu texture, {}kb -> {}kb in {:?}", + "Encoded basisu texture \"{}\", {}kb -> {}kb in {:?}", + asset_path, asset.data.as_deref().unwrap_or(&[]).len() as f32 / 1000.0, result.len() as f32 / 1000.0, time.elapsed(),