1use 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
52fn 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 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}