Skip to main content

fastpack_compress/backends/
dxt.rs

1use image::GenericImageView;
2
3use crate::{
4    compressor::{CompressInput, CompressOutput, Compressor},
5    error::CompressError,
6};
7
8// DDS header field constants.
9const DDSD_CAPS: u32 = 0x1;
10const DDSD_HEIGHT: u32 = 0x2;
11const DDSD_WIDTH: u32 = 0x4;
12const DDSD_PIXELFORMAT: u32 = 0x1000;
13const DDSD_LINEARSIZE: u32 = 0x80000;
14const DDPF_FOURCC: u32 = 0x4;
15const DDSCAPS_TEXTURE: u32 = 0x1000;
16
17/// DXT1 / BC1 compressor. No per-pixel alpha. Output is a DDS file.
18///
19/// Uses a fast min/max bounding-box endpoint selector. Quality is suitable
20/// for game atlases; it is not the highest-quality iterative cluster-fit
21/// approach, but produces valid BC1 blocks with no external dependencies.
22pub struct Dxt1Compressor;
23
24impl Compressor for Dxt1Compressor {
25    fn compress(&self, input: &CompressInput<'_>) -> Result<CompressOutput, CompressError> {
26        let (w, h) = input.image.dimensions();
27        let rgb = input.image.to_rgb8();
28        let blocks = encode_bc1(rgb.as_raw(), w, h);
29        Ok(CompressOutput {
30            data: build_dds(w, h, b"DXT1", &blocks),
31        })
32    }
33
34    fn format_id(&self) -> &'static str {
35        "dxt1"
36    }
37
38    fn file_extension(&self) -> &'static str {
39        "dds"
40    }
41}
42
43/// DXT5 / BC3 compressor. Full alpha channel. Output is a DDS file.
44///
45/// Alpha is encoded with the BC3 6-interpolation alpha block; colour is
46/// encoded with BC1 in 4-colour mode.
47pub struct Dxt5Compressor;
48
49impl Compressor for Dxt5Compressor {
50    fn compress(&self, input: &CompressInput<'_>) -> Result<CompressOutput, CompressError> {
51        let (w, h) = input.image.dimensions();
52        let rgba = input.image.to_rgba8();
53        let blocks = encode_bc3(rgba.as_raw(), w, h);
54        Ok(CompressOutput {
55            data: build_dds(w, h, b"DXT5", &blocks),
56        })
57    }
58
59    fn format_id(&self) -> &'static str {
60        "dxt5"
61    }
62
63    fn file_extension(&self) -> &'static str {
64        "dds"
65    }
66}
67
68fn write_u32(buf: &mut Vec<u8>, v: u32) {
69    buf.extend_from_slice(&v.to_le_bytes());
70}
71
72fn build_dds(w: u32, h: u32, fourcc: &[u8; 4], blocks: &[u8]) -> Vec<u8> {
73    let mut dds = Vec::with_capacity(4 + 124 + blocks.len());
74    dds.extend_from_slice(b"DDS ");
75    write_u32(&mut dds, 124); // dwSize
76    write_u32(
77        &mut dds,
78        DDSD_CAPS | DDSD_HEIGHT | DDSD_WIDTH | DDSD_PIXELFORMAT | DDSD_LINEARSIZE,
79    ); // dwFlags
80    write_u32(&mut dds, h); // dwHeight
81    write_u32(&mut dds, w); // dwWidth
82    write_u32(&mut dds, blocks.len() as u32); // dwPitchOrLinearSize
83    write_u32(&mut dds, 0); // dwDepth
84    write_u32(&mut dds, 1); // dwMipMapCount
85    for _ in 0..11 {
86        write_u32(&mut dds, 0); // dwReserved1[11]
87    }
88    // DDS_PIXELFORMAT (32 bytes)
89    write_u32(&mut dds, 32); // dwSize
90    write_u32(&mut dds, DDPF_FOURCC); // dwFlags
91    dds.extend_from_slice(fourcc); // dwFourCC
92    for _ in 0..5 {
93        write_u32(&mut dds, 0); // dwRGBBitCount, dwRBitMask, dwGBitMask, dwBBitMask, dwABitMask
94    }
95    write_u32(&mut dds, DDSCAPS_TEXTURE); // dwCaps
96    for _ in 0..4 {
97        write_u32(&mut dds, 0); // dwCaps2, dwCaps3, dwCaps4, dwReserved2
98    }
99    dds.extend_from_slice(blocks);
100    dds
101}
102
103fn encode_bc1(rgb: &[u8], w: u32, h: u32) -> Vec<u8> {
104    let bw = w.div_ceil(4);
105    let bh = h.div_ceil(4);
106    let mut out = Vec::with_capacity((bw * bh * 8) as usize);
107    for by in 0..bh {
108        for bx in 0..bw {
109            let block = extract_block_rgb(rgb, w, h, bx * 4, by * 4);
110            out.extend_from_slice(&encode_bc1_block(&block));
111        }
112    }
113    out
114}
115
116fn encode_bc3(rgba: &[u8], w: u32, h: u32) -> Vec<u8> {
117    let bw = w.div_ceil(4);
118    let bh = h.div_ceil(4);
119    let mut out = Vec::with_capacity((bw * bh * 16) as usize);
120    for by in 0..bh {
121        for bx in 0..bw {
122            let block = extract_block_rgba(rgba, w, h, bx * 4, by * 4);
123            out.extend_from_slice(&encode_bc3_alpha_block(&block));
124            let rgb_block: [[u8; 3]; 16] =
125                std::array::from_fn(|i| [block[i][0], block[i][1], block[i][2]]);
126            out.extend_from_slice(&encode_bc1_block(&rgb_block));
127        }
128    }
129    out
130}
131
132fn extract_block_rgb(rgb: &[u8], img_w: u32, img_h: u32, bx: u32, by: u32) -> [[u8; 3]; 16] {
133    let mut block = [[0u8; 3]; 16];
134    for y in 0..4u32 {
135        for x in 0..4u32 {
136            let px = (bx + x).min(img_w - 1) as usize;
137            let py = (by + y).min(img_h - 1) as usize;
138            let i = py * img_w as usize + px;
139            block[(y * 4 + x) as usize] = [rgb[i * 3], rgb[i * 3 + 1], rgb[i * 3 + 2]];
140        }
141    }
142    block
143}
144
145fn extract_block_rgba(rgba: &[u8], img_w: u32, img_h: u32, bx: u32, by: u32) -> [[u8; 4]; 16] {
146    let mut block = [[0u8; 4]; 16];
147    for y in 0..4u32 {
148        for x in 0..4u32 {
149            let px = (bx + x).min(img_w - 1) as usize;
150            let py = (by + y).min(img_h - 1) as usize;
151            let i = py * img_w as usize + px;
152            block[(y * 4 + x) as usize] = [
153                rgba[i * 4],
154                rgba[i * 4 + 1],
155                rgba[i * 4 + 2],
156                rgba[i * 4 + 3],
157            ];
158        }
159    }
160    block
161}
162
163fn rgb_to_565(r: u8, g: u8, b: u8) -> u16 {
164    ((r as u16 >> 3) << 11) | ((g as u16 >> 2) << 5) | (b as u16 >> 3)
165}
166
167fn rgb_from_565(v: u16) -> [u8; 3] {
168    let r5 = (v >> 11) as u8;
169    let g6 = ((v >> 5) & 0x3f) as u8;
170    let b5 = (v & 0x1f) as u8;
171    [
172        (r5 << 3) | (r5 >> 2),
173        (g6 << 2) | (g6 >> 4),
174        (b5 << 3) | (b5 >> 2),
175    ]
176}
177
178fn color_dist_sq(a: [u8; 3], b: [u8; 3]) -> u32 {
179    let dr = a[0] as i32 - b[0] as i32;
180    let dg = a[1] as i32 - b[1] as i32;
181    let db = a[2] as i32 - b[2] as i32;
182    (dr * dr + dg * dg + db * db) as u32
183}
184
185fn encode_bc1_block(pixels: &[[u8; 3]; 16]) -> [u8; 8] {
186    let (rmin, rmax, gmin, gmax, bmin, bmax) = pixels.iter().fold(
187        (255u8, 0u8, 255u8, 0u8, 255u8, 0u8),
188        |(rn, rx, gn, gx, bn, bx), p| {
189            (
190                rn.min(p[0]),
191                rx.max(p[0]),
192                gn.min(p[1]),
193                gx.max(p[1]),
194                bn.min(p[2]),
195                bx.max(p[2]),
196            )
197        },
198    );
199
200    let mut c0 = rgb_to_565(rmax, gmax, bmax);
201    let mut c1 = rgb_to_565(rmin, gmin, bmin);
202
203    // 4-colour mode requires c0 > c1 as raw u16 values.
204    if c0 < c1 {
205        std::mem::swap(&mut c0, &mut c1);
206    } else if c0 == c1 {
207        // Degenerate: all pixels the same colour. Adjust to force 4-colour mode
208        // so index 3 never resolves to transparent.
209        if c0 > 0 {
210            c1 -= 1;
211        } else {
212            c0 += 1;
213        }
214    }
215
216    let col0 = rgb_from_565(c0);
217    let col1 = rgb_from_565(c1);
218    let col2 = [
219        ((2 * col0[0] as u32 + col1[0] as u32 + 1) / 3) as u8,
220        ((2 * col0[1] as u32 + col1[1] as u32 + 1) / 3) as u8,
221        ((2 * col0[2] as u32 + col1[2] as u32 + 1) / 3) as u8,
222    ];
223    let col3 = [
224        ((col0[0] as u32 + 2 * col1[0] as u32 + 1) / 3) as u8,
225        ((col0[1] as u32 + 2 * col1[1] as u32 + 1) / 3) as u8,
226        ((col0[2] as u32 + 2 * col1[2] as u32 + 1) / 3) as u8,
227    ];
228
229    let mut indices = 0u32;
230    for (i, p) in pixels.iter().enumerate() {
231        let d0 = color_dist_sq(*p, col0);
232        let d1 = color_dist_sq(*p, col1);
233        let d2 = color_dist_sq(*p, col2);
234        let d3 = color_dist_sq(*p, col3);
235        let idx = if d0 <= d1 && d0 <= d2 && d0 <= d3 {
236            0u32
237        } else if d1 <= d2 && d1 <= d3 {
238            1
239        } else if d2 <= d3 {
240            2
241        } else {
242            3
243        };
244        indices |= idx << (i * 2);
245    }
246
247    let mut out = [0u8; 8];
248    out[0..2].copy_from_slice(&c0.to_le_bytes());
249    out[2..4].copy_from_slice(&c1.to_le_bytes());
250    out[4..8].copy_from_slice(&indices.to_le_bytes());
251    out
252}
253
254fn encode_bc3_alpha_block(pixels: &[[u8; 4]; 16]) -> [u8; 8] {
255    let a0 = pixels.iter().map(|p| p[3]).max().unwrap_or(255);
256    let a1 = pixels.iter().map(|p| p[3]).min().unwrap_or(0);
257
258    // 6-interpolation mode (a0 > a1): 6 evenly-spaced alphas between a0 and a1.
259    let refs: [u8; 8] = if a0 > a1 {
260        [
261            a0,
262            a1,
263            ((6 * a0 as u16 + a1 as u16 + 3) / 7) as u8,
264            ((5 * a0 as u16 + 2 * a1 as u16 + 3) / 7) as u8,
265            ((4 * a0 as u16 + 3 * a1 as u16 + 3) / 7) as u8,
266            ((3 * a0 as u16 + 4 * a1 as u16 + 3) / 7) as u8,
267            ((2 * a0 as u16 + 5 * a1 as u16 + 3) / 7) as u8,
268            ((a0 as u16 + 6 * a1 as u16 + 3) / 7) as u8,
269        ]
270    } else {
271        // All pixels share the same alpha; index 0 maps to a0 for all.
272        [a0; 8]
273    };
274
275    let mut bits: u64 = 0;
276    for (i, p) in pixels.iter().enumerate() {
277        let a = p[3];
278        let idx = refs
279            .iter()
280            .enumerate()
281            .min_by_key(|&(_, r)| (*r as i16 - a as i16).unsigned_abs())
282            .map(|(idx, _)| idx)
283            .unwrap_or(0) as u64;
284        bits |= idx << (i * 3);
285    }
286
287    let mut out = [0u8; 8];
288    out[0] = a0;
289    out[1] = a1;
290    out[2..8].copy_from_slice(&bits.to_le_bytes()[0..6]);
291    out
292}
293
294#[cfg(test)]
295mod tests {
296    use super::*;
297
298    #[test]
299    fn bc1_block_solid_colour() {
300        let pixels: [[u8; 3]; 16] = [[255, 0, 0]; 16];
301        let block = encode_bc1_block(&pixels);
302        // c0 must be > c1 (4-colour mode).
303        let c0 = u16::from_le_bytes([block[0], block[1]]);
304        let c1 = u16::from_le_bytes([block[2], block[3]]);
305        assert!(c0 > c1, "4-colour mode: c0({c0}) must be > c1({c1})");
306    }
307
308    #[test]
309    fn bc3_alpha_block_full_range() {
310        let mut pixels = [[0u8; 4]; 16];
311        for (i, p) in pixels.iter_mut().enumerate() {
312            p[3] = (i * 17) as u8;
313        }
314        let block = encode_bc3_alpha_block(&pixels);
315        assert_eq!(block[0], 255); // a0 = max
316        assert_eq!(block[1], 0); // a1 = min
317    }
318
319    #[test]
320    fn dds_header_magic_and_size() {
321        let blocks = vec![0u8; 8];
322        let dds = build_dds(4, 4, b"DXT1", &blocks);
323        assert_eq!(&dds[0..4], b"DDS ");
324        assert_eq!(u32::from_le_bytes(dds[4..8].try_into().unwrap()), 124);
325    }
326
327    #[test]
328    fn dxt1_output_byte_count() {
329        let compressor = Dxt1Compressor;
330        let img = image::DynamicImage::new_rgb8(8, 8);
331        let out = compressor
332            .compress(&CompressInput {
333                image: &img,
334                pack_mode: fastpack_core::types::config::PackMode::Fast,
335                quality: 0,
336            })
337            .unwrap();
338        // 8×8 → 2×2 blocks × 8 bytes = 32 bytes data + 128 bytes DDS header.
339        assert_eq!(out.data.len(), 4 + 124 + 4 * 8);
340    }
341
342    #[test]
343    fn dxt5_output_byte_count() {
344        let compressor = Dxt5Compressor;
345        let img = image::DynamicImage::new_rgba8(4, 4);
346        let out = compressor
347            .compress(&CompressInput {
348                image: &img,
349                pack_mode: fastpack_core::types::config::PackMode::Fast,
350                quality: 0,
351            })
352            .unwrap();
353        // 4×4 → 1 block × 16 bytes + 128 bytes DDS header.
354        assert_eq!(out.data.len(), 4 + 124 + 16);
355    }
356}