Skip to main content

dxil_reader/
psv.rs

1//! The pipeline state validation (`PSV0`) part of a Shader Model 6 program container: the runtime information, resource
2//! records and signature elements a pipeline is validated against, which the validator writes from the program's own
3//! metadata. Little-endian.
4//!
5//! ```text
6//! u32 runtime-info size (36, 48 or 52), then the runtime info
7//! u32 resource count; when it is not zero, u32 record size (16 or 24), then the records
8//! u32 string-table size, then the table
9//! u32 semantic-index count, then the indices (u32 each)
10//! u32 element size (16) when there are any input, output or patch-constant elements, then the input, the output and
11//!   the patch-constant elements
12//! the rest: the input-to-output dependency tables, as words
13//! ```
14//!
15//! The runtime info is 16 bytes whose meaning depends on the stage, the lowest and highest wave lane counts (u32 each),
16//! then from 36 bytes the stage (u8), the uses-view-id flag (u8), a u16 (a geometry program's largest output vertex
17//! count; in the low byte a hull or domain program's patch-constant vector count), the input, output and
18//! patch-constant element counts (u8 each), the input vector count (u8) and four output vector counts (u8 each), from
19//! 48 bytes the thread group size (three u32), and at 52 bytes the offset of the entry point's name in the string table.
20//! The stage-dependent 16 bytes are: vertex, whether an `SV_Position` output exists (word 3); pixel, whether it writes
21//! depth (byte 0) and whether it shades per sample (byte 1); hull, the input and output control point counts, the
22//! domain and the output primitive; domain, the input control point count; geometry, the input primitive, the output
23//! topology, the output stream mask and whether it writes a position. The record sizes follow the validator that wrote
24//! the part: validator 1.5 writes a 36-byte runtime info and 16-byte resource records, 1.6 writes 48 and 24, and 1.10
25//! writes 52 and 24 (the 16-byte records have no kind or flags). The reader reads these three sizes only.
26//!
27//! Every field above was checked against the metadata of the same program in the stock containers of skate. (19,240
28//! with a `PSV0` part, validator 1.10) and Elden Ring (46,002; 10 with validator 1.5, 45,992 with 1.6): the layout
29//! consumes each part to its last byte, and the stage, the thread group, the resource ranges, the elements' name,
30//! semantic indices, size, kind, interpolation and placement, the vector counts and the stage unions equal what
31//! [`crate::reflection`] reads from the metadata. The dependency tables are kept as stored words; the part's remainder
32//! after the elements is as long as the vectors state (the input vectors' components times the words of the output
33//! vectors' components).
34use primase_foundations::bytes::View;
35
36use crate::{Result, error};
37
38/// Most resource records a part may state.
39const MAX_RECORDS: usize = 1 << 20;
40
41/// The runtime information of a `PSV0` part.
42#[derive(Clone, Copy, Debug, PartialEq, Eq)]
43pub struct RuntimeInfo {
44    /// The stage-dependent 16 bytes, as four words (see the module documentation).
45    pub stage_words: [u32; 4],
46    /// The smallest wave lane count the program expects (0 where it states none).
47    pub min_wave_lanes: u32,
48    /// The largest wave lane count the program expects (`u32::MAX` where it states none).
49    pub max_wave_lanes: u32,
50    /// The shader kind (0 pixel, 1 vertex, 2 geometry, 3 hull, 4 domain, 5 compute, 13 mesh, ...), as in the program
51    /// header.
52    pub stage: u8,
53    /// The program uses `SV_ViewID`.
54    pub uses_view_id: bool,
55    /// A geometry program's largest output vertex count; in the low byte a hull or domain program's patch-constant
56    /// vector count.
57    pub stage_data: u16,
58    /// The input, output and patch-constant element counts as stored (the lengths of [`Psv::inputs`] and the others).
59    pub element_counts: [u8; 3],
60    /// How many input vectors (registers) the program reads.
61    pub input_vectors: u8,
62    /// How many output vectors it writes, per stream.
63    pub output_vectors: [u8; 4],
64    /// The thread group size, from 48 bytes of runtime info.
65    pub num_threads: Option<[u32; 3]>,
66    /// The offset of the entry point's name in the string table, from 52 bytes of runtime info.
67    pub entry_name: Option<u32>,
68}
69
70/// A resource record.
71#[derive(Clone, Copy, Debug, PartialEq, Eq)]
72pub struct Resource {
73    /// 1 sampler, 2 constant buffer, 3 typed SRV, 4 raw SRV, 5 structured SRV, 6 typed UAV, 7 raw UAV, 8 structured UAV,
74    /// 9 structured UAV with a counter.
75    pub resource_type: u32,
76    /// The register space.
77    pub space: u32,
78    /// The first register.
79    pub lower: u32,
80    /// The last register (`u32::MAX` for an unbounded range).
81    pub upper: u32,
82    /// The resource kind (the metadata's `DxilResourceKind` for an SRV or UAV, 13 for a constant buffer, 14 for a
83    /// sampler), in the 24-byte record only.
84    pub kind: Option<u32>,
85    /// The record's flags, in the 24-byte record only.
86    pub flags: Option<u32>,
87}
88
89/// A signature element record (16 bytes).
90#[derive(Clone, Copy, Debug, PartialEq, Eq)]
91pub struct Element {
92    /// The offset of the semantic name in the string table (an empty string for a system value).
93    pub name: u32,
94    /// The offset of the element's first semantic index in the semantic-index table.
95    pub indices: u32,
96    /// How many registers the element spans.
97    pub rows: u8,
98    /// The first register.
99    pub start_row: u8,
100    /// How many components of each register it uses.
101    pub columns: u8,
102    /// The first component (0 x to 3 w).
103    pub start_column: u8,
104    /// The packer placed the element in registers.
105    pub allocated: bool,
106    /// DXIL semantic kind (0 arbitrary, 3 `SV_Position`, ...).
107    pub semantic_kind: u8,
108    /// The component type (1 u32, 2 i32, 3 f32: not the metadata's numbers, which are 5, 4 and 9).
109    pub component_type: u8,
110    /// DXIL interpolation mode.
111    pub interpolation: u8,
112    /// The components indexed dynamically, a 4-bit mask.
113    pub dynamic_index_mask: u8,
114    /// The output stream (0 to 3).
115    pub stream: u8,
116    /// The record's last byte, as stored.
117    pub reserved: u8,
118}
119
120/// A `PSV0` part.
121#[derive(Clone, Debug, PartialEq, Eq)]
122pub struct Psv {
123    /// The runtime information.
124    pub runtime: RuntimeInfo,
125    /// The resource records, in the part's order (constant buffers, samplers, SRVs, UAVs).
126    pub resources: Vec<Resource>,
127    /// The string table: names, null-terminated.
128    pub strings: Vec<u8>,
129    /// The semantic-index table.
130    pub semantic_indices: Vec<u32>,
131    /// The input elements.
132    pub inputs: Vec<Element>,
133    /// The output elements.
134    pub outputs: Vec<Element>,
135    /// The patch-constant elements.
136    pub patch_constants: Vec<Element>,
137    /// The part after the elements: the dependency tables, as words.
138    pub dependencies: Vec<u32>,
139}
140
141impl Psv {
142    /// The string at `offset` of the string table (an error where it is outside the table, has no end, or is not UTF-8).
143    pub fn string(&self, offset: u32) -> Result<&str> {
144        let rest = usize::try_from(offset)
145            .ok()
146            .and_then(|at| self.strings.get(at..))
147            .ok_or_else(|| error(0, format!("PSV0: string {offset} past the string table")))?;
148        let end = rest
149            .iter()
150            .position(|b| *b == 0)
151            .ok_or_else(|| error(0, "PSV0: a string without its end"))?;
152        std::str::from_utf8(rest.get(..end).unwrap_or_default())
153            .map_err(|_| error(0, "PSV0: a string that is not UTF-8"))
154    }
155
156    /// The semantic indices of `element`, one for each of its rows.
157    pub fn element_indices(&self, element: &Element) -> Result<&[u32]> {
158        usize::try_from(element.indices)
159            .ok()
160            .and_then(|start| {
161                let end = start.checked_add(usize::from(element.rows))?;
162                self.semantic_indices.get(start..end)
163            })
164            .ok_or_else(|| error(0, "PSV0: an element's semantic indices lie past the index table"))
165    }
166}
167
168/// Reads the `PSV0` part `data`. Anything outside the layout above (a runtime info or record of another size, a table
169/// past the part's end, a remainder that is not whole words) is an error naming the bit.
170pub fn read(data: &[u8]) -> Result<Psv> {
171    let view = View::new(data);
172    let bit = |at: usize| at.saturating_mul(8);
173    let word = |at: usize| -> Result<u32> {
174        view.array::<4>(at)
175            .map(u32::from_le_bytes)
176            .map_err(|e| error(bit(at), format!("PSV0: {e}")))
177    };
178    let count = |at: usize, what: &str| -> Result<usize> {
179        usize::try_from(word(at)?).map_err(|_| error(bit(at), format!("PSV0: {what} past the address space")))
180    };
181    let advance = |at: usize, by: usize| {
182        at.checked_add(by)
183            .ok_or_else(|| error(bit(at), "PSV0: a table past the address space"))
184    };
185
186    let info_size = count(0, "the runtime info size")?;
187    let mut at = 4usize;
188    if !matches!(info_size, 36 | 48 | 52) {
189        return Err(error(
190            0,
191            format!("PSV0: a runtime info of {info_size} bytes, where only 36, 48 and 52 are read"),
192        ));
193    }
194    let info = view
195        .slice(at, info_size)
196        .map_err(|e| error(bit(at), format!("PSV0: {e}")))?;
197    let info_word = |i: usize| -> Result<u32> {
198        let start = i.saturating_mul(4);
199        info.get(start..start.saturating_add(4))
200            .and_then(|w| <[u8; 4]>::try_from(w).ok())
201            .map(u32::from_le_bytes)
202            .ok_or_else(|| error(bit(at), "PSV0: the runtime info is cut short"))
203    };
204    let byte = |i: usize| -> Result<u8> {
205        info.get(i)
206            .copied()
207            .ok_or_else(|| error(bit(at), "PSV0: the runtime info is cut short"))
208    };
209    let runtime = RuntimeInfo {
210        stage_words: [info_word(0)?, info_word(1)?, info_word(2)?, info_word(3)?],
211        min_wave_lanes: info_word(4)?,
212        max_wave_lanes: info_word(5)?,
213        stage: byte(24)?,
214        uses_view_id: byte(25)? != 0,
215        stage_data: u16::from_le_bytes([byte(26)?, byte(27)?]),
216        element_counts: [byte(28)?, byte(29)?, byte(30)?],
217        input_vectors: byte(31)?,
218        output_vectors: [byte(32)?, byte(33)?, byte(34)?, byte(35)?],
219        num_threads: if info_size >= 48 {
220            Some([info_word(9)?, info_word(10)?, info_word(11)?])
221        } else {
222            None
223        },
224        entry_name: if info_size >= 52 {
225            Some(info_word(12)?)
226        } else {
227            None
228        },
229    };
230    at = advance(at, info_size)?;
231
232    let resource_count = count(at, "the resource count")?;
233    at = advance(at, 4)?;
234    let mut resources = Vec::new();
235    if resource_count > 0 {
236        if resource_count > MAX_RECORDS {
237            return Err(error(bit(at), format!("PSV0: {resource_count} resource records")));
238        }
239        let size = count(at, "the resource record size")?;
240        at = advance(at, 4)?;
241        if !matches!(size, 16 | 24) {
242            return Err(error(
243                bit(at),
244                format!("PSV0: resource records of {size} bytes, where only 16 and 24 are read"),
245            ));
246        }
247        for _ in 0..resource_count {
248            let long = size == 24;
249            resources.push(Resource {
250                resource_type: word(at)?,
251                space: word(advance(at, 4)?)?,
252                lower: word(advance(at, 8)?)?,
253                upper: word(advance(at, 12)?)?,
254                kind: if long { Some(word(advance(at, 16)?)?) } else { None },
255                flags: if long { Some(word(advance(at, 20)?)?) } else { None },
256            });
257            at = advance(at, size)?;
258        }
259    }
260
261    let strings_size = count(at, "the string table size")?;
262    at = advance(at, 4)?;
263    let strings = view
264        .slice(at, strings_size)
265        .map_err(|e| error(bit(at), format!("PSV0: {e}")))?
266        .to_vec();
267    at = advance(at, strings_size)?;
268
269    let index_count = count(at, "the semantic index count")?;
270    at = advance(at, 4)?;
271    let index_bytes = view
272        .slice(
273            at,
274            index_count
275                .checked_mul(4)
276                .ok_or_else(|| error(bit(at), "PSV0: the semantic index count"))?,
277        )
278        .map_err(|e| error(bit(at), format!("PSV0: {e}")))?;
279    let semantic_indices = index_bytes
280        .as_chunks::<4>()
281        .0
282        .iter()
283        .map(|w| u32::from_le_bytes(*w))
284        .collect();
285    at = advance(at, index_bytes.len())?;
286
287    let mut elements = [Vec::new(), Vec::new(), Vec::new()];
288    if runtime.element_counts.iter().any(|n| *n > 0) {
289        let size = count(at, "the element size")?;
290        at = advance(at, 4)?;
291        if size != 16 {
292            return Err(error(
293                bit(at),
294                format!("PSV0: signature elements of {size} bytes, where only 16 are read"),
295            ));
296        }
297        for (list, n) in elements.iter_mut().zip(runtime.element_counts) {
298            for _ in 0..n {
299                let raw = view
300                    .array::<16>(at)
301                    .map_err(|e| error(bit(at), format!("PSV0: {e}")))?;
302                let [
303                    n0,
304                    n1,
305                    n2,
306                    n3,
307                    i0,
308                    i1,
309                    i2,
310                    i3,
311                    rows,
312                    start_row,
313                    packed,
314                    kind,
315                    component,
316                    interpolation,
317                    masks,
318                    reserved,
319                ] = raw;
320                list.push(Element {
321                    name: u32::from_le_bytes([n0, n1, n2, n3]),
322                    indices: u32::from_le_bytes([i0, i1, i2, i3]),
323                    rows,
324                    start_row,
325                    columns: packed & 15,
326                    start_column: (packed >> 4) & 3,
327                    allocated: (packed >> 6) & 1 == 1,
328                    semantic_kind: kind,
329                    component_type: component,
330                    interpolation,
331                    dynamic_index_mask: masks & 15,
332                    stream: (masks >> 4) & 3,
333                    reserved,
334                });
335                at = advance(at, 16)?;
336            }
337        }
338    }
339    let [inputs, outputs, patch_constants] = elements;
340
341    let rest = data.get(at..).unwrap_or_default();
342    let (words, ragged) = rest.as_chunks::<4>();
343    if !ragged.is_empty() {
344        return Err(error(
345            bit(at),
346            format!(
347                "PSV0: {} bytes after the elements, not a whole number of words",
348                rest.len()
349            ),
350        ));
351    }
352    Ok(Psv {
353        runtime,
354        resources,
355        strings,
356        semantic_indices,
357        inputs,
358        outputs,
359        patch_constants,
360        dependencies: words.iter().map(|w| u32::from_le_bytes(*w)).collect(),
361    })
362}
363
364/// The `PSV0` part of a container's directory, if it has one (a library has none; it has runtime data, [`crate::rdat`]).
365pub fn part(chunks: &[dxcontainer::Chunk<'_>]) -> Result<Option<Psv>> {
366    chunks
367        .iter()
368        .find(|c| &c.kind == b"PSV0")
369        .map(|c| read(c.data))
370        .transpose()
371}