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