use ctt_compressonator as cmp;
use crate::alpha::AlphaMode;
use crate::encoders::Quality;
use crate::encoders::backend::Encoder;
use crate::error::{Error, Result};
use crate::surface::{ColorSpace, Surface};
use crate::vk_format::FormatExt as _;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum AmdUsage {
#[default]
Color,
NormalMap,
Data,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum AmdBc7Alpha {
#[default]
Auto,
Opaque,
Full,
Restricted,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct AmdSettings {
pub usage: AmdUsage,
pub channel_weights: Option<[f32; 3]>,
pub bc7_alpha: AmdBc7Alpha,
pub bc7_mode_mask: Option<u8>,
pub bc6h_mode_mask: Option<u32>,
}
pub struct CompressonatorEncoder;
impl Encoder for CompressonatorEncoder {
type Settings = AmdSettings;
fn name() -> &'static str {
"amd"
}
fn supported_formats() -> &'static [ktx2::Format] {
&[
ktx2::Format::BC1_RGBA_UNORM_BLOCK,
ktx2::Format::BC2_UNORM_BLOCK,
ktx2::Format::BC3_UNORM_BLOCK,
ktx2::Format::BC4_UNORM_BLOCK,
ktx2::Format::BC4_SNORM_BLOCK,
ktx2::Format::BC5_UNORM_BLOCK,
ktx2::Format::BC5_SNORM_BLOCK,
ktx2::Format::BC6H_UFLOAT_BLOCK,
ktx2::Format::BC6H_SFLOAT_BLOCK,
ktx2::Format::BC7_UNORM_BLOCK,
]
}
fn required_input_format(format: ktx2::Format, _settings: &AmdSettings) -> ktx2::Format {
use ktx2::Format as F;
match format {
F::BC4_UNORM_BLOCK | F::BC4_SNORM_BLOCK => F::R8_UNORM,
F::BC5_UNORM_BLOCK | F::BC5_SNORM_BLOCK => F::R8G8_UNORM,
F::BC6H_UFLOAT_BLOCK | F::BC6H_SFLOAT_BLOCK => F::R16G16B16_SFLOAT,
_ => F::R8G8B8A8_UNORM,
}
}
fn compress(
surface: &Surface,
format: ktx2::Format,
quality: Quality,
settings: &AmdSettings,
) -> Result<Vec<u8>> {
let q = quality_to_float(quality);
let (base, _) = format.normalize();
let (data, width, height) = (&*surface.data, surface.width, surface.height);
let is_srgb = surface.color_space == ColorSpace::Srgb;
let weights = settings
.channel_weights
.unwrap_or_else(|| default_rgb_weights(settings.usage));
use ktx2::Format as F;
match base {
F::BC1_RGBA_UNORM_BLOCK => {
let mut opts = cmp::bc1::Options::new().map_err(cmp_err)?;
opts.set_quality(q).map_err(cmp_err)?;
opts.set_channel_weights(weights[0], weights[1], weights[2])
.map_err(cmp_err)?;
opts.set_srgb(is_srgb).map_err(cmp_err)?;
cmp::bc1::compress_blocks(data, width, height, &opts).map_err(cmp_err)
}
F::BC2_UNORM_BLOCK => {
let mut opts = cmp::bc2::Options::new().map_err(cmp_err)?;
opts.set_quality(q).map_err(cmp_err)?;
opts.set_channel_weights(weights[0], weights[1], weights[2])
.map_err(cmp_err)?;
opts.set_srgb(is_srgb).map_err(cmp_err)?;
cmp::bc2::compress_blocks(data, width, height, &opts).map_err(cmp_err)
}
F::BC3_UNORM_BLOCK => {
let mut opts = cmp::bc3::Options::new().map_err(cmp_err)?;
opts.set_quality(q).map_err(cmp_err)?;
opts.set_channel_weights(weights[0], weights[1], weights[2])
.map_err(cmp_err)?;
opts.set_srgb(is_srgb).map_err(cmp_err)?;
cmp::bc3::compress_blocks(data, width, height, &opts).map_err(cmp_err)
}
F::BC4_UNORM_BLOCK => {
let mut opts = cmp::bc4::Options::new().map_err(cmp_err)?;
opts.set_quality(q).map_err(cmp_err)?;
cmp::bc4::compress_blocks(data, width, height, &opts).map_err(cmp_err)
}
F::BC4_SNORM_BLOCK => {
let mut opts = cmp::bc4::Options::new().map_err(cmp_err)?;
opts.set_quality(q).map_err(cmp_err)?;
let src: &[i8] = bytemuck::cast_slice(data);
cmp::bc4s::compress_blocks(src, width, height, &opts).map_err(cmp_err)
}
F::BC5_UNORM_BLOCK => {
let mut opts = cmp::bc5::Options::new().map_err(cmp_err)?;
opts.set_quality(q).map_err(cmp_err)?;
cmp::bc5::compress_blocks(data, width, height, &opts).map_err(cmp_err)
}
F::BC5_SNORM_BLOCK => {
let mut opts = cmp::bc5::Options::new().map_err(cmp_err)?;
opts.set_quality(q).map_err(cmp_err)?;
let src: &[i8] = bytemuck::cast_slice(data);
cmp::bc5s::compress_blocks(src, width, height, &opts).map_err(cmp_err)
}
F::BC6H_UFLOAT_BLOCK => {
let mut opts = cmp::bc6h::Options::new().map_err(cmp_err)?;
opts.set_quality(q).map_err(cmp_err)?;
if let Some(mask) = settings.bc6h_mode_mask {
opts.set_mask(mask).map_err(cmp_err)?;
}
let src: &[u16] = bytemuck::cast_slice(data);
cmp::bc6h::compress_blocks(src, width, height, &opts).map_err(cmp_err)
}
F::BC6H_SFLOAT_BLOCK => {
let mut opts = cmp::bc6h::Options::new().map_err(cmp_err)?;
opts.set_quality(q).map_err(cmp_err)?;
opts.set_signed(true).map_err(cmp_err)?;
if let Some(mask) = settings.bc6h_mode_mask {
opts.set_mask(mask).map_err(cmp_err)?;
}
let src: &[u16] = bytemuck::cast_slice(data);
cmp::bc6h::compress_blocks(src, width, height, &opts).map_err(cmp_err)
}
F::BC7_UNORM_BLOCK => {
let mut opts = cmp::bc7::Options::new().map_err(cmp_err)?;
opts.set_quality(q).map_err(cmp_err)?;
let (image_needs_alpha, colour_restrict, alpha_restrict) =
resolve_bc7_alpha(settings.bc7_alpha, surface.alpha);
opts.set_alpha_options(image_needs_alpha, colour_restrict, alpha_restrict)
.map_err(cmp_err)?;
if let Some(mask) = settings.bc7_mode_mask {
opts.set_mask(mask).map_err(cmp_err)?;
}
cmp::bc7::compress_blocks(data, width, height, &opts).map_err(cmp_err)
}
_ => unreachable!("format not in supported_formats()"),
}
}
}
fn default_rgb_weights(usage: AmdUsage) -> [f32; 3] {
match usage {
AmdUsage::Color => [0.3086, 0.6094, 0.0820],
AmdUsage::NormalMap | AmdUsage::Data => [1.0, 1.0, 1.0],
}
}
fn resolve_bc7_alpha(choice: AmdBc7Alpha, surface_alpha: AlphaMode) -> (bool, bool, bool) {
let resolved = match choice {
AmdBc7Alpha::Auto => match surface_alpha {
AlphaMode::Opaque => AmdBc7Alpha::Opaque,
_ => AmdBc7Alpha::Full,
},
other => other,
};
match resolved {
AmdBc7Alpha::Opaque => (false, false, false),
AmdBc7Alpha::Full => (true, false, false),
AmdBc7Alpha::Restricted => (true, true, true),
AmdBc7Alpha::Auto => unreachable!(),
}
}
fn quality_to_float(quality: Quality) -> f32 {
match quality {
Quality::UltraFast => 0.01,
Quality::VeryFast => 0.05,
Quality::Fast => 0.1,
Quality::Basic => 0.5,
Quality::Slow => 0.8,
Quality::VerySlow => 1.0,
}
}
fn cmp_err(e: cmp::Error) -> Error {
Error::Compression(e.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::alpha::AlphaMode;
use crate::surface::ColorSpace;
fn solid_red(width: u32, height: u32) -> Surface {
let mut data = Vec::with_capacity((width * height * 4) as usize);
for _ in 0..(width * height) {
data.extend_from_slice(&[255, 0, 0, 255]);
}
Surface {
data,
width,
height,
depth: 1,
stride: width * 4,
slice_stride: 0,
format: ktx2::Format::R8G8B8A8_UNORM,
color_space: ColorSpace::Linear,
alpha: AlphaMode::Opaque,
}
}
#[test]
fn bc7_non_aligned_5x5() {
let surface = solid_red(5, 5);
let out = CompressonatorEncoder::compress(
&surface,
ktx2::Format::BC7_UNORM_BLOCK,
Quality::Slow,
&AmdSettings::default(),
)
.unwrap();
assert_eq!(out.len(), 4 * 16);
for chunk in out.chunks_exact(16) {
let block: [u8; 16] = chunk.try_into().unwrap();
let decoded = ctt_compressonator::bc7::decompress_block(&block).unwrap();
for pixel in decoded.chunks_exact(4) {
assert!(pixel[0] > 200, "compressonator BC7 edge R={}", pixel[0]);
}
}
}
#[test]
fn bc1_non_aligned_7x3() {
let surface = solid_red(7, 3);
let out = CompressonatorEncoder::compress(
&surface,
ktx2::Format::BC1_RGBA_UNORM_BLOCK,
Quality::UltraFast,
&AmdSettings::default(),
)
.unwrap();
assert_eq!(out.len(), 2 * 8);
for chunk in out.chunks_exact(8) {
let block: [u8; 8] = chunk.try_into().unwrap();
let decoded = ctt_compressonator::bc1::decompress_block(&block).unwrap();
for pixel in decoded.chunks_exact(4) {
assert!(pixel[0] > 200, "compressonator BC1 edge R={}", pixel[0]);
}
}
}
#[test]
fn bc7_alpha_auto_follows_surface() {
assert_eq!(
resolve_bc7_alpha(AmdBc7Alpha::Auto, AlphaMode::Opaque),
(false, false, false),
);
assert_eq!(
resolve_bc7_alpha(AmdBc7Alpha::Auto, AlphaMode::Straight),
(true, false, false),
);
assert_eq!(
resolve_bc7_alpha(AmdBc7Alpha::Auto, AlphaMode::Premultiplied),
(true, false, false),
);
}
#[test]
fn bc7_alpha_explicit_overrides_surface() {
assert_eq!(
resolve_bc7_alpha(AmdBc7Alpha::Opaque, AlphaMode::Straight),
(false, false, false),
);
assert_eq!(
resolve_bc7_alpha(AmdBc7Alpha::Restricted, AlphaMode::Opaque),
(true, true, true),
);
}
}