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    // 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.register, &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    constants.sort_by(|a, b| a.name.as_bytes().cmp(b.name.as_bytes()));
314    out.push_str("//\n// Parameters:\n//\n");
315    // a uniform with a range in two register sets is one parameter
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    let swizzled = if s.register.kind == RegisterType::Sampler {
449        String::new()
450    } else {
451        swizzle(s.swizzle)
452    };
453    format!("{sign}{body}{swizzled}")
454}
455
456fn destination(d: &Destination, program: &Program) -> String {
457    let mask = if d.mask == 15 {
458        String::new()
459    } else {
460        let lanes: String = (0..4u8).filter(|n| d.mask & (1 << n) != 0).map(lane).collect();
461        format!(".{lanes}")
462    };
463    format!(
464        "{}{}{mask}",
465        register(d.register, program),
466        relative(d.relative, program)
467    )
468}
469
470/// The result modifiers after a mnemonic: the pixel 1.x shift, `_sat`, `_pp`, `_centroid`.
471fn suffixes(d: Option<&Destination>) -> String {
472    let Some(d) = d else { return String::new() };
473    let mut s = match d.shift {
474        1 => "_x2",
475        2 => "_x4",
476        3 => "_x8",
477        -1 => "_d2",
478        -2 => "_d4",
479        -3 => "_d8",
480        _ => "",
481    }
482    .to_owned();
483    if d.saturate {
484        s.push_str("_sat");
485    }
486    if d.partial_precision {
487        s.push_str("_pp");
488    }
489    if d.centroid {
490        s.push_str("_centroid");
491    }
492    s
493}
494
495fn mnemonic(i: &Instruction, program: &Program) -> String {
496    let v = program.version;
497    // `texld` and `texcrd` from pixel 1.4 and from 2.0 in both stages
498    let from14 = v.major > 1 || (v.stage == Stage::Pixel && v.minor == 4);
499    let comparison = i.comparison().map_or("", |c| match c {
500        Comparison::Greater => "_gt",
501        Comparison::Equal => "_eq",
502        Comparison::GreaterEqual => "_ge",
503        Comparison::Less => "_lt",
504        Comparison::NotEqual => "_ne",
505        Comparison::LessEqual => "_le",
506    });
507    match i.opcode {
508        Opcode::Tex if from14 => match i.control {
509            0 => "texld",
510            1 => "texldp",
511            _ => "texldb",
512        }
513        .to_owned(),
514        Opcode::TexCoord if from14 => "texcrd".into(),
515        Opcode::IfC => format!("if{comparison}"),
516        Opcode::BreakC => format!("break{comparison}"),
517        Opcode::SetP => format!("setp{comparison}"),
518        other => other.name().into(),
519    }
520}
521
522fn instruction(i: &Instruction, program: &Program) -> String {
523    if i.opcode == Opcode::BreakP {
524        return breakp(i, program);
525    }
526    let mut args = Vec::new();
527    if let Some(d) = &i.destination {
528        args.push(destination(d, program));
529    }
530    args.extend(i.sources.iter().map(|s| source(s, program)));
531    let predicate = i
532        .predicate
533        .as_ref()
534        .map_or(String::new(), |p| format!("({}) ", source(p, program)));
535    format!(
536        "{predicate}{}{} {}",
537        mnemonic(i, program),
538        suffixes(i.destination.as_ref()),
539        args.join(", ")
540    )
541}
542
543/// `breakp` as Microsoft's disassembler writes it: `break`, then its predicate source read as though its token were a
544/// destination's. The source's swizzle bits (16 to 23) are a write mask (16 to 19) and the result modifiers `_sat`,
545/// `_pp` and `_centroid` (20 to 22), and its modifier bits (24 to 27) a result shift, so `breakp p0.x` is listed
546/// `break p0.`, `breakp p0.y` `break_sat_centroid p0.xz` and `breakp !p0.x` `break_d8 p0.` (`D3DAssemble`d programs
547/// in `tests/assembled`).
548fn breakp(i: &Instruction, program: &Program) -> String {
549    let Some(p) = i.sources.first() else {
550        return "break".to_owned();
551    };
552    let bits = p.swizzle.iter().enumerate().fold(0u8, |bits, (lane, from)| {
553        let at = u32::try_from(lane).map_or(8, |l| l.saturating_mul(2));
554        bits | (from & 3).checked_shl(at).unwrap_or(0)
555    });
556    // the modifier's four bits as a signed shift: `!` (13) is -3, `_d8`
557    let shift = i8::try_from(p.modifier.code()).map_or(0, |m| if m >= 8 { m.wrapping_sub(16) } else { m });
558    let read = Destination {
559        word: p.word,
560        register: p.register,
561        mask: bits & 15,
562        saturate: bits & 16 != 0,
563        partial_precision: bits & 32 != 0,
564        centroid: bits & 64 != 0,
565        shift,
566        relative: None,
567    };
568    format!("break{} {}", suffixes(Some(&read)), destination(&read, program))
569}
570
571/// The usage names of `dcl_<usage><index>`.
572const USAGES: [&str; 14] = [
573    "position",
574    "blendweight",
575    "blendindices",
576    "normal",
577    "psize",
578    "texcoord",
579    "tangent",
580    "binormal",
581    "tessfactor",
582    "positiont",
583    "color",
584    "fog",
585    "depth",
586    "sample",
587];
588
589fn declaration(d: &crate::Declaration, program: &Program) -> String {
590    let what = match d.declared {
591        Declared::Sampler(TextureType::D2) => "_2d".to_owned(),
592        Declared::Sampler(TextureType::Cube) => "_cube".to_owned(),
593        Declared::Sampler(TextureType::Volume) => "_volume".to_owned(),
594        // a pixel program's input of usage 0 (position) and index 0 has no usage, whatever its register
595        Declared::Semantic { usage: 0, index: 0 } if program.version.stage == Stage::Pixel => String::new(),
596        Declared::Semantic { usage, index } => {
597            let index = if index == 0 {
598                String::new()
599            } else {
600                index.to_string()
601            };
602            let usage = USAGES.get(usize::from(usage)).copied().unwrap_or("unknown");
603            format!("_{usage}{index}")
604        }
605    };
606    format!(
607        "dcl{what}{} {}",
608        suffixes(Some(&d.destination)),
609        destination(&d.destination, program)
610    )
611}
612
613fn definition(literal: &Literal, register_: Register, program: &Program, options: &Options) -> String {
614    let (op, values) = match literal {
615        Literal::Float(f) => (
616            "def",
617            f.iter()
618                .map(|&w| {
619                    if options.hex_literals {
620                        format!("0x{w:08x}")
621                    } else {
622                        general(f32::from_bits(w))
623                    }
624                })
625                .collect::<Vec<_>>()
626                .join(", "),
627        ),
628        Literal::Integer(i) => (
629            "defi",
630            i.iter().map(ToString::to_string).collect::<Vec<_>>().join(", "),
631        ),
632        Literal::Bool(b) => ("defb", (if *b != 0 { "true" } else { "false" }).to_owned()),
633    };
634    format!("{op} {}, {values}", register(register_, program))
635}
636
637/// A float as C's `%.9g` writes it: nine significant digits, trailing zeros dropped, an exponent below 1e-4 and from
638/// 1e9.
639fn general(value: f32) -> String {
640    let value = f64::from(value);
641    if value == 0.0 {
642        return if value.is_sign_negative() { "-0" } else { "0" }.to_owned();
643    }
644    if !value.is_finite() {
645        return format!("{value}");
646    }
647    let scientific = format!("{value:.8e}");
648    let (mantissa, exponent) = scientific.split_once('e').unwrap_or((&scientific, "0"));
649    let exponent: i32 = exponent.parse().unwrap_or(0);
650    let trim = |text: &str| -> String {
651        if text.contains('.') {
652            text.trim_end_matches('0').trim_end_matches('.').to_owned()
653        } else {
654            text.to_owned()
655        }
656    };
657    if !(-4..9).contains(&exponent) {
658        let sign = if exponent < 0 { '-' } else { '+' };
659        return format!("{}e{sign}{:03}", trim(mantissa), exponent.unsigned_abs());
660    }
661    let decimals = usize::try_from(8i32.saturating_sub(exponent)).unwrap_or(0);
662    trim(&format!("{value:.decimals$}"))
663}
664
665/// The texture and arithmetic instruction slots an instruction takes, as Microsoft's listings count them: checked
666/// against every footer of a vs_3_0 and ps_3_0 corpus, against `D3DAssemble`d programs of every operation alone in each
667/// version (`tests/assembled`), and of co-issue and predication. `loop`, `endloop`, `call`, `callnz` and `ret` take
668/// what Microsoft's instruction reference states, unchecked.
669fn slots(i: &Instruction, first: bool, program: &Program) -> (u32, u32) {
670    // a co-issued instruction runs in the slot of the one before it (the program's first takes its own), and a `nop`
671    // takes none
672    if (i.coissue && !first) || i.opcode == Opcode::Nop {
673        return (0, 0);
674    }
675    let v = program.version;
676    let pixel1 = v.stage == Stage::Pixel && v.major == 1;
677    let vertex1 = v.stage == Stage::Vertex && v.major == 1;
678    let (texture, arithmetic): (u32, u32) = match i.opcode {
679        Opcode::Tex => (1, 0),
680        // pixel 1.x's texture addressing operations, `texcrd` of 1.4 among them, in whatever version; `texkill`,
681        // `texdepth` and `bem` are arithmetic
682        Opcode::TexCoord
683        | Opcode::TexBem
684        | Opcode::TexBemL
685        | Opcode::TexReg2Ar
686        | Opcode::TexReg2Gb
687        | Opcode::TexReg2Rgb
688        | Opcode::TexM3x2Pad
689        | Opcode::TexM3x2Tex
690        | Opcode::TexM3x2Depth
691        | Opcode::TexM3x3Pad
692        | Opcode::TexM3x3Tex
693        | Opcode::TexM3x3Spec
694        | Opcode::TexM3x3VSpec
695        | Opcode::TexM3x3
696        | Opcode::TexDp3
697        | Opcode::TexDp3Tex => (1, 0),
698        Opcode::TexLdl => (2, 0),
699        Opcode::TexLdd => (1, 2),
700        // vertex 1.1's full-precision `exp` and `log` and its `frc` are macros
701        Opcode::Exp | Opcode::Log if vertex1 => (0, 10),
702        Opcode::Frc if vertex1 => (0, 3),
703        Opcode::SinCos => (0, 8),
704        Opcode::M4x4 | Opcode::M3x4 => (0, 4),
705        Opcode::Nrm | Opcode::Pow | Opcode::M4x3 | Opcode::M3x3 | Opcode::Sgn => (0, 3),
706        Opcode::If
707        | Opcode::IfC
708        | Opcode::BreakC
709        | Opcode::BreakP
710        | Opcode::CallNz
711        | Opcode::Rep
712        | Opcode::Loop => (0, 3),
713        Opcode::EndRep
714        | Opcode::EndLoop
715        | Opcode::Call
716        | Opcode::M3x2
717        | Opcode::Crs
718        | Opcode::Dsx
719        | Opcode::Dsy
720        | Opcode::Dp2Add
721        | Opcode::Bem => (0, 2),
722        Opcode::Label | Opcode::Phase => (0, 0),
723        _ => (0, 1),
724    };
725    // a pixel 1.x instruction takes one arithmetic slot at most, even one of a later version's macros
726    if pixel1 {
727        return (texture, arithmetic.min(1));
728    }
729    // a predicated instruction takes one arithmetic slot more, a predicated sample too
730    let predicate = u32::from(i.predicate.is_some());
731    (texture, arithmetic.saturating_add(predicate))
732}
733
734/// `// approximately N instruction slots used`, split into texture and arithmetic slots when the program samples.
735fn footer(out: &mut String, program: &Program) {
736    let (texture, arithmetic) = program
737        .instructions()
738        .enumerate()
739        .fold((0u32, 0u32), |(t, a), (n, i)| {
740            let (ti, ai) = slots(i, n == 0, program);
741            (t.saturating_add(ti), a.saturating_add(ai))
742        });
743    let total = texture.saturating_add(arithmetic);
744    let slots = if total == 1 { "slot" } else { "slots" };
745    let _ = write!(out, "// approximately {total} instruction {slots} used");
746    if texture > 0 {
747        let _ = write!(out, " ({texture} texture, {arithmetic} arithmetic)");
748    }
749    out.push('\n');
750}