Skip to main content

dxil_reader/
rdat.rs

1//! The runtime data (`RDAT`) part of a library container: its subobjects (state object and pipeline configurations,
2//! root signatures and their associations, hit groups), which a ray tracing pipeline is built from.
3//!
4//! Layout (little-endian): a version (0x10) and a part count, then the offset of each part; a part is its type and byte
5//! size, then its bytes. The parts read here are the string buffer (1; strings null-terminated, referenced by offset),
6//! the index arrays (2; `u32` words, an array being its count then its items, referenced by word index), the raw bytes
7//! (5) and the subobject table (6: a record count, a record stride, then the records). A subobject record is its kind
8//! and name (a string reference) then, by kind, its data:
9//!
10//! | kind | data |
11//! |------|------|
12//! | 0 state object config | flags |
13//! | 1, 2 global and local root signature | offset and size in the raw bytes |
14//! | 8 association | subobject name (string), exports (index array of strings) |
15//! | 9 shader config | max payload bytes, max attribute bytes |
16//! | 10 pipeline config | max recursion depth |
17//! | 11 hit group | type (0 triangles, 1 procedural), any-hit, closest-hit, intersection (strings) |
18//! | 12 pipeline config 1 | max recursion depth, flags |
19//!
20//! These are DXC's definitions (`DxilRuntimeReflection.h`, `RDAT_SubobjectTypes.inl`, `DXIL::SubobjectKind` in
21//! `DxilConstants.h`, DirectXShaderCompiler main as of 2026-10): kinds 3 to 7 are reserved there and refused here. A
22//! record is the kind, the name and the data (a union: its fields start at the third word); the table's stride is the
23//! largest record's.
24//!
25//! A string reference of all ones, or to an empty string, is none. A library without a subobject table has no
26//! subobjects. Anything outside this (another version, a kind not listed, a reference outside its part) is refused.
27#![deny(
28    clippy::arithmetic_side_effects,
29    clippy::indexing_slicing,
30    clippy::cast_possible_truncation
31)]
32
33use crate::{Result, error};
34
35/// A subobject record of the table, by its kind, with its stored numbers. A string reference that is none (all ones,
36/// or an empty string) is `None`.
37#[derive(Clone, Debug, PartialEq, Eq)]
38pub enum Subobject {
39    /// Kind 0: the state object's flags (`D3D12_STATE_OBJECT_FLAGS`).
40    StateObjectConfig {
41        /// The subobject's name.
42        name: String,
43        /// The flags as stored.
44        flags: u32,
45    },
46    /// Kind 1: a root signature as D3D serialised it, its bytes from the raw bytes part.
47    GlobalRootSignature {
48        /// The subobject's name.
49        name: String,
50        /// The serialised root signature.
51        signature: Vec<u8>,
52    },
53    /// Kind 2: a local root signature, as kind 1.
54    LocalRootSignature {
55        /// The subobject's name.
56        name: String,
57        /// The serialised root signature.
58        signature: Vec<u8>,
59    },
60    /// Kind 8: the subobject named, associated with these exports.
61    ExportsAssociation {
62        /// The subobject's name.
63        name: String,
64        /// The name of the subobject associated.
65        subobject: String,
66        /// The exports it is associated with.
67        exports: Vec<String>,
68    },
69    /// Kind 9: the largest payload and hit attributes.
70    ShaderConfig {
71        /// The subobject's name.
72        name: String,
73        /// The largest ray payload in bytes.
74        max_payload_bytes: u32,
75        /// The largest hit attribute structure in bytes.
76        max_attribute_bytes: u32,
77    },
78    /// Kind 10: the deepest recursion.
79    PipelineConfig {
80        /// The subobject's name.
81        name: String,
82        /// The deepest ray recursion.
83        max_recursion_depth: u32,
84    },
85    /// Kind 11: a hit group; `kind` is its `D3D12_HIT_GROUP_TYPE` (0 triangles, 1 procedural primitives).
86    HitGroup {
87        /// The subobject's name.
88        name: String,
89        /// The hit group type.
90        kind: u32,
91        /// The any-hit shader's name, where it has one.
92        any_hit: Option<String>,
93        /// The closest-hit shader's name, where it has one.
94        closest_hit: Option<String>,
95        /// The intersection shader's name, where it has one.
96        intersection: Option<String>,
97    },
98    /// Kind 12: the deepest recursion and the pipeline flags (`D3D12_RAYTRACING_PIPELINE_FLAGS`).
99    PipelineConfig1 {
100        /// The subobject's name.
101        name: String,
102        /// The deepest ray recursion.
103        max_recursion_depth: u32,
104        /// The pipeline flags as stored.
105        flags: u32,
106    },
107}
108
109const VERSION: u32 = 0x10;
110const STRINGS: u32 = 1;
111const INDEX_ARRAYS: u32 = 2;
112const RAW_BYTES: u32 = 5;
113const SUBOBJECTS: u32 = 6;
114const NULL: u32 = u32::MAX;
115
116fn word(data: &[u8], at: usize) -> Result<u32> {
117    at.checked_add(4)
118        .and_then(|end| data.get(at..end))
119        .and_then(|b| b.try_into().ok())
120        .map(u32::from_le_bytes)
121        .ok_or_else(|| error(at.saturating_mul(8), "RDAT: a word past the end of its part"))
122}
123
124/// The part of type `wanted`, if the data has one.
125fn part(rdat: &[u8], wanted: u32) -> Result<Option<&[u8]>> {
126    let version = word(rdat, 0)?;
127    if version != VERSION {
128        return Err(error(
129            0,
130            format!("RDAT version {version:#x}: only {VERSION:#x} is read"),
131        ));
132    }
133    let count = word(rdat, 4)?;
134    for k in 0..count {
135        let at = usize::try_from(k)
136            .ok()
137            .and_then(|k| k.checked_mul(4))
138            .and_then(|k| k.checked_add(8))
139            .ok_or_else(|| error(32, "RDAT: part count"))?;
140        let offset = usize::try_from(word(rdat, at)?).map_err(|_| error(0, "RDAT: part offset"))?;
141        if word(rdat, offset)? != wanted {
142            continue;
143        }
144        let size = usize::try_from(word(rdat, offset.saturating_add(4))?)
145            .map_err(|_| error(0, "RDAT: part size"))?;
146        let start = offset.saturating_add(8);
147        return rdat
148            .get(start..start.saturating_add(size))
149            .map(Some)
150            .ok_or_else(|| error(offset.saturating_mul(8), "RDAT: a part past the end of the data"));
151    }
152    Ok(None)
153}
154
155struct Strings<'a>(&'a [u8]);
156impl Strings<'_> {
157    /// The string at `offset`; none for a null reference or an empty string.
158    fn get(&self, offset: u32) -> Result<Option<String>> {
159        if offset == NULL {
160            return Ok(None);
161        }
162        let start = usize::try_from(offset).map_err(|_| error(0, "RDAT: string offset"))?;
163        let rest = self
164            .0
165            .get(start..)
166            .ok_or_else(|| error(0, format!("RDAT: string {offset} past the string buffer")))?;
167        let end = rest
168            .iter()
169            .position(|b| *b == 0)
170            .ok_or_else(|| error(0, "RDAT: a string without its end"))?;
171        let text = std::str::from_utf8(rest.get(..end).unwrap_or(&[]))
172            .map_err(|_| error(0, "RDAT: a string that is not UTF-8"))?;
173        Ok((!text.is_empty()).then(|| text.to_owned()))
174    }
175
176    fn required(&self, offset: u32, what: &str) -> Result<String> {
177        self.get(offset)?
178            .ok_or_else(|| error(0, format!("RDAT: {what} has no name")))
179    }
180}
181
182/// The subobjects of the library `rdat` (an `RDAT` part's bytes).
183pub fn subobjects(rdat: &[u8]) -> Result<Vec<Subobject>> {
184    let Some(table) = part(rdat, SUBOBJECTS)? else {
185        return Ok(Vec::new());
186    };
187    let strings = Strings(part(rdat, STRINGS)?.unwrap_or(&[]));
188    let arrays = part(rdat, INDEX_ARRAYS)?.unwrap_or(&[]);
189    let raw = part(rdat, RAW_BYTES)?.unwrap_or(&[]);
190    let count = usize::try_from(word(table, 0)?).map_err(|_| error(0, "RDAT: subobject count"))?;
191    let stride = usize::try_from(word(table, 4)?).map_err(|_| error(0, "RDAT: subobject stride"))?;
192    if stride < 8 {
193        return Err(error(0, format!("RDAT: subobject records of {stride} bytes")));
194    }
195    let mut out = Vec::new();
196    for k in 0..count {
197        let at = k
198            .checked_mul(stride)
199            .and_then(|o| o.checked_add(8))
200            .ok_or_else(|| error(0, "RDAT: subobject table size"))?;
201        let record = table
202            .get(at..at.saturating_add(stride))
203            .ok_or_else(|| error(0, "RDAT: the subobject table is shorter than its count says"))?;
204        let field = |n: usize| word(record, n.saturating_mul(4));
205        let kind = field(0)?;
206        let name = strings.required(field(1)?, "a subobject")?;
207        out.push(match kind {
208            0 => Subobject::StateObjectConfig {
209                name,
210                flags: field(2)?,
211            },
212            1 | 2 => {
213                let offset =
214                    usize::try_from(field(2)?).map_err(|_| error(0, "RDAT: root signature offset"))?;
215                let size = usize::try_from(field(3)?).map_err(|_| error(0, "RDAT: root signature size"))?;
216                let signature = raw
217                    .get(offset..offset.saturating_add(size))
218                    .ok_or_else(|| error(0, format!("RDAT: root signature {name} past the raw bytes")))?
219                    .to_vec();
220                if kind == 1 {
221                    Subobject::GlobalRootSignature { name, signature }
222                } else {
223                    Subobject::LocalRootSignature { name, signature }
224                }
225            }
226            8 => {
227                let subobject = strings.required(field(2)?, "an association's subobject")?;
228                // an index array: its count, then its items (string references)
229                let first = usize::try_from(field(3)?).map_err(|_| error(0, "RDAT: exports index"))?;
230                let n = usize::try_from(word(arrays, first.saturating_mul(4))?)
231                    .map_err(|_| error(0, "RDAT: exports count"))?;
232                let mut exports = Vec::new();
233                for i in 0..n {
234                    let at = first.saturating_add(1).saturating_add(i).saturating_mul(4);
235                    exports.push(strings.required(word(arrays, at)?, "an export")?);
236                }
237                Subobject::ExportsAssociation {
238                    name,
239                    subobject,
240                    exports,
241                }
242            }
243            9 => Subobject::ShaderConfig {
244                name,
245                max_payload_bytes: field(2)?,
246                max_attribute_bytes: field(3)?,
247            },
248            10 => Subobject::PipelineConfig {
249                name,
250                max_recursion_depth: field(2)?,
251            },
252            11 => Subobject::HitGroup {
253                name,
254                kind: field(2)?,
255                any_hit: strings.get(field(3)?)?,
256                closest_hit: strings.get(field(4)?)?,
257                intersection: strings.get(field(5)?)?,
258            },
259            12 => Subobject::PipelineConfig1 {
260                name,
261                max_recursion_depth: field(2)?,
262                flags: field(3)?,
263            },
264            3..=7 => return Err(error(0, format!("RDAT: subobject kind {kind} is reserved"))),
265            other => return Err(error(0, format!("RDAT: subobject kind {other}"))),
266        });
267    }
268    Ok(out)
269}
270
271/// A function's export name from its name in the module: `\u{1}?closest_hit@@YAXXZ` is `closest_hit`.
272pub fn export_name(function: &str) -> String {
273    let undecorated = function.strip_prefix('\u{1}').unwrap_or(function);
274    match undecorated.strip_prefix('?') {
275        Some(rest) => rest.split("@@").next().unwrap_or(rest).to_owned(),
276        None => undecorated.to_owned(),
277    }
278}