Skip to main content

d3d9_reader/
disasm.rs

1//! A text listing of a Direct3D 9 program in the dialect of Microsoft's disassembler (`D3DDisassemble`): the
2//! constant table as comments, the program's lines indented four spaces, and the instruction slots it uses.
3use std::collections::BTreeMap;
4use std::fmt::Write;
5
6use crate::{
7    Comparison, Constant, ConstantTable, DebugInfo, Declared, Destination, Error, Instruction, Item, Literal,
8    Modifier, Opcode, Program, Register, RegisterType, Relative, Result, Source, SourceLine, Stage,
9    TextureType, Type, read,
10};
11
12/// What the listing writes, as the `D3D_DISASM_*` flags of Microsoft's disassembler choose it.
13#[derive(Clone, Debug, PartialEq, Eq)]
14#[non_exhaustive]
15pub struct Options {
16    /// The constant table's comments (cleared by `D3D_DISASM_INSTRUCTION_ONLY`, 0x40; the slot count is written
17    /// either way).
18    pub header: bool,
19    /// The arithmetic slots before each instruction, `%4u: ` (texture slots not counted; `%4u  ` before an offset),
20    /// and six spaces before a co-issued instruction, a `nop` and the other lines but the version
21    /// (`D3D_DISASM_ENABLE_INSTRUCTION_NUMBERING`, 0x04).
22    pub numbering: bool,
23    /// Each line's byte offset from the version token, `0x%08X: `, before every line but the version
24    /// (`D3D_DISASM_ENABLE_INSTRUCTION_OFFSET`, 0x20).
25    pub offsets: bool,
26    /// A `def`'s floats as their bits, `0x%08x` (`D3D_DISASM_PRINT_HEX_LITERALS`, 0x80); `defi` stays decimal.
27    pub hex_literals: bool,
28    /// What a `/Zi` build's `DBUG` comment says: a `#line` before an instruction whose source line moved, and after a
29    /// line the variables it writes (cleared by `D3D_DISASM_DISABLE_DEBUG_INFO`, 0x10).
30    pub debug_info: bool,
31}
32
33impl Default for Options {
34    fn default() -> Self {
35        Self {
36            header: true,
37            numbering: false,
38            offsets: false,
39            hex_literals: false,
40            debug_info: true,
41        }
42    }
43}
44
45impl Options {
46    /// The options of a `D3D_DISASM_*` flags word. 0x02 changes nothing. Refuses 0x01 (an HTML listing), 0x08
47    /// (which Microsoft's disassembler refuses too) and bits it does not define, naming the bit.
48    pub fn from_flags(flags: u32) -> Result<Self> {
49        let refuse = |detail: String| Error { word: 0, detail };
50        if flags & 0x01 != 0 {
51            return Err(refuse(
52                "flag 0x01, D3D_DISASM_ENABLE_COLOR_CODE (an HTML listing), is not written".into(),
53            ));
54        }
55        if flags & 0x08 != 0 {
56            return Err(refuse(
57                "flag 0x08, D3D_DISASM_ENABLE_INSTRUCTION_CYCLE, which Microsoft's disassembler refuses too, is not \
58                 written"
59                    .into(),
60            ));
61        }
62        let unknown = flags & !0xf6;
63        if unknown != 0 {
64            return Err(refuse(format!(
65                "flag bit 0x{:x} is not a D3D_DISASM flag",
66                1u32 << unknown.trailing_zeros()
67            )));
68        }
69        Ok(Self {
70            header: flags & 0x40 == 0,
71            numbering: flags & 0x04 != 0,
72            offsets: flags & 0x20 != 0,
73            hex_literals: flags & 0x80 != 0,
74            debug_info: flags & 0x10 == 0,
75        })
76    }
77}
78
79const LANES: [char; 4] = ['x', 'y', 'z', 'w'];
80
81/// The listing of a Direct3D 9 program (its version token to `END`, no container) as Microsoft's disassembler
82/// writes it with no flags, its lines ending in `\n` (the disassembler's blob also ends in a zero byte, which is not
83/// part of the text). Refuses what [`read`] refuses.
84pub fn disassemble(bytes: &[u8]) -> Result<String> {
85    disassemble_with(bytes, &Options::default())
86}
87
88/// The listing [`disassemble`] writes, with what `options` choose.
89pub fn disassemble_with(bytes: &[u8], options: &Options) -> Result<String> {
90    let program = read(bytes)?;
91    let mut out = String::new();
92    // a program without a constant table starts at its version (as inside a feature level 9 container's listing)
93    if let Some(table) = program.constants.as_ref().filter(|_| options.header) {
94        header(&mut out, table);
95    }
96    let _ = writeln!(out, "    {}", program.version);
97    let debug = program.debug.as_ref().filter(|_| options.debug_info);
98    let notes = debug.map(variables).unwrap_or_default();
99    // the line record of each record, by its byte offset
100    let records: BTreeMap<u32, usize> = debug
101        .map(|d| d.lines.iter().enumerate().map(|(n, l)| (l.offset, n)).collect())
102        .unwrap_or_default();
103    // the file and line of the last record that had one
104    let mut source: Option<(u16, u16)> = None;
105    let mut depth: usize = 0;
106    // the arithmetic slots before the next instruction, which the numbering writes (texture slots don't count)
107    let mut slot: u32 = 0;
108    for item in &program.items {
109        if matches!(item, Item::Comment(_)) {
110            continue;
111        }
112        let record = u32::try_from(item.word().saturating_mul(4))
113            .ok()
114            .and_then(|offset| records.get(&offset))
115            .copied();
116        // a `#line` where the file changes or the line moves by more than 2, after a blank line and before the prefix
117        let line = record.and_then(|n| debug.and_then(|d| d.lines.get(n)));
118        if let (
119            Some(d),
120            Some(SourceLine {
121                line,
122                file: Some(file),
123                ..
124            }),
125        ) = (debug, line)
126        {
127            match source {
128                Some((f, l)) if f == *file => {
129                    if line.abs_diff(l) > 2 {
130                        let _ = writeln!(out, "\n#line {line}");
131                    }
132                }
133                _ => {
134                    let path = d.files.get(usize::from(*file)).map_or("", String::as_str);
135                    let _ = writeln!(out, "\n#line {line} \"{path}\"");
136                }
137            }
138            source = Some((*file, *line));
139        }
140        // the number (`%4u: `, or `%4u  ` before an offset; a co-issued instruction, a `nop` and a `label` have none),
141        // then the offset
142        let mut prefix = String::new();
143        if options.numbering {
144            match item {
145                Item::Instruction(i) if !i.coissue && !matches!(i.opcode, Opcode::Nop | Opcode::Label) => {
146                    let colon = if options.offsets { ' ' } else { ':' };
147                    let _ = write!(prefix, "{slot:>4}{colon} ");
148                    slot = slot.saturating_add(slots(i, &program).1);
149                }
150                _ => prefix.push_str("      "),
151            }
152        }
153        if options.offsets {
154            let _ = write!(prefix, "0x{:08X}: ", item.word().saturating_mul(4));
155        }
156        // a co-issued instruction's `+` takes the place of the indent's last two spaces
157        let indent = match item {
158            Item::Instruction(i) if i.coissue => "  + ",
159            _ => "    ",
160        };
161        let line = match item {
162            Item::Comment(_) => continue,
163            Item::Declaration(d) => declaration(d, &program),
164            Item::Definition(d) => definition(&d.literal, d.destination.register, &program, options),
165            Item::Instruction(i) => {
166                if matches!(
167                    i.opcode,
168                    Opcode::Else | Opcode::EndIf | Opcode::EndLoop | Opcode::EndRep
169                ) {
170                    depth = depth.saturating_sub(1);
171                }
172                let text = instruction(i, &program);
173                let line = format!("{}{text}", "  ".repeat(depth));
174                if matches!(
175                    i.opcode,
176                    Opcode::If | Opcode::IfC | Opcode::Else | Opcode::Loop | Opcode::Rep
177                ) {
178                    depth = depth.saturating_add(1);
179                }
180                line
181            }
182        };
183        let _ = write!(out, "{prefix}{indent}{}", line.trim_end());
184        if let Some(written) = record.and_then(|n| notes.get(n)).filter(|w| !w.is_empty()) {
185            let _ = write!(out, "  // {}", written.join(", "));
186        }
187        out.push('\n');
188    }
189    out.push('\n');
190    footer(&mut out, &program);
191    Ok(out)
192}
193
194/// What each line record's instruction writes, by the record's place: `name<components>` for each location of each
195/// variable, in the variable table's order. A variable of the entry point's parameters is its bare name, one of another
196/// function or a constant buffer `scope::name`, and a local or a function's return value `::name`; the components are
197/// those of the lanes that hold one, in lane order.
198fn variables(debug: &DebugInfo) -> Vec<Vec<String>> {
199    let mut notes = vec![Vec::new(); debug.lines.len()];
200    for v in &debug.variables {
201        let name = match &v.scope {
202            Some(scope) if *scope == debug.entry => v.name.clone(),
203            Some(scope) => format!("{scope}::{}", v.name),
204            None => format!("::{}", v.name),
205        };
206        for l in &v.locations {
207            let components: Vec<String> = l.components.iter().flatten().map(u16::to_string).collect();
208            if let Some(n) = usize::try_from(l.instruction).ok().and_then(|n| notes.get_mut(n)) {
209                n.push(format!("{name}<{}>", components.join(",")));
210            }
211        }
212    }
213    notes
214}
215
216/// `D3DXPARAMETER_TYPE` as HLSL names it.
217fn type_kind(kind: u16) -> &'static str {
218    match kind {
219        0 => "void",
220        1 => "bool",
221        2 => "int",
222        3 => "float",
223        4 => "string",
224        5 => "texture",
225        6 => "texture1D",
226        7 => "texture2D",
227        8 => "texture3D",
228        9 => "textureCUBE",
229        10 => "sampler",
230        11 => "sampler1D",
231        12 => "sampler2D",
232        13 => "sampler3D",
233        14 => "samplerCUBE",
234        _ => "unknown",
235    }
236}
237
238/// A parameter's type as the Parameters list writes it before the name: `float4`, `row_major float4x4`,
239/// `sampler2D`.
240fn type_name(ty: &Type) -> String {
241    let kind = type_kind(ty.kind);
242    match ty.class {
243        1 => format!("{kind}{}", ty.columns),
244        2 => format!("row_major {kind}{}x{}", ty.rows, ty.columns),
245        3 => format!("{kind}{}x{}", ty.rows, ty.columns),
246        _ => kind.to_owned(),
247    }
248}
249
250/// The register bank of a `D3DXREGISTER_SET`: `b`, `i`, `c`, `s`.
251fn bank(set: u16) -> &'static str {
252    match set {
253        0 => "b",
254        1 => "i",
255        3 => "s",
256        _ => "c",
257    }
258}
259
260/// The constant table as comments: the compiler, every parameter in name order, and the registers in register
261/// order, then a blank line. A table with no constants is the compiler line alone, with no blank line after it.
262fn header(out: &mut String, table: &ConstantTable) {
263    let _ = writeln!(out, "//\n// Generated by {}", table.creator);
264    let mut constants: Vec<&Constant> = table.constants.iter().collect();
265    if constants.is_empty() {
266        return;
267    }
268    constants.sort_by(|a, b| a.name.as_bytes().cmp(b.name.as_bytes()));
269    out.push_str("//\n// Parameters:\n//\n");
270    // a uniform with a range in two register sets is one parameter
271    let mut parameters = constants.clone();
272    parameters.dedup_by(|a, b| a.name == b.name);
273    for c in &parameters {
274        parameter(out, 3, &c.ty, &c.name);
275    }
276    out.push_str("//\n//\n// Registers:\n//\n");
277    constants.sort_by_key(|c| (c.register_set, c.register));
278    // the name column is never narrower than 12
279    let width = constants.iter().map(|c| c.name.len()).max().unwrap_or(0).max(12);
280    let _ = writeln!(out, "//   {:<width$} Reg   Size", "Name");
281    let _ = writeln!(out, "//   {} ----- ----", "-".repeat(width));
282    for c in &constants {
283        let register = format!("{}{}", bank(c.register_set), c.register);
284        let _ = writeln!(out, "//   {:<width$} {register:<5} {:>4}", c.name, c.count);
285    }
286    out.push_str("//\n\n");
287}
288
289/// One line of the Parameters list, `type name[elements];`, at `indent` spaces after the `//`. A struct is a block, with
290/// its members `indent + 4` in and a blank comment line before and after it (one between two structs):
291///
292/// ```text
293/// //
294/// //   struct
295/// //   {
296/// //       float4 colour;
297/// //
298/// //   } lights[3];
299/// //
300/// ```
301fn parameter(out: &mut String, indent: usize, ty: &Type, name: &str) {
302    let pad = " ".repeat(indent);
303    let array = if ty.elements > 1 {
304        format!("[{}]", ty.elements)
305    } else {
306        String::new()
307    };
308    if ty.class == 5 {
309        let blank = format!("//{pad}\n");
310        if !out.ends_with(&blank) {
311            out.push_str(&blank);
312        }
313        let _ = write!(out, "//{pad}struct\n//{pad}{{\n");
314        for member in &ty.members {
315            parameter(out, indent.saturating_add(4), &member.ty, &member.name);
316        }
317        let _ = write!(out, "//\n//{pad}}} {name}{array};\n//{pad}\n");
318    } else {
319        let _ = writeln!(out, "//{pad}{} {name}{array};", type_name(ty));
320    }
321}
322
323fn register(r: Register, program: &Program) -> String {
324    let n = r.number;
325    let vertex3 = program.version.stage == Stage::Vertex && program.version.major == 3;
326    let indexed = |names: &[&str]| -> String {
327        usize::try_from(n)
328            .ok()
329            .and_then(|i| names.get(i))
330            .map_or_else(|| format!("?{n}"), |s| (*s).to_owned())
331    };
332    match r.kind {
333        RegisterType::Temp => format!("r{n}"),
334        RegisterType::Input => format!("v{n}"),
335        RegisterType::Const => format!("c{n}"),
336        RegisterType::Const2 => format!("c{}", n.saturating_add(2048)),
337        RegisterType::Const3 => format!("c{}", n.saturating_add(4096)),
338        RegisterType::Const4 => format!("c{}", n.saturating_add(6144)),
339        RegisterType::Address if program.version.stage == Stage::Pixel => format!("t{n}"),
340        RegisterType::Address => "a0".into(),
341        RegisterType::RasterizerOut => indexed(&["oPos", "oFog", "oPts"]),
342        RegisterType::AttributeOut => format!("oD{n}"),
343        RegisterType::Output if vertex3 => format!("o{n}"),
344        RegisterType::Output => format!("oT{n}"),
345        RegisterType::ConstInt => format!("i{n}"),
346        RegisterType::ColorOut => format!("oC{n}"),
347        RegisterType::DepthOut => "oDepth".into(),
348        RegisterType::Sampler => format!("s{n}"),
349        RegisterType::ConstBool => format!("b{n}"),
350        RegisterType::Loop => "aL".into(),
351        RegisterType::Misc => indexed(&["vPos", "vFace"]),
352        RegisterType::Label => format!("l{n}"),
353        RegisterType::Predicate => format!("p{n}"),
354        RegisterType::Texture => format!("t{n}"),
355    }
356}
357
358fn lane(l: u8) -> char {
359    LANES.get(usize::from(l)).copied().unwrap_or('?')
360}
361
362/// A source swizzle: none for `.xyzw`, one letter for a replicate, all four otherwise.
363fn swizzle(s: [u8; 4]) -> String {
364    match s {
365        [0, 1, 2, 3] => String::new(),
366        [a, b, c, d] if a == b && b == c && c == d => format!(".{}", lane(a)),
367        _ => format!(".{}", s.iter().map(|&l| lane(l)).collect::<String>()),
368    }
369}
370
371fn relative(r: Option<Relative>, program: &Program) -> String {
372    r.map_or(String::new(), |r| {
373        format!("[{}{}]", register(r.register, program), swizzle(r.swizzle))
374    })
375}
376
377fn source(s: &Source, program: &Program) -> String {
378    let name = format!(
379        "{}{}",
380        register(s.register, program),
381        relative(s.relative, program)
382    );
383    let body = match s.modifier {
384        Modifier::None | Modifier::Negate | Modifier::Not => name,
385        Modifier::Bias | Modifier::BiasNegate => format!("{name}_bias"),
386        Modifier::Sign | Modifier::SignNegate => format!("{name}_bx2"),
387        Modifier::Complement => format!("1-{name}"),
388        Modifier::X2 | Modifier::X2Negate => format!("{name}_x2"),
389        Modifier::DivideZ => format!("{name}_dz"),
390        Modifier::DivideW => format!("{name}_dw"),
391        Modifier::Abs | Modifier::AbsNegate => format!("{name}_abs"),
392    };
393    let sign = match s.modifier {
394        Modifier::Negate
395        | Modifier::BiasNegate
396        | Modifier::SignNegate
397        | Modifier::X2Negate
398        | Modifier::AbsNegate => "-",
399        Modifier::Not => "!",
400        _ => "",
401    };
402    let swizzled = if s.register.kind == RegisterType::Sampler {
403        String::new()
404    } else {
405        swizzle(s.swizzle)
406    };
407    format!("{sign}{body}{swizzled}")
408}
409
410fn destination(d: &Destination, program: &Program) -> String {
411    let mask = if d.mask == 15 {
412        String::new()
413    } else {
414        let lanes: String = (0..4u8).filter(|n| d.mask & (1 << n) != 0).map(lane).collect();
415        format!(".{lanes}")
416    };
417    format!(
418        "{}{}{mask}",
419        register(d.register, program),
420        relative(d.relative, program)
421    )
422}
423
424/// The result modifiers after a mnemonic: the pixel 1.x shift, `_sat`, `_pp`, `_centroid`.
425fn suffixes(d: Option<&Destination>) -> String {
426    let Some(d) = d else { return String::new() };
427    let mut s = match d.shift {
428        1 => "_x2",
429        2 => "_x4",
430        3 => "_x8",
431        -1 => "_d2",
432        -2 => "_d4",
433        -3 => "_d8",
434        _ => "",
435    }
436    .to_owned();
437    if d.saturate {
438        s.push_str("_sat");
439    }
440    if d.partial_precision {
441        s.push_str("_pp");
442    }
443    if d.centroid {
444        s.push_str("_centroid");
445    }
446    s
447}
448
449fn mnemonic(i: &Instruction, program: &Program) -> String {
450    let v = program.version;
451    let from14 = v.stage == Stage::Pixel && (v.major > 1 || v.minor == 4);
452    let comparison = i.comparison().map_or("", |c| match c {
453        Comparison::Greater => "_gt",
454        Comparison::Equal => "_eq",
455        Comparison::GreaterEqual => "_ge",
456        Comparison::Less => "_lt",
457        Comparison::NotEqual => "_ne",
458        Comparison::LessEqual => "_le",
459    });
460    match i.opcode {
461        Opcode::Tex if from14 => match i.control {
462            0 => "texld",
463            1 => "texldp",
464            _ => "texldb",
465        }
466        .to_owned(),
467        Opcode::TexCoord if from14 => "texcrd".into(),
468        Opcode::IfC => format!("if{comparison}"),
469        Opcode::BreakC => format!("break{comparison}"),
470        Opcode::SetP => format!("setp{comparison}"),
471        other => other.name().into(),
472    }
473}
474
475fn instruction(i: &Instruction, program: &Program) -> String {
476    if i.opcode == Opcode::BreakP {
477        return breakp(i, program);
478    }
479    let mut args = Vec::new();
480    if let Some(d) = &i.destination {
481        args.push(destination(d, program));
482    }
483    args.extend(i.sources.iter().map(|s| source(s, program)));
484    let predicate = i
485        .predicate
486        .as_ref()
487        .map_or(String::new(), |p| format!("({}) ", source(p, program)));
488    format!(
489        "{predicate}{}{} {}",
490        mnemonic(i, program),
491        suffixes(i.destination.as_ref()),
492        args.join(", ")
493    )
494}
495
496/// `breakp` as Microsoft's disassembler writes it: `break`, then its predicate source read as though its token were a
497/// destination's. The source's swizzle bits (16 to 23) are a write mask (16 to 19) and the result modifiers `_sat`,
498/// `_pp` and `_centroid` (20 to 22), and its modifier bits (24 to 27) a result shift, so `breakp p0.x` is listed
499/// `break p0.`, `breakp p0.y` `break_sat_centroid p0.xz` and `breakp !p0.x` `break_d8 p0.` (`D3DAssemble`d programs
500/// in `tests/assembled`).
501fn breakp(i: &Instruction, program: &Program) -> String {
502    let Some(p) = i.sources.first() else {
503        return "break".to_owned();
504    };
505    let bits = p.swizzle.iter().enumerate().fold(0u8, |bits, (lane, from)| {
506        let at = u32::try_from(lane).map_or(8, |l| l.saturating_mul(2));
507        bits | (from & 3).checked_shl(at).unwrap_or(0)
508    });
509    // the modifier's four bits as a signed shift: `!` (13) is -3, `_d8`
510    let shift = i8::try_from(p.modifier.code()).map_or(0, |m| if m >= 8 { m.wrapping_sub(16) } else { m });
511    let read = Destination {
512        word: p.word,
513        register: p.register,
514        mask: bits & 15,
515        saturate: bits & 16 != 0,
516        partial_precision: bits & 32 != 0,
517        centroid: bits & 64 != 0,
518        shift,
519        relative: None,
520    };
521    format!("break{} {}", suffixes(Some(&read)), destination(&read, program))
522}
523
524/// The usage names of `dcl_<usage><index>`.
525const USAGES: [&str; 14] = [
526    "position",
527    "blendweight",
528    "blendindices",
529    "normal",
530    "psize",
531    "texcoord",
532    "tangent",
533    "binormal",
534    "tessfactor",
535    "positiont",
536    "color",
537    "fog",
538    "depth",
539    "sample",
540];
541
542fn declaration(d: &crate::Declaration, program: &Program) -> String {
543    let what = match d.declared {
544        Declared::Sampler(TextureType::D2) => "_2d".to_owned(),
545        Declared::Sampler(TextureType::Cube) => "_cube".to_owned(),
546        Declared::Sampler(TextureType::Volume) => "_volume".to_owned(),
547        Declared::Semantic { .. }
548            if matches!(
549                d.destination.register.kind,
550                RegisterType::Texture | RegisterType::Misc
551            ) || (program.version.stage == Stage::Pixel && program.version.major == 2) =>
552        {
553            String::new()
554        }
555        Declared::Semantic { usage, index } => {
556            let index = if index == 0 {
557                String::new()
558            } else {
559                index.to_string()
560            };
561            let usage = USAGES.get(usize::from(usage)).copied().unwrap_or("unknown");
562            format!("_{usage}{index}")
563        }
564    };
565    format!(
566        "dcl{what}{} {}",
567        suffixes(Some(&d.destination)),
568        destination(&d.destination, program)
569    )
570}
571
572fn definition(literal: &Literal, register_: Register, program: &Program, options: &Options) -> String {
573    let (op, values) = match literal {
574        Literal::Float(f) => (
575            "def",
576            f.iter()
577                .map(|&w| {
578                    if options.hex_literals {
579                        format!("0x{w:08x}")
580                    } else {
581                        general(f32::from_bits(w))
582                    }
583                })
584                .collect::<Vec<_>>()
585                .join(", "),
586        ),
587        Literal::Integer(i) => (
588            "defi",
589            i.iter().map(ToString::to_string).collect::<Vec<_>>().join(", "),
590        ),
591        Literal::Bool(b) => ("defb", (if *b != 0 { "true" } else { "false" }).to_owned()),
592    };
593    format!("{op} {}, {values}", register(register_, program))
594}
595
596/// A float as C's `%.9g` writes it: nine significant digits, trailing zeros dropped, an exponent below 1e-4 and from
597/// 1e9.
598fn general(value: f32) -> String {
599    let value = f64::from(value);
600    if value == 0.0 {
601        return if value.is_sign_negative() { "-0" } else { "0" }.to_owned();
602    }
603    if !value.is_finite() {
604        return format!("{value}");
605    }
606    let scientific = format!("{value:.8e}");
607    let (mantissa, exponent) = scientific.split_once('e').unwrap_or((&scientific, "0"));
608    let exponent: i32 = exponent.parse().unwrap_or(0);
609    let trim = |text: &str| -> String {
610        if text.contains('.') {
611            text.trim_end_matches('0').trim_end_matches('.').to_owned()
612        } else {
613            text.to_owned()
614        }
615    };
616    if !(-4..9).contains(&exponent) {
617        let sign = if exponent < 0 { '-' } else { '+' };
618        return format!("{}e{sign}{:03}", trim(mantissa), exponent.unsigned_abs());
619    }
620    let decimals = usize::try_from(8i32.saturating_sub(exponent)).unwrap_or(0);
621    trim(&format!("{value:.decimals$}"))
622}
623
624/// The texture and arithmetic instruction slots an instruction takes, as Microsoft's listings count them: checked
625/// against every footer of a vs_3_0 and ps_3_0 corpus, against `D3DAssemble`d programs of every operation alone in each
626/// version (`tests/assembled`), and of co-issue and predication. `loop`, `endloop`, `call`, `callnz` and `ret` take
627/// what Microsoft's instruction reference states, unchecked.
628fn slots(i: &Instruction, program: &Program) -> (u32, u32) {
629    // a co-issued instruction runs in the slot of the one before it, and a `nop` takes none
630    if i.coissue || i.opcode == Opcode::Nop {
631        return (0, 0);
632    }
633    let v = program.version;
634    let pixel = v.stage == Stage::Pixel;
635    let pixel1 = pixel && v.major == 1;
636    let vertex1 = !pixel && v.major == 1;
637    let (texture, arithmetic): (u32, u32) = match i.opcode {
638        Opcode::Tex => (1, 0),
639        // pixel 1.x's texture addressing operations, `texcrd` of 1.4 among them; `texkill`, `texdepth` and `bem` are
640        // arithmetic
641        Opcode::TexCoord
642        | Opcode::TexBem
643        | Opcode::TexBemL
644        | Opcode::TexReg2Ar
645        | Opcode::TexReg2Gb
646        | Opcode::TexReg2Rgb
647        | Opcode::TexM3x2Pad
648        | Opcode::TexM3x2Tex
649        | Opcode::TexM3x2Depth
650        | Opcode::TexM3x3Pad
651        | Opcode::TexM3x3Tex
652        | Opcode::TexM3x3Spec
653        | Opcode::TexM3x3VSpec
654        | Opcode::TexM3x3
655        | Opcode::TexDp3
656        | Opcode::TexDp3Tex
657            if pixel1 =>
658        {
659            (1, 0)
660        }
661        Opcode::TexLdl => (2, 0),
662        Opcode::TexLdd => (1, 2),
663        // vertex 1.1's full-precision `exp` and `log` and its `frc` are macros
664        Opcode::Exp | Opcode::Log if vertex1 => (0, 10),
665        Opcode::Frc if vertex1 => (0, 3),
666        Opcode::SinCos => (0, 8),
667        Opcode::M4x4 | Opcode::M3x4 => (0, 4),
668        Opcode::Nrm | Opcode::Pow | Opcode::M4x3 | Opcode::M3x3 | Opcode::Sgn => (0, 3),
669        Opcode::If
670        | Opcode::IfC
671        | Opcode::BreakC
672        | Opcode::BreakP
673        | Opcode::CallNz
674        | Opcode::Rep
675        | Opcode::Loop => (0, 3),
676        Opcode::EndRep
677        | Opcode::EndLoop
678        | Opcode::Call
679        | Opcode::M3x2
680        | Opcode::Crs
681        | Opcode::Dsx
682        | Opcode::Dsy => (0, 2),
683        Opcode::Dp2Add if pixel => (0, 2),
684        Opcode::Label | Opcode::Phase => (0, 0),
685        _ => (0, 1),
686    };
687    // a predicated instruction takes one arithmetic slot more, a predicated sample too
688    let predicate = u32::from(i.predicate.is_some());
689    (texture, arithmetic.saturating_add(predicate))
690}
691
692/// `// approximately N instruction slots used`, split into texture and arithmetic slots when the program samples.
693fn footer(out: &mut String, program: &Program) {
694    let (texture, arithmetic) = program.instructions().fold((0u32, 0u32), |(t, a), i| {
695        let (ti, ai) = slots(i, program);
696        (t.saturating_add(ti), a.saturating_add(ai))
697    });
698    let total = texture.saturating_add(arithmetic);
699    let slots = if total == 1 { "slot" } else { "slots" };
700    let _ = write!(out, "// approximately {total} instruction {slots} used");
701    if texture > 0 {
702        let _ = write!(out, " ({texture} texture, {arithmetic} arithmetic)");
703    }
704    out.push('\n');
705}