pub use ctt_astcenc as astc;
use crate::encoders::Quality;
use crate::encoders::backend::Encoder;
use crate::error::Result;
use crate::surface::{ColorSpace, Surface};
use crate::vk_format::FormatExt as _;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum NormalSwizzle {
#[default]
AstcDefault,
Bc5Compat,
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum AstcencUsage {
#[default]
Color,
NormalMap { swizzle: NormalSwizzle },
SingleChannel,
TwoChannel,
HdrRgb,
HdrRgba,
Rgbm,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct AstcencSettings {
pub usage: AstcencUsage,
pub preset: Option<astc::Preset>,
pub use_alpha_weight: bool,
pub perceptual: bool,
pub decode_unorm8: bool,
pub channel_weights: Option<[f32; 4]>,
pub rgbm_m_scale: Option<f32>,
}
const SUPPORTED_FORMATS: &[ktx2::Format] = &[
ktx2::Format::ASTC_4x4_UNORM_BLOCK,
ktx2::Format::ASTC_5x4_UNORM_BLOCK,
ktx2::Format::ASTC_5x5_UNORM_BLOCK,
ktx2::Format::ASTC_6x5_UNORM_BLOCK,
ktx2::Format::ASTC_6x6_UNORM_BLOCK,
ktx2::Format::ASTC_8x5_UNORM_BLOCK,
ktx2::Format::ASTC_8x6_UNORM_BLOCK,
ktx2::Format::ASTC_8x8_UNORM_BLOCK,
ktx2::Format::ASTC_10x5_UNORM_BLOCK,
ktx2::Format::ASTC_10x6_UNORM_BLOCK,
ktx2::Format::ASTC_10x8_UNORM_BLOCK,
ktx2::Format::ASTC_10x10_UNORM_BLOCK,
ktx2::Format::ASTC_12x10_UNORM_BLOCK,
ktx2::Format::ASTC_12x12_UNORM_BLOCK,
];
pub struct AstcencEncoder;
impl Encoder for AstcencEncoder {
type Settings = AstcencSettings;
fn name() -> &'static str {
"astcenc"
}
fn supported_formats() -> &'static [ktx2::Format] {
SUPPORTED_FORMATS
}
fn required_input_format(_format: ktx2::Format, settings: &AstcencSettings) -> ktx2::Format {
match settings.usage {
AstcencUsage::HdrRgb | AstcencUsage::HdrRgba => ktx2::Format::R16G16B16A16_SFLOAT,
_ => ktx2::Format::R8G8B8A8_UNORM,
}
}
fn compress(
surface: &Surface,
format: ktx2::Format,
quality: Quality,
settings: &AstcencSettings,
) -> Result<Vec<u8>> {
let (base, color_space) = format.normalize();
let (block_width, block_height) =
base.block_size().expect("ASTC format must have block size");
check_i32_dims(surface.width, surface.height)?;
let profile = profile_for(settings.usage, color_space);
let preset = settings.preset.unwrap_or_else(|| default_preset(quality));
let flags = flags_for(settings);
let swizzle = swizzle_for(settings.usage);
let data_type = data_type_for(settings.usage);
let mut config = astc::config_init(
profile,
block_width as u32,
block_height as u32,
1,
preset,
flags,
)
.map_err(|e| crate::error::Error::Compression(e.to_string()))?;
if let Some([r, g, b, a]) = settings.channel_weights {
config.cw_r_weight = r;
config.cw_g_weight = g;
config.cw_b_weight = b;
config.cw_a_weight = a;
}
if let Some(scale) = settings.rgbm_m_scale {
config.rgbm_m_scale = scale;
}
#[cfg(feature = "rayon")]
let ctx = astc::Context::new_parallel(&config);
#[cfg(not(feature = "rayon"))]
let ctx = astc::Context::new(&config);
let mut ctx = ctx.map_err(|e| crate::error::Error::Compression(e.to_string()))?;
let tight = surface.tight_data();
let mut data_ptr = tight.as_ptr() as *mut std::ffi::c_void;
let mut img = astc::bindings::astcenc_image {
dim_x: surface.width,
dim_y: surface.height,
dim_z: 1,
data_type,
data: &mut data_ptr,
};
let output_size = astc_output_size(
surface.width,
surface.height,
block_width as u32,
block_height as u32,
)?;
let mut output = Vec::new();
output.try_reserve_exact(output_size).map_err(|e| {
crate::error::Error::Compression(format!(
"cannot allocate {output_size} bytes for ASTC output: {e}"
))
})?;
output.resize(output_size, 0);
ctx.compress(&mut img, swizzle, &mut output)
.map_err(|e| crate::error::Error::Compression(e.to_string()))?;
Ok(output)
}
}
fn profile_for(usage: AstcencUsage, color_space: ColorSpace) -> astc::Profile {
match usage {
AstcencUsage::HdrRgb => astc::Profile::HdrRgbLdrA,
AstcencUsage::HdrRgba => astc::Profile::Hdr,
AstcencUsage::NormalMap { .. } | AstcencUsage::SingleChannel | AstcencUsage::TwoChannel => {
astc::Profile::Ldr
}
AstcencUsage::Color | AstcencUsage::Rgbm => match color_space {
ColorSpace::Srgb => astc::Profile::LdrSrgb,
ColorSpace::Linear => astc::Profile::Ldr,
},
}
}
fn flags_for(settings: &AstcencSettings) -> astc::Flags {
let mut flags = astc::Flags::SELF_DECOMPRESS_ONLY;
match settings.usage {
AstcencUsage::NormalMap { .. } => flags |= astc::Flags::MAP_NORMAL,
AstcencUsage::Rgbm => flags |= astc::Flags::MAP_RGBM,
_ => {}
}
if settings.use_alpha_weight {
flags |= astc::Flags::USE_ALPHA_WEIGHT;
}
if settings.perceptual {
flags |= astc::Flags::USE_PERCEPTUAL;
}
if settings.decode_unorm8 {
flags |= astc::Flags::USE_DECODE_UNORM8;
}
flags
}
fn swizzle_for(usage: AstcencUsage) -> astc::Swizzle {
match usage {
AstcencUsage::NormalMap {
swizzle: NormalSwizzle::AstcDefault,
} => astc::Swizzle::RRRG,
AstcencUsage::NormalMap {
swizzle: NormalSwizzle::Bc5Compat,
} => astc::Swizzle::GGGR,
AstcencUsage::SingleChannel => astc::Swizzle::RRR1,
AstcencUsage::TwoChannel => astc::Swizzle::RRRG,
AstcencUsage::Color | AstcencUsage::HdrRgb | AstcencUsage::HdrRgba | AstcencUsage::Rgbm => {
astc::Swizzle::IDENTITY
}
}
}
fn data_type_for(usage: AstcencUsage) -> astc::bindings::astcenc_type {
use astc::bindings::{astcenc_type_ASTCENC_TYPE_F16, astcenc_type_ASTCENC_TYPE_U8};
match usage {
AstcencUsage::HdrRgb | AstcencUsage::HdrRgba => astcenc_type_ASTCENC_TYPE_F16,
_ => astcenc_type_ASTCENC_TYPE_U8,
}
}
fn default_preset(quality: Quality) -> astc::Preset {
match quality {
Quality::UltraFast => astc::Preset::Fastest, Quality::VeryFast => astc::Preset::Fast, Quality::Fast => astc::Preset::Custom(35.0),
Quality::Basic => astc::Preset::Medium, Quality::Slow => astc::Preset::Thorough, Quality::VerySlow => astc::Preset::Exhaustive, }
}
fn astc_output_size(width: u32, height: u32, block_width: u32, block_height: u32) -> Result<usize> {
let blocks_x = width.div_ceil(block_width) as usize;
let blocks_y = height.div_ceil(block_height) as usize;
blocks_x
.checked_mul(blocks_y)
.and_then(|blocks| blocks.checked_mul(16))
.ok_or_else(|| {
crate::error::Error::InvalidDimensions(format!(
"ASTC output size overflows usize for {width}x{height} with \
{block_width}x{block_height} blocks"
))
})
}
fn check_i32_dims(width: u32, height: u32) -> Result<()> {
if i32::try_from(width).is_err() || i32::try_from(height).is_err() {
return Err(crate::error::Error::InvalidDimensions(format!(
"astcenc encoder requires width/height to fit in i32, got {width}x{height}"
)));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::alpha::AlphaMode;
use crate::surface::ColorSpace;
fn patterned(width: u32, height: u32, stride: u32) -> Surface {
assert!(stride >= width * 4);
let mut data = vec![0xABu8; (stride * height) as usize];
for y in 0..height {
for x in 0..width {
let off = (y * stride + x * 4) as usize;
let v = (x.wrapping_mul(7).wrapping_add(y.wrapping_mul(13)) & 0xff) as u8;
data[off..off + 4].copy_from_slice(&[
v,
v.wrapping_add(50),
v.wrapping_add(100),
255,
]);
}
}
Surface {
data,
width,
height,
depth: 1,
stride,
slice_stride: 0,
format: ktx2::Format::R8G8B8A8_UNORM,
color_space: ColorSpace::Linear,
alpha: AlphaMode::Opaque,
}
}
#[cfg(target_pointer_width = "64")]
#[test]
fn output_size_does_not_wrap() {
let size = astc_output_size(65536, 65536, 4, 4).unwrap();
assert_eq!(size, 16384usize * 16384 * 16);
assert!(size > u32::MAX as usize);
}
#[cfg(target_pointer_width = "32")]
#[test]
fn output_size_overflow_is_rejected() {
assert!(astc_output_size(65536, 65536, 4, 4).is_err());
}
#[test]
fn quality_presets_distinct_and_monotonic() {
let tiers = [
Quality::UltraFast,
Quality::VeryFast,
Quality::Fast,
Quality::Basic,
Quality::Slow,
Quality::VerySlow,
];
let values: Vec<f32> = tiers
.iter()
.map(|&q| default_preset(q).as_float())
.collect();
for pair in values.windows(2) {
assert!(
pair[0] < pair[1],
"quality presets must strictly increase, got {values:?}",
);
}
assert_eq!(default_preset(Quality::Basic).as_float(), 60.0);
}
#[test]
fn padded_stride_matches_tight() {
let tight = patterned(8, 8, 8 * 4);
let padded = patterned(8, 8, 8 * 4 + 16);
let a = AstcencEncoder::compress(
&tight,
ktx2::Format::ASTC_4x4_UNORM_BLOCK,
Quality::Fast,
&AstcencSettings::default(),
)
.unwrap();
let b = AstcencEncoder::compress(
&padded,
ktx2::Format::ASTC_4x4_UNORM_BLOCK,
Quality::Fast,
&AstcencSettings::default(),
)
.unwrap();
assert_eq!(a, b, "padded-stride encode must match tight encode");
}
#[cfg(feature = "rayon")]
#[test]
fn shared_context_matches_single_worker() {
let surface = patterned(19, 13, 19 * 4 + 12);
crate::encoders::assert_parallel_matches_serial(|| {
AstcencEncoder::compress(
&surface,
ktx2::Format::ASTC_4x4_UNORM_BLOCK,
Quality::Fast,
&AstcencSettings::default(),
)
.unwrap()
});
}
}