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