mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-23 17:35:56 +02:00
- gpu: the test modules share one GPU_LOCK, and timing and vector_mask take it too, so one process drives one wgpu device at a time. - ui-egui: the wgpu tests in pattern_preview_canvas, adjust_preview_gpu, marquee_autoscroll and layout_perf take gpu_lock(). - affinity compound_limits: the child process selects its test by the module path (compound_limits::<name>); with --exact, the bare name matches no test and the child passes without running the check. - xtask scorecard reads crates/io/tests/it/corpus.rs. - Docs and comments name the new test paths and the --test it filters.
376 lines
18 KiB
Rust
376 lines
18 KiB
Rust
//! Writing DDS textures: every storage read back through the reader, the header fields, sizes
|
||
//! that aren't a multiple of four, the mip chain, and the block-compression error on ramps.
|
||
|
||
use photocraft_codecs::{ChannelLayout, DdsCompression, DdsOptions, EncodeOptions, FidelityWarning, Format, Image, decode, encode, fidelity_warnings_with};
|
||
|
||
const DDSD_PITCH: u32 = 0x8;
|
||
const DDSD_MIPMAPCOUNT: u32 = 0x2_0000;
|
||
const DDSD_LINEARSIZE: u32 = 0x8_0000;
|
||
const REQUIRED: u32 = 0x1007; // CAPS | HEIGHT | WIDTH | PIXELFORMAT
|
||
const DDSCAPS_COMPLEX: u32 = 0x8;
|
||
const DDSCAPS_TEXTURE: u32 = 0x1000;
|
||
const DDSCAPS_MIPMAP: u32 = 0x40_0000;
|
||
const DDPF_ALPHAPIXELS: u32 = 0x1;
|
||
const DDPF_FOURCC: u32 = 0x4;
|
||
const DDPF_RGB: u32 = 0x40;
|
||
const DDPF_LUMINANCE: u32 = 0x2_0000;
|
||
|
||
const BLOCKS: [DdsCompression; 3] = [DdsCompression::Bc1, DdsCompression::Bc2, DdsCompression::Bc3];
|
||
|
||
fn opts(compression: DdsCompression, mipmaps: bool) -> EncodeOptions {
|
||
EncodeOptions { dds: DdsOptions { compression, mipmaps }, ..Default::default() }
|
||
}
|
||
|
||
fn le32(b: &[u8], at: usize) -> u32 {
|
||
u32::from_le_bytes(b[at..at + 4].try_into().unwrap())
|
||
}
|
||
|
||
/// A ramp along one line through RGB (red up, green up half as fast, blue down) across the
|
||
/// texture, and alpha falling from opaque down it: every block's colours lie on a line, which
|
||
/// is what a colour block stores.
|
||
fn ramp(w: u32, h: u32, layout: ChannelLayout) -> Image {
|
||
let ch = layout.channels();
|
||
let at = |v: u32, n: u32| (v * 255 / (n - 1).max(1)) as u8;
|
||
let mut data = Vec::new();
|
||
for y in 0..h {
|
||
for x in 0..w {
|
||
let r = at(x, w);
|
||
let rgba = [r, 64 + r / 2, 255 - r, 255 - at(y, h)];
|
||
match ch {
|
||
1 => data.push(rgba[0]),
|
||
2 => data.extend([rgba[0], rgba[3]]),
|
||
_ => data.extend(&rgba[..ch]),
|
||
}
|
||
}
|
||
}
|
||
Image::from_u8(w, h, layout, data).unwrap()
|
||
}
|
||
|
||
/// The same RGBA image, fully opaque.
|
||
fn opaque(img: &Image) -> Image {
|
||
let data = img.data().chunks(4).flat_map(|p| [p[0], p[1], p[2], 255]).collect();
|
||
Image::from_u8(img.width(), img.height(), ChannelLayout::Rgba, data).unwrap()
|
||
}
|
||
|
||
/// Largest difference in channel `c` (of four) between RGBA images.
|
||
fn max_diff_channel(a: &Image, b: &Image, c: usize) -> u8 {
|
||
a.data().chunks(4).zip(b.data().chunks(4)).map(|(x, y)| x[c].abs_diff(y[c])).max().unwrap_or(0)
|
||
}
|
||
|
||
/// Largest colour difference between RGBA images where `b` is visible (BC1 turns texels under
|
||
/// half opacity into transparent black).
|
||
fn max_colour_diff(a: &Image, b: &Image) -> u8 {
|
||
a.data().chunks(4).zip(b.data().chunks(4)).filter(|(_, y)| y[3] != 0).flat_map(|(x, y)| (0..3).map(|c| x[c].abs_diff(y[c]))).max().unwrap_or(0)
|
||
}
|
||
|
||
/// A 2D gradient: red across, green down and blue along the diagonal, opaque.
|
||
fn gradient(w: u32, h: u32) -> Image {
|
||
let data = (0..h).flat_map(|y| (0..w).flat_map(move |x| [(x * 4).min(255) as u8, (y * 4).min(255) as u8, ((x + y) * 2).min(255) as u8, 255])).collect();
|
||
Image::from_u8(w, h, ChannelLayout::Rgba, data).unwrap()
|
||
}
|
||
|
||
/// PSNR of the colour (not the alpha) of two RGBA images.
|
||
fn psnr(a: &Image, b: &Image) -> f64 {
|
||
let rgb = |i: &Image| i.data().chunks(4).flat_map(|p| [p[0], p[1], p[2]]).map(f64::from).collect::<Vec<_>>();
|
||
let (a, b) = (rgb(a), rgb(b));
|
||
let mse = a.iter().zip(&b).map(|(x, y)| (x - y).powi(2)).sum::<f64>() / a.len() as f64;
|
||
if mse == 0.0 { f64::INFINITY } else { 10.0 * (255.0f64.powi(2) / mse).log10() }
|
||
}
|
||
|
||
/// Bytes of one level as a reader computes them from the header's pixel format.
|
||
fn level_bytes(file: &[u8], w: usize, h: usize) -> usize {
|
||
if le32(file, 80) & DDPF_FOURCC != 0 {
|
||
let block = if &file[84..88] == b"DXT1" { 8 } else { 16 };
|
||
w.div_ceil(4) * h.div_ceil(4) * block
|
||
} else {
|
||
w * h * le32(file, 88) as usize / 8
|
||
}
|
||
}
|
||
|
||
/// Mip level `i` of a written texture, cut out as a single-level texture and decoded.
|
||
fn level(file: &[u8], i: u32) -> Image {
|
||
let (w, h) = (le32(file, 16) as usize, le32(file, 12) as usize);
|
||
let size = |i: u32| ((w >> i).max(1), (h >> i).max(1));
|
||
let start = 128 + (0..i).map(|j| level_bytes(file, size(j).0, size(j).1)).sum::<usize>();
|
||
let (lw, lh) = size(i);
|
||
let mut b = file[..128].to_vec();
|
||
b[12..16].copy_from_slice(&(lh as u32).to_le_bytes());
|
||
b[16..20].copy_from_slice(&(lw as u32).to_le_bytes());
|
||
b[28..32].copy_from_slice(&1u32.to_le_bytes());
|
||
// The pitch field is a row stride for uncompressed textures and the level size for BC.
|
||
let pitch = if le32(file, 80) & DDPF_FOURCC != 0 { level_bytes(file, lw, lh) } else { lw * le32(file, 88) as usize / 8 };
|
||
b[20..24].copy_from_slice(&(pitch as u32).to_le_bytes());
|
||
b.extend(&file[start..start + level_bytes(file, lw, lh)]);
|
||
decode(&b).unwrap()
|
||
}
|
||
|
||
#[test]
|
||
fn uncompressed_textures_round_trip_every_layout_exactly() {
|
||
for (layout, pf_flags, bits, masks) in [
|
||
(ChannelLayout::Gray, DDPF_LUMINANCE, 8, [0xff, 0, 0, 0]),
|
||
(ChannelLayout::GrayA, DDPF_LUMINANCE | DDPF_ALPHAPIXELS, 16, [0xff, 0, 0, 0xff00]),
|
||
(ChannelLayout::Rgb, DDPF_RGB, 32, [0xff_0000, 0xff00, 0xff, 0]),
|
||
(ChannelLayout::Rgba, DDPF_RGB | DDPF_ALPHAPIXELS, 32, [0xff_0000, 0xff00, 0xff, 0xff00_0000]),
|
||
] {
|
||
for (w, h) in [(1, 1), (5, 3), (17, 9)] {
|
||
let img = ramp(w, h, layout);
|
||
let b = encode(&img, Format::Dds, &EncodeOptions::default()).unwrap();
|
||
let back = decode(&b).unwrap();
|
||
assert_eq!(back.layout(), layout, "{layout:?} {w}x{h}");
|
||
assert_eq!(back.data(), img.data(), "{layout:?} {w}x{h}");
|
||
|
||
assert_eq!((&b[..4], le32(&b, 4), le32(&b, 76)), (&b"DDS "[..], 124, 32));
|
||
assert_eq!(le32(&b, 8), REQUIRED | DDSD_PITCH, "pitch, not linear size, and no mip count");
|
||
assert_eq!((le32(&b, 12), le32(&b, 16)), (h, w));
|
||
assert_eq!(le32(&b, 20), w * bits / 8, "pitch of {layout:?} {w}x{h}");
|
||
assert_eq!((le32(&b, 28), le32(&b, 108)), (1, DDSCAPS_TEXTURE));
|
||
assert_eq!((le32(&b, 80), le32(&b, 88)), (pf_flags, bits));
|
||
assert_eq!([le32(&b, 92), le32(&b, 96), le32(&b, 100), le32(&b, 104)], masks);
|
||
assert_eq!(b.len(), 128 + (w * h * bits / 8) as usize);
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn block_compressed_textures_round_trip_through_the_reader() {
|
||
for (compression, fourcc, block) in [(DdsCompression::Bc1, b"DXT1", 8), (DdsCompression::Bc2, b"DXT3", 16), (DdsCompression::Bc3, b"DXT5", 16)] {
|
||
// Not multiples of four, and the 1×1 and 2×2 textures smaller than one block.
|
||
for (w, h) in [(1, 1), (2, 2), (4, 4), (5, 3), (7, 13), (33, 17)] {
|
||
let img = ramp(w, h, ChannelLayout::Rgba);
|
||
let b = encode(&img, Format::Dds, &opts(compression, false)).unwrap();
|
||
let linear = w.div_ceil(4) * h.div_ceil(4) * block;
|
||
assert_eq!(&b[84..88], fourcc);
|
||
assert_eq!((le32(&b, 80), le32(&b, 88)), (DDPF_FOURCC, 0));
|
||
assert_eq!(le32(&b, 8), REQUIRED | DDSD_LINEARSIZE, "linear size, not pitch");
|
||
assert_eq!(le32(&b, 20), linear, "{compression:?} {w}x{h}: the top level's size");
|
||
assert_eq!(b.len(), 128 + linear as usize);
|
||
|
||
let back = decode(&b).unwrap();
|
||
assert_eq!((back.dimensions(), back.layout()), ((w, h), ChannelLayout::Rgba));
|
||
// Steep ramps across tiny textures put three colours where a block interpolates at
|
||
// thirds, so this bound is loose; it still catches swapped channels or misplaced
|
||
// blocks. `block_compression_error_on_gradients_is_bounded` is the tight one. A BC1
|
||
// block with transparent texels has only three colours, so BC1's colours are
|
||
// checked on the opaque ramp.
|
||
let (src, back_colour) = if compression == DdsCompression::Bc1 {
|
||
let o = opaque(&img);
|
||
let back = decode(&encode(&o, Format::Dds, &opts(compression, false)).unwrap()).unwrap();
|
||
(o, back)
|
||
} else {
|
||
(img.clone(), back.clone())
|
||
};
|
||
let colour = max_colour_diff(&src, &back_colour);
|
||
assert!(colour <= 24, "{compression:?} {w}x{h}: colour off by {colour}");
|
||
let alpha = max_diff_channel(&img, &back, 3);
|
||
match compression {
|
||
DdsCompression::Bc1 => assert!(back.data().chunks(4).zip(img.data().chunks(4)).all(|(b, i)| b[3] == if i[3] < 128 { 0 } else { 255 })),
|
||
_ => assert!(alpha <= 8, "{compression:?} {w}x{h}: alpha off by {alpha}"),
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn gray_and_rgb_sources_are_written_as_rgba_blocks() {
|
||
for layout in [ChannelLayout::Gray, ChannelLayout::GrayA, ChannelLayout::Rgb] {
|
||
for compression in BLOCKS {
|
||
let img = ramp(8, 8, layout);
|
||
let back = decode(&encode(&img, Format::Dds, &opts(compression, false)).unwrap()).unwrap();
|
||
let want = img.convert(ChannelLayout::Rgba, img.sample_type());
|
||
assert_eq!(back.layout(), ChannelLayout::Rgba);
|
||
let colour = max_colour_diff(&want, &back);
|
||
assert!(colour <= 8, "{layout:?} {compression:?}: colour off by {colour}");
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn block_compression_error_on_gradients_is_bounded() {
|
||
let (line, plane) = (ramp(64, 64, ChannelLayout::Rgba), gradient(64, 64));
|
||
for compression in BLOCKS {
|
||
let roundtrip = |img: &Image| decode(&encode(img, Format::Dds, &opts(compression, false)).unwrap()).unwrap();
|
||
let (line_back, plane_back) = (roundtrip(&line), roundtrip(&plane));
|
||
let (line_err, plane_err) = (max_colour_diff(&line, &line_back), max_colour_diff(&plane, &plane_back));
|
||
let p = psnr(&plane, &plane_back);
|
||
let alpha = max_diff_channel(&line, &line_back, 3);
|
||
// Colours on a line only lose the RGB565 rounding of the endpoints (up to 4 levels in
|
||
// red and blue). A gradient in two directions is not on a line: the spread off a
|
||
// block's best line alone limits this one to about 40 dB, and RGB565 costs a little
|
||
// more (the encoder measures 38.6 dB and 9 levels at most).
|
||
assert!(line_err <= 5, "{compression:?}: colour ramp off by {line_err}");
|
||
assert!(plane_err <= 10, "{compression:?}: 2D gradient off by {plane_err}");
|
||
assert!(p >= 38.0, "{compression:?}: 2D gradient PSNR {p:.1} dB");
|
||
match compression {
|
||
DdsCompression::Bc1 => assert!(line_back.data().chunks(4).zip(line.data().chunks(4)).all(|(b, i)| b[3] == if i[3] < 128 { 0 } else { 255 })),
|
||
// Four bits: within half a step of 17.
|
||
DdsCompression::Bc2 => assert!(alpha <= 8, "BC2 alpha off by {alpha}"),
|
||
// Eight values across a block's few levels of alpha.
|
||
_ => assert!(alpha <= 2, "BC3 alpha off by {alpha}"),
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn solid_and_cutout_textures_keep_their_values() {
|
||
// RGB565-exact colours and BC2's 4-bit alpha steps come back exactly.
|
||
let solid = Image::from_u8(6, 5, ChannelLayout::Rgba, [255, 0, 255, 170].repeat(30)).unwrap();
|
||
for compression in BLOCKS {
|
||
let back = decode(&encode(&solid, Format::Dds, &opts(compression, false)).unwrap()).unwrap();
|
||
let want = if compression == DdsCompression::Bc1 { [255, 0, 255, 255] } else { [255, 0, 255, 170] };
|
||
assert!(back.data().chunks(4).all(|p| p == want), "{compression:?}: {:?}", &back.data()[..4]);
|
||
}
|
||
// A cutout: BC1's transparent texels decode as transparent black, the rest stay opaque.
|
||
let cutout: Vec<u8> = (0..36).flat_map(|i| if (i / 6 + i % 6) % 2 == 0 { [200, 40, 40, 255] } else { [10, 10, 10, 0] }).collect();
|
||
let cutout = Image::from_u8(6, 6, ChannelLayout::Rgba, cutout).unwrap();
|
||
for compression in BLOCKS {
|
||
let back = decode(&encode(&cutout, Format::Dds, &opts(compression, false)).unwrap()).unwrap();
|
||
assert_eq!(max_diff_channel(&cutout, &back, 3), 0, "{compression:?}");
|
||
if compression == DdsCompression::Bc1 {
|
||
assert!(back.data().chunks(4).filter(|p| p[3] == 0).all(|p| p == [0, 0, 0, 0]));
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn mipmaps_write_the_full_chain_box_filtered() {
|
||
// 16×8 halves to 8×4, 4×2, 2×1 and 1×1: five levels.
|
||
let img = ramp(16, 8, ChannelLayout::Rgba);
|
||
let b = encode(&img, Format::Dds, &opts(DdsCompression::None, true)).unwrap();
|
||
assert_eq!(le32(&b, 8), REQUIRED | DDSD_PITCH | DDSD_MIPMAPCOUNT);
|
||
assert_eq!(le32(&b, 28), 5);
|
||
assert_eq!(le32(&b, 108), DDSCAPS_TEXTURE | DDSCAPS_COMPLEX | DDSCAPS_MIPMAP);
|
||
assert_eq!(b.len(), 128 + 4 * (16 * 8 + 8 * 4 + 4 * 2 + 2 + 1));
|
||
assert_eq!(decode(&b).unwrap().data(), img.data(), "the reader still reads the top level");
|
||
let mut above = img.clone();
|
||
for i in 1..5 {
|
||
let got = level(&b, i);
|
||
assert_eq!(got.dimensions(), ((16 >> i).max(1), (8 >> i).max(1)));
|
||
let want = box_filter(&above);
|
||
assert_eq!(got.data(), want.data(), "level {i}");
|
||
above = got;
|
||
}
|
||
// 1×1 has a single level.
|
||
let one = encode(&ramp(1, 1, ChannelLayout::Rgba), Format::Dds, &opts(DdsCompression::None, true)).unwrap();
|
||
assert_eq!((le32(&one, 28), le32(&one, 8) & DDSD_MIPMAPCOUNT), (1, 0));
|
||
}
|
||
|
||
/// The documented filter, written independently: each pixel averages the 2×2 pixels it covers,
|
||
/// and the last column and row of an odd-sized level take in the leftover pixel too.
|
||
fn box_filter(src: &Image) -> Image {
|
||
let (w, h) = (src.width() as usize, src.height() as usize);
|
||
let (nw, nh) = ((w / 2).max(1), (h / 2).max(1));
|
||
let ch = src.layout().channels();
|
||
let span = |i: usize, n: usize, size: usize| (2 * i, if i + 1 == n { size } else { 2 * i + 2 });
|
||
let mut out = Vec::new();
|
||
for y in 0..nh {
|
||
for x in 0..nw {
|
||
let ((x0, x1), (y0, y1)) = (span(x, nw, w), span(y, nh, h));
|
||
for c in 0..ch {
|
||
let values: Vec<u32> =
|
||
(y0..y1).flat_map(|sy| (x0..x1).map(move |sx| (sy, sx))).map(|(sy, sx)| u32::from(src.data()[(sy * w + sx) * ch + c])).collect();
|
||
let n = values.len() as u32;
|
||
out.push(((values.iter().sum::<u32>() + n / 2) / n) as u8);
|
||
}
|
||
}
|
||
}
|
||
Image::from_u8(nw as u32, nh as u32, src.layout(), out).unwrap()
|
||
}
|
||
|
||
#[test]
|
||
fn odd_sized_mip_chains_keep_every_pixel() {
|
||
// 5×3 → 2×1 → 1×1. The 2×1 level's right pixel averages columns 2–4 of all three rows.
|
||
let img = ramp(5, 3, ChannelLayout::Gray);
|
||
let b = encode(&img, Format::Dds, &opts(DdsCompression::None, true)).unwrap();
|
||
assert_eq!(le32(&b, 28), 3);
|
||
assert_eq!(b.len(), 128 + 5 * 3 + 2 + 1);
|
||
let second = level(&b, 1);
|
||
assert_eq!(second.data(), box_filter(&img).data());
|
||
let px = |x: usize, y: usize| u32::from(img.data()[y * 5 + x]);
|
||
let right: u32 = (0..3).flat_map(|y| (2..5).map(move |x| (x, y))).map(|(x, y)| px(x, y)).sum();
|
||
assert_eq!(u32::from(second.data()[1]), (right + 4) / 9);
|
||
}
|
||
|
||
#[test]
|
||
fn block_compressed_mip_chains_have_a_block_per_small_level() {
|
||
for compression in BLOCKS {
|
||
let block = if compression == DdsCompression::Bc1 { 8 } else { 16 };
|
||
let img = ramp(12, 6, ChannelLayout::Rgba);
|
||
let b = encode(&img, Format::Dds, &opts(compression, true)).unwrap();
|
||
// 12×6, 6×3, 3×1, 1×1: 3×2, 2×1, 1 and 1 blocks.
|
||
assert_eq!(le32(&b, 28), 4);
|
||
assert_eq!(le32(&b, 8), REQUIRED | DDSD_LINEARSIZE | DDSD_MIPMAPCOUNT);
|
||
assert_eq!(le32(&b, 20), 6 * block);
|
||
assert_eq!(b.len(), 128 + (6 + 2 + 1 + 1) * block as usize);
|
||
for i in 0..4 {
|
||
let got = level(&b, i);
|
||
assert_eq!(got.dimensions(), ((12 >> i).max(1), (6 >> i).max(1)), "{compression:?} level {i}");
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn strips_and_tiny_textures_encode_with_every_option() {
|
||
for (w, h) in [(1, 1), (1, 9), (9, 1), (2, 3), (3, 2)] {
|
||
for compression in DdsCompression::ALL {
|
||
for mipmaps in [false, true] {
|
||
let img = ramp(w, h, ChannelLayout::Rgba);
|
||
let b = encode(&img, Format::Dds, &opts(compression, mipmaps)).unwrap_or_else(|e| panic!("{w}x{h} {compression:?} {mipmaps}: {e}"));
|
||
assert_eq!(decode(&b).unwrap().dimensions(), (w, h));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn fidelity_warnings_follow_the_compression() {
|
||
let translucent = ramp(8, 8, ChannelLayout::Rgba);
|
||
let warns = |c| fidelity_warnings_with(&translucent, Format::Dds, &opts(c, false));
|
||
assert!(warns(DdsCompression::None).is_empty(), "uncompressed RGBA loses nothing");
|
||
for c in BLOCKS {
|
||
assert!(warns(c).contains(&FidelityWarning::LossyCompression), "{c:?}");
|
||
}
|
||
assert!(warns(DdsCompression::Bc1).contains(&FidelityWarning::AlphaBinarized));
|
||
assert!(!warns(DdsCompression::Bc3).contains(&FidelityWarning::AlphaBinarized));
|
||
}
|
||
|
||
#[test]
|
||
fn compression_names_round_trip() {
|
||
for c in DdsCompression::ALL {
|
||
assert_eq!(DdsCompression::from_name(c.name()), Some(c));
|
||
}
|
||
assert_eq!(DdsCompression::from_name("DXT5"), Some(DdsCompression::Bc3));
|
||
assert_eq!(DdsCompression::from_name("dxt1"), Some(DdsCompression::Bc1));
|
||
for bad in ["", "bc7", "dxt2", "uncompressed "] {
|
||
assert_eq!(DdsCompression::from_name(bad), None, "{bad:?}");
|
||
}
|
||
}
|
||
|
||
/// Encode timings on a synthetic ~30 MP texture (6336×4752). Ignored by default; run in release:
|
||
///
|
||
/// ```sh
|
||
/// cargo test --release -p photocraft-codecs --test it -- dds_write:: --ignored --nocapture
|
||
/// ```
|
||
#[test]
|
||
#[ignore]
|
||
fn dds_encode_timings_on_30_mp() {
|
||
let (w, h) = (6336u32, 4752u32);
|
||
let mut s = 0x1234_5678u32;
|
||
let mut data = Vec::with_capacity(w as usize * h as usize * 4);
|
||
for y in 0..h {
|
||
for x in 0..w {
|
||
s ^= s << 13;
|
||
s ^= s >> 17;
|
||
s ^= s << 5;
|
||
data.extend([(x / 25 + y / 19) as u8, (x / 31) as u8 ^ (s as u8 & 3), (y / 23) as u8, (255 - x / 25) as u8]);
|
||
}
|
||
}
|
||
let img = Image::from_u8(w, h, ChannelLayout::Rgba, data).unwrap();
|
||
for compression in DdsCompression::ALL {
|
||
for mipmaps in [false, true] {
|
||
let t = std::time::Instant::now();
|
||
let b = encode(&img, Format::Dds, &opts(compression, mipmaps)).unwrap();
|
||
eprintln!("{compression:?} mipmaps={mipmaps}: {:.0} ms, {} MB", t.elapsed().as_secs_f64() * 1e3, b.len() >> 20);
|
||
}
|
||
}
|
||
}
|