use ctt_etcpak as ep;
use crate::encoders::Quality;
use crate::encoders::backend::Encoder;
use crate::encoders::edge;
use crate::error::Result;
use crate::surface::Surface;
use crate::vk_format::FormatExt as _;
#[derive(Debug, Clone, Copy, Default)]
pub struct EtcpakSettings {
pub dither: bool,
pub use_heuristics: bool,
}
pub struct EtcpakEncoder;
impl Encoder for EtcpakEncoder {
type Settings = EtcpakSettings;
fn name() -> &'static str {
"etcpak"
}
fn supported_formats() -> &'static [ktx2::Format] {
&[
ktx2::Format::ETC2_R8G8B8_UNORM_BLOCK,
ktx2::Format::ETC2_R8G8B8A8_UNORM_BLOCK,
ktx2::Format::EAC_R11_UNORM_BLOCK,
ktx2::Format::EAC_R11G11_UNORM_BLOCK,
ktx2::Format::BC1_RGBA_UNORM_BLOCK,
ktx2::Format::BC3_UNORM_BLOCK,
ktx2::Format::BC4_UNORM_BLOCK,
ktx2::Format::BC5_UNORM_BLOCK,
]
}
fn required_input_format(format: ktx2::Format, _settings: &EtcpakSettings) -> ktx2::Format {
use ktx2::Format as F;
match format {
F::ETC2_R8G8B8_UNORM_BLOCK
| F::ETC2_R8G8B8A8_UNORM_BLOCK
| F::EAC_R11_UNORM_BLOCK
| F::EAC_R11G11_UNORM_BLOCK => F::B8G8R8A8_UNORM,
_ => F::R8G8B8A8_UNORM,
}
}
fn compress(
surface: &Surface,
format: ktx2::Format,
quality: Quality,
settings: &EtcpakSettings,
) -> Result<Vec<u8>> {
let (base, _) = format.normalize();
let use_heuristics = settings.use_heuristics;
let dither = settings.dither;
let bytes_per_block =
base.bytes_per_block()
.expect("supported etcpak format has known block size") as u32;
use ktx2::Format as F;
let encode_aligned = |s: &[u8], w: u32, h: u32, dst: &mut [u8]| {
let surf = ep::Surface::new(s, w, h);
match base {
F::ETC2_R8G8B8_UNORM_BLOCK => {
if dither {
ep::etc1::compress_blocks_dither_into(&surf, dst);
} else {
match quality {
Quality::UltraFast | Quality::VeryFast | Quality::Fast => {
ep::etc1::compress_blocks_into(&surf, dst);
}
_ => ep::etc2_rgb::compress_blocks_into(&surf, use_heuristics, dst),
}
}
}
F::ETC2_R8G8B8A8_UNORM_BLOCK => {
ep::etc2_rgba::compress_blocks_into(&surf, use_heuristics, dst);
}
F::EAC_R11_UNORM_BLOCK => ep::eac_r::compress_blocks_into(&surf, dst),
F::EAC_R11G11_UNORM_BLOCK => ep::eac_rg::compress_blocks_into(&surf, dst),
F::BC1_RGBA_UNORM_BLOCK => {
if dither {
ep::bc1::compress_blocks_dither_into(&surf, dst);
} else {
ep::bc1::compress_blocks_into(&surf, dst);
}
}
F::BC3_UNORM_BLOCK => ep::bc3::compress_blocks_into(&surf, dst),
F::BC4_UNORM_BLOCK => ep::bc4::compress_blocks_into(&surf, dst),
F::BC5_UNORM_BLOCK => ep::bc5::compress_blocks_into(&surf, dst),
_ => unreachable!("format not in supported_formats()"),
}
};
Ok(encode_with_edges(
&surface.data,
surface.width,
surface.height,
surface.stride,
bytes_per_block,
encode_aligned,
))
}
}
fn encode_with_edges(
data: &[u8],
width: u32,
height: u32,
stride: u32,
bytes_per_block: u32,
encode: impl Fn(&[u8], u32, u32, &mut [u8]) + Sync,
) -> Vec<u8> {
const BPP: u32 = 4;
let bx = width.div_ceil(4);
let by = height.div_ceil(4);
let block_bytes = bytes_per_block as usize;
let row_bytes = (bx * bytes_per_block) as usize;
let mut out = vec![0u8; (bx as usize) * (by as usize) * block_bytes];
if width.is_multiple_of(4) && height.is_multiple_of(4) && stride == width * BPP {
crate::encoders::parallel::for_each_row_chunk(
&mut out,
row_bytes,
|start_row, row_count, output| {
let src_start = start_row * 4 * width as usize * BPP as usize;
encode(&data[src_start..], width, (row_count * 4) as u32, output);
},
);
return out;
}
let scratch_w = bx * 4;
crate::encoders::parallel::for_each_row_chunk(
&mut out,
row_bytes,
|start_row, row_count, output| {
let mut scratch = vec![0u8; (scratch_w * 4 * BPP) as usize];
for local_row in 0..row_count {
let by_idx = (start_row + local_row) as u32;
edge::fill_clamped_block_row(
data,
width,
height,
stride,
BPP,
by_idx,
scratch_w,
&mut scratch,
);
let dst_start = local_row * row_bytes;
encode(
&scratch,
scratch_w,
4,
&mut output[dst_start..dst_start + row_bytes],
);
}
},
);
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::alpha::AlphaMode;
use crate::surface::ColorSpace;
fn solid_surface(width: u32, height: u32, pixel: [u8; 4]) -> Surface {
let mut data = Vec::with_capacity((width * height * 4) as usize);
for _ in 0..(width * height) {
data.extend_from_slice(&pixel);
}
Surface {
data,
width,
height,
depth: 1,
stride: width * 4,
slice_stride: 0,
format: ktx2::Format::R8G8B8A8_UNORM,
color_space: ColorSpace::Linear,
alpha: AlphaMode::Opaque,
}
}
#[cfg(feature = "rayon")]
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(feature = "rayon")]
#[test]
fn dithered_parallel_matches_single_worker() {
let surface = patterned(19, 13, 19 * 4 + 12);
let settings = EtcpakSettings {
dither: true,
..Default::default()
};
crate::encoders::assert_parallel_matches_serial(|| {
EtcpakEncoder::compress(
&surface,
ktx2::Format::BC1_RGBA_UNORM_BLOCK,
Quality::Fast,
&settings,
)
.unwrap()
});
}
#[test]
fn bc3_non_aligned_7x5() {
let surface = solid_surface(7, 5, [255, 0, 0, 255]);
let out = EtcpakEncoder::compress(
&surface,
ktx2::Format::BC3_UNORM_BLOCK,
Quality::Fast,
&EtcpakSettings::default(),
)
.unwrap();
assert_eq!(out.len(), 2 * 2 * 16);
let decoded = ctt_etcpak::decode::decode_bc3(&out, 8, 8);
for pixel in decoded.chunks_exact(4) {
assert!(pixel[0] > 200, "decoded BC3 edge pixel R={}", pixel[0]);
}
}
#[test]
fn etc2_rgba_non_aligned_5x5() {
let surface = solid_surface(5, 5, [255, 255, 255, 255]);
let out = EtcpakEncoder::compress(
&surface,
ktx2::Format::ETC2_R8G8B8A8_UNORM_BLOCK,
Quality::Fast,
&EtcpakSettings::default(),
)
.unwrap();
assert_eq!(out.len(), 4 * 16);
let decoded = ctt_etcpak::decode::decode_rgba(&out, 8, 8);
for pixel in decoded.chunks_exact(4) {
assert!(
pixel[0] > 240 && pixel[1] > 240 && pixel[2] > 240,
"ETC2 decode {:?}",
pixel
);
}
}
#[test]
fn aligned_zero_copy_path() {
let surface = solid_surface(8, 8, [128, 128, 128, 255]);
let out = EtcpakEncoder::compress(
&surface,
ktx2::Format::BC3_UNORM_BLOCK,
Quality::Fast,
&EtcpakSettings::default(),
)
.unwrap();
assert_eq!(out.len(), 4 * 16);
}
}