Skip to main content

pairs_dxil/
pairs_dxil.rs

1//! `cargo run -p dxil-reader --example pairs_dxil -- <file or folder>...`: for every Shader Model 6 program under the
2//! paths, print one tab-separated line per distinct texture read: the FNV-1a 64 hash of the file, its path, the stage,
3//! the texture's space and register (`t5`, `u2`; `t*` where the register is computed, `?` where the handle is not
4//! resolved), the sampler's space and register (`-` for a load), whether the operation compares, the sampler's declared
5//! kind (`-` where it states none), how many such calls the program has and their operation names. DXBC programs of
6//! Shader Model 4 and 5 and other files are skipped (see the `pairs_dxbc` example of `dxbc-reader`).
7use std::collections::BTreeMap;
8use std::path::{Path, PathBuf};
9
10use dxil_reader::sampling::{Handle, texture_reads};
11
12fn fnv1a64(bytes: &[u8]) -> u64 {
13    bytes.iter().fold(0xcbf2_9ce4_8422_2325, |h, &b| {
14        (h ^ u64::from(b)).wrapping_mul(0x0100_0000_01b3)
15    })
16}
17
18fn files(path: &Path, out: &mut Vec<PathBuf>) -> std::io::Result<()> {
19    if path.is_dir() {
20        let mut entries: Vec<PathBuf> = std::fs::read_dir(path)?
21            .map(|e| e.map(|e| e.path()))
22            .collect::<Result<_, _>>()?;
23        entries.sort();
24        for entry in entries {
25            files(&entry, out)?;
26        }
27    } else {
28        out.push(path.to_owned());
29    }
30    Ok(())
31}
32
33fn operation(opcode: u16) -> String {
34    let name = match opcode {
35        60 => "sample",
36        61 => "sampleBias",
37        62 => "sampleLevel",
38        63 => "sampleGrad",
39        64 => "sampleCmp",
40        65 => "sampleCmpLevelZero",
41        66 => "textureLoad",
42        68 => "bufferLoad",
43        73 => "textureGather",
44        74 => "textureGatherCmp",
45        81 => "calculateLOD",
46        139 => "rawBufferLoad",
47        other => return other.to_string(),
48    };
49    name.to_owned()
50}
51
52/// The space and register columns of a handle.
53fn columns(handle: Option<Handle>) -> (String, String) {
54    let Some(h) = handle else {
55        return ("?".to_owned(), "?".to_owned());
56    };
57    let letter = ["t", "u", "b", "s"]
58        .get(usize::from(h.class))
59        .copied()
60        .unwrap_or("?");
61    let register = h
62        .register
63        .map_or_else(|| format!("{letter}*"), |r| format!("{letter}{r}"));
64    (h.space.to_string(), register)
65}
66
67fn stage(kind: u32) -> String {
68    ["ps", "vs", "gs", "hs", "ds", "cs", "lib"]
69        .get(usize::try_from(kind).unwrap_or(usize::MAX))
70        .map_or_else(|| format!("kind{kind}"), |s| (*s).to_owned())
71}
72
73fn main() -> Result<(), Box<dyn std::error::Error>> {
74    let mut paths = Vec::new();
75    for argument in std::env::args().skip(1) {
76        files(Path::new(&argument), &mut paths)?;
77    }
78    let (mut programs, mut skipped) = (0_usize, 0_usize);
79    for path in &paths {
80        let bytes = std::fs::read(path)?;
81        let Ok(chunks) = dxil_reader::parts(&bytes) else {
82            skipped += 1;
83            continue;
84        };
85        let module = match dxil_reader::module(&chunks, b"DXIL") {
86            Ok(Some(module)) => module,
87            Ok(None) => {
88                skipped += 1;
89                continue;
90            }
91            Err(e) => {
92                eprintln!("{}: {e}", path.display());
93                continue;
94            }
95        };
96        let kind = dxil_reader::part(&bytes)?.program_version >> 16;
97        let reads = match texture_reads(&module) {
98            Ok(reads) => reads,
99            Err(e) => {
100                eprintln!("{}: {e}", path.display());
101                continue;
102            }
103        };
104        let samplers: Vec<_> = dxil_reader::reflection::ranges(&module)?
105            .into_iter()
106            .filter(|(range, _, _)| range.class == 3)
107            .map(|(range, _, _)| range)
108            .collect();
109        programs += 1;
110        let hash = fnv1a64(&bytes);
111        // (texture, sampler, comparison, declared kind) -> call count and operation names
112        type Key = ((String, String), (String, String), bool, String);
113        let mut pairs: BTreeMap<Key, (usize, BTreeMap<String, usize>)> = BTreeMap::new();
114        for read in reads {
115            let sampler = if read.sampled {
116                columns(read.sampler)
117            } else {
118                ("-".to_owned(), "-".to_owned())
119            };
120            let declared = read
121                .sampler
122                .and_then(|h| {
123                    let register = h.register?;
124                    samplers.iter().find(|r| {
125                        r.space == h.space
126                            && register >= r.lower
127                            && register
128                                .checked_sub(r.lower)
129                                .is_some_and(|offset| offset < r.count)
130                    })
131                })
132                .map_or_else(|| "-".to_owned(), |r| r.extra.to_string());
133            let entry = pairs
134                .entry((columns(read.texture), sampler, read.comparison, declared))
135                .or_default();
136            entry.0 += 1;
137            *entry.1.entry(operation(read.opcode)).or_default() += 1;
138        }
139        for ((texture, sampler, comparison, declared), (count, names)) in pairs {
140            let names: Vec<String> = names.iter().map(|(n, c)| format!("{n}x{c}")).collect();
141            println!(
142                "{hash:016x}\t{}\t{}\t{}\t{}\t{}\t{}\t{}\t{declared}\t{count}\t{}",
143                path.display(),
144                stage(kind),
145                texture.0,
146                texture.1,
147                sampler.0,
148                sampler.1,
149                u8::from(comparison),
150                names.join(",")
151            );
152        }
153    }
154    eprintln!("{programs} programs, {skipped} files skipped");
155    Ok(())
156}