Skip to main content

dxil_reader/
sampling.rs

1//! Which sampler each texture read of a module uses: every `dx.op` call that samples, gathers, queries the level of
2//! detail of or loads a resource, with the register, space and class of the texture's handle and of the sampler's
3//! handle, and whether the operation compares.
4use std::collections::HashMap;
5
6use crate::Result;
7use crate::module::{Constant, Function, Module, Op, Value, ValueId};
8use crate::reflection::ranges;
9
10const CREATE_HANDLE: i64 = 57;
11const ANNOTATE_HANDLE: i64 = 216;
12const CREATE_HANDLE_FROM_BINDING: i64 = 217;
13
14/// A resource handle as the program made it: the register of the resource in its class and space.
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub struct Handle {
17    /// 0 shader resource view, 1 unordered-access view, 2 constant buffer, 3 sampler.
18    pub class: u8,
19    /// The register space.
20    pub space: u32,
21    /// The register (`t5`, `s2`): the handle's index, which the operations number absolutely within the class and
22    /// space. None where the index is computed (an array of resources indexed by a variable).
23    pub register: Option<u32>,
24    /// The range id of a `createHandle` handle (the id of the `dx.resources` record); none for a handle made from a
25    /// binding, which names no record.
26    pub range: Option<u32>,
27}
28
29/// One call of a texture-reading operation.
30#[derive(Clone, Debug, PartialEq, Eq)]
31pub struct TextureRead {
32    /// The function the call is in.
33    pub function: ValueId,
34    /// The call's position among the function's instructions.
35    pub instruction: usize,
36    /// The operation's `dx.op` opcode (60 `sample`, 61 `sampleBias`, 62 `sampleLevel`, 63 `sampleGrad`, 64 `sampleCmp`,
37    /// 65 `sampleCmpLevelZero`, 66 `textureLoad`, 68 `bufferLoad`, 73 `textureGather`, 74 `textureGatherCmp`,
38    /// 81 `calculateLOD`, 139 `rawBufferLoad`).
39    pub opcode: u16,
40    /// The resource read; none where its handle is not the result of a `createHandle` or `createHandleFromBinding`
41    /// call (through `annotateHandle`) of the same function, for instance a handle chosen by a `select` or `phi`, or
42    /// one made from the descriptor heap.
43    pub texture: Option<Handle>,
44    /// The operation takes a sampler (every one but the loads).
45    pub sampled: bool,
46    /// The sampler, where the operation takes one and its handle is resolved as for [`TextureRead::texture`].
47    pub sampler: Option<Handle>,
48    /// The operation compares the sampled value with a reference (`sampleCmp`, `sampleCmpLevelZero`,
49    /// `textureGatherCmp`).
50    pub comparison: bool,
51}
52
53/// How an opcode reads a texture: whether it takes a sampler and whether it compares.
54fn kind(opcode: i64) -> Option<(bool, bool)> {
55    Some(match opcode {
56        60..=63 | 73 | 81 => (true, false),
57        64 | 65 | 74 => (true, true),
58        66 | 68 | 139 => (false, false),
59        _ => return None,
60    })
61}
62
63/// A value of the module or of the function's body.
64fn value<'a>(m: &'a Module, f: &'a Function, id: ValueId) -> Option<&'a Value> {
65    match id.checked_sub(f.first) {
66        None => m.values.get(id),
67        Some(local) => f.locals.get(local),
68    }
69}
70
71/// The integer a constant is. Zero is stored as the null constant of its type.
72fn integer(m: &Module, f: &Function, id: ValueId) -> Option<i64> {
73    match value(m, f, id)? {
74        Value::Constant(Constant::Integer(i)) => Some(*i),
75        Value::Constant(Constant::Null) => Some(0),
76        _ => None,
77    }
78}
79
80fn number(m: &Module, f: &Function, id: ValueId) -> Option<u32> {
81    integer(m, f, id).and_then(|i| u32::try_from(i).ok())
82}
83
84/// The handle the value `id` is: a `createHandle` or `createHandleFromBinding` call, looked through any
85/// `annotateHandle`. `spaces` maps a range's class and id to its register space.
86fn handle(
87    m: &Module,
88    f: &Function,
89    calls: &HashMap<ValueId, usize>,
90    spaces: &HashMap<(u8, u32), u32>,
91    id: ValueId,
92) -> Option<Handle> {
93    let mut id = id;
94    // an annotated handle is the handle it annotates; a chain is at most a few calls long
95    for _ in 0..8 {
96        let Op::Call { args, .. } = &f.instructions.get(*calls.get(&id)?)?.op else {
97            return None;
98        };
99        match integer(m, f, *args.first()?)? {
100            ANNOTATE_HANDLE => id = *args.get(1)?,
101            CREATE_HANDLE => {
102                let class = u8::try_from(integer(m, f, *args.get(1)?)?).ok()?;
103                let range = number(m, f, *args.get(2)?)?;
104                return Some(Handle {
105                    class,
106                    space: *spaces.get(&(class, range))?,
107                    register: number(m, f, *args.get(3)?),
108                    range: Some(range),
109                });
110            }
111            CREATE_HANDLE_FROM_BINDING => {
112                // the binding is a constant { lower bound, upper bound, space, class }
113                let Value::Constant(Constant::Aggregate(fields)) = value(m, f, *args.get(1)?)? else {
114                    return None;
115                };
116                return Some(Handle {
117                    class: u8::try_from(integer(m, f, *fields.get(3)?)?).ok()?,
118                    space: number(m, f, *fields.get(2)?)?,
119                    register: number(m, f, *args.get(2)?),
120                    range: None,
121                });
122            }
123            _ => return None,
124        }
125    }
126    None
127}
128
129/// Every texture-reading call of the module's function bodies, in order. A call whose handles can't be resolved is
130/// listed with `None` for them.
131pub fn texture_reads(m: &Module) -> Result<Vec<TextureRead>> {
132    let spaces: HashMap<(u8, u32), u32> = ranges(m)?
133        .into_iter()
134        .map(|(range, _, _)| ((range.class, range.id), range.space))
135        .collect();
136    // whether a callee is a `dx.op` function, by value id
137    let mut operations: HashMap<ValueId, bool> = HashMap::new();
138    let mut reads = Vec::new();
139    for f in &m.functions {
140        // the call that defines each instruction result
141        let calls: HashMap<ValueId, usize> = f
142            .instructions
143            .iter()
144            .enumerate()
145            .filter_map(|(at, i)| i.result.map(|(id, _)| (id, at)))
146            .collect();
147        for (at, instruction) in f.instructions.iter().enumerate() {
148            let Op::Call { function, args, .. } = &instruction.op else {
149                continue;
150            };
151            let is_operation = *operations
152                .entry(*function)
153                .or_insert_with(|| m.name(*function).is_some_and(|n| n.starts_with("dx.op.")));
154            if !is_operation {
155                continue;
156            }
157            let Some(opcode) = args.first().and_then(|a| integer(m, f, *a)) else {
158                continue;
159            };
160            let Some((sampled, comparison)) = kind(opcode) else {
161                continue;
162            };
163            let Ok(opcode) = u16::try_from(opcode) else {
164                continue;
165            };
166            let resolve =
167                |position: usize| args.get(position).and_then(|a| handle(m, f, &calls, &spaces, *a));
168            reads.push(TextureRead {
169                function: f.value,
170                instruction: at,
171                opcode,
172                texture: resolve(1),
173                sampled,
174                sampler: if sampled { resolve(2) } else { None },
175                comparison,
176            });
177        }
178    }
179    Ok(reads)
180}