-
-
Notifications
You must be signed in to change notification settings - Fork 4.7k
CompressedImageSaver revamp redux #24223
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
alice-i-cecile
merged 65 commits into
bevyengine:main
from
JMS55:compressed-image-saver3
Jul 7, 2026
Merged
Changes from 55 commits
Commits
Show all changes
65 commits
Select commit
Hold shift + click to select a range
f28892c
WIP
JMS55 18cfdbb
add todo
JMS55 ef98abd
WIP
JMS55 ad31db5
Disable selector RDO for linear, based on tune_for_normal_maps
JMS55 a0d8313
WIP
JMS55 23f25b2
Misc
JMS55 bd2ed87
WIP
JMS55 fa171ae
Assert
JMS55 c2454e1
WIP
JMS55 73d0687
Merge commit '842fa5c4559303edbd1774013bc0f3d0dd529799' into compress…
JMS55 108518d
Merge commit '6dc30ca38789a1023df17649198b006e1df9cf5c' into compress…
JMS55 542d427
WIP
JMS55 a027777
Add TODO
JMS55 f1346ba
Misc
JMS55 48f9c44
Docs
JMS55 5b90771
Fix docs
JMS55 f92fb42
ASTC support
JMS55 8b721de
Docs
JMS55 f71c585
Feedback
JMS55 595ebf4
Doc tweaks
JMS55 3978fbf
Typo
JMS55 5f0f2b2
Use BC6H for rgba16float
JMS55 6e1255c
Zstd supercompression
JMS55 31f74f4
Use published versions
JMS55 41c550a
Migration
JMS55 bd7dcc2
Merge branch 'main' into compressed-image-saver2
JMS55 f036f14
Merge commit '489818930b7ec268455fe371b3c5b0fb1c0c46c3' into compress…
JMS55 cf1c4a7
Fix merge
JMS55 ff2024a
Fixes
JMS55 277c7bc
Fix grayscale conversion
JMS55 7ad83ba
Bugfix
JMS55 996d8a5
Add example
JMS55 e19d6ab
Merge branch 'main' into compressed-image-saver2
JMS55 975526e
Update release notes for CompressedImageSaver
JMS55 a8f0e0d
Build templated pages
JMS55 83470c1
Split processor up
JMS55 6fdfc8f
Clippy
JMS55 3b28d93
User-configurable alpha modes, clippy
JMS55 d7295c2
Clippy
JMS55 2a7c4bb
Clippy I hate you
JMS55 2a6d7d5
Require zstd
JMS55 ba11f5b
Alpha mode rename
JMS55 5e2b49a
Merge branch 'main' into compressed-image-saver2
JMS55 503d2bb
Merge commit '7dda2bc7fcd29e801752346c811484515226a2a9' into compress…
JMS55 1168350
Two component normal maps, sharper mip filter, fix mipmap docs
JMS55 1c95bba
Use https assets
JMS55 2906e0d
Fix ASTC normal maps
JMS55 0099046
Fix example
JMS55 d56d7ac
Fmt
JMS55 f40b021
Merge branch 'main' into compressed-image-saver3
JMS55 205f3a6
Clippy
JMS55 b26b65c
Merge branch 'compressed-image-saver3' of https://github.com/JMS55/be…
JMS55 68d86e2
Add comment
JMS55 39e40e8
Clippy
JMS55 f72bd3a
Clippy
JMS55 37f2220
Update _release-content/migration-guides/compressed_image_saver.md
JMS55 3a714f1
Update _release-content/release-notes/compressed_image_saver.md
JMS55 ef755dc
Update to ctt 0.4
JMS55 c33b5ce
Merge branch 'main' into compressed-image-saver3
JMS55 2cf3fbc
Merge branch 'main' into compressed-image-saver3
JMS55 46aa7dc
Make mipmaps optional
JMS55 4aad16c
Use ImageLoader::SUPPORTED_FILE_EXTENSIONS
JMS55 5e5dea2
Fix outdated comments
JMS55 4b75128
Revert "Use ImageLoader::SUPPORTED_FILE_EXTENSIONS"
JMS55 4cc9855
Merge branch 'main' into compressed-image-saver3
JMS55 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
20 changes: 20 additions & 0 deletions
20
_release-content/migration-guides/compressed_image_saver.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| --- | ||
| title: "`CompressedImageSaver` improvements" | ||
| pull_requests: [23567] | ||
| --- | ||
|
|
||
| The `compressed_image_saver` Cargo feature has been reworked. The old behavior (Basis Universal UASTC compression) has been moved to a new feature called `compressed_image_saver_universal`, and the `compressed_image_saver` feature now uses the `ctt` library to compress textures into BCn (desktop) or ASTC (mobile) formats instead. | ||
|
|
||
| If you were using the `compressed_image_saver` feature and want to keep the previous Basis Universal behavior, rename the feature in your `Cargo.toml`: | ||
|
|
||
| ```toml | ||
| # Before | ||
| bevy = { version = "0.18", features = ["compressed_image_saver"] } | ||
|
|
||
| # After (keeps old Basis Universal behavior) | ||
| bevy = { version = "0.19", features = ["compressed_image_saver_universal"] } | ||
| ``` | ||
|
|
||
| Alternatively, keep using `compressed_image_saver` to get the new BCn/ASTC compression backend. This produces higher-quality output and supports a wider range of input formats, but does not support all platforms in a single file like UASTC does. We recommend sticking to `compressed_image_saver_universal` when targeting the web. | ||
|
|
||
| `CompressedImageSaverError` has a new variant `CompressionFailed`. If you were matching exhaustively on this enum, add a branch for it. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| --- | ||
| title: CompressedImageSaver Improvements | ||
| authors: ["@JMS55", "@cwfitzgerald"] | ||
| pull_requests: [23567] | ||
|
JMS55 marked this conversation as resolved.
Outdated
|
||
| --- | ||
|
|
||
| Bevy's `CompressedImageSaver` asset processor has been significantly upgraded with a new compression backend powered by the [`ctt`](https://github.com/cwfitzgerald/ctt) library. | ||
|
|
||
| The new `compressed_image_saver` feature compresses textures into BCn formats (for desktop GPUs) or ASTC formats (for mobile GPUs), producing higher-quality output than the previous Basis Universal approach. The compressor automatically selects the best output format based on the input texture's channel count and type — for example, single-channel textures get BC4, HDR textures get BC6H, and standard RGBA textures get BC7. | ||
|
|
||
| Try out the new `compressed_image_saver` example to see it in action. | ||
|
|
||
| ## Automatic Mipmap Generation | ||
|
|
||
| No more manually generating mipmaps! The new backend automatically produces a full mip chain during compression. This means less aliasing when textures are viewed at a distance and better GPU cache utilization — all for free, just by running your textures through the asset processor. | ||
|
|
||
| ## ASTC for Mobile | ||
|
|
||
| To target mobile GPUs, set the `BEVY_COMPRESSED_IMAGE_SAVER_ASTC` environment variable with your desired block size (e.g. `4x4`, `6x6`, `8x8`). Larger blocks give smaller files at the cost of quality. All 14 ASTC block sizes are supported. | ||
|
|
||
| ## Basis Universal is Still Available | ||
|
|
||
| The previous Basis Universal compression behavior has been moved to the `compressed_image_saver_universal` feature. This remains the best choice for cross-platform distribution (including WebGPU), since UASTC can be transcoded at load time to whatever format the target GPU supports. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,123 @@ | ||
| use bevy_asset::{io::Writer, saver::SavedAsset, AssetPath, AsyncWriteExt}; | ||
|
|
||
| use super::{ | ||
| ctt_helpers::{ | ||
| bevy_to_ctt_alpha_mode, choose_ctt_compressed_format, wgpu_to_ctt_texture_format, | ||
| }, | ||
| CompressedImageSaverError, CompressedImageSaverSettings, | ||
| }; | ||
| use crate::{Image, ImageFormat, ImageFormatSetting, ImageLoaderSettings}; | ||
|
|
||
| #[derive(Default)] | ||
| pub struct CompressedImageSaverCtt; | ||
|
|
||
| impl CompressedImageSaverCtt { | ||
| pub async fn save( | ||
| &self, | ||
| writer: &mut Writer, | ||
| image: SavedAsset<'_, '_, Image>, | ||
| settings: &CompressedImageSaverSettings, | ||
| _asset_path: AssetPath<'_>, | ||
| ) -> Result<ImageLoaderSettings, CompressedImageSaverError> { | ||
| let Some(ref data) = image.data else { | ||
| return Err(CompressedImageSaverError::UninitializedImage); | ||
| }; | ||
|
|
||
| if image.texture_descriptor.mip_level_count != 1 { | ||
| return Err(CompressedImageSaverError::CompressionFailed( | ||
| "Expected texture_descriptor.mip_level_count to be 1".into(), | ||
| )); | ||
| } | ||
|
|
||
| let input_format = wgpu_to_ctt_texture_format(image.texture_descriptor.format)?; | ||
| let output_format = | ||
| choose_ctt_compressed_format(image.texture_descriptor.format, settings.is_normal_map)?; | ||
|
|
||
| let is_srgb = image.texture_descriptor.format.is_srgb(); | ||
| let color_space = if is_srgb { | ||
| ctt::ColorSpace::Srgb | ||
| } else { | ||
| ctt::ColorSpace::Linear | ||
| }; | ||
|
|
||
| let is_cubemap = matches!( | ||
| image.texture_view_descriptor, | ||
| Some(wgpu_types::TextureViewDescriptor { | ||
| dimension: Some(wgpu_types::TextureViewDimension::Cube), | ||
| .. | ||
| }) | ||
| ); | ||
|
|
||
| let bytes_per_pixel = | ||
| crate::TextureFormatPixelInfo::pixel_size(&image.texture_descriptor.format).map_err( | ||
| |_| CompressedImageSaverError::UnsupportedFormat(image.texture_descriptor.format), | ||
| )? as u32; | ||
|
|
||
| let surfaces = data | ||
| .chunks_exact((image.width() * image.height() * bytes_per_pixel) as usize) | ||
| .map(|layer_data| { | ||
| vec![ctt::Surface { | ||
| data: layer_data.to_vec(), | ||
| width: image.width(), | ||
| height: image.height(), | ||
| stride: image.width() * bytes_per_pixel, | ||
| format: input_format, | ||
| color_space, | ||
| alpha: bevy_to_ctt_alpha_mode(settings.input_alpha_mode), | ||
| }] | ||
| }) | ||
| .collect(); | ||
| let ctt_image = ctt::Image { | ||
| surfaces, | ||
| is_cubemap, | ||
| }; | ||
|
|
||
| // TODO: https://github.com/cwfitzgerald/ctt/issues/66 | ||
| // Convert to two-component normal map so that ASTC does not waste bits on other channels | ||
|
JMS55 marked this conversation as resolved.
Outdated
|
||
| // (BC5 does this implicitly) | ||
| let swizzle = settings.is_normal_map.then_some(ctt::Swizzle([ | ||
| ctt::SwizzleChannel::R, | ||
| ctt::SwizzleChannel::G, | ||
| ctt::SwizzleChannel::Zero, | ||
| ctt::SwizzleChannel::One, | ||
| ])); | ||
|
|
||
| let settings = ctt::ConvertSettings { | ||
| format: Some(output_format), | ||
| container: ctt::Container::ktx2_zstd(0), | ||
| quality: ctt::Quality::default(), | ||
| output_color_space: None, | ||
| output_alpha: Some(bevy_to_ctt_alpha_mode(settings.output_alpha_mode)), | ||
| swizzle, | ||
| mipmap: true, | ||
|
JMS55 marked this conversation as resolved.
Outdated
|
||
| mipmap_count: None, | ||
| mipmap_filter: if settings.is_normal_map { | ||
| // TODO: https://github.com/cwfitzgerald/ctt/issues/65 | ||
| ctt::MipmapFilter::Triangle | ||
| } else { | ||
| ctt::MipmapFilter::Lanczos3 | ||
| }, | ||
| encoder_settings: None, | ||
| registry: None, | ||
| }; | ||
|
|
||
| let output = ctt::convert(ctt_image, settings) | ||
| .map_err(|e| CompressedImageSaverError::CompressionFailed(Box::new(e)))?; | ||
| let ctt::PipelineOutput::Encoded(compressed_bytes) = &output else { | ||
| return Err(CompressedImageSaverError::CompressionFailed( | ||
| "Expected encoded output from ctt".into(), | ||
| )); | ||
| }; | ||
|
|
||
| writer.write_all(compressed_bytes).await?; | ||
|
|
||
| Ok(ImageLoaderSettings { | ||
| format: ImageFormatSetting::Format(ImageFormat::Ktx2), | ||
| is_srgb, | ||
| sampler: image.sampler.clone(), | ||
| asset_usage: image.asset_usage, | ||
| texture_format: None, | ||
| array_layout: None, | ||
| }) | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.