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}