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