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, Precision, Program, Register, RegisterType, Relative, Result, Source, SourceLine,
9    Stage, 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    // no instruction yet
109    let mut first = true;
110    for item in &program.items {
111        if matches!(item, Item::Comment(_)) {
112            continue;
113        }
114        let record = u32::try_from(item.word().saturating_mul(4))
115            .ok()
116            .and_then(|offset| records.get(&offset))
117            .copied();
118        // a `#line` where the file changes or the line moves by more than 2, after a blank line and before the prefix
119        let line = record.and_then(|n| debug.and_then(|d| d.lines.get(n)));
120        if let (
121            Some(d),
122            Some(SourceLine {
123                line,
124                file: Some(file),
125                ..
126            }),
127        ) = (debug, line)
128        {
129            match source {
130                Some((f, l)) if f == *file => {
131                    if line.abs_diff(l) > 2 {
132                        let _ = writeln!(out, "\n#line {line}");
133                    }
134                }
135                _ => {
136                    let path = d.files.get(usize::from(*file)).map_or("", String::as_str);
137                    let _ = writeln!(out, "\n#line {line} \"{path}\"");
138                }
139            }
140            source = Some((*file, *line));
141        }
142        // the number (`%4u: `, or `%4u  ` before an offset; a co-issued instruction, a `nop` and a `label` have none,
143        // though a co-issued first instruction's slot is counted), then the offset
144        let mut prefix = String::new();
145        if options.numbering {
146            match item {
147                Item::Instruction(i) if !i.coissue && !matches!(i.opcode, Opcode::Nop | Opcode::Label) => {
148                    let colon = if options.offsets { ' ' } else { ':' };
149                    let _ = write!(prefix, "{slot:>4}{colon} ");
150                    slot = slot.saturating_add(slots(i, false, &program).1);
151                }
152                Item::Instruction(i) => {
153                    slot = slot.saturating_add(slots(i, first, &program).1);
154                    prefix.push_str("      ");
155                }
156                _ => prefix.push_str("      "),
157            }
158        }
159        if matches!(item, Item::Instruction(_)) {
160            first = false;
161        }
162        if options.offsets {
163            let _ = write!(prefix, "0x{:08X}: ", item.word().saturating_mul(4));
164        }
165        // a co-issued instruction's `+` takes the place of the indent's last two spaces
166        let indent = match item {
167            Item::Instruction(i) if i.coissue => "  + ",
168            _ => "    ",
169        };
170        let line = match item {
171            Item::Comment(_) => continue,
172            // a declaration or definition inside flow control is indented as an instruction there is
173            Item::Declaration(d) => format!("{}{}", "  ".repeat(depth), declaration(d, &program)),
174            Item::Definition(d) => format!(
175                "{}{}",
176                "  ".repeat(depth),
177                definition(&d.literal, &d.destination, &program, options)
178            ),
179            Item::Instruction(i) => {
180                if matches!(
181                    i.opcode,
182                    Opcode::Else | Opcode::EndIf | Opcode::EndLoop | Opcode::EndRep
183                ) {
184                    depth = depth.saturating_sub(1);
185                }
186                let text = instruction(i, &program);
187                let line = format!("{}{text}", "  ".repeat(depth));
188                if matches!(
189                    i.opcode,
190                    Opcode::If | Opcode::IfC | Opcode::Else | Opcode::Loop | Opcode::Rep
191                ) {
192                    depth = depth.saturating_add(1);
193                }
194                line
195            }
196        };
197        let _ = write!(out, "{prefix}{indent}{}", line.trim_end());
198        if let Some(written) = record.and_then(|n| notes.get(n)).filter(|w| !w.is_empty()) {
199            let _ = write!(out, "  // {}", written.join(", "));
200        }
201        out.push('\n');
202    }
203    out.push('\n');
204    footer(&mut out, &program);
205    Ok(out)
206}
207
208/// What each line record's instruction writes, by the record's place: `name<components>` for each location of each
209/// variable, in the variable table's order. A variable of the entry point's parameters is its bare name, one of another
210/// function or a constant buffer `scope::name`, and a local or a function's return value `::name`; the components are
211/// those of the lanes that hold one, in lane order.
212fn variables(debug: &DebugInfo) -> Vec<Vec<String>> {
213    let mut notes = vec![Vec::new(); debug.lines.len()];
214    for v in &debug.variables {
215        let name = match &v.scope {
216            Some(scope) if *scope == debug.entry => v.name.clone(),
217            Some(scope) => format!("{scope}::{}", v.name),
218            None => format!("::{}", v.name),
219        };
220        for l in &v.locations {
221            let components: Vec<String> = l.components.iter().flatten().map(u16::to_string).collect();
222            if let Some(n) = usize::try_from(l.instruction).ok().and_then(|n| notes.get_mut(n)) {
223                n.push(format!("{name}<{}>", components.join(",")));
224            }
225        }
226    }
227    notes
228}
229
230/// A parameter's `D3DXPARAMETER_TYPE` as Microsoft's listing names it: the names of `D3D_SHADER_VARIABLE_TYPE`, which
231/// extends it, with `UNKNOWN` for the buffer, UAV and structured-buffer types and any number past them.
232fn type_kind(kind: u16) -> &'static str {
233    match kind {
234        0 => "void",
235        1 => "bool",
236        2 => "int",
237        3 => "float",
238        4 => "string",
239        5 => "Texture",
240        6 => "Texture1D",
241        7 => "Texture2D",
242        8 => "Texture3D",
243        9 => "TextureCube",
244        10 => "sampler",
245        11 => "sampler1D",
246        12 => "sampler2D",
247        13 => "sampler3D",
248        14 => "samplerCUBE",
249        15 => "PixelShader",
250        16 => "VertexShader",
251        17 => "pixelfragment",
252        18 => "vertexfragment",
253        19 => "uint",
254        20 => "byte",
255        21 => "GeometryShader",
256        22 => "RasterizerState",
257        23 => "DepthStencilState",
258        24 => "BlendState",
259        25 => "Buffer",
260        28 => "Texture1DArray",
261        29 => "Texture2DArray",
262        30 => "RenderTargetView",
263        31 => "DepthStencilView",
264        32 => "Texture2DMS",
265        33 => "Texture2DMSArray",
266        34 => "TextureCubeArray",
267        35 => "HullShader",
268        36 => "DomainShader",
269        37 => "interface",
270        38 => "ComputeShader",
271        39 => "double",
272        52 => "min8float",
273        53 => "min10float",
274        54 => "min16float",
275        55 => "min12int",
276        56 => "min16int",
277        57 => "min16uint",
278        _ => "UNKNOWN",
279    }
280}
281
282/// A parameter's type as the Parameters list writes it before the name: `float4`, `row_major float4x4`,
283/// `sampler2D`; nothing for a class past struct.
284fn type_name(ty: &Type) -> String {
285    let kind = type_kind(ty.kind);
286    match ty.class {
287        0 | 4 => kind.to_owned(),
288        1 => format!("{kind}{}", ty.columns),
289        2 => format!("row_major {kind}{}x{}", ty.rows, ty.columns),
290        3 => format!("{kind}{}x{}", ty.rows, ty.columns),
291        _ => String::new(),
292    }
293}
294
295/// The register bank of a `D3DXREGISTER_SET`: `b`, `i`, `c`, `s`.
296fn bank(set: u16) -> &'static str {
297    match set {
298        0 => "b",
299        1 => "i",
300        3 => "s",
301        _ => "c",
302    }
303}
304
305/// The constant table as comments: the compiler, every parameter in name order, and the registers in register
306/// order, then a blank line. A table with no constants is the compiler line alone, with no blank line after it.
307fn header(out: &mut String, table: &ConstantTable) {
308    let _ = writeln!(out, "//\n// Generated by {}", table.creator);
309    let mut constants: Vec<&Constant> = table.constants.iter().collect();
310    if constants.is_empty() {
311        return;
312    }
313    // the parameters in the table's order (fxc writes it sorted by name); a uniform with a range in two register sets
314    // is one parameter
315    out.push_str("//\n// Parameters:\n//\n");
316    let mut parameters = constants.clone();
317    parameters.dedup_by(|a, b| a.name == b.name);
318    let parameters: Vec<(&Type, &str)> = parameters.iter().map(|c| (&c.ty, c.name.as_str())).collect();
319    list(out, 3, &parameters);
320    out.push_str("//\n//\n// Registers:\n//\n");
321    constants.sort_by_key(|c| (c.register_set, c.register));
322    // the name column is never narrower than 12
323    let width = constants.iter().map(|c| c.name.len()).max().unwrap_or(0).max(12);
324    let _ = writeln!(out, "//   {:<width$} Reg   Size", "Name");
325    let _ = writeln!(out, "//   {} ----- ----", "-".repeat(width));
326    for c in &constants {
327        let register = format!("{}{}", bank(c.register_set), c.register);
328        let _ = writeln!(out, "//   {:<width$} {register:<5} {:>4}", c.name, c.count);
329    }
330    out.push_str("//\n\n");
331}
332
333/// The Parameters list or a struct's members, `type name[elements];` at `indent` spaces after the `//`, with a blank
334/// comment line (`//` and the indent's spaces) between a struct and the item before or after it (one between two
335/// structs) but not before the first item or after the last. A struct is a block with its members `indent + 4` in:
336///
337/// ```text
338/// //   struct
339/// //   {
340/// //       float4 colour;
341/// //
342/// //   } lights[3];
343/// ```
344fn list(out: &mut String, indent: usize, items: &[(&Type, &str)]) {
345    let pad = " ".repeat(indent);
346    let mut previous: Option<&Type> = None;
347    for &(ty, name) in items {
348        if previous.is_some_and(|p| p.class == 5 || ty.class == 5) {
349            let _ = writeln!(out, "//{pad}");
350        }
351        previous = Some(ty);
352        let array = if ty.elements > 1 {
353            format!("[{}]", ty.elements)
354        } else {
355            String::new()
356        };
357        if ty.class == 5 {
358            let _ = write!(out, "//{pad}struct\n//{pad}{{\n");
359            let members: Vec<(&Type, &str)> = ty.members.iter().map(|m| (&m.ty, m.name.as_str())).collect();
360            list(out, indent.saturating_add(4), &members);
361            let _ = writeln!(out, "//\n//{pad}}} {name}{array};");
362        } else {
363            let _ = writeln!(out, "//{pad}{} {name}{array};", type_name(ty));
364        }
365    }
366}
367
368fn register(r: Register, program: &Program) -> String {
369    let n = r.number;
370    // type 6 is `oTN` before 3.0 and `oN` in 3.0, in either stage
371    let sm3 = program.version.major == 3;
372    let indexed = |names: &[&str]| -> String {
373        usize::try_from(n)
374            .ok()
375            .and_then(|i| names.get(i))
376            .map_or_else(|| format!("?{n}"), |s| (*s).to_owned())
377    };
378    match r.kind {
379        RegisterType::Temp => format!("r{n}"),
380        RegisterType::Input => format!("v{n}"),
381        RegisterType::Const => format!("c{n}"),
382        RegisterType::Const2 => format!("c{}", n.saturating_add(2048)),
383        RegisterType::Const3 => format!("c{}", n.saturating_add(4096)),
384        RegisterType::Const4 => format!("c{}", n.saturating_add(6144)),
385        RegisterType::Address if program.version.stage == Stage::Pixel => format!("t{n}"),
386        RegisterType::Address => "a0".into(),
387        RegisterType::RasterizerOut => indexed(&["oPos", "oFog", "oPts"]),
388        RegisterType::AttributeOut => format!("oD{n}"),
389        RegisterType::Output if sm3 => format!("o{n}"),
390        RegisterType::Output => format!("oT{n}"),
391        RegisterType::ConstInt => format!("i{n}"),
392        RegisterType::ColorOut => format!("oC{n}"),
393        RegisterType::DepthOut => "oDepth".into(),
394        RegisterType::Sampler => format!("s{n}"),
395        RegisterType::ConstBool => format!("b{n}"),
396        RegisterType::Loop => "aL".into(),
397        RegisterType::Misc => indexed(&["vPos", "vFace"]),
398        RegisterType::Label => format!("l{n}"),
399        RegisterType::Predicate => format!("p{n}"),
400        RegisterType::Texture => format!("t{n}"),
401    }
402}
403
404fn lane(l: u8) -> char {
405    LANES.get(usize::from(l)).copied().unwrap_or('?')
406}
407
408/// A source swizzle: none for `.xyzw`, one letter for a replicate, all four otherwise.
409fn swizzle(s: [u8; 4]) -> String {
410    match s {
411        [0, 1, 2, 3] => String::new(),
412        [a, b, c, d] if a == b && b == c && c == d => format!(".{}", lane(a)),
413        _ => format!(".{}", s.iter().map(|&l| lane(l)).collect::<String>()),
414    }
415}
416
417fn relative(r: Option<Relative>, program: &Program) -> String {
418    r.map_or(String::new(), |r| {
419        format!("[{}{}]", register(r.register, program), swizzle(r.swizzle))
420    })
421}
422
423fn source(s: &Source, program: &Program) -> String {
424    let name = format!(
425        "{}{}",
426        register(s.register, program),
427        relative(s.relative, program)
428    );
429    let body = match s.modifier {
430        Modifier::None | Modifier::Negate | Modifier::Not => name,
431        Modifier::Bias | Modifier::BiasNegate => format!("{name}_bias"),
432        Modifier::Sign | Modifier::SignNegate => format!("{name}_bx2"),
433        Modifier::Complement => format!("1-{name}"),
434        Modifier::X2 | Modifier::X2Negate => format!("{name}_x2"),
435        Modifier::DivideZ => format!("{name}_dz"),
436        Modifier::DivideW => format!("{name}_dw"),
437        Modifier::Abs | Modifier::AbsNegate => format!("{name}_abs"),
438    };
439    let sign = match s.modifier {
440        Modifier::Negate
441        | Modifier::BiasNegate
442        | Modifier::SignNegate
443        | Modifier::X2Negate
444        | Modifier::AbsNegate => "-",
445        Modifier::Not => "!",
446        _ => "",
447    };
448    format!("{sign}{body}{}{}", swizzle(s.swizzle), precision(s.precision))
449}
450
451/// An operand's minimum precision as the listing writes it after the operand.
452fn precision(p: Precision) -> &'static str {
453    match p {
454        Precision::Min16 => " {min16f}",
455        Precision::Min2_8 => " {min2_8f}",
456        Precision::Default | Precision::Unnamed => "",
457    }
458}
459
460fn destination(d: &Destination, program: &Program) -> String {
461    let mask = if d.mask == 15 {
462        String::new()
463    } else {
464        let lanes: String = (0..4u8).filter(|n| d.mask & (1 << n) != 0).map(lane).collect();
465        format!(".{lanes}")
466    };
467    format!(
468        "{}{}{mask}{}",
469        register(d.register, program),
470        relative(d.relative, program),
471        precision(d.precision)
472    )
473}
474
475/// The result modifiers after a mnemonic: the pixel 1.x shift, `_sat`, `_pp`, `_centroid`.
476fn suffixes(d: Option<&Destination>) -> String {
477    let Some(d) = d else { return String::new() };
478    let mut s = match d.shift {
479        1 => "_x2",
480        2 => "_x4",
481        3 => "_x8",
482        -1 => "_d2",
483        -2 => "_d4",
484        -3 => "_d8",
485        _ => "",
486    }
487    .to_owned();
488    if d.saturate {
489        s.push_str("_sat");
490    }
491    if d.partial_precision {
492        s.push_str("_pp");
493    }
494    if d.centroid {
495        s.push_str("_centroid");
496    }
497    s
498}
499
500fn mnemonic(i: &Instruction, program: &Program) -> String {
501    let v = program.version;
502    // `texld` and `texcrd` from pixel 1.4 and from 2.0 in both stages
503    let from14 = v.major > 1 || (v.stage == Stage::Pixel && v.minor == 4);
504    let comparison = i.comparison().map_or("", |c| match c {
505        Comparison::Greater => "_gt",
506        Comparison::Equal => "_eq",
507        Comparison::GreaterEqual => "_ge",
508        Comparison::Less => "_lt",
509        Comparison::NotEqual => "_ne",
510        Comparison::LessEqual => "_le",
511    });
512    match i.opcode {
513        Opcode::Tex if from14 => match i.control {
514            0 => "texld",
515            1 => "texldp",
516            _ => "texldb",
517        }
518        .to_owned(),
519        Opcode::TexCoord if from14 => "texcrd".into(),
520        Opcode::IfC => format!("if{comparison}"),
521        Opcode::BreakC => format!("break{comparison}"),
522        Opcode::SetP => format!("setp{comparison}"),
523        other => other.name().into(),
524    }
525}
526
527fn instruction(i: &Instruction, program: &Program) -> String {
528    if i.opcode == Opcode::BreakP {
529        return breakp(i, program);
530    }
531    let mut args = Vec::new();
532    if let Some(d) = &i.destination {
533        args.push(destination(d, program));
534    }
535    args.extend(i.sources.iter().map(|s| source(s, program)));
536    // a predicate token's precision bits mean nothing, and the listing leaves them out
537    let predicate = i.predicate.as_ref().map_or(String::new(), |p| {
538        let p = Source {
539            precision: Precision::Default,
540            ..*p
541        };
542        format!("({}) ", source(&p, program))
543    });
544    format!(
545        "{predicate}{}{} {}",
546        mnemonic(i, program),
547        suffixes(i.destination.as_ref()),
548        args.join(", ")
549    )
550}
551
552/// `breakp` as Microsoft's disassembler writes it: `break`, then its predicate source read as though its token were a
553/// destination's. The source's swizzle bits (16 to 23) are a write mask (16 to 19) and the result modifiers `_sat`,
554/// `_pp` and `_centroid` (20 to 22), and its modifier bits (24 to 27) a result shift, so `breakp p0.x` is listed
555/// `break p0.`, `breakp p0.y` `break_sat_centroid p0.xz` and `breakp !p0.x` `break_d8 p0.` (`D3DAssemble`d programs
556/// in `tests/assembled`).
557fn breakp(i: &Instruction, program: &Program) -> String {
558    let Some(p) = i.sources.first() else {
559        return "break".to_owned();
560    };
561    let bits = p.swizzle.iter().enumerate().fold(0u8, |bits, (lane, from)| {
562        let at = u32::try_from(lane).map_or(8, |l| l.saturating_mul(2));
563        bits | (from & 3).checked_shl(at).unwrap_or(0)
564    });
565    // the modifier's four bits as a signed shift: `!` (13) is -3, `_d8`
566    let shift = i8::try_from(p.modifier.code()).map_or(0, |m| if m >= 8 { m.wrapping_sub(16) } else { m });
567    let read = Destination {
568        word: p.word,
569        register: p.register,
570        mask: bits & 15,
571        saturate: bits & 16 != 0,
572        partial_precision: bits & 32 != 0,
573        centroid: bits & 64 != 0,
574        shift,
575        relative: None,
576        precision: p.precision,
577    };
578    format!("break{} {}", suffixes(Some(&read)), destination(&read, program))
579}
580
581/// The usage names of `dcl_<usage><index>`.
582const USAGES: [&str; 14] = [
583    "position",
584    "blendweight",
585    "blendindices",
586    "normal",
587    "psize",
588    "texcoord",
589    "tangent",
590    "binormal",
591    "tessfactor",
592    "positiont",
593    "color",
594    "fog",
595    "depth",
596    "sample",
597];
598
599fn declaration(d: &crate::Declaration, program: &Program) -> String {
600    let what = match d.declared {
601        Declared::Sampler(TextureType::D2) => "_2d".to_owned(),
602        Declared::Sampler(TextureType::Cube) => "_cube".to_owned(),
603        Declared::Sampler(TextureType::Volume) => "_volume".to_owned(),
604        // a pixel program's input of usage 0 (position) and index 0 has no usage, whatever its register
605        Declared::Semantic { usage: 0, index: 0 } if program.version.stage == Stage::Pixel => String::new(),
606        Declared::Semantic { usage, index } => {
607            let index = if index == 0 {
608                String::new()
609            } else {
610                index.to_string()
611            };
612            let usage = USAGES.get(usize::from(usage)).copied().unwrap_or("unknown");
613            format!("_{usage}{index}")
614        }
615    };
616    format!(
617        "dcl{what}{} {}",
618        suffixes(Some(&d.destination)),
619        destination(&d.destination, program)
620    )
621}
622
623/// A `def`, `defi` or `defb`, whose destination Microsoft writes as any other: its result modifiers after the mnemonic
624/// and a mask that is not all four lanes.
625fn definition(literal: &Literal, register_: &Destination, program: &Program, options: &Options) -> String {
626    let (op, values) = match literal {
627        Literal::Float(f) => (
628            "def",
629            f.iter()
630                .map(|&w| {
631                    if options.hex_literals {
632                        format!("0x{w:08x}")
633                    } else {
634                        general(f32::from_bits(w))
635                    }
636                })
637                .collect::<Vec<_>>()
638                .join(", "),
639        ),
640        Literal::Integer(i) => (
641            "defi",
642            i.iter().map(ToString::to_string).collect::<Vec<_>>().join(", "),
643        ),
644        Literal::Bool(b) => ("defb", (if *b != 0 { "true" } else { "false" }).to_owned()),
645    };
646    format!(
647        "{op}{} {}, {values}",
648        suffixes(Some(register_)),
649        destination(register_, program)
650    )
651}
652
653/// A float as C's `%.9g` writes it: nine significant digits, trailing zeros dropped, an exponent below 1e-4 and from
654/// 1e9; an infinity or NaN as MSVC spells it (`1.#INF`, `1.#QNAN`, and `-1.#IND` for the default NaN, 0xffc00000).
655/// The float is widened to a double first, which quiets a signaling NaN.
656fn general(value: f32) -> String {
657    let bits = value.to_bits();
658    let value = f64::from(value);
659    if value == 0.0 {
660        return if value.is_sign_negative() { "-0" } else { "0" }.to_owned();
661    }
662    if !value.is_finite() {
663        let sign = if value.is_sign_negative() { "-" } else { "" };
664        let name = if value.is_infinite() {
665            "INF"
666        } else if bits == 0xffc0_0000 {
667            "IND"
668        } else {
669            "QNAN"
670        };
671        return format!("{sign}1.#{name}");
672    }
673    let scientific = format!("{value:.8e}");
674    let (mantissa, exponent) = scientific.split_once('e').unwrap_or((&scientific, "0"));
675    let exponent: i32 = exponent.parse().unwrap_or(0);
676    let trim = |text: &str| -> String {
677        if text.contains('.') {
678            text.trim_end_matches('0').trim_end_matches('.').to_owned()
679        } else {
680            text.to_owned()
681        }
682    };
683    if !(-4..9).contains(&exponent) {
684        let sign = if exponent < 0 { '-' } else { '+' };
685        return format!("{}e{sign}{:03}", trim(mantissa), exponent.unsigned_abs());
686    }
687    let decimals = usize::try_from(8i32.saturating_sub(exponent)).unwrap_or(0);
688    trim(&format!("{value:.decimals$}"))
689}
690
691/// The texture and arithmetic instruction slots an instruction takes, as Microsoft's listings count them: checked
692/// against every footer of a vs_3_0 and ps_3_0 corpus, against `D3DAssemble`d programs of every operation alone in each
693/// version (`tests/assembled`), and of co-issue and predication. `loop`, `endloop`, `call`, `callnz` and `ret` take
694/// what Microsoft's instruction reference states, unchecked.
695fn slots(i: &Instruction, first: bool, program: &Program) -> (u32, u32) {
696    // a co-issued instruction runs in the slot of the one before it (the program's first takes its own), and a `nop`
697    // takes none
698    if (i.coissue && !first) || i.opcode == Opcode::Nop {
699        return (0, 0);
700    }
701    let v = program.version;
702    let pixel1 = v.stage == Stage::Pixel && v.major == 1;
703    let vertex1 = v.stage == Stage::Vertex && v.major == 1;
704    let (texture, arithmetic): (u32, u32) = match i.opcode {
705        Opcode::Tex => (1, 0),
706        // pixel 1.x's texture addressing operations, `texcrd` of 1.4 among them, in whatever version; `texkill`,
707        // `texdepth` and `bem` are arithmetic
708        Opcode::TexCoord
709        | Opcode::TexBem
710        | Opcode::TexBemL
711        | Opcode::TexReg2Ar
712        | Opcode::TexReg2Gb
713        | Opcode::TexReg2Rgb
714        | Opcode::TexM3x2Pad
715        | Opcode::TexM3x2Tex
716        | Opcode::TexM3x2Depth
717        | Opcode::TexM3x3Pad
718        | Opcode::TexM3x3Tex
719        | Opcode::TexM3x3Spec
720        | Opcode::TexM3x3VSpec
721        | Opcode::TexM3x3
722        | Opcode::TexDp3
723        | Opcode::TexDp3Tex => (1, 0),
724        Opcode::TexLdl => (2, 0),
725        Opcode::TexLdd => (1, 2),
726        // vertex 1.1's full-precision `exp` and `log` and its `frc` are macros
727        Opcode::Exp | Opcode::Log if vertex1 => (0, 10),
728        Opcode::Frc if vertex1 => (0, 3),
729        Opcode::SinCos => (0, 8),
730        Opcode::M4x4 | Opcode::M3x4 => (0, 4),
731        Opcode::Nrm | Opcode::Pow | Opcode::M4x3 | Opcode::M3x3 | Opcode::Sgn => (0, 3),
732        Opcode::If
733        | Opcode::IfC
734        | Opcode::BreakC
735        | Opcode::BreakP
736        | Opcode::CallNz
737        | Opcode::Rep
738        | Opcode::Loop => (0, 3),
739        Opcode::EndRep
740        | Opcode::EndLoop
741        | Opcode::Call
742        | Opcode::M3x2
743        | Opcode::Crs
744        | Opcode::Dsx
745        | Opcode::Dsy
746        | Opcode::Dp2Add
747        | Opcode::Bem => (0, 2),
748        Opcode::Label | Opcode::Phase => (0, 0),
749        _ => (0, 1),
750    };
751    // a pixel 1.x instruction takes one arithmetic slot at most, even one of a later version's macros
752    if pixel1 {
753        return (texture, arithmetic.min(1));
754    }
755    // a predicated instruction takes one arithmetic slot more, a predicated sample too
756    let predicate = u32::from(i.predicate.is_some());
757    (texture, arithmetic.saturating_add(predicate))
758}
759
760/// `// approximately N instruction slots used`, split into texture and arithmetic slots when the program samples.
761fn footer(out: &mut String, program: &Program) {
762    let (texture, arithmetic) = program
763        .instructions()
764        .enumerate()
765        .fold((0u32, 0u32), |(t, a), (n, i)| {
766            let (ti, ai) = slots(i, n == 0, program);
767            (t.saturating_add(ti), a.saturating_add(ai))
768        });
769    let total = texture.saturating_add(arithmetic);
770    let slots = if total == 1 { "slot" } else { "slots" };
771    let _ = write!(out, "// approximately {total} instruction {slots} used");
772    if texture > 0 {
773        let _ = write!(out, " ({texture} texture, {arithmetic} arithmetic)");
774    }
775    out.push('\n');
776}