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}