Skip to main content

dxil_reader/
reflection.rs

1//! The entry point's metadata, resource records, type annotations and signature parts of an SM6 container, as the
2//! records the file stores: the entry point and what its properties state (thread group, geometry, mesh, hull and
3//! domain state), its signature elements and their packing, the bound resource ranges of `dx.resources` with their
4//! tags, the field annotations of `dx.typeAnnotations`, and the rows of an `ISG1`/`OSG1`/`PSG1` part, each with DXIL's
5//! and D3D's own numbers, and the entry point's other property tags, the tail tags of its signature elements, the
6//! optional fields of the resource records and the string attributes of the entry function. What the numbers mean for a
7//! graphics API is the caller's to decide.
8use crate::module::{Constant, Metadata, MetadataId, Module, Type, TypeId, Value, ValueId};
9use crate::{Result, error};
10
11/// One signature element of the entry point: its id (the `sigId` loadInput and storeOutput name), its first
12/// register and column, and its size.
13#[derive(Clone, Debug, PartialEq, Eq)]
14pub struct Element {
15    /// The id the `loadInput` and `storeOutput` operations name the element by.
16    pub id: u32,
17    /// The semantic name.
18    pub name: String,
19    /// The first semantic index.
20    pub index: u32,
21    /// DXIL component type (5 u32, 4 i32, 9 f32, ...).
22    pub component_type: u8,
23    /// DXIL semantic kind (0 arbitrary, 1 SV_VertexID, 3 SV_Position, 13 SV_IsFrontFace, ...).
24    pub system_value: u8,
25    /// DXIL interpolation mode (1 constant, 2 linear, 4 linear noperspective, ...).
26    pub interpolation: u8,
27    /// The first register the element occupies.
28    pub row: u32,
29    /// The first component (0 x to 3 w) of that register.
30    pub column: u8,
31    /// How many registers it spans.
32    pub rows: u32,
33    /// How many components of each register it uses.
34    pub columns: u8,
35    /// An element the packer did not place (system values read another way): no register.
36    pub placed: bool,
37}
38/// A bound resource range: class (0 SRV, 1 UAV, 2 constant buffer, 3 sampler), range id, first register, count.
39#[derive(Clone, Copy, Debug, PartialEq, Eq)]
40pub struct Range {
41    /// 0 SRV, 1 UAV, 2 constant buffer, 3 sampler.
42    pub class: u8,
43    /// The range id the resource operations name the range by.
44    pub id: u32,
45    /// The first register of the range.
46    pub lower: u32,
47    /// The number of registers; `u32::MAX` where the range is unbounded.
48    pub count: u32,
49    /// Constant buffers: their size in bytes; SRVs and UAVs: their shape (`DxilResourceKind`, 2 a 2D texture);
50    /// samplers: their kind (0 default, 1 comparison, 2 mono).
51    pub extra: u32,
52    /// The register space.
53    pub space: u32,
54    /// An SRV's or UAV's element type tag (`DxilComponentType`), where its tags state one.
55    pub element: Option<u32>,
56    /// A structured SRV's or UAV's element stride in bytes, where its tags state one.
57    pub stride: Option<u32>,
58    /// An unordered-access view with a hidden counter (an append or consume buffer, `IncrementCounter`).
59    pub counter: bool,
60    /// The module global a library names the resource by (a program of a library makes its handles from the
61    /// global's value).
62    pub variable: Option<ValueId>,
63}
64
65/// A metadata node being read. A problem with it is reported at the bit of the bitcode its record starts at.
66#[derive(Clone, Copy, Debug)]
67struct Node<'a> {
68    m: &'a Module,
69    id: MetadataId,
70    ops: &'a [Option<MetadataId>],
71    /// What the node is, for the messages.
72    what: &'static str,
73}
74
75impl<'a> Node<'a> {
76    /// The node `id`; where it is not a node the error is at the bit `at` (the record that refers to it).
77    fn new(m: &'a Module, at: usize, id: MetadataId, what: &'static str) -> Result<Self> {
78        match m.metadata.get(id) {
79            Some(Metadata::Node(ops)) => Ok(Node { m, id, ops, what }),
80            _ => Err(error(at, format!("{what}: expected a metadata node"))),
81        }
82    }
83    fn bit(&self) -> usize {
84        self.m.metadata_at.get(self.id).copied().unwrap_or(self.m.at)
85    }
86    /// The error for something wrong in this node.
87    fn fail(&self, detail: impl std::fmt::Display) -> crate::Error {
88        error(self.bit(), format!("{}: {detail}", self.what))
89    }
90    /// The node an operand names, which must not be null.
91    fn node(&self, id: Option<MetadataId>, what: &'static str) -> Result<Node<'a>> {
92        let id = id.ok_or_else(|| self.fail(format!("{what} is null")))?;
93        Node::new(self.m, self.bit(), id, what)
94    }
95    /// Operand `i`, which must exist (it may be null).
96    fn op(&self, i: usize) -> Result<Option<MetadataId>> {
97        self.ops
98            .get(i)
99            .copied()
100            .ok_or_else(|| self.fail(format!("node has no operand {i}")))
101    }
102    /// Operand `i`, null where the node has none.
103    fn maybe(&self, i: usize) -> Option<MetadataId> {
104        self.ops.get(i).copied().flatten()
105    }
106    /// An integer constant metadata operand.
107    fn int_of(&self, id: Option<MetadataId>, what: &str) -> Result<i64> {
108        match id.and_then(|i| self.m.metadata.get(i)) {
109            Some(Metadata::Value { value, .. }) => match self.m.values.get(*value) {
110                Some(Value::Constant(Constant::Integer(v))) => Ok(*v),
111                // Zero is stored as the null constant.
112                Some(Value::Constant(Constant::Null)) => Ok(0),
113                _ => Err(self.fail(format!("{what}: expected an integer constant"))),
114            },
115            _ => Err(self.fail(format!("{what}: expected an integer"))),
116        }
117    }
118    fn int(&self, i: usize, what: &str) -> Result<i64> {
119        self.int_of(self.op(i)?, what)
120    }
121    /// An integer operand in the range of `T`.
122    fn number_of<T: TryFrom<i64>>(&self, id: Option<MetadataId>, what: &str) -> Result<T> {
123        let v = self.int_of(id, what)?;
124        T::try_from(v).map_err(|_| self.fail(format!("{what}: {v} is out of range")))
125    }
126    fn number<T: TryFrom<i64>>(&self, i: usize, what: &str) -> Result<T> {
127        self.number_of(self.op(i)?, what)
128    }
129    /// The string an operand names: empty where it names none; an error where it is not UTF-8.
130    fn string_of(&self, id: Option<MetadataId>, what: &str) -> Result<String> {
131        match id.and_then(|i| self.m.metadata.get(i)) {
132            Some(Metadata::String(s)) => std::str::from_utf8(s).map(str::to_owned).map_err(|e| {
133                self.fail(format!(
134                    "{what}: not UTF-8 (valid up to byte {})",
135                    e.valid_up_to()
136                ))
137            }),
138            _ => Ok(String::new()),
139        }
140    }
141}
142
143/// The named metadata `name`: the bit its record starts at and the nodes it lists.
144fn named<'a>(m: &'a Module, name: &str) -> Option<(usize, &'a [MetadataId])> {
145    let i = m.named_metadata.iter().position(|(n, _)| n == name)?;
146    let (_, ids) = m.named_metadata.get(i)?;
147    Some((
148        m.named_metadata_at.get(i).copied().unwrap_or(m.at),
149        ids.as_slice(),
150    ))
151}
152
153/// The shader kind a library's entry point states (properties tag 8): the numbers of a program's kind (7 ray
154/// generation to 12 callable, 13 mesh).
155pub fn library_kind(m: &Module) -> Result<u32> {
156    let entry = the_entry(m)?;
157    let properties = entry
158        .node(entry.maybe(4), "entry point properties")
159        .map_err(|_| entry.fail("a library entry point without properties"))?;
160    for pair in properties.ops.chunks(2) {
161        let [tag, value] = pair else {
162            return Err(properties.fail("a tag without a value"));
163        };
164        if properties.int_of(*tag, "property tag")? == 8 {
165            return properties.number_of(*value, "shader kind");
166        }
167    }
168    Err(properties.fail("a library entry point that states no shader kind"))
169}
170
171/// The one entry point's record. A library also has a record with no function, which holds its resources.
172fn the_entry(m: &Module) -> Result<Node<'_>> {
173    let (at, entries) =
174        named(m, "dx.entryPoints").ok_or_else(|| error(m.at, "the module has no dx.entryPoints metadata"))?;
175    let mut with_function = Vec::new();
176    for &id in entries {
177        let entry = Node::new(m, at, id, "entry point")?;
178        if entry.op(0)?.is_some() {
179            with_function.push(entry);
180        }
181    }
182    match with_function.as_slice() {
183        [entry] => Ok(*entry),
184        _ => Err(error(
185            at,
186            format!(
187                "dx.entryPoints: expected exactly one entry point with a function, found {}",
188                with_function.len()
189            ),
190        )),
191    }
192}
193
194/// The entry point's function value.
195pub fn entry(m: &Module) -> Result<ValueId> {
196    let entry = the_entry(m)?;
197    match entry.op(0)?.and_then(|i| m.metadata.get(i)) {
198        Some(Metadata::Value { value, .. }) => Ok(*value),
199        _ => Err(entry.fail("operand 0 names no function")),
200    }
201}
202
203/// The entry point's thread group size (`numthreads`): the value of the properties' tag 4 (a mesh or amplification
204/// program's, the first field of its state, tag 9 or 10). `None` when the entry point states none (every stage but
205/// compute, mesh and amplification).
206pub fn num_threads(m: &Module) -> Result<Option<[u32; 3]>> {
207    let entry = the_entry(m)?;
208    let Some(properties) = entry.maybe(4) else {
209        return Ok(None);
210    };
211    let properties = entry.node(Some(properties), "entry point properties")?;
212    for pair in properties.ops.chunks(2) {
213        let [tag, value] = pair else {
214            return Err(properties.fail("a tag without a value"));
215        };
216        let tag = properties.int_of(*tag, "property tag")?;
217        if tag != 4 && tag != 9 && tag != 10 {
218            continue;
219        }
220        let mut size = properties.node(*value, "numthreads")?;
221        // a mesh or amplification program states them first in its state
222        if tag == 9 || tag == 10 {
223            size = size.node(size.op(0)?, "numthreads")?;
224        }
225        return Ok(Some([
226            size.number(0, "numthreads x")?,
227            size.number(1, "numthreads y")?,
228            size.number(2, "numthreads z")?,
229        ]));
230    }
231    Ok(None)
232}
233
234/// The entry point's shader flags: the value of the properties' tag 0, a 64-bit mask kept as stored. `None` when the
235/// entry point states none (the properties are null, or have no tag 0). The compiler writes the tag in nearly every
236/// entry point of the games read; this reader attaches no meaning to its bits.
237pub fn shader_flags(m: &Module) -> Result<Option<u64>> {
238    let entry = the_entry(m)?;
239    let Some(properties) = entry.maybe(4) else {
240        return Ok(None);
241    };
242    let properties = entry.node(Some(properties), "entry point properties")?;
243    for pair in properties.ops.chunks(2) {
244        let [tag, value] = pair else {
245            return Err(properties.fail("a tag without a value"));
246        };
247        if properties.int_of(*tag, "property tag")? == 0 {
248            let flags = properties.int_of(*value, "shader flags")?;
249            return Ok(Some(u64::from_le_bytes(flags.to_le_bytes())));
250        }
251    }
252    Ok(None)
253}
254
255/// The usage mask of each signature element (tag 3 of the element's trailing tag list: the components of the element
256/// the program reads or writes, bit 0 the element's first component, so a scalar packed into component w states 1,
257/// not 8), in the order of [`elements`]' inputs and outputs and [`patch_elements`]; `None` for an element whose trailing tag list is absent or has no tag 3. In the stock programs
258/// the only tag the lists hold is 3 (138,044 of 145,462 elements of skate.'s, 459,288 of 545,303 of Elden Ring's);
259/// any other tag is ignored (see [`element_tags`] for them).
260pub fn usage_masks(m: &Module) -> Result<[Vec<Option<u8>>; 3]> {
261    Ok(element_tags(m)?.map(|list| list.into_iter().map(|tags| tags.usage_mask).collect()))
262}
263
264/// The trailing tags of one signature element (the element record's operand 10, a list of tag and value pairs). The
265/// compiler writes tag 0 for an output of another stream than 0, tag 2 for an input indexed dynamically and tag 3 for
266/// the components in use. Of the 691,000 elements of skate.'s and Elden Ring's programs only tag 3 occurs.
267#[derive(Clone, Debug, Default, PartialEq, Eq)]
268pub struct ElementTags {
269    /// Tag 0: the stream (of a geometry program) the element is written to; `None` where the element states none.
270    pub stream: Option<u32>,
271    /// Tag 2: the components of the element that are indexed dynamically, bit 0 the element's first component.
272    pub dynamic_index_mask: Option<u8>,
273    /// Tag 3: the usage mask ([`usage_masks`]).
274    pub usage_mask: Option<u8>,
275    /// Every other tag with an integer value, as stored.
276    pub other: Vec<(i64, i64)>,
277}
278
279/// The trailing tags of each signature element, in the order of [`elements`]' inputs and outputs and
280/// [`patch_elements`]; an element with no tag list has [`ElementTags::default`].
281pub fn element_tags(m: &Module) -> Result<[Vec<ElementTags>; 3]> {
282    let entry = the_entry(m)?;
283    let Some(signatures) = entry.op(2)? else {
284        return Ok([Vec::new(), Vec::new(), Vec::new()]);
285    };
286    let signatures = entry.node(Some(signatures), "signatures")?;
287    let list = |i: usize| -> Result<Vec<ElementTags>> {
288        let Some(list) = signatures.op(i)? else {
289            return Ok(Vec::new());
290        };
291        let list = signatures.node(Some(list), "signature")?;
292        list.ops
293            .iter()
294            .map(|e| {
295                let e = list.node(*e, "signature element")?;
296                let Some(tail) = e.maybe(10) else {
297                    return Ok(ElementTags::default());
298                };
299                let tail = e.node(Some(tail), "element tags")?;
300                let (pairs, rest) = tail.ops.as_chunks::<2>();
301                if !rest.is_empty() {
302                    return Err(tail.fail("a tag without its value"));
303                }
304                let mut tags = ElementTags::default();
305                for [tag, value] in pairs {
306                    match tail.int_of(*tag, "element tag")? {
307                        0 => tags.stream = Some(tail.number_of(*value, "element stream")?),
308                        2 => tags.dynamic_index_mask = Some(tail.number_of(*value, "dynamic index mask")?),
309                        3 => tags.usage_mask = Some(tail.number_of(*value, "usage mask")?),
310                        other => {
311                            if let Ok(v) = tail.int_of(*value, "element tag value") {
312                                tags.other.push((other, v));
313                            }
314                        }
315                    }
316                }
317                Ok(tags)
318            })
319            .collect()
320    };
321    Ok([list(0)?, list(1)?, list(2)?])
322}
323
324/// The entry point's input and output elements, by id.
325pub fn elements(m: &Module) -> Result<(Vec<Element>, Vec<Element>)> {
326    let [inputs, outputs, _] = signature_lists(m)?;
327    Ok((inputs, outputs))
328}
329
330/// A hull or domain program's patch-constant elements, by id.
331pub fn patch_elements(m: &Module) -> Result<Vec<Element>> {
332    let [_, _, patch] = signature_lists(m)?;
333    Ok(patch)
334}
335
336/// What a hull program's entry point states about its patch (the properties' tag 3): the patch-constant function, the
337/// control-point counts, the tessellator's domain, partitioning and output primitive and the largest factor.
338#[derive(Clone, Debug, PartialEq)]
339pub struct HullState {
340    /// The patch-constant function.
341    pub patch_constant: ValueId,
342    /// The number of input control points.
343    pub input_control_points: u32,
344    /// The number of output control points.
345    pub output_control_points: u32,
346    /// The tessellator domain (`D3D11_TESSELLATOR_DOMAIN`).
347    pub domain: u32,
348    /// The partitioning (`D3D11_TESSELLATOR_PARTITIONING`).
349    pub partitioning: u32,
350    /// The output primitive (`D3D11_TESSELLATOR_OUTPUT_PRIMITIVE`).
351    pub output_primitive: u32,
352    /// The largest tessellation factor.
353    pub max_tess_factor: f32,
354}
355
356/// The value of a properties tag of the entry point, if the entry point states it.
357fn entry_property(m: &Module, wanted: i64) -> Result<Option<Node<'_>>> {
358    let entry = the_entry(m)?;
359    let Some(properties) = entry.maybe(4) else {
360        return Ok(None);
361    };
362    let properties = entry.node(Some(properties), "entry point properties")?;
363    for pair in properties.ops.chunks(2) {
364        let [tag, value] = pair else {
365            return Err(properties.fail("a tag without a value"));
366        };
367        if properties.int_of(*tag, "property tag")? == wanted {
368            return properties.node(*value, "property").map(Some);
369        }
370    }
371    Ok(None)
372}
373
374/// What a geometry program's entry point states (the properties' tag 1), as stored: the primitive it reads (`input`, a
375/// `D3D_PRIMITIVE`: 1 point, 2 line, 3 triangle, 6 line and 7 triangle with adjacency), how many vertices it emits at
376/// most, the streams it writes (a mask), the topology its stream makes (a `D3D_PRIMITIVE_TOPOLOGY`: 1 point list, 3
377/// line strip, 5 triangle strip) and how many invocations run for a primitive.
378#[derive(Clone, Copy, Debug, PartialEq, Eq)]
379pub struct GeometryState {
380    /// The input primitive (`D3D_PRIMITIVE`).
381    pub input: u32,
382    /// The most vertices it emits.
383    pub max_vertices: u32,
384    /// The streams it writes, a mask.
385    pub streams: u32,
386    /// The topology of its output stream (`D3D_PRIMITIVE_TOPOLOGY`).
387    pub topology: u32,
388    /// How many invocations run for a primitive.
389    pub instances: u32,
390}
391
392/// A geometry program's state.
393pub fn geometry_state(m: &Module) -> Result<GeometryState> {
394    let state = entry_property(m, 1)?.ok_or_else(|| {
395        the_entry(m).map_or_else(
396            |e| e,
397            |e| e.fail("a geometry program without its state (properties tag 1)"),
398        )
399    })?;
400    Ok(GeometryState {
401        input: state.number(0, "geometry input primitive")?,
402        max_vertices: state.number(1, "geometry maximum vertices")?,
403        streams: state.number(2, "geometry streams")?,
404        topology: state.number(3, "geometry topology")?,
405        instances: state.number(4, "geometry instances")?,
406    })
407}
408
409/// What a mesh program's entry point states (the properties' tag 9) after its thread group, as stored: the most
410/// vertices and primitives it writes, the topology of its primitives (1 line, 2 triangle) and the size in bytes of the
411/// payload it reads from a task program. A null field is zero.
412#[derive(Clone, Copy, Debug, PartialEq, Eq)]
413pub struct MeshState {
414    /// The most vertices it writes.
415    pub max_vertices: u32,
416    /// The most primitives it writes.
417    pub max_primitives: u32,
418    /// The topology of its primitives (1 line, 2 triangle).
419    pub topology: u32,
420    /// The size in bytes of the payload it reads from a task program.
421    pub payload: u32,
422}
423
424/// A mesh program's state.
425pub fn mesh_state(m: &Module) -> Result<MeshState> {
426    let state = entry_property(m, 9)?.ok_or_else(|| {
427        the_entry(m).map_or_else(
428            |e| e,
429            |e| e.fail("a mesh program without its state (properties tag 9)"),
430        )
431    })?;
432    // a null field is zero
433    let field = |k: usize, what: &str| -> Result<u32> {
434        match state.op(k)? {
435            None => Ok(0),
436            some => state.number_of(some, what),
437        }
438    };
439    Ok(MeshState {
440        max_vertices: field(1, "mesh maximum vertices")?,
441        max_primitives: field(2, "mesh maximum primitives")?,
442        topology: field(3, "mesh topology")?,
443        payload: field(4, "mesh payload size")?,
444    })
445}
446
447/// A hull program's patch state.
448pub fn hull_state(m: &Module) -> Result<HullState> {
449    let state = entry_property(m, 3)?.ok_or_else(|| {
450        the_entry(m).map_or_else(
451            |e| e,
452            |e| e.fail("a hull program without its patch state (properties tag 3)"),
453        )
454    })?;
455    let small = |k: usize, what: &str| -> Result<u32> { state.number_of(state.maybe(k), what) };
456    let patch_constant = match state.maybe(0).and_then(|i| m.metadata.get(i)) {
457        Some(Metadata::Value { value, .. }) => *value,
458        _ => return Err(state.fail("a hull program without its patch-constant function")),
459    };
460    let max = match state.maybe(6).and_then(|i| m.metadata.get(i)) {
461        Some(Metadata::Value { value, .. }) => match m.values.get(*value) {
462            Some(Value::Constant(Constant::Float(bits))) => {
463                let bits = u32::try_from(*bits)
464                    .map_err(|_| state.fail("the maximum tessellation factor is wider than a float"))?;
465                f32::from_bits(bits)
466            }
467            _ => return Err(state.fail("the maximum tessellation factor is not a float")),
468        },
469        _ => return Err(state.fail("hull state without a maximum tessellation factor")),
470    };
471    Ok(HullState {
472        patch_constant,
473        input_control_points: small(1, "hull input control points")?,
474        output_control_points: small(2, "hull output control points")?,
475        domain: small(3, "hull domain")?,
476        partitioning: small(4, "hull partitioning")?,
477        output_primitive: small(5, "hull output primitive")?,
478        max_tess_factor: max,
479    })
480}
481
482/// A domain program's patch state (tag 2): the tessellator domain and the input control point count.
483pub fn domain_state(m: &Module) -> Result<(u32, u32)> {
484    let state = entry_property(m, 2)?.ok_or_else(|| {
485        the_entry(m).map_or_else(
486            |e| e,
487            |e| e.fail("a domain program without its patch state (properties tag 2)"),
488        )
489    })?;
490    Ok((
491        state.number_of(state.maybe(0), "domain")?,
492        state.number_of(state.maybe(1), "domain input control points")?,
493    ))
494}
495
496/// One property of the entry point as stored: the tag and the metadata record holding the value (an integer constant
497/// or a node), `None` where the value is null. The tags the compiler writes: 0 shader flags, 1 geometry state, 2 domain
498/// state, 3 hull state, 4 thread group, 5 auto binding space, 6 ray payload size, 7 ray attribute size, 8 shader kind
499/// (libraries), 9 mesh state, 10 amplification state, 11 wave size, 12 root signature, 23 wave size range.
500#[derive(Clone, Copy, Debug, PartialEq, Eq)]
501pub struct EntryProperty {
502    /// The tag.
503    pub tag: i64,
504    /// The value's metadata record.
505    pub value: Option<MetadataId>,
506}
507
508/// Every property of the entry point, in the order stored; none where the entry point states none.
509pub fn entry_properties(m: &Module) -> Result<Vec<EntryProperty>> {
510    let entry = the_entry(m)?;
511    properties_of(entry)
512}
513
514fn properties_of(entry: Node<'_>) -> Result<Vec<EntryProperty>> {
515    let Some(properties) = entry.maybe(4) else {
516        return Ok(Vec::new());
517    };
518    let properties = entry.node(Some(properties), "entry point properties")?;
519    let (pairs, rest) = properties.ops.as_chunks::<2>();
520    if !rest.is_empty() {
521        return Err(properties.fail("a tag without a value"));
522    }
523    pairs
524        .iter()
525        .map(|[tag, value]| {
526            Ok(EntryProperty {
527                tag: properties.int_of(*tag, "property tag")?,
528                value: *value,
529            })
530        })
531        .collect()
532}
533
534/// The integer a property states directly (tags 6, 7 and 8), or the first integer of the node it states (tags 5 and
535/// 11); `None` where the entry point has no such property.
536fn property_integer(m: &Module, wanted: i64) -> Result<Option<u32>> {
537    let entry = the_entry(m)?;
538    for property in properties_of(entry)? {
539        if property.tag != wanted {
540            continue;
541        }
542        let record = entry.node(property.value, "property").ok();
543        return match record {
544            Some(node) => node.number(0, "property value").map(Some),
545            None => entry.number_of(property.value, "property value").map(Some),
546        };
547    }
548    Ok(None)
549}
550
551/// The size in bytes of the ray payload a ray tracing program's entry point states (tag 6); `None` for a program
552/// that states none (a ray generation program, or any other stage).
553pub fn ray_payload_size(m: &Module) -> Result<Option<u32>> {
554    property_integer(m, 6)
555}
556
557/// The size in bytes of the hit attributes a ray tracing program's entry point states (tag 7); `None` where it states none.
558pub fn ray_attribute_size(m: &Module) -> Result<Option<u32>> {
559    property_integer(m, 7)
560}
561
562/// The register space automatic bindings take, as the entry point states it (tag 5, a one-integer node); `None` where
563/// it states none. The stock libraries state 0 on the record that holds their resources (which has no function) and
564/// the compiler option `-auto-binding-space` changes that record's value: [`library_binding_space`] reads it.
565pub fn auto_binding_space(m: &Module) -> Result<Option<u32>> {
566    property_integer(m, 5)
567}
568
569/// The auto binding space the library's resource record (the entry record with no function) states (tag 5). `None`
570/// where the module has no such record or it states none.
571pub fn library_binding_space(m: &Module) -> Result<Option<u32>> {
572    let (at, entries) =
573        named(m, "dx.entryPoints").ok_or_else(|| error(m.at, "the module has no dx.entryPoints metadata"))?;
574    for &id in entries {
575        let entry = Node::new(m, at, id, "entry point")?;
576        if entry.op(0)?.is_some() {
577            continue;
578        }
579        for property in properties_of(entry)? {
580            if property.tag == 5 {
581                let node = entry.node(property.value, "auto binding space")?;
582                return node.number(0, "auto binding space").map(Some);
583            }
584        }
585    }
586    Ok(None)
587}
588
589/// The wave size a compute, mesh or amplification program's entry point requires (tag 11, a node): its integers as
590/// stored, a single one for a fixed size; `None` where the entry point states none.
591pub fn wave_size(m: &Module) -> Result<Option<Vec<u32>>> {
592    let Some(node) = entry_property(m, 11)? else {
593        return Ok(None);
594    };
595    node.ops
596        .iter()
597        .map(|id| node.number_of(*id, "wave size"))
598        .collect::<Result<Vec<u32>>>()
599        .map(Some)
600}
601
602/// The payload size in bytes an amplification program's entry point states (the second field of its state, tag 10);
603/// `None` for any other program. The first field is the thread group ([`num_threads`]).
604pub fn amplification_payload_size(m: &Module) -> Result<Option<u32>> {
605    let Some(state) = entry_property(m, 10)? else {
606        return Ok(None);
607    };
608    state.number(1, "amplification payload size").map(Some)
609}
610
611/// The denormal mode an entry function states for 32-bit floats, as the string attribute `fp32-denorm-mode` has it
612/// (`any`, `preserve` or `ftz`). `None` where the function carries no such attribute, which is the case in all of the
613/// 69,728 programs of skate. and Elden Ring: the compiler writes the attribute only where the option asks for a mode.
614pub fn fp32_denorm_mode(m: &Module) -> Result<Option<String>> {
615    let function = entry(m)?;
616    Ok(m.string_attribute(function, "fp32-denorm-mode")
617        .map(str::to_owned))
618}
619
620fn signature_lists(m: &Module) -> Result<[Vec<Element>; 3]> {
621    let entry = the_entry(m)?;
622    let Some(signatures) = entry.op(2)? else {
623        return Ok([Vec::new(), Vec::new(), Vec::new()]);
624    };
625    let signatures = entry.node(Some(signatures), "signatures")?;
626    let list = |i: usize| -> Result<Vec<Element>> {
627        let Some(list) = signatures.op(i)? else {
628            return Ok(Vec::new());
629        };
630        let list = signatures.node(Some(list), "signature")?;
631        list.ops
632            .iter()
633            .map(|e| {
634                let e = list.node(*e, "signature element")?;
635                let row = e.int(8, "row")?;
636                let element = Element {
637                    id: e.number(0, "element id")?,
638                    name: e.string_of(e.op(1)?, "element name")?,
639                    index: match e.op(4)? {
640                        Some(list) => match e.node(Some(list), "semantic indices")?.ops.first() {
641                            Some(first) => e.number_of(*first, "semantic index")?,
642                            None => 0,
643                        },
644                        None => 0,
645                    },
646                    component_type: e.number(2, "component type")?,
647                    system_value: e.number(3, "system value")?,
648                    interpolation: e.number(5, "interpolation")?,
649                    rows: e.number(6, "rows")?,
650                    columns: e.number(7, "columns")?,
651                    placed: row >= 0,
652                    row: u32::try_from(row.max(0))
653                        .map_err(|_| e.fail(format!("row {row} is out of range")))?,
654                    column: u8::try_from(e.int(9, "column")?.max(0))
655                        .map_err(|_| e.fail("column is out of range"))?,
656                };
657                // Four lanes per register; at most 32 registers.
658                if element.rows == 0
659                    || element.columns == 0
660                    || u32::from(element.column)
661                        .checked_add(u32::from(element.columns))
662                        .is_none_or(|end| end > 4)
663                    || element.row.checked_add(element.rows).is_none_or(|end| end > 32)
664                {
665                    return Err(e.fail("the element lies outside the 32 four-lane registers"));
666                }
667                Ok(element)
668            })
669            .collect()
670    };
671    Ok([list(0)?, list(1)?, list(2)?])
672}
673
674/// A resource's tag list (alternating tag and value): the element type (tag 0) and the structured stride (tag 1).
675fn tags(resource: Node<'_>, list: Option<MetadataId>) -> Result<(Option<u32>, Option<u32>)> {
676    let Some(list) = list else {
677        return Ok((None, None));
678    };
679    let ops = resource.node(Some(list), "resource tags")?;
680    let (pairs, rest) = ops.ops.as_chunks::<2>();
681    if !rest.is_empty() {
682        return Err(ops.fail("a tag without its value"));
683    }
684    let (mut element, mut stride) = (None, None);
685    for [tag, value] in pairs {
686        let value = ops.number_of(*value, "tag value")?;
687        match ops.int_of(*tag, "tag")? {
688            0 => element = Some(value),
689            1 => stride = Some(value),
690            _ => {}
691        }
692    }
693    Ok((element, stride))
694}
695
696/// The module global a resource record's variable names: the global itself, or the one it is a cast of.
697fn variable_of(m: &Module, id: Option<MetadataId>) -> Option<ValueId> {
698    let Some(Metadata::Value { value, .. }) = id.and_then(|i| m.metadata.get(i)) else {
699        return None;
700    };
701    match m.values.get(*value)? {
702        Value::Constant(Constant::Cast { value, .. }) => Some(*value),
703        Value::Global { .. } => Some(*value),
704        _ => None,
705    }
706}
707
708/// The bound resource ranges of `dx.resources` (SRVs, UAVs, constant buffers, samplers), each with its variable's name
709/// and the type its variable points to (the resource's handle type).
710pub fn ranges(m: &Module) -> Result<Vec<(Range, String, Option<TypeId>)>> {
711    let Some((at, [resources])) = named(m, "dx.resources") else {
712        return Ok(Vec::new());
713    };
714    let lists = Node::new(m, at, *resources, "dx.resources")?;
715    let mut out = Vec::new();
716    for (class, list) in lists.ops.iter().enumerate() {
717        let Some(list) = list else { continue };
718        let class =
719            u8::try_from(class).map_err(|_| lists.fail(format!("resource class {class} is out of range")))?;
720        let list = lists.node(Some(*list), "resource list")?;
721        for r in list.ops {
722            let r = list.node(*r, "resource")?;
723            // The record's variable is an undef pointer to the resource's type.
724            let ty = match r.op(1)?.and_then(|i| m.metadata.get(i)) {
725                Some(Metadata::Value { ty, .. }) => match m.types.get(*ty) {
726                    Some(Type::Pointer(t, _)) => Some(*t),
727                    _ => None,
728                },
729                _ => None,
730            };
731            let extra = r.number(6, "resource field")?;
732            let (element, stride) = match class {
733                0 => tags(r, r.maybe(8))?,
734                1 => tags(r, r.maybe(10))?,
735                _ => (None, None),
736            };
737            out.push((
738                Range {
739                    class,
740                    id: r.number(0, "resource id")?,
741                    lower: r.number(4, "lower bound")?,
742                    // an unbounded range is written -1 (all ones in 32 bits)
743                    count: {
744                        let v = r.int(5, "range size")?;
745                        u32::try_from(v)
746                            .or_else(|_| i32::try_from(v).map(|v| u32::from_ne_bytes(v.to_ne_bytes())))
747                            .map_err(|_| r.fail(format!("range size {v} is out of range")))?
748                    },
749                    extra,
750                    space: r.number(3, "register space")?,
751                    element,
752                    stride,
753                    // a UAV's record states whether it has a counter after the shape and the coherence
754                    counter: class == 1 && r.maybe(8).is_some() && r.int(8, "counter")? != 0,
755                    variable: variable_of(m, r.op(1)?),
756                },
757                r.string_of(r.op(2)?, "resource name")?,
758                ty,
759            ));
760        }
761    }
762    Ok(out)
763}
764
765/// The optional fields of a resource record that [`Range`] leaves out, keyed by the record's class and id.
766#[derive(Clone, Debug, Default, PartialEq, Eq)]
767pub struct ResourceExtras {
768    /// The class (0 SRV, 1 UAV, 2 constant buffer, 3 sampler), as in [`Range::class`].
769    pub class: u8,
770    /// The range id, as in [`Range::id`].
771    pub id: u32,
772    /// A shader resource view's sample count (operand 7): 0 for a single-sampled view, 4 for a `Texture2DMS<_, 4>`.
773    pub sample_count: Option<u32>,
774    /// An unordered-access view declared `globallycoherent` (operand 7).
775    pub globally_coherent: bool,
776    /// An unordered-access view that is rasterizer ordered (operand 9).
777    pub rasterizer_ordered: bool,
778    /// Tag 2 of the record's tag list: the kind of the sampler-feedback texture, where it states one.
779    pub sampler_feedback: Option<u32>,
780    /// Tag 3 of the record's tag list: the view is the target of a 64-bit atomic operation.
781    pub atomic64: bool,
782    /// Every tag of the record's tag list with its value, as stored (0 element type, 1 stride, 2 sampler feedback
783    /// kind, 3 64-bit atomics).
784    pub tags: Vec<(i64, i64)>,
785}
786
787/// The optional fields of every resource record, in the order of [`ranges`]. A constant buffer's and a sampler's record
788/// have a tag list as their last operand as well.
789pub fn resource_extras(m: &Module) -> Result<Vec<ResourceExtras>> {
790    let Some((at, [resources])) = named(m, "dx.resources") else {
791        return Ok(Vec::new());
792    };
793    let lists = Node::new(m, at, *resources, "dx.resources")?;
794    let mut out = Vec::new();
795    for (class, list) in lists.ops.iter().enumerate() {
796        let Some(list) = list else { continue };
797        let class =
798            u8::try_from(class).map_err(|_| lists.fail(format!("resource class {class} is out of range")))?;
799        let list = lists.node(Some(*list), "resource list")?;
800        for r in list.ops {
801            let r = list.node(*r, "resource")?;
802            // the tag list is the last operand: 8 for an SRV, 10 for a UAV, 7 for a constant buffer and a sampler
803            let tag_operand = match class {
804                0 => 8,
805                1 => 10,
806                _ => 7,
807            };
808            let mut tags = Vec::new();
809            if let Some(tag_list) = r.maybe(tag_operand) {
810                let tag_list = r.node(Some(tag_list), "resource tags")?;
811                let (pairs, rest) = tag_list.ops.as_chunks::<2>();
812                if !rest.is_empty() {
813                    return Err(tag_list.fail("a tag without its value"));
814                }
815                for [tag, value] in pairs {
816                    tags.push((
817                        tag_list.int_of(*tag, "tag")?,
818                        tag_list.int_of(*value, "tag value")?,
819                    ));
820                }
821            }
822            let tag_value = |wanted: i64| tags.iter().find(|(t, _)| *t == wanted).map(|(_, v)| *v);
823            out.push(ResourceExtras {
824                class,
825                id: r.number(0, "resource id")?,
826                sample_count: match (class, r.maybe(7)) {
827                    (0, Some(_)) => Some(r.number(7, "sample count")?),
828                    _ => None,
829                },
830                globally_coherent: class == 1 && r.maybe(7).is_some() && r.int(7, "globally coherent")? != 0,
831                rasterizer_ordered: class == 1
832                    && r.maybe(9).is_some()
833                    && r.int(9, "rasterizer ordered")? != 0,
834                sampler_feedback: match tag_value(2) {
835                    Some(v) => Some(
836                        u32::try_from(v)
837                            .map_err(|_| r.fail(format!("sampler feedback kind {v} is out of range")))?,
838                    ),
839                    None => None,
840                },
841                atomic64: tag_value(3).is_some_and(|v| v != 0),
842                tags,
843            });
844        }
845    }
846    Ok(out)
847}
848
849/// One field of a struct type, as its annotation in `dx.typeAnnotations` states it.
850#[derive(Clone, Debug, PartialEq, Eq)]
851pub struct FieldAnnotation {
852    /// The field's name.
853    pub name: String,
854    /// Byte offset in the constant buffer.
855    pub offset: u32,
856    /// Its `DxilComponentType`: LLVM integers carry no signedness, the annotation does.
857    pub component: Option<u32>,
858    /// A matrix field's rows, columns and orientation (1 row-major, 2 column-major): LLVM lowers a matrix to an array
859    /// of its lines, so only the annotation says it is one.
860    pub matrix: Option<(u32, u32, u32)>,
861}
862
863/// Fields of a struct type from `dx.typeAnnotations` (field tag 6 the name, 3 the constant-buffer offset, 7 the
864/// component type). Empty when the module has no annotation for it.
865pub fn fields(m: &Module, t: TypeId) -> Result<Vec<FieldAnnotation>> {
866    let Some((at, annotations)) = named(m, "dx.typeAnnotations") else {
867        return Ok(Vec::new());
868    };
869    for &a in annotations {
870        let a = Node::new(m, at, a, "type annotations")?;
871        // Struct annotations: kind 0, then (struct type, annotation) pairs.
872        if a.int(0, "annotation kind")? != 0 {
873            continue;
874        }
875        let (pairs, rest) = a.ops.get(1..).unwrap_or_default().as_chunks::<2>();
876        if !rest.is_empty() {
877            return Err(a.fail("a struct type without its annotation"));
878        }
879        for [struct_type, annotation] in pairs {
880            let ty = match struct_type.and_then(|i| m.metadata.get(i)) {
881                Some(Metadata::Value { ty, .. }) => *ty,
882                _ => continue,
883            };
884            if ty != t {
885                continue;
886            }
887            let annotation = a.node(*annotation, "struct annotation")?;
888            // The first operand is the struct's own record; the fields follow.
889            let fields = annotation
890                .ops
891                .get(1..)
892                .ok_or_else(|| annotation.fail("a struct annotation without its first operand"))?;
893            return fields
894                .iter()
895                .map(|f| {
896                    let f = annotation.node(*f, "field annotation")?;
897                    let (mut name, mut offset, mut component, mut matrix) = (String::new(), 0, None, None);
898                    let (tags, rest) = f.ops.as_chunks::<2>();
899                    if !rest.is_empty() {
900                        return Err(f.fail("a tag without its value"));
901                    }
902                    for [tag, value] in tags {
903                        match f.int_of(*tag, "field tag")? {
904                            6 => name = f.string_of(*value, "field name")?,
905                            3 => offset = f.number_of(*value, "field offset")?,
906                            2 => {
907                                let ops = f.node(*value, "matrix annotation")?;
908                                matrix = Some((
909                                    ops.number(0, "matrix rows")?,
910                                    ops.number(1, "matrix columns")?,
911                                    ops.number(2, "matrix orientation")?,
912                                ));
913                            }
914                            7 => component = Some(f.number_of(*value, "field component type")?),
915                            _ => {}
916                        }
917                    }
918                    Ok(FieldAnnotation {
919                        name,
920                        offset,
921                        component,
922                        matrix,
923                    })
924                })
925                .collect();
926        }
927    }
928    Ok(Vec::new())
929}
930
931/// One element of an `ISG1`/`OSG1`/`PSG1` part as stored.
932#[derive(Clone, Debug, PartialEq, Eq)]
933pub struct SignatureElement {
934    /// The semantic name.
935    pub name: String,
936    /// The semantic index.
937    pub index: u32,
938    /// `D3D_NAME`.
939    pub system_value: u32,
940    /// `D3D_REGISTER_COMPONENT_TYPE`.
941    pub component_type: u32,
942    /// The register the element is packed into.
943    pub register: u32,
944    /// The components the element occupies, one bit each (x is bit 0).
945    pub mask: u8,
946    /// The components a program reads (inputs) or never writes (outputs), as stored.
947    pub read_write_mask: u8,
948    /// Where the record starts, in bits of the part.
949    pub bit: usize,
950}
951
952/// The elements of a signature part (`ISG1`, `OSG1` or `PSG1`): a count and the offset of the first record, then
953/// records of 32 bytes (stream, name offset, semantic index, system value, component type, register, mask and
954/// read-write mask, minimum precision). A name ends within 256 bytes.
955pub fn signature(part: &[u8]) -> Result<Vec<SignatureElement>> {
956    let bit = |byte: usize| byte.saturating_mul(8);
957    let v = primase_foundations::bytes::View::new(part);
958    let word = |at: usize| -> Result<u32> {
959        v.array::<4>(at)
960            .map(u32::from_le_bytes)
961            .map_err(|e| error(bit(at), e.to_string()))
962    };
963    // the word `delta` bytes after a record's start
964    let field = |record: usize, delta: usize| -> Result<u32> {
965        word(
966            record
967                .checked_add(delta)
968                .ok_or_else(|| error(bit(record), "signature element offset"))?,
969        )
970    };
971    let n = word(0)? as usize;
972    let first = word(4)? as usize;
973    // the records lie in the part: bound the count before anything is allocated
974    let table = n
975        .checked_mul(32)
976        .and_then(|size| first.checked_add(size))
977        .filter(|&end| end <= part.len())
978        .ok_or_else(|| {
979            error(
980                0,
981                format!(
982                    "{n} records of 32 bytes from byte {first} do not fit the part's {} bytes",
983                    part.len()
984                ),
985            )
986        })?;
987    let mut out = Vec::with_capacity(n);
988    for i in 0..n {
989        let e = i
990            .checked_mul(32)
991            .and_then(|o| o.checked_add(first))
992            .filter(|&e| e < table)
993            .ok_or_else(|| error(32, "signature element offset"))?;
994        let name_at = field(e, 4)? as usize;
995        let tail = part
996            .get(name_at..)
997            .ok_or_else(|| error(bit(name_at), "signature name offset"))?;
998        let end = tail
999            .iter()
1000            .position(|&b| b == 0)
1001            .filter(|&n| n <= 256)
1002            .ok_or_else(|| error(bit(name_at), "signature name"))?;
1003        let name = tail
1004            .get(..end)
1005            .and_then(|name| std::str::from_utf8(name).ok())
1006            .ok_or_else(|| error(bit(name_at), "signature name is not UTF-8"))?;
1007        let [mask, read_write_mask] = v
1008            .array::<2>(
1009                e.checked_add(24)
1010                    .ok_or_else(|| error(bit(e), "signature element offset"))?,
1011            )
1012            .map_err(|x| error(bit(e), x.to_string()))?;
1013        out.push(SignatureElement {
1014            name: name.to_owned(),
1015            index: field(e, 8)?,
1016            system_value: field(e, 12)?,
1017            component_type: field(e, 16)?,
1018            register: field(e, 20)?,
1019            mask,
1020            read_write_mask,
1021            bit: bit(e),
1022        });
1023    }
1024    Ok(out)
1025}