1use 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#[derive(Clone, Debug, PartialEq, Eq)]
14#[non_exhaustive]
15pub struct Options {
16 pub header: bool,
19 pub numbering: bool,
23 pub offsets: bool,
26 pub hex_literals: bool,
28 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 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
81pub fn disassemble(bytes: &[u8]) -> Result<String> {
85 disassemble_with(bytes, &Options::default())
86}
87
88pub fn disassemble_with(bytes: &[u8], options: &Options) -> Result<String> {
90 let program = read(bytes)?;
91 let mut out = String::new();
92 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 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 let mut source: Option<(u16, u16)> = None;
105 let mut depth: usize = 0;
106 let mut slot: u32 = 0;
108 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 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 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 let indent = match item {
167 Item::Instruction(i) if i.coissue => " + ",
168 _ => " ",
169 };
170 let line = match item {
171 Item::Comment(_) => continue,
172 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
208fn 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
230fn 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
282fn 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
295fn bank(set: u16) -> &'static str {
297 match set {
298 0 => "b",
299 1 => "i",
300 3 => "s",
301 _ => "c",
302 }
303}
304
305fn 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 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, ¶meters);
320 out.push_str("//\n//\n// Registers:\n//\n");
321 constants.sort_by_key(|c| (c.register_set, c.register));
322 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
333fn 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 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
408fn 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}{}", precision(s.precision))
454}
455
456fn precision(p: Precision) -> &'static str {
458 match p {
459 Precision::Min16 => " {min16f}",
460 Precision::Min2_8 => " {min2_8f}",
461 Precision::Default | Precision::Unnamed => "",
462 }
463}
464
465fn destination(d: &Destination, program: &Program) -> String {
466 let mask = if d.mask == 15 {
467 String::new()
468 } else {
469 let lanes: String = (0..4u8).filter(|n| d.mask & (1 << n) != 0).map(lane).collect();
470 format!(".{lanes}")
471 };
472 format!(
473 "{}{}{mask}{}",
474 register(d.register, program),
475 relative(d.relative, program),
476 precision(d.precision)
477 )
478}
479
480fn suffixes(d: Option<&Destination>) -> String {
482 let Some(d) = d else { return String::new() };
483 let mut s = match d.shift {
484 1 => "_x2",
485 2 => "_x4",
486 3 => "_x8",
487 -1 => "_d2",
488 -2 => "_d4",
489 -3 => "_d8",
490 _ => "",
491 }
492 .to_owned();
493 if d.saturate {
494 s.push_str("_sat");
495 }
496 if d.partial_precision {
497 s.push_str("_pp");
498 }
499 if d.centroid {
500 s.push_str("_centroid");
501 }
502 s
503}
504
505fn mnemonic(i: &Instruction, program: &Program) -> String {
506 let v = program.version;
507 let from14 = v.major > 1 || (v.stage == Stage::Pixel && v.minor == 4);
509 let comparison = i.comparison().map_or("", |c| match c {
510 Comparison::Greater => "_gt",
511 Comparison::Equal => "_eq",
512 Comparison::GreaterEqual => "_ge",
513 Comparison::Less => "_lt",
514 Comparison::NotEqual => "_ne",
515 Comparison::LessEqual => "_le",
516 });
517 match i.opcode {
518 Opcode::Tex if from14 => match i.control {
519 0 => "texld",
520 1 => "texldp",
521 _ => "texldb",
522 }
523 .to_owned(),
524 Opcode::TexCoord if from14 => "texcrd".into(),
525 Opcode::IfC => format!("if{comparison}"),
526 Opcode::BreakC => format!("break{comparison}"),
527 Opcode::SetP => format!("setp{comparison}"),
528 other => other.name().into(),
529 }
530}
531
532fn instruction(i: &Instruction, program: &Program) -> String {
533 if i.opcode == Opcode::BreakP {
534 return breakp(i, program);
535 }
536 let mut args = Vec::new();
537 if let Some(d) = &i.destination {
538 args.push(destination(d, program));
539 }
540 args.extend(i.sources.iter().map(|s| source(s, program)));
541 let predicate = i.predicate.as_ref().map_or(String::new(), |p| {
543 let p = Source {
544 precision: Precision::Default,
545 ..*p
546 };
547 format!("({}) ", source(&p, program))
548 });
549 format!(
550 "{predicate}{}{} {}",
551 mnemonic(i, program),
552 suffixes(i.destination.as_ref()),
553 args.join(", ")
554 )
555}
556
557fn breakp(i: &Instruction, program: &Program) -> String {
563 let Some(p) = i.sources.first() else {
564 return "break".to_owned();
565 };
566 let bits = p.swizzle.iter().enumerate().fold(0u8, |bits, (lane, from)| {
567 let at = u32::try_from(lane).map_or(8, |l| l.saturating_mul(2));
568 bits | (from & 3).checked_shl(at).unwrap_or(0)
569 });
570 let shift = i8::try_from(p.modifier.code()).map_or(0, |m| if m >= 8 { m.wrapping_sub(16) } else { m });
572 let read = Destination {
573 word: p.word,
574 register: p.register,
575 mask: bits & 15,
576 saturate: bits & 16 != 0,
577 partial_precision: bits & 32 != 0,
578 centroid: bits & 64 != 0,
579 shift,
580 relative: None,
581 precision: p.precision,
582 };
583 format!("break{} {}", suffixes(Some(&read)), destination(&read, program))
584}
585
586const USAGES: [&str; 14] = [
588 "position",
589 "blendweight",
590 "blendindices",
591 "normal",
592 "psize",
593 "texcoord",
594 "tangent",
595 "binormal",
596 "tessfactor",
597 "positiont",
598 "color",
599 "fog",
600 "depth",
601 "sample",
602];
603
604fn declaration(d: &crate::Declaration, program: &Program) -> String {
605 let what = match d.declared {
606 Declared::Sampler(TextureType::D2) => "_2d".to_owned(),
607 Declared::Sampler(TextureType::Cube) => "_cube".to_owned(),
608 Declared::Sampler(TextureType::Volume) => "_volume".to_owned(),
609 Declared::Semantic { usage: 0, index: 0 } if program.version.stage == Stage::Pixel => String::new(),
611 Declared::Semantic { usage, index } => {
612 let index = if index == 0 {
613 String::new()
614 } else {
615 index.to_string()
616 };
617 let usage = USAGES.get(usize::from(usage)).copied().unwrap_or("unknown");
618 format!("_{usage}{index}")
619 }
620 };
621 format!(
622 "dcl{what}{} {}",
623 suffixes(Some(&d.destination)),
624 destination(&d.destination, program)
625 )
626}
627
628fn definition(literal: &Literal, register_: &Destination, program: &Program, options: &Options) -> String {
629 let (op, values) = match literal {
630 Literal::Float(f) => (
631 "def",
632 f.iter()
633 .map(|&w| {
634 if options.hex_literals {
635 format!("0x{w:08x}")
636 } else {
637 general(f32::from_bits(w))
638 }
639 })
640 .collect::<Vec<_>>()
641 .join(", "),
642 ),
643 Literal::Integer(i) => (
644 "defi",
645 i.iter().map(ToString::to_string).collect::<Vec<_>>().join(", "),
646 ),
647 Literal::Bool(b) => ("defb", (if *b != 0 { "true" } else { "false" }).to_owned()),
648 };
649 format!(
650 "{op} {}{}, {values}",
651 register(register_.register, program),
652 precision(register_.precision)
653 )
654}
655
656fn general(value: f32) -> String {
660 let bits = value.to_bits();
661 let value = f64::from(value);
662 if value == 0.0 {
663 return if value.is_sign_negative() { "-0" } else { "0" }.to_owned();
664 }
665 if !value.is_finite() {
666 let sign = if value.is_sign_negative() { "-" } else { "" };
667 let name = if value.is_infinite() {
668 "INF"
669 } else if bits == 0xffc0_0000 {
670 "IND"
671 } else {
672 "QNAN"
673 };
674 return format!("{sign}1.#{name}");
675 }
676 let scientific = format!("{value:.8e}");
677 let (mantissa, exponent) = scientific.split_once('e').unwrap_or((&scientific, "0"));
678 let exponent: i32 = exponent.parse().unwrap_or(0);
679 let trim = |text: &str| -> String {
680 if text.contains('.') {
681 text.trim_end_matches('0').trim_end_matches('.').to_owned()
682 } else {
683 text.to_owned()
684 }
685 };
686 if !(-4..9).contains(&exponent) {
687 let sign = if exponent < 0 { '-' } else { '+' };
688 return format!("{}e{sign}{:03}", trim(mantissa), exponent.unsigned_abs());
689 }
690 let decimals = usize::try_from(8i32.saturating_sub(exponent)).unwrap_or(0);
691 trim(&format!("{value:.decimals$}"))
692}
693
694fn slots(i: &Instruction, first: bool, program: &Program) -> (u32, u32) {
699 if (i.coissue && !first) || i.opcode == Opcode::Nop {
702 return (0, 0);
703 }
704 let v = program.version;
705 let pixel1 = v.stage == Stage::Pixel && v.major == 1;
706 let vertex1 = v.stage == Stage::Vertex && v.major == 1;
707 let (texture, arithmetic): (u32, u32) = match i.opcode {
708 Opcode::Tex => (1, 0),
709 Opcode::TexCoord
712 | Opcode::TexBem
713 | Opcode::TexBemL
714 | Opcode::TexReg2Ar
715 | Opcode::TexReg2Gb
716 | Opcode::TexReg2Rgb
717 | Opcode::TexM3x2Pad
718 | Opcode::TexM3x2Tex
719 | Opcode::TexM3x2Depth
720 | Opcode::TexM3x3Pad
721 | Opcode::TexM3x3Tex
722 | Opcode::TexM3x3Spec
723 | Opcode::TexM3x3VSpec
724 | Opcode::TexM3x3
725 | Opcode::TexDp3
726 | Opcode::TexDp3Tex => (1, 0),
727 Opcode::TexLdl => (2, 0),
728 Opcode::TexLdd => (1, 2),
729 Opcode::Exp | Opcode::Log if vertex1 => (0, 10),
731 Opcode::Frc if vertex1 => (0, 3),
732 Opcode::SinCos => (0, 8),
733 Opcode::M4x4 | Opcode::M3x4 => (0, 4),
734 Opcode::Nrm | Opcode::Pow | Opcode::M4x3 | Opcode::M3x3 | Opcode::Sgn => (0, 3),
735 Opcode::If
736 | Opcode::IfC
737 | Opcode::BreakC
738 | Opcode::BreakP
739 | Opcode::CallNz
740 | Opcode::Rep
741 | Opcode::Loop => (0, 3),
742 Opcode::EndRep
743 | Opcode::EndLoop
744 | Opcode::Call
745 | Opcode::M3x2
746 | Opcode::Crs
747 | Opcode::Dsx
748 | Opcode::Dsy
749 | Opcode::Dp2Add
750 | Opcode::Bem => (0, 2),
751 Opcode::Label | Opcode::Phase => (0, 0),
752 _ => (0, 1),
753 };
754 if pixel1 {
756 return (texture, arithmetic.min(1));
757 }
758 let predicate = u32::from(i.predicate.is_some());
760 (texture, arithmetic.saturating_add(predicate))
761}
762
763fn footer(out: &mut String, program: &Program) {
765 let (texture, arithmetic) = program
766 .instructions()
767 .enumerate()
768 .fold((0u32, 0u32), |(t, a), (n, i)| {
769 let (ti, ai) = slots(i, n == 0, program);
770 (t.saturating_add(ti), a.saturating_add(ai))
771 });
772 let total = texture.saturating_add(arithmetic);
773 let slots = if total == 1 { "slot" } else { "slots" };
774 let _ = write!(out, "// approximately {total} instruction {slots} used");
775 if texture > 0 {
776 let _ = write!(out, " ({texture} texture, {arithmetic} arithmetic)");
777 }
778 out.push('\n');
779}