Skip to main content

rakata_formats/dds/
mod.rs

1//! DDS (DirectDraw Surface) reader and writer.
2//!
3//! This module provides container-level support for KotOR-era DDS files.
4//! Parsing and serialization are backed by `ddsfile`, but the public API is kept
5//! focused on D3D9-era headers and formats that KotOR actually uses.
6//!
7//! ## KotOR Notes
8//! - Reader support includes:
9//!   - standard `DDS ` containers (D3D9-era headers; canonical compressed formats are DXT1/DXT5).
10//!   - a `CResDDS` prefix-header container variant used by vanilla resource paths.
11//! - Canonical KotOR compressed paths currently target DXT1 and DXT5.
12//! - Writer support currently emits standard `DDS ` containers only.
13//!
14//! ## Two variants, told apart by the first four bytes
15//!
16//! A standard DDS opens with the `DDS ` magic and a 124-byte D3D9-era header.
17//! The `CResDDS` variant vanilla resource paths use has no magic at all: twenty
18//! bytes of proprietary prefix metadata sit where the header would be, and the
19//! surface payload follows directly. The prefix replaces the DDS header rather
20//! than preceding it.
21//!
22//! Both are read and only the standard form is written. Emitting the prefixed
23//! variant would reproduce a container the engine reads and nothing else does.
24//! The prefix field map and the engine's own read are in
25//! `docs/src/formats/textures/dds.md`.
26
27mod reader;
28mod writer;
29
30pub use reader::{read_dds, read_dds_from_bytes};
31pub use writer::{write_dds, write_dds_to_vec};
32
33use thiserror::Error;
34
35use ddsfile::{Caps2, D3DFormat, Dds as DdsFile, Error, Header, NewD3dParams};
36
37use crate::binary::{DecodeBinary, EncodeBinary};
38
39/// DDS D3D-format enum re-export.
40pub type DdsD3dFormat = D3DFormat;
41/// DDS caps2 bitflag re-export.
42pub type DdsCaps2 = Caps2;
43/// Constructor parameter set for D3D-style DDS creation.
44pub type DdsNewD3dParams = NewD3dParams;
45
46/// In-memory DDS container.
47#[derive(Debug, Clone)]
48pub struct Dds {
49    /// Standard DDS header.
50    pub header: Header,
51    /// Raw surface payload bytes.
52    pub data: Vec<u8>,
53    /// Source container flavor used on decode.
54    pub source_flavor: DdsSourceFlavor,
55}
56
57/// DDS source container flavor.
58#[derive(Debug, Clone, Copy, PartialEq)]
59pub enum DdsSourceFlavor {
60    /// Standard `DDS ` container.
61    Standard,
62    /// CResDDS compressed-raster prefix container used by vanilla resource paths.
63    ///
64    /// Observed in `swkotor.exe` `CResDDS::OnResourceServiced` (`0x00710f30`) and
65    /// `CResDDS::GetDDSAttrib` (`0x00710ee0`): a 20-byte metadata header precedes
66    /// surface payload bytes.
67    CResDds(CResDdsHeader),
68}
69
70/// Parsed `CResDDS` prefix metadata header.
71#[derive(Debug, Clone, Copy, PartialEq)]
72pub struct CResDdsHeader {
73    /// Base width in pixels.
74    pub width: u32,
75    /// Base height in pixels.
76    pub height: u32,
77    /// Prefix bytes-per-pixel code as stored on disk.
78    ///
79    /// Canonically observed values:
80    /// - `3` => DXT1 payload sizing
81    /// - `4` => 16-byte block-family payload sizing (modeled as DXT5 here)
82    pub bytes_per_pixel_code: u8,
83    /// Reserved bytes from offsets `+0x09..+0x0B`.
84    ///
85    /// Native K1 attribute-read paths do not consume these bytes, but they are
86    /// retained for loss-aware inspection/debugging.
87    pub reserved_gap_bytes: [u8; 3],
88    /// Base-level payload size field from header.
89    pub base_level_data_size: u32,
90    /// Prefix alpha-mean metadata float (`+0x10`).
91    ///
92    /// Native K1 attribute-read paths surface this value directly as part of the
93    /// compressed-texture attribute tuple and route it into texture alpha-mean
94    /// handling.
95    pub alpha_mean: f32,
96}
97
98impl Dds {
99    /// Creates a DDS container with a D3D format.
100    ///
101    /// # Errors
102    ///
103    /// [`DdsBinaryError::InvalidHeader`] when `params.format` is DXT3, which
104    /// vanilla never writes and this crate refuses to produce, and
105    /// [`DdsBinaryError::Ddsfile`] when the dimensions or mipmap count do not
106    /// describe a buildable surface.
107    pub fn new_d3d(params: DdsNewD3dParams) -> Result<Self, DdsBinaryError> {
108        validate_canonical_standard_d3d_format(params.format)?;
109        let dds = DdsFile::new_d3d(params).map_err(DdsBinaryError::from)?;
110        Ok(Self::from_ddsfile(dds))
111    }
112
113    /// Returns the image width from the DDS header.
114    pub fn width(&self) -> u32 {
115        self.header.width
116    }
117
118    /// Returns the image height from the DDS header.
119    pub fn height(&self) -> u32 {
120        self.header.height
121    }
122
123    /// Returns depth for 3D textures; defaults to `1` for 2D textures.
124    pub fn depth(&self) -> u32 {
125        self.header.depth.unwrap_or(1)
126    }
127
128    /// Returns the mipmap level count (at least `1`).
129    pub fn mipmap_levels(&self) -> u32 {
130        self.header.mip_map_count.unwrap_or(1)
131    }
132
133    /// Returns the array-layer count derived from headers.
134    pub fn array_layers(&self) -> u32 {
135        if self.header.caps2.contains(Caps2::CUBEMAP) {
136            6
137        } else {
138            1
139        }
140    }
141
142    /// Returns whether the container is marked as a cubemap.
143    pub fn is_cubemap(&self) -> bool {
144        self.header.caps2.contains(Caps2::CUBEMAP)
145    }
146
147    /// Returns the parsed D3D format when representable from pixel format fields.
148    pub fn d3d_format(&self) -> Option<DdsD3dFormat> {
149        D3DFormat::try_from_pixel_format(&self.header.spf)
150    }
151
152    pub(super) fn from_ddsfile(dds: DdsFile) -> Self {
153        Self {
154            header: dds.header,
155            data: dds.data,
156            source_flavor: DdsSourceFlavor::Standard,
157        }
158    }
159
160    pub(super) fn to_ddsfile(&self) -> DdsFile {
161        DdsFile {
162            header: self.header.clone(),
163            header10: None,
164            data: self.data.clone(),
165        }
166    }
167}
168
169impl DecodeBinary for Dds {
170    type Error = DdsBinaryError;
171
172    fn decode_binary(bytes: &[u8]) -> Result<Self, Self::Error> {
173        read_dds_from_bytes(bytes)
174    }
175}
176
177impl EncodeBinary for Dds {
178    type Error = DdsBinaryError;
179
180    fn encode_binary(&self) -> Result<Vec<u8>, Self::Error> {
181        write_dds_to_vec(self)
182    }
183}
184
185/// Errors produced while parsing or serializing DDS data.
186#[derive(Debug, Error)]
187pub enum DdsBinaryError {
188    /// I/O read/write failure.
189    #[error(transparent)]
190    Io(#[from] std::io::Error),
191    /// DDS parser/writer error from `ddsfile`.
192    #[error("DDS parse/write error: {0}")]
193    Ddsfile(#[source] Error),
194    /// Header/body layout is invalid.
195    #[error("invalid DDS header: {0}")]
196    InvalidHeader(String),
197}
198
199impl From<Error> for DdsBinaryError {
200    fn from(value: Error) -> Self {
201        match value {
202            Error::Io(error) => Self::Io(error),
203            other => Self::Ddsfile(other),
204        }
205    }
206}
207
208impl From<crate::binary::BinaryLayoutError> for DdsBinaryError {
209    fn from(error: crate::binary::BinaryLayoutError) -> Self {
210        Self::InvalidHeader(error.to_string())
211    }
212}
213
214fn validate_canonical_standard_d3d_format(format: D3DFormat) -> Result<(), DdsBinaryError> {
215    if format == D3DFormat::DXT3 {
216        return Err(DdsBinaryError::InvalidHeader(
217            "DXT3 is non-canonical for vanilla KotOR DDS handling; canonical support is DXT1/DXT5"
218                .to_string(),
219        ));
220    }
221    Ok(())
222}