Skip to main content

rakata_formats/dds/
reader.rs

1//! DDS binary reader.
2
3use std::io::{Cursor, Read};
4
5use ddsfile::{D3DFormat, Dds as DdsFile, NewD3dParams};
6
7use crate::binary;
8
9use super::{
10    validate_canonical_standard_d3d_format, CResDdsHeader, Dds, DdsBinaryError, DdsSourceFlavor,
11};
12
13const DDS_DDPF_FLAGS_OFFSET: usize = 84;
14const DDS_DDPF_FOURCC_OFFSET: usize = 88;
15const DDPF_FOURCC_FLAG: u32 = 0x4;
16const CRESDDS_HEADER_SIZE: usize = 20;
17const CRESDDS_DXT1_CODE: u8 = 3;
18const CRESDDS_DXT5_CODE: u8 = 4;
19
20/// Reads DDS data from a reader.
21///
22/// # Errors
23///
24/// [`DdsBinaryError::Io`] when the stream will not read to end, and whatever
25/// [`read_dds_from_bytes`] reports for the bytes it collected.
26#[cfg_attr(
27    feature = "tracing",
28    tracing::instrument(level = "debug", skip(reader))
29)]
30pub fn read_dds<R: Read>(reader: &mut R) -> Result<Dds, DdsBinaryError> {
31    let mut bytes = Vec::new();
32    reader.read_to_end(&mut bytes)?;
33    read_dds_from_bytes(&bytes)
34}
35
36/// Reads DDS data from bytes.
37///
38/// # Errors
39///
40/// [`DdsBinaryError::InvalidHeader`] when `bytes` are neither a standard `DDS `
41/// container nor the engine's prefixed variant, and
42/// [`DdsBinaryError::Ddsfile`] when the standard header parses as malformed.
43/// Which of the two forms was read is recorded on the result, so a round trip
44/// re-emits the one it came from.
45#[cfg_attr(
46    feature = "tracing",
47    tracing::instrument(level = "debug", skip(bytes), fields(bytes_len = bytes.len()))
48)]
49pub fn read_dds_from_bytes(bytes: &[u8]) -> Result<Dds, DdsBinaryError> {
50    if !bytes.starts_with(b"DDS ") {
51        return read_cresdds_header(bytes);
52    }
53
54    if has_dx10_extension_header(bytes) {
55        return Err(DdsBinaryError::InvalidHeader(
56            "DX10 extension headers are unsupported for KotOR-focused DDS handling".into(),
57        ));
58    }
59
60    let dds = DdsFile::read(Cursor::new(bytes)).map_err(DdsBinaryError::from)?;
61    if dds.header10.is_some() {
62        return Err(DdsBinaryError::InvalidHeader(
63            "DX10 extension headers are unsupported for KotOR-focused DDS handling".into(),
64        ));
65    }
66    let parsed = Dds::from_ddsfile(dds);
67    if let Some(format) = parsed.d3d_format() {
68        validate_canonical_standard_d3d_format(format)?;
69    }
70    Ok(parsed)
71}
72
73fn has_dx10_extension_header(bytes: &[u8]) -> bool {
74    if bytes.len() < DDS_DDPF_FOURCC_OFFSET + 4 {
75        return false;
76    }
77
78    let flags = u32::from_le_bytes([
79        bytes[DDS_DDPF_FLAGS_OFFSET],
80        bytes[DDS_DDPF_FLAGS_OFFSET + 1],
81        bytes[DDS_DDPF_FLAGS_OFFSET + 2],
82        bytes[DDS_DDPF_FLAGS_OFFSET + 3],
83    ]);
84    let fourcc = &bytes[DDS_DDPF_FOURCC_OFFSET..DDS_DDPF_FOURCC_OFFSET + 4];
85    flags & DDPF_FOURCC_FLAG != 0 && fourcc == b"DX10"
86}
87
88fn read_cresdds_header(bytes: &[u8]) -> Result<Dds, DdsBinaryError> {
89    if bytes.len() < CRESDDS_HEADER_SIZE {
90        return Err(DdsBinaryError::InvalidHeader(
91            "missing standard DDS magic (`DDS `) and too short for CResDDS prefix header"
92                .to_string(),
93        ));
94    }
95
96    let width = binary::read_u32(bytes, 0)?;
97    let height = binary::read_u32(bytes, 4)?;
98    if width == 0 || height == 0 {
99        return Err(DdsBinaryError::InvalidHeader(
100            "CResDDS prefix dimensions must be non-zero".into(),
101        ));
102    }
103    if !width.is_power_of_two() || !height.is_power_of_two() {
104        return Err(DdsBinaryError::InvalidHeader(
105            "CResDDS prefix dimensions must be powers of two".into(),
106        ));
107    }
108
109    // K1 `CResDDS::GetDDSAttrib` reads this as a single byte at +0x08.
110    let bytes_per_pixel_code = bytes[8];
111    let reserved_gap_bytes = [bytes[9], bytes[10], bytes[11]];
112    let format = match bytes_per_pixel_code {
113        CRESDDS_DXT1_CODE => D3DFormat::DXT1,
114        CRESDDS_DXT5_CODE => D3DFormat::DXT5,
115        _ => {
116            return Err(DdsBinaryError::InvalidHeader(format!(
117                "unsupported CResDDS prefix bytes-per-pixel code: {bytes_per_pixel_code}"
118            )));
119        }
120    };
121
122    let base_level_data_size = binary::read_u32(bytes, 12)?;
123    let expected_base_level_size = compressed_mipmap_size(width, height, format)?;
124    if base_level_data_size != expected_base_level_size {
125        return Err(DdsBinaryError::InvalidHeader(format!(
126            "CResDDS prefix base-level size mismatch: header={base_level_data_size}, expected={expected_base_level_size}"
127        )));
128    }
129
130    let alpha_mean = binary::read_f32(bytes, 16)?;
131    let payload = &bytes[CRESDDS_HEADER_SIZE..];
132    let mipmap_levels = infer_compressed_mipmap_count(payload.len(), width, height, format)?;
133
134    let mut dds = Dds::new_d3d(NewD3dParams {
135        height,
136        width,
137        depth: None,
138        format,
139        mipmap_levels: Some(mipmap_levels),
140        caps2: None,
141    })?;
142    if dds.data.len() != payload.len() {
143        return Err(DdsBinaryError::InvalidHeader(format!(
144            "CResDDS prefix payload size mismatch: expected {} bytes for inferred mip levels, got {}",
145            dds.data.len(),
146            payload.len()
147        )));
148    }
149    dds.data.copy_from_slice(payload);
150    dds.source_flavor = DdsSourceFlavor::CResDds(CResDdsHeader {
151        width,
152        height,
153        bytes_per_pixel_code,
154        reserved_gap_bytes,
155        base_level_data_size,
156        alpha_mean,
157    });
158
159    Ok(dds)
160}
161
162fn infer_compressed_mipmap_count(
163    payload_len: usize,
164    mut width: u32,
165    mut height: u32,
166    format: D3DFormat,
167) -> Result<u32, DdsBinaryError> {
168    let mut consumed = 0_usize;
169    let mut levels = 0_u32;
170    loop {
171        let level_size =
172            usize::try_from(compressed_mipmap_size(width, height, format)?).map_err(|_| {
173                DdsBinaryError::InvalidHeader("prefix mip size exceeds host usize".into())
174            })?;
175        let next = consumed.checked_add(level_size).ok_or_else(|| {
176            DdsBinaryError::InvalidHeader("prefix mip size accumulation overflow".into())
177        })?;
178        if next > payload_len {
179            break;
180        }
181        consumed = next;
182        levels += 1;
183        if width == 1 && height == 1 {
184            break;
185        }
186        width = (width / 2).max(1);
187        height = (height / 2).max(1);
188    }
189
190    if levels == 0 {
191        return Err(DdsBinaryError::InvalidHeader(
192            "CResDDS prefix payload does not contain enough data for base mip".into(),
193        ));
194    }
195    if consumed != payload_len {
196        return Err(DdsBinaryError::InvalidHeader(format!(
197            "CResDDS prefix payload size does not align to inferred mip chain: consumed={consumed}, payload={payload_len}"
198        )));
199    }
200
201    Ok(levels)
202}
203
204fn compressed_mipmap_size(
205    width: u32,
206    height: u32,
207    format: D3DFormat,
208) -> Result<u32, DdsBinaryError> {
209    let block_bytes = match format {
210        D3DFormat::DXT1 => 8_u32,
211        D3DFormat::DXT5 => 16_u32,
212        _ => {
213            return Err(DdsBinaryError::InvalidHeader(
214                "prefix mip sizing only supports DXT1/DXT5".into(),
215            ));
216        }
217    };
218    let blocks_w = width.div_ceil(4).max(1);
219    let blocks_h = height.div_ceil(4).max(1);
220    blocks_w
221        .checked_mul(blocks_h)
222        .and_then(|blocks| blocks.checked_mul(block_bytes))
223        .ok_or_else(|| DdsBinaryError::InvalidHeader("prefix mip size overflow".into()))
224}
225
226#[cfg(test)]
227mod tests {
228    use ddsfile::{Caps2, D3DFormat, Dds as DdsFile, NewD3dParams};
229
230    use super::*;
231    use crate::binary::{DecodeBinary, EncodeBinary};
232    use crate::dds::{write_dds_to_vec, DdsNewD3dParams, DdsSourceFlavor};
233
234    #[test]
235    fn roundtrip_synthetic_d3d_dxt1_dds() {
236        let mut dds = Dds::new_d3d(DdsNewD3dParams {
237            height: 4,
238            width: 4,
239            depth: None,
240            format: D3DFormat::DXT1,
241            mipmap_levels: Some(1),
242            caps2: None,
243        })
244        .expect("create DXT1 DDS");
245
246        dds.data.copy_from_slice(&[0x11; 8]);
247
248        let bytes = write_dds_to_vec(&dds).expect("write should succeed");
249        let parsed = read_dds_from_bytes(&bytes).expect("read should succeed");
250
251        assert_eq!(parsed.width(), 4);
252        assert_eq!(parsed.height(), 4);
253        assert_eq!(parsed.mipmap_levels(), 1);
254        assert_eq!(parsed.d3d_format(), Some(D3DFormat::DXT1));
255        assert_eq!(parsed.data, dds.data);
256    }
257
258    #[test]
259    fn writer_is_deterministic_for_synthetic_dxt1_dds() {
260        let mut dds = Dds::new_d3d(DdsNewD3dParams {
261            height: 4,
262            width: 4,
263            depth: None,
264            format: D3DFormat::DXT1,
265            mipmap_levels: Some(1),
266            caps2: None,
267        })
268        .expect("create DXT1 DDS");
269        dds.data.copy_from_slice(&[0x11; 8]);
270
271        let first = write_dds_to_vec(&dds).expect("first write should succeed");
272        let second = write_dds_to_vec(&dds).expect("second write should succeed");
273        assert_eq!(first, second, "canonical DDS writer output drifted");
274    }
275
276    #[test]
277    fn supports_canonical_kotor_dxt_formats() {
278        for format in [D3DFormat::DXT1, D3DFormat::DXT5] {
279            let mut dds = Dds::new_d3d(DdsNewD3dParams {
280                height: 4,
281                width: 4,
282                depth: None,
283                format,
284                mipmap_levels: Some(1),
285                caps2: None,
286            })
287            .expect("create D3D DDS");
288
289            dds.data.fill(0x7F);
290            let bytes = write_dds_to_vec(&dds).expect("write should succeed");
291            let parsed = read_dds_from_bytes(&bytes).expect("read should succeed");
292
293            assert_eq!(parsed.d3d_format(), Some(format));
294            assert_eq!(parsed.data, dds.data);
295        }
296    }
297
298    #[test]
299    fn rejects_standard_dxt3_on_read() {
300        let mut dds = DdsFile::new_d3d(NewD3dParams {
301            height: 4,
302            width: 4,
303            depth: None,
304            format: D3DFormat::DXT3,
305            mipmap_levels: Some(1),
306            caps2: None,
307        })
308        .expect("create DXT3 DDS via backend");
309        dds.data.fill(0xAB);
310        let mut cursor = std::io::Cursor::new(Vec::new());
311        dds.write(&mut cursor).expect("serialize backend DDS");
312        let bytes = cursor.into_inner();
313
314        let err = read_dds_from_bytes(&bytes).expect_err("canonical reader must reject DXT3");
315        assert!(matches!(err, DdsBinaryError::InvalidHeader(_)));
316    }
317
318    #[test]
319    fn rejects_standard_dxt3_on_write() {
320        let mut dds = DdsFile::new_d3d(NewD3dParams {
321            height: 4,
322            width: 4,
323            depth: None,
324            format: D3DFormat::DXT3,
325            mipmap_levels: Some(1),
326            caps2: None,
327        })
328        .expect("create DXT3 DDS via backend");
329        dds.data.fill(0xCD);
330
331        let model = Dds {
332            header: dds.header,
333            data: dds.data,
334            source_flavor: DdsSourceFlavor::Standard,
335        };
336        let err = write_dds_to_vec(&model).expect_err("canonical writer must reject DXT3");
337        assert!(matches!(err, DdsBinaryError::InvalidHeader(_)));
338    }
339
340    #[test]
341    fn reports_cubemap_from_caps2() {
342        let dds = Dds::new_d3d(DdsNewD3dParams {
343            height: 4,
344            width: 4,
345            depth: None,
346            format: D3DFormat::DXT1,
347            mipmap_levels: Some(1),
348            caps2: Some(Caps2::CUBEMAP | Caps2::CUBEMAP_ALLFACES),
349        })
350        .expect("create cubemap DDS");
351
352        assert!(dds.is_cubemap());
353        assert_eq!(dds.array_layers(), 6);
354    }
355
356    #[test]
357    fn rejects_missing_dds_magic() {
358        let err = read_dds_from_bytes(&[0_u8; 16]).expect_err("must fail");
359        assert!(matches!(err, DdsBinaryError::InvalidHeader(_)));
360    }
361
362    #[test]
363    fn rejects_truncated_standard_dds_header() {
364        let bytes = b"DDS ".to_vec();
365        let err = read_dds_from_bytes(&bytes).expect_err("must fail");
366        assert!(matches!(
367            err,
368            DdsBinaryError::InvalidHeader(_) | DdsBinaryError::Ddsfile(_) | DdsBinaryError::Io(_)
369        ));
370    }
371
372    fn make_cresdds_prefix_header(
373        width: u32,
374        height: u32,
375        bytes_per_pixel_code: u8,
376        base_level_data_size: u32,
377        alpha_mean: f32,
378    ) -> Vec<u8> {
379        make_cresdds_prefix_header_with_reserved(
380            width,
381            height,
382            bytes_per_pixel_code,
383            [0_u8; 3],
384            base_level_data_size,
385            alpha_mean,
386        )
387    }
388
389    fn make_cresdds_prefix_header_with_reserved(
390        width: u32,
391        height: u32,
392        bytes_per_pixel_code: u8,
393        reserved_gap_bytes: [u8; 3],
394        base_level_data_size: u32,
395        alpha_mean: f32,
396    ) -> Vec<u8> {
397        let mut out = Vec::with_capacity(CRESDDS_HEADER_SIZE);
398        out.extend_from_slice(&width.to_le_bytes());
399        out.extend_from_slice(&height.to_le_bytes());
400        out.push(bytes_per_pixel_code);
401        out.extend_from_slice(&reserved_gap_bytes);
402        out.extend_from_slice(&base_level_data_size.to_le_bytes());
403        out.extend_from_slice(&alpha_mean.to_le_bytes());
404        out
405    }
406
407    #[test]
408    fn parses_cresdds_prefix_dxt1_header_variant() {
409        let width = 8_u32;
410        let height = 8_u32;
411        let base_size = compressed_mipmap_size(width, height, D3DFormat::DXT1).expect("base size");
412        let mut bytes = make_cresdds_prefix_header(width, height, 3, base_size, 1.0);
413        // 8x8 DXT1 mip chain: 32 + 8 + 8 = 48
414        bytes.extend_from_slice(&[0xAA; 48]);
415
416        let parsed = read_dds_from_bytes(&bytes).expect("prefix parse should succeed");
417        assert_eq!(parsed.width(), width);
418        assert_eq!(parsed.height(), height);
419        assert_eq!(parsed.d3d_format(), Some(D3DFormat::DXT1));
420        assert_eq!(parsed.mipmap_levels(), 3);
421        assert_eq!(parsed.data.len(), 48);
422        match parsed.source_flavor {
423            DdsSourceFlavor::CResDds(header) => {
424                assert_eq!(header.bytes_per_pixel_code, 3);
425                assert_eq!(header.reserved_gap_bytes, [0_u8; 3]);
426                assert_eq!(header.base_level_data_size, base_size);
427                assert_eq!(header.alpha_mean, 1.0);
428            }
429            DdsSourceFlavor::Standard => panic!("expected prefix flavor"),
430        }
431    }
432
433    #[test]
434    fn parses_cresdds_prefix_dxt5_header_variant() {
435        let width = 4_u32;
436        let height = 4_u32;
437        let base_size = compressed_mipmap_size(width, height, D3DFormat::DXT5).expect("base size");
438        let mut bytes = make_cresdds_prefix_header(width, height, 4, base_size, 0.0);
439        // 4x4 has one mip in this synthetic payload.
440        bytes.extend_from_slice(&[0x11; 16]);
441
442        let parsed = read_dds_from_bytes(&bytes).expect("prefix parse should succeed");
443        assert_eq!(parsed.d3d_format(), Some(D3DFormat::DXT5));
444        assert_eq!(parsed.mipmap_levels(), 1);
445        assert_eq!(parsed.data.len(), 16);
446    }
447
448    #[test]
449    fn rejects_cresdds_prefix_with_unknown_bpp_code() {
450        let mut bytes = make_cresdds_prefix_header(4, 4, 9, 16, 0.0);
451        bytes.extend_from_slice(&[0_u8; 16]);
452
453        let err = read_dds_from_bytes(&bytes).expect_err("must fail");
454        assert!(matches!(err, DdsBinaryError::InvalidHeader(_)));
455    }
456
457    #[test]
458    fn parses_cresdds_prefix_with_nonzero_reserved_gap_bytes() {
459        let width = 4_u32;
460        let height = 4_u32;
461        let base_size = compressed_mipmap_size(width, height, D3DFormat::DXT1).expect("base size");
462        let mut bytes = make_cresdds_prefix_header_with_reserved(
463            width,
464            height,
465            3,
466            [0xAA, 0xBB, 0xCC],
467            base_size,
468            1.0,
469        );
470        bytes.extend_from_slice(&[0x42; 8]);
471
472        let parsed = read_dds_from_bytes(&bytes).expect("prefix parse should succeed");
473        assert_eq!(parsed.d3d_format(), Some(D3DFormat::DXT1));
474        match parsed.source_flavor {
475            DdsSourceFlavor::CResDds(header) => {
476                assert_eq!(header.bytes_per_pixel_code, 3);
477                assert_eq!(header.reserved_gap_bytes, [0xAA, 0xBB, 0xCC]);
478            }
479            DdsSourceFlavor::Standard => panic!("expected prefix flavor"),
480        }
481    }
482
483    #[test]
484    fn parses_cresdds_prefix_alpha_mean_metadata() {
485        let width = 4_u32;
486        let height = 4_u32;
487        let base_size = compressed_mipmap_size(width, height, D3DFormat::DXT1).expect("base size");
488        let mut bytes = make_cresdds_prefix_header(width, height, 3, base_size, 0.625);
489        bytes.extend_from_slice(&[0x42; 8]);
490
491        let parsed = read_dds_from_bytes(&bytes).expect("prefix parse should succeed");
492        match parsed.source_flavor {
493            DdsSourceFlavor::CResDds(header) => {
494                assert_eq!(header.alpha_mean, 0.625);
495            }
496            DdsSourceFlavor::Standard => panic!("expected prefix flavor"),
497        }
498    }
499
500    #[test]
501    fn rejects_cresdds_prefix_non_power_of_two() {
502        let mut bytes = make_cresdds_prefix_header(6, 4, 3, 12, 0.0);
503        bytes.extend_from_slice(&[0_u8; 12]);
504
505        let err = read_dds_from_bytes(&bytes).expect_err("must fail");
506        assert!(matches!(err, DdsBinaryError::InvalidHeader(_)));
507    }
508
509    #[test]
510    fn rejects_cresdds_prefix_payload_size_mismatch() {
511        let mut bytes = make_cresdds_prefix_header(8, 8, 3, 32, 1.0);
512        // Not aligned to full mip chain.
513        bytes.extend_from_slice(&[0x22; 41]);
514
515        let err = read_dds_from_bytes(&bytes).expect_err("must fail");
516        assert!(matches!(err, DdsBinaryError::InvalidHeader(_)));
517    }
518
519    #[test]
520    fn decode_encode_traits_roundtrip() {
521        let dds = Dds::new_d3d(DdsNewD3dParams {
522            height: 4,
523            width: 4,
524            depth: None,
525            format: D3DFormat::DXT1,
526            mipmap_levels: Some(1),
527            caps2: None,
528        })
529        .expect("create DDS");
530
531        let bytes = dds.encode_binary().expect("encode");
532        let decoded = Dds::decode_binary(&bytes).expect("decode");
533
534        assert_eq!(decoded.width(), 4);
535        assert_eq!(decoded.height(), 4);
536        assert_eq!(decoded.d3d_format(), Some(D3DFormat::DXT1));
537        assert_eq!(decoded.data.len(), dds.data.len());
538        assert_eq!(decoded.source_flavor, DdsSourceFlavor::Standard);
539    }
540
541    #[test]
542    fn rejects_unsupported_extension_header() {
543        let dds = Dds::new_d3d(DdsNewD3dParams {
544            height: 4,
545            width: 4,
546            depth: None,
547            format: D3DFormat::DXT1,
548            mipmap_levels: Some(1),
549            caps2: None,
550        })
551        .expect("create DDS");
552        let mut bytes = write_dds_to_vec(&dds).expect("write should succeed");
553
554        bytes[DDS_DDPF_FLAGS_OFFSET..DDS_DDPF_FLAGS_OFFSET + 4]
555            .copy_from_slice(&DDPF_FOURCC_FLAG.to_le_bytes());
556        bytes[DDS_DDPF_FOURCC_OFFSET..DDS_DDPF_FOURCC_OFFSET + 4].copy_from_slice(b"DX10");
557
558        let err =
559            read_dds_from_bytes(&bytes).expect_err("unsupported DDS extension header must fail");
560        assert!(matches!(err, DdsBinaryError::InvalidHeader(_)));
561    }
562}