Skip to main content

siglus_cfx_decompiler/
wgsl.rs

1use std::collections::{BTreeMap, BTreeSet};
2
3use crate::ctab::{ConstantInfo, ConstantTable, RegisterSet, ValueType};
4use crate::disasm::{
5    DeclUsage, Instruction, Opcode, RegisterKey, RegisterType, ResultModifier, SamplerTextureType,
6    ShaderKind, SourceModifier, mask_len,
7};
8use crate::disasm::{ShaderModel, parse_shader};
9
10#[derive(Debug, Clone)]
11struct DeclInfo {
12    reg: RegisterKey,
13    semantic: String,
14}
15
16#[derive(Debug, Clone)]
17struct DefFloat {
18    values: [f32; 4],
19}
20
21#[derive(Debug, Clone)]
22struct DefInt {
23    values: [i32; 4],
24}
25
26#[derive(Debug, Clone)]
27struct DefBool {
28    value: bool,
29}
30
31#[derive(Debug, Clone)]
32struct Context<'a> {
33    shader: &'a ShaderModel,
34    ctab: Option<&'a ConstantTable>,
35    decls: BTreeMap<RegisterKey, DeclInfo>,
36    sampler_decls: BTreeMap<u16, SamplerTextureType>,
37    def_float: BTreeMap<u16, DefFloat>,
38    def_int: BTreeMap<u16, DefInt>,
39    def_bool: BTreeMap<u16, DefBool>,
40    used_inputs: BTreeSet<RegisterKey>,
41    used_outputs: BTreeSet<RegisterKey>,
42    used_temps: BTreeSet<RegisterKey>,
43    used_samplers: BTreeSet<u16>,
44    used_consts: BTreeSet<u16>,
45    used_int_consts: BTreeSet<u16>,
46    used_bool_consts: BTreeSet<u16>,
47    uses_lit: bool,
48    uses_dst: bool,
49}
50
51pub fn decompile_wgsl(data: &[u8], ctab: Option<&ConstantTable>) -> String {
52    match parse_shader(data) {
53        Ok(shader) => {
54            let ctx = analyze(&shader, ctab);
55            emit_wgsl(&ctx)
56        }
57        Err(_) => String::new(),
58    }
59}
60
61fn emit_wgsl(ctx: &Context<'_>) -> String {
62    let mut out = String::new();
63    emit_register_bindings(&mut out, ctx);
64    emit_texture_bindings(&mut out, ctx);
65    emit_def_constants(&mut out, ctx);
66    emit_helpers(&mut out, ctx);
67    emit_structs(&mut out, ctx);
68    emit_main(&mut out, ctx);
69    out
70}
71
72fn emit_register_bindings(out: &mut String, ctx: &Context<'_>) {
73    if !ctx.used_consts.is_empty() {
74        out.push_str("struct FloatRegs {\n");
75        out.push_str("    c: array<vec4<f32>, 256>,\n");
76        out.push_str("};\n");
77        out.push_str("@group(0) @binding(0) var<uniform> float_regs: FloatRegs;\n\n");
78    }
79    if !ctx.used_int_consts.is_empty() {
80        out.push_str("struct IntRegs {\n");
81        out.push_str("    i: array<vec4<i32>, 16>,\n");
82        out.push_str("};\n");
83        out.push_str("@group(0) @binding(1) var<uniform> int_regs: IntRegs;\n\n");
84    }
85    if !ctx.used_bool_consts.is_empty() {
86        out.push_str("struct BoolRegs {\n");
87        out.push_str("    b: array<u32, 16>,\n");
88        out.push_str("};\n");
89        out.push_str("@group(0) @binding(2) var<uniform> bool_regs: BoolRegs;\n\n");
90    }
91}
92
93fn emit_texture_bindings(out: &mut String, ctx: &Context<'_>) {
94    for sampler in &ctx.used_samplers {
95        let tex_binding = (*sampler as u32) * 2;
96        let samp_binding = tex_binding + 1;
97        let tex_ty = wgsl_texture_type(sampler_texture_type(ctx, *sampler));
98        out.push_str(&format!(
99            "@group(1) @binding({}) var tex_s{}: {};\n",
100            tex_binding, sampler, tex_ty
101        ));
102        out.push_str(&format!(
103            "@group(1) @binding({}) var samp_s{}: sampler;\n",
104            samp_binding, sampler
105        ));
106    }
107    if !ctx.used_samplers.is_empty() {
108        out.push('\n');
109    }
110}
111
112fn emit_def_constants(out: &mut String, ctx: &Context<'_>) {
113    let mut any = false;
114    for (idx, def) in &ctx.def_float {
115        out.push_str(&format!(
116            "const c{}: vec4<f32> = vec4<f32>({}, {}, {}, {});\n",
117            idx,
118            fmt_f32(def.values[0]),
119            fmt_f32(def.values[1]),
120            fmt_f32(def.values[2]),
121            fmt_f32(def.values[3])
122        ));
123        any = true;
124    }
125    for (idx, def) in &ctx.def_int {
126        out.push_str(&format!(
127            "const i{}: vec4<i32> = vec4<i32>({}, {}, {}, {});\n",
128            idx, def.values[0], def.values[1], def.values[2], def.values[3]
129        ));
130        any = true;
131    }
132    for (idx, def) in &ctx.def_bool {
133        out.push_str(&format!(
134            "const b{}: bool = {};\n",
135            idx,
136            if def.value { "true" } else { "false" }
137        ));
138        any = true;
139    }
140    if any {
141        out.push('\n');
142    }
143}
144
145fn emit_helpers(out: &mut String, ctx: &Context<'_>) {
146    if ctx.uses_lit {
147        out.push_str("fn sm2_lit(v: vec4<f32>) -> vec4<f32> {\n");
148        out.push_str("    let y = max(v.x, 0.0);\n");
149        out.push_str("    let z = select(0.0, pow(max(v.y, 0.0), v.w), v.x > 0.0 && v.y > 0.0);\n");
150        out.push_str("    return vec4<f32>(1.0, y, z, 1.0);\n");
151        out.push_str("}\n\n");
152    }
153    if ctx.uses_dst {
154        out.push_str("fn sm2_dst(a: vec4<f32>, b: vec4<f32>) -> vec4<f32> {\n");
155        out.push_str("    return vec4<f32>(1.0, a.y * b.y, a.z, b.w);\n");
156        out.push_str("}\n\n");
157    }
158}
159
160fn emit_structs(out: &mut String, ctx: &Context<'_>) {
161    let input_name = input_struct_name(ctx.shader.kind);
162    let output_name = output_struct_name(ctx.shader.kind);
163
164    out.push_str(&format!("struct {} {{\n", input_name));
165    for decl in ctx.decls.values() {
166        let field = input_field_name(decl.reg);
167        let ty = wgsl_input_field_type(ctx, decl.reg);
168        let attr = input_attr(ctx.shader.kind, &decl.semantic);
169        out.push_str(&format!("    {} {}: {},\n", attr, field, ty));
170    }
171    out.push_str("};\n\n");
172
173    out.push_str(&format!("struct {} {{\n", output_name));
174    for reg in &ctx.used_outputs {
175        let field = output_field_name(*reg);
176        let sem = output_semantic(ctx, *reg);
177        let ty = wgsl_output_field_type(*reg);
178        let attr = output_attr(ctx.shader.kind, &sem, *reg);
179        out.push_str(&format!("    {} {}: {},\n", attr, field, ty));
180    }
181    out.push_str("};\n\n");
182}
183
184fn emit_main(out: &mut String, ctx: &Context<'_>) {
185    let stage = match ctx.shader.kind {
186        ShaderKind::Vertex => "vertex",
187        ShaderKind::Pixel => "fragment",
188    };
189    let input_name = input_struct_name(ctx.shader.kind);
190    let output_name = output_struct_name(ctx.shader.kind);
191    out.push_str(&format!("@{}\n", stage));
192    out.push_str(&format!(
193        "fn main(input: {}) -> {} {{\n",
194        input_name, output_name
195    ));
196    out.push_str(&format!("    var output: {};\n", output_name));
197
198    for reg in &ctx.used_outputs {
199        out.push_str(&format!(
200            "    output.{} = {};\n",
201            output_field_name(*reg),
202            zero_value(wgsl_output_field_type(*reg))
203        ));
204    }
205    for reg in &ctx.used_temps {
206        out.push_str(&format!(
207            "    var {}: {} = {};\n",
208            temp_name(*reg, ctx.shader.kind),
209            temp_type(*reg),
210            zero_value(temp_type(*reg))
211        ));
212    }
213    if !ctx.used_outputs.is_empty() || !ctx.used_temps.is_empty() {
214        out.push('\n');
215    }
216
217    let mut indent = 1usize;
218    for inst in &ctx.shader.instructions {
219        emit_instruction(out, ctx, inst, &mut indent);
220    }
221
222    out.push_str("    return output;\n");
223    out.push_str("}\n");
224}
225
226fn emit_instruction(out: &mut String, ctx: &Context<'_>, inst: &Instruction, indent: &mut usize) {
227    match inst.opcode {
228        Opcode::Comment
229        | Opcode::Dcl
230        | Opcode::Def
231        | Opcode::DefI
232        | Opcode::DefB
233        | Opcode::Nop
234        | Opcode::End => {}
235        Opcode::TexKill => {
236            if let Some(reg) = inst.dest_register() {
237                let expr = format!(
238                    "{}{}",
239                    register_base(ctx, reg),
240                    wgsl_mask_suffix(inst.dest_write_mask())
241                );
242                let n = mask_len(inst.dest_write_mask());
243                if n == 1 {
244                    line(out, *indent, &format!("if ({} < 0.0) {{ discard; }}", expr));
245                } else {
246                    line(
247                        out,
248                        *indent,
249                        &format!("if (any({} < {})) {{ discard; }}", expr, zero_vector(n)),
250                    );
251                }
252            }
253        }
254        Opcode::If => {
255            let cond = source_expr(ctx, inst, 0, 1);
256            line(out, *indent, &format!("if ({}) {{", scalar_bool_expr(cond)));
257            *indent += 1;
258        }
259        Opcode::IfC => {
260            let a = source_expr(ctx, inst, 0, 4);
261            let b = source_expr(ctx, inst, 1, 4);
262            line(
263                out,
264                *indent,
265                &format!(
266                    "if ({}) {{",
267                    compare_all_expr(a, cmp_op(inst.comparison()), b, 4)
268                ),
269            );
270            *indent += 1;
271        }
272        Opcode::Else => {
273            if *indent > 0 {
274                *indent -= 1;
275            }
276            line(out, *indent, "} else {");
277            *indent += 1;
278        }
279        Opcode::EndIf => {
280            if *indent > 0 {
281                *indent -= 1;
282            }
283            line(out, *indent, "}");
284        }
285        Opcode::Break => line(out, *indent, "break;"),
286        Opcode::BreakC => {
287            let a = source_expr(ctx, inst, 0, 4);
288            let b = source_expr(ctx, inst, 1, 4);
289            line(
290                out,
291                *indent,
292                &format!(
293                    "if ({}) {{ break; }}",
294                    compare_all_expr(a, cmp_op(inst.comparison()), b, 4)
295                ),
296            );
297        }
298        Opcode::Rep => {
299            let n = source_expr(ctx, inst, 0, 1);
300            line(
301                out,
302                *indent,
303                &format!(
304                    "for (var _rep{}: i32 = 0; _rep{} < i32({}); _rep{} = _rep{} + 1) {{",
305                    inst.offset, inst.offset, n, inst.offset, inst.offset
306                ),
307            );
308            *indent += 1;
309        }
310        Opcode::Loop => {
311            line(
312                out,
313                *indent,
314                &format!(
315                    "for (var _loop{}: i32 = 0; ; _loop{} = _loop{} + 1) {{",
316                    inst.offset, inst.offset, inst.offset
317                ),
318            );
319            *indent += 1;
320        }
321        Opcode::EndRep | Opcode::EndLoop => {
322            if *indent > 0 {
323                *indent -= 1;
324            }
325            line(out, *indent, "}");
326        }
327        Opcode::Ret => line(out, *indent, "return output;"),
328        _ if inst.opcode.has_destination() => {
329            if let Some((dst, rhs)) = assignment_expr(ctx, inst) {
330                line(out, *indent, &format!("{} = {};", dst, rhs));
331            }
332        }
333        _ => {}
334    }
335}
336
337fn assignment_expr(ctx: &Context<'_>, inst: &Instruction) -> Option<(String, String)> {
338    let reg = inst.dest_register()?;
339    let mask = inst.dest_write_mask();
340    let dst_count = mask_len(mask);
341    let dst = format!("{}{}", register_base(ctx, reg), wgsl_mask_suffix(mask));
342
343    let (raw_rhs, raw_width) = match inst.opcode {
344        Opcode::Mov | Opcode::MovA => (source_expr(ctx, inst, 1, dst_count), dst_count),
345        Opcode::Add => (bin(ctx, inst, dst_count, "+"), dst_count),
346        Opcode::Sub => (bin(ctx, inst, dst_count, "-"), dst_count),
347        Opcode::Mul => (bin(ctx, inst, dst_count, "*"), dst_count),
348        Opcode::Mad => (
349            format!(
350                "({} * {} + {})",
351                source_expr(ctx, inst, 1, dst_count),
352                source_expr(ctx, inst, 2, dst_count),
353                source_expr(ctx, inst, 3, dst_count)
354            ),
355            dst_count,
356        ),
357        Opcode::Rcp => (format!("(1.0 / {})", source_expr(ctx, inst, 1, 1)), 1),
358        Opcode::Rsq => (format!("inverseSqrt({})", source_expr(ctx, inst, 1, 1)), 1),
359        Opcode::Dp3 => (
360            format!(
361                "dot({}, {})",
362                source_expr(ctx, inst, 1, 3),
363                source_expr(ctx, inst, 2, 3)
364            ),
365            1,
366        ),
367        Opcode::Dp4 => (
368            format!(
369                "dot({}, {})",
370                source_expr(ctx, inst, 1, 4),
371                source_expr(ctx, inst, 2, 4)
372            ),
373            1,
374        ),
375        Opcode::Min => (
376            format!(
377                "min({}, {})",
378                source_expr(ctx, inst, 1, dst_count),
379                source_expr(ctx, inst, 2, dst_count)
380            ),
381            dst_count,
382        ),
383        Opcode::Max => (
384            format!(
385                "max({}, {})",
386                source_expr(ctx, inst, 1, dst_count),
387                source_expr(ctx, inst, 2, dst_count)
388            ),
389            dst_count,
390        ),
391        Opcode::Slt => (select_float(ctx, inst, dst_count, "<"), dst_count),
392        Opcode::Sge => (select_float(ctx, inst, dst_count, ">="), dst_count),
393        Opcode::Exp | Opcode::ExpP => (format!("exp2({})", source_expr(ctx, inst, 1, 1)), 1),
394        Opcode::Log | Opcode::LogP => (format!("log2({})", source_expr(ctx, inst, 1, 1)), 1),
395        Opcode::Lit => (format!("sm2_lit({})", source_expr(ctx, inst, 1, 4)), 4),
396        Opcode::Dst => (
397            format!(
398                "sm2_dst({}, {})",
399                source_expr(ctx, inst, 1, 4),
400                source_expr(ctx, inst, 2, 4)
401            ),
402            4,
403        ),
404        Opcode::Lrp => (
405            format!(
406                "mix({}, {}, {})",
407                source_expr(ctx, inst, 3, dst_count),
408                source_expr(ctx, inst, 2, dst_count),
409                source_expr(ctx, inst, 1, dst_count)
410            ),
411            dst_count,
412        ),
413        Opcode::Frc => (
414            format!("fract({})", source_expr(ctx, inst, 1, dst_count)),
415            dst_count,
416        ),
417        Opcode::Pow => (
418            format!(
419                "pow({}, {})",
420                source_expr(ctx, inst, 1, 1),
421                source_expr(ctx, inst, 2, 1)
422            ),
423            1,
424        ),
425        Opcode::Crs => (
426            format!(
427                "cross({}, {})",
428                source_expr(ctx, inst, 1, 3),
429                source_expr(ctx, inst, 2, 3)
430            ),
431            3,
432        ),
433        Opcode::Sgn => (
434            format!("sign({})", source_expr(ctx, inst, 1, dst_count)),
435            dst_count,
436        ),
437        Opcode::Abs => (
438            format!("abs({})", source_expr(ctx, inst, 1, dst_count)),
439            dst_count,
440        ),
441        Opcode::Nrm => (format!("normalize({})", source_expr(ctx, inst, 1, 3)), 3),
442        Opcode::SinCos => {
443            let width = if dst_count <= 1 { 1 } else { 2 };
444            (sincos_expr(ctx, inst, dst_count), width)
445        }
446        Opcode::Cmp => (select_expr(ctx, inst, dst_count, ">="), dst_count),
447        Opcode::Cnd => (select_expr(ctx, inst, dst_count, ">"), dst_count),
448        Opcode::Dp2Add => (
449            format!(
450                "(dot({}, {}) + {})",
451                source_expr(ctx, inst, 1, 2),
452                source_expr(ctx, inst, 2, 2),
453                source_expr(ctx, inst, 3, 1)
454            ),
455            1,
456        ),
457        Opcode::M4x4 => (matrix_mul_expr(ctx, inst, 4, 4), 4),
458        Opcode::M4x3 => (matrix_mul_expr(ctx, inst, 4, 3), 3),
459        Opcode::M3x4 => (matrix_mul_expr(ctx, inst, 3, 4), 4),
460        Opcode::M3x3 => (matrix_mul_expr(ctx, inst, 3, 3), 3),
461        Opcode::M3x2 => (matrix_mul_expr(ctx, inst, 3, 2), 2),
462        Opcode::Tex | Opcode::TexLdl | Opcode::TexLdd => (texture_expr(ctx, inst), 4),
463        Opcode::TexCoord => (source_expr(ctx, inst, 1, dst_count), dst_count),
464        Opcode::Dsx => (
465            format!("dpdx({})", source_expr(ctx, inst, 1, dst_count)),
466            dst_count,
467        ),
468        Opcode::Dsy => (
469            format!("dpdy({})", source_expr(ctx, inst, 1, dst_count)),
470            dst_count,
471        ),
472        _ => return None,
473    };
474
475    let mut rhs = coerce_expr_width(raw_rhs, raw_width, dst_count);
476    rhs = apply_result_modifier(rhs, inst.dest_modifier(), dst_count);
477    Some((dst, rhs))
478}
479fn bin(ctx: &Context<'_>, inst: &Instruction, n: usize, op: &str) -> String {
480    format!(
481        "({} {} {})",
482        source_expr(ctx, inst, 1, n),
483        op,
484        source_expr(ctx, inst, 2, n)
485    )
486}
487
488fn select_float(ctx: &Context<'_>, inst: &Instruction, n: usize, op: &str) -> String {
489    let a = source_expr(ctx, inst, 1, n);
490    let b = source_expr(ctx, inst, 2, n);
491    let zero = zero_vector(n);
492    let one = one_vector(n);
493    format!("select({}, {}, {} {} {})", zero, one, a, op, b)
494}
495
496fn select_expr(ctx: &Context<'_>, inst: &Instruction, n: usize, op: &str) -> String {
497    let a = source_expr(ctx, inst, 1, n);
498    let yes = source_expr(ctx, inst, 2, n);
499    let no = source_expr(ctx, inst, 3, n);
500    let pivot = if op == ">" {
501        half_vector(n)
502    } else {
503        zero_vector(n)
504    };
505    format!("select({}, {}, {} {} {})", no, yes, a, op, pivot)
506}
507
508fn sincos_expr(ctx: &Context<'_>, inst: &Instruction, n: usize) -> String {
509    if n <= 1 {
510        format!("cos({})", source_expr(ctx, inst, 1, 1))
511    } else {
512        format!(
513            "vec2<f32>(cos({}), sin({}))",
514            source_expr(ctx, inst, 1, 1),
515            source_expr(ctx, inst, 1, 1)
516        )
517    }
518}
519
520fn matrix_mul_expr(
521    ctx: &Context<'_>,
522    inst: &Instruction,
523    vec_len: usize,
524    out_len: usize,
525) -> String {
526    let v = source_expr(ctx, inst, 1, vec_len);
527    let mut rows = Vec::new();
528    if let Some(reg) = inst.source_register(2) {
529        if reg.ty == RegisterType::Const {
530            for i in 0..out_len {
531                rows.push(format!(
532                    "dot({}, {})",
533                    v,
534                    const_row_expr(ctx, reg.number + i as u16)
535                ));
536            }
537        } else {
538            for _ in 0..out_len {
539                rows.push(format!(
540                    "dot({}, {})",
541                    v,
542                    source_expr(ctx, inst, 2, vec_len)
543                ));
544            }
545        }
546    }
547    vector_constructor(&rows)
548}
549
550fn texture_expr(ctx: &Context<'_>, inst: &Instruction) -> String {
551    let sampler = inst.source_register(2).map(|r| r.number).unwrap_or(0);
552    let sampler_ty = sampler_texture_type(ctx, sampler);
553    let dim = sampler_ty.hlsl_dim();
554    let tex = format!("tex_s{}", sampler);
555    let samp = format!("samp_s{}", sampler);
556    let controls = inst.texld_controls();
557    match (inst.opcode, controls) {
558        (Opcode::TexLdl, _) => {
559            let coord = source_expr(ctx, inst, 1, dim);
560            let lod = source_component_expr(ctx, inst, 1, 3);
561            format!("textureSampleLevel({}, {}, {}, {})", tex, samp, coord, lod)
562        }
563        (Opcode::TexLdd, _) => {
564            let coord = source_expr(ctx, inst, 1, dim);
565            let ddx = source_expr(ctx, inst, 3, dim);
566            let ddy = source_expr(ctx, inst, 4, dim);
567            format!(
568                "textureSampleGrad({}, {}, {}, {}, {})",
569                tex, samp, coord, ddx, ddy
570            )
571        }
572        (Opcode::Tex, 1) => {
573            let coord = source_expr(ctx, inst, 1, dim);
574            let w = source_component_expr(ctx, inst, 1, 3);
575            format!("textureSample({}, {}, ({} / {}))", tex, samp, coord, w)
576        }
577        (Opcode::Tex, 2) => {
578            let coord = source_expr(ctx, inst, 1, dim);
579            let bias = source_component_expr(ctx, inst, 1, 3);
580            format!("textureSampleBias({}, {}, {}, {})", tex, samp, coord, bias)
581        }
582        _ => {
583            let coord = source_expr(ctx, inst, 1, dim);
584            format!("textureSample({}, {}, {})", tex, samp, coord)
585        }
586    }
587}
588fn source_expr(ctx: &Context<'_>, inst: &Instruction, param_index: usize, count: usize) -> String {
589    let Some(reg) = inst.source_register(param_index) else {
590        return zero_vector(count);
591    };
592    let count = count.clamp(1, 4);
593    let mut expr = register_base(ctx, reg);
594    if !register_is_scalar_source(ctx, reg) {
595        let swz = inst.source_swizzle(param_index);
596        expr.push_str(&wgsl_source_swizzle_suffix(swz, count));
597    }
598    apply_source_modifier(expr, inst.source_modifier(param_index), count)
599}
600
601fn source_component_expr(
602    ctx: &Context<'_>,
603    inst: &Instruction,
604    param_index: usize,
605    component: usize,
606) -> String {
607    let Some(reg) = inst.source_register(param_index) else {
608        return "0.0".to_string();
609    };
610    let mut expr = register_base(ctx, reg);
611    if !register_is_scalar_source(ctx, reg) {
612        let swz = inst.source_swizzle(param_index);
613        let comp = swz[component.clamp(0, 3)];
614        expr.push_str(component_suffix(comp));
615    }
616    apply_source_modifier(expr, inst.source_modifier(param_index), 1)
617}
618
619fn register_is_scalar_source(ctx: &Context<'_>, reg: RegisterKey) -> bool {
620    match (reg, ctx.shader.kind) {
621        (
622            RegisterKey {
623                ty: RegisterType::ConstBool | RegisterType::Loop | RegisterType::Label,
624                ..
625            },
626            _,
627        ) => true,
628        (
629            RegisterKey {
630                ty: RegisterType::MiscType,
631                number: 1,
632            },
633            ShaderKind::Pixel,
634        ) => true,
635        _ => false,
636    }
637}
638fn register_base(ctx: &Context<'_>, reg: RegisterKey) -> String {
639    match (reg.ty, ctx.shader.kind) {
640        (RegisterType::Temp | RegisterType::TempFloat16 | RegisterType::Predicate, _) => {
641            temp_name(reg, ctx.shader.kind)
642        }
643        (RegisterType::Texture, ShaderKind::Vertex) => temp_name(reg, ctx.shader.kind),
644        (RegisterType::Texture | RegisterType::Input | RegisterType::MiscType, _) => {
645            format!("input.{}", input_field_name(reg))
646        }
647        (RegisterType::Const, _) => const_row_expr(ctx, reg.number),
648        (RegisterType::ConstInt, _) => int_const_expr(ctx, reg.number),
649        (RegisterType::ConstBool, _) => bool_const_expr(ctx, reg.number),
650        (RegisterType::Sampler, _) => format!("samp_s{}", reg.number),
651        (
652            RegisterType::ColorOut
653            | RegisterType::DepthOut
654            | RegisterType::RastOut
655            | RegisterType::AttrOut
656            | RegisterType::Output,
657            _,
658        ) => format!("output.{}", output_field_name(reg)),
659        (RegisterType::Loop, _) => "_loop".to_string(),
660        (RegisterType::Label, _) => format!("label{}", reg.number),
661        _ => format!("u{}", reg.number),
662    }
663}
664
665fn const_row_expr(ctx: &Context<'_>, index: u16) -> String {
666    if ctx.def_float.contains_key(&index) {
667        return format!("c{}", index);
668    }
669    format!("float_regs.c[{}]", index)
670}
671
672fn int_const_expr(ctx: &Context<'_>, index: u16) -> String {
673    if ctx.def_int.contains_key(&index) {
674        return format!("i{}", index);
675    }
676    format!("int_regs.i[{}]", index)
677}
678
679fn bool_const_expr(ctx: &Context<'_>, index: u16) -> String {
680    if ctx.def_bool.contains_key(&index) {
681        return format!("b{}", index);
682    }
683    format!("(bool_regs.b[{}] != 0u)", index)
684}
685
686fn analyze<'a>(shader: &'a ShaderModel, ctab: Option<&'a ConstantTable>) -> Context<'a> {
687    let mut ctx = Context {
688        shader,
689        ctab,
690        decls: BTreeMap::new(),
691        sampler_decls: BTreeMap::new(),
692        def_float: BTreeMap::new(),
693        def_int: BTreeMap::new(),
694        def_bool: BTreeMap::new(),
695        used_inputs: BTreeSet::new(),
696        used_outputs: BTreeSet::new(),
697        used_temps: BTreeSet::new(),
698        used_samplers: BTreeSet::new(),
699        used_consts: BTreeSet::new(),
700        used_int_consts: BTreeSet::new(),
701        used_bool_consts: BTreeSet::new(),
702        uses_lit: false,
703        uses_dst: false,
704    };
705
706    for inst in &shader.instructions {
707        match inst.opcode {
708            Opcode::Dcl => {
709                if let Some(reg) = inst.dest_register() {
710                    if reg.ty == RegisterType::Sampler {
711                        ctx.sampler_decls
712                            .insert(reg.number, inst.decl_sampler_type());
713                        ctx.used_samplers.insert(reg.number);
714                    } else {
715                        let semantic = semantic_from_decl(shader.kind, inst);
716                        ctx.decls.insert(reg, DeclInfo { reg, semantic });
717                    }
718                }
719            }
720            Opcode::Def => {
721                if let Some(reg) = inst.dest_register()
722                    && reg.ty == RegisterType::Const
723                {
724                    ctx.def_float.insert(
725                        reg.number,
726                        DefFloat {
727                            values: [
728                                inst.get_float_param(1),
729                                inst.get_float_param(2),
730                                inst.get_float_param(3),
731                                inst.get_float_param(4),
732                            ],
733                        },
734                    );
735                }
736            }
737            Opcode::DefI => {
738                if let Some(reg) = inst.dest_register()
739                    && reg.ty == RegisterType::ConstInt
740                {
741                    ctx.def_int.insert(
742                        reg.number,
743                        DefInt {
744                            values: [
745                                inst.get_int_param(1),
746                                inst.get_int_param(2),
747                                inst.get_int_param(3),
748                                inst.get_int_param(4),
749                            ],
750                        },
751                    );
752                }
753            }
754            Opcode::DefB => {
755                if let Some(reg) = inst.dest_register()
756                    && reg.ty == RegisterType::ConstBool
757                {
758                    ctx.def_bool.insert(
759                        reg.number,
760                        DefBool {
761                            value: inst.get_int_param(1) != 0,
762                        },
763                    );
764                }
765            }
766            Opcode::Lit => ctx.uses_lit = true,
767            Opcode::Dst => ctx.uses_dst = true,
768            _ => {}
769        }
770
771        if inst.opcode == Opcode::End || inst.opcode == Opcode::Comment {
772            continue;
773        }
774
775        if inst.opcode == Opcode::TexKill {
776            if let Some(reg) = inst.dest_register() {
777                classify_source_register(&mut ctx, reg);
778            }
779            continue;
780        }
781
782        if inst.opcode.has_destination()
783            && let Some(reg) = inst.dest_register()
784        {
785            classify_dest_register(&mut ctx, reg);
786        }
787
788        let first_src = if inst.opcode.has_destination() { 1 } else { 0 };
789        for pi in first_src..inst.params.len() {
790            if matches!(
791                inst.opcode,
792                Opcode::Dcl | Opcode::Def | Opcode::DefI | Opcode::DefB
793            ) {
794                continue;
795            }
796            if let Some(reg) = inst.source_register(pi) {
797                classify_source_register(&mut ctx, reg);
798            }
799        }
800    }
801
802    infer_missing_decls(&mut ctx);
803    ctx
804}
805
806fn classify_dest_register(ctx: &mut Context<'_>, reg: RegisterKey) {
807    match reg.ty {
808        RegisterType::Temp
809        | RegisterType::TempFloat16
810        | RegisterType::Texture
811        | RegisterType::Predicate => {
812            if !(ctx.shader.kind == ShaderKind::Pixel && reg.ty == RegisterType::Texture) {
813                ctx.used_temps.insert(reg);
814            }
815        }
816        RegisterType::RastOut
817        | RegisterType::AttrOut
818        | RegisterType::Output
819        | RegisterType::ColorOut
820        | RegisterType::DepthOut => {
821            ctx.used_outputs.insert(reg);
822        }
823        _ => {}
824    }
825}
826
827fn classify_source_register(ctx: &mut Context<'_>, reg: RegisterKey) {
828    match reg.ty {
829        RegisterType::Input | RegisterType::MiscType => {
830            ctx.used_inputs.insert(reg);
831        }
832        RegisterType::Texture => {
833            if ctx.shader.kind == ShaderKind::Pixel {
834                ctx.used_inputs.insert(reg);
835            } else {
836                ctx.used_temps.insert(reg);
837            }
838        }
839        RegisterType::Temp | RegisterType::TempFloat16 | RegisterType::Predicate => {
840            ctx.used_temps.insert(reg);
841        }
842        RegisterType::Const => {
843            ctx.used_consts.insert(reg.number);
844        }
845        RegisterType::ConstInt => {
846            ctx.used_int_consts.insert(reg.number);
847        }
848        RegisterType::ConstBool => {
849            ctx.used_bool_consts.insert(reg.number);
850        }
851        RegisterType::Sampler => {
852            ctx.used_samplers.insert(reg.number);
853        }
854        RegisterType::RastOut
855        | RegisterType::AttrOut
856        | RegisterType::Output
857        | RegisterType::ColorOut
858        | RegisterType::DepthOut => {
859            ctx.used_outputs.insert(reg);
860        }
861        _ => {}
862    }
863}
864
865fn infer_missing_decls(ctx: &mut Context<'_>) {
866    for reg in ctx.used_inputs.clone() {
867        if ctx.decls.contains_key(&reg) {
868            continue;
869        }
870        let semantic = inferred_input_semantic(ctx.shader.kind, reg);
871        ctx.decls.insert(reg, DeclInfo { reg, semantic });
872    }
873    for sampler in ctx.used_samplers.clone() {
874        ctx.sampler_decls
875            .entry(sampler)
876            .or_insert(SamplerTextureType::TwoD);
877    }
878    if ctx.shader.kind == ShaderKind::Pixel && ctx.used_outputs.is_empty() {
879        ctx.used_outputs.insert(RegisterKey {
880            ty: RegisterType::ColorOut,
881            number: 0,
882        });
883    }
884}
885
886fn semantic_from_decl(kind: ShaderKind, inst: &Instruction) -> String {
887    if let Some(reg) = inst.dest_register() {
888        if reg.ty == RegisterType::MiscType {
889            return match reg.number {
890                0 => "VPOS".to_string(),
891                1 => "VFACE".to_string(),
892                _ => format!("TEXCOORD{}", reg.number),
893            };
894        }
895        if kind == ShaderKind::Pixel {
896            match reg.ty {
897                RegisterType::Texture => return format!("TEXCOORD{}", reg.number),
898                RegisterType::Input => return format!("COLOR{}", reg.number),
899                _ => {}
900            }
901        }
902    }
903    let usage = inst.decl_usage();
904    let index = inst.decl_index();
905    let prefix = usage.semantic_prefix();
906    if index == 0
907        && !matches!(
908            usage,
909            DeclUsage::TexCoord | DeclUsage::Color | DeclUsage::Position
910        )
911    {
912        prefix.to_string()
913    } else {
914        format!("{}{}", prefix, index)
915    }
916}
917
918fn inferred_input_semantic(kind: ShaderKind, reg: RegisterKey) -> String {
919    match (kind, reg.ty) {
920        (ShaderKind::Pixel, RegisterType::Texture) => format!("TEXCOORD{}", reg.number),
921        (ShaderKind::Pixel, RegisterType::Input) => format!("COLOR{}", reg.number),
922        (_, RegisterType::Input) => match reg.number {
923            0 => "POSITION0".to_string(),
924            1 => "NORMAL0".to_string(),
925            n => format!("TEXCOORD{}", n.saturating_sub(2)),
926        },
927        (_, RegisterType::MiscType) => match reg.number {
928            0 => "VPOS".to_string(),
929            1 => "VFACE".to_string(),
930            _ => format!("TEXCOORD{}", reg.number),
931        },
932        _ => format!("TEXCOORD{}", reg.number),
933    }
934}
935
936fn output_semantic(ctx: &Context<'_>, reg: RegisterKey) -> String {
937    if let Some(decl) = ctx.decls.get(&reg) {
938        return decl.semantic.clone();
939    }
940    match reg.ty {
941        RegisterType::ColorOut => format!("COLOR{}", reg.number),
942        RegisterType::DepthOut => "DEPTH".to_string(),
943        RegisterType::RastOut => match reg.number {
944            0 => "POSITION0".to_string(),
945            1 => "FOG".to_string(),
946            2 => "PSIZE".to_string(),
947            n => format!("TEXCOORD{}", n),
948        },
949        RegisterType::AttrOut => format!("COLOR{}", reg.number),
950        RegisterType::Output => format!("TEXCOORD{}", reg.number),
951        _ => format!("TEXCOORD{}", reg.number),
952    }
953}
954
955fn input_attr(kind: ShaderKind, semantic: &str) -> String {
956    let upper = semantic.to_ascii_uppercase();
957    if kind == ShaderKind::Pixel && (upper == "VPOS" || upper == "POSITION" || upper == "POSITION0")
958    {
959        return "@builtin(position)".to_string();
960    }
961    if kind == ShaderKind::Pixel && upper == "VFACE" {
962        return "@builtin(front_facing)".to_string();
963    }
964    format!("@location({})", semantic_location(semantic))
965}
966fn output_attr(kind: ShaderKind, semantic: &str, reg: RegisterKey) -> String {
967    if kind == ShaderKind::Vertex && is_position_semantic(semantic) {
968        "@builtin(position)".to_string()
969    } else if reg.ty == RegisterType::DepthOut || semantic == "DEPTH" {
970        "@builtin(frag_depth)".to_string()
971    } else {
972        format!("@location({})", semantic_location(semantic))
973    }
974}
975
976fn semantic_location(semantic: &str) -> u32 {
977    let upper = semantic.to_ascii_uppercase();
978    if let Some(n) = parse_semantic_index(&upper, "POSITION") {
979        return n;
980    }
981    if let Some(n) = parse_semantic_index(&upper, "NORMAL") {
982        return 1 + n;
983    }
984    if let Some(n) = parse_semantic_index(&upper, "COLOR") {
985        return 2 + n;
986    }
987    if let Some(n) = parse_semantic_index(&upper, "TEXCOORD") {
988        return 4 + n;
989    }
990    if let Some(n) = parse_semantic_index(&upper, "BLENDWEIGHT") {
991        return 12 + n;
992    }
993    if let Some(n) = parse_semantic_index(&upper, "BLENDINDICES") {
994        return 14 + n;
995    }
996    if let Some(n) = parse_semantic_index(&upper, "TANGENT") {
997        return 16 + n;
998    }
999    if let Some(n) = parse_semantic_index(&upper, "BINORMAL") {
1000        return 18 + n;
1001    }
1002    if upper == "FOG" {
1003        return 20;
1004    }
1005    if upper == "PSIZE" {
1006        return 21;
1007    }
1008    31
1009}
1010
1011fn parse_semantic_index(s: &str, prefix: &str) -> Option<u32> {
1012    if !s.starts_with(prefix) {
1013        return None;
1014    }
1015    let rest = &s[prefix.len()..];
1016    if rest.is_empty() {
1017        Some(0)
1018    } else {
1019        rest.parse::<u32>().ok()
1020    }
1021}
1022
1023fn is_position_semantic(s: &str) -> bool {
1024    let upper = s.to_ascii_uppercase();
1025    upper == "POSITION" || upper == "POSITION0" || upper == "POSITIONT" || upper == "POSITIONT0"
1026}
1027
1028fn wgsl_input_field_type(ctx: &Context<'_>, reg: RegisterKey) -> &'static str {
1029    match reg {
1030        RegisterKey {
1031            ty: RegisterType::MiscType,
1032            number: 1,
1033        } => "bool",
1034        _ => {
1035            if let Some(decl) = ctx.decls.get(&reg)
1036                && (decl.semantic.starts_with("TEXCOORD") || decl.semantic.starts_with("COLOR"))
1037            {
1038                return "vec4<f32>";
1039            }
1040            "vec4<f32>"
1041        }
1042    }
1043}
1044
1045fn wgsl_output_field_type(reg: RegisterKey) -> &'static str {
1046    match reg {
1047        RegisterKey {
1048            ty: RegisterType::DepthOut,
1049            ..
1050        } => "f32",
1051        RegisterKey {
1052            ty: RegisterType::RastOut,
1053            number: 1 | 2,
1054        } => "f32",
1055        _ => "vec4<f32>",
1056    }
1057}
1058
1059fn input_struct_name(kind: ShaderKind) -> &'static str {
1060    match kind {
1061        ShaderKind::Vertex => "VSInput",
1062        ShaderKind::Pixel => "PSInput",
1063    }
1064}
1065
1066fn output_struct_name(kind: ShaderKind) -> &'static str {
1067    match kind {
1068        ShaderKind::Vertex => "VSOutput",
1069        ShaderKind::Pixel => "PSOutput",
1070    }
1071}
1072
1073fn input_field_name(reg: RegisterKey) -> String {
1074    match reg.ty {
1075        RegisterType::Input => format!("v{}", reg.number),
1076        RegisterType::Texture => format!("t{}", reg.number),
1077        RegisterType::MiscType => match reg.number {
1078            0 => "vPos".to_string(),
1079            1 => "vFace".to_string(),
1080            _ => format!("vMisc{}", reg.number),
1081        },
1082        _ => format!("in{}", reg.number),
1083    }
1084}
1085
1086fn output_field_name(reg: RegisterKey) -> String {
1087    match reg.ty {
1088        RegisterType::ColorOut => format!("oC{}", reg.number),
1089        RegisterType::DepthOut => "oDepth".to_string(),
1090        RegisterType::RastOut => match reg.number {
1091            0 => "oPos".to_string(),
1092            1 => "oFog".to_string(),
1093            2 => "oPts".to_string(),
1094            _ => format!("o{}", reg.number),
1095        },
1096        RegisterType::AttrOut => format!("oD{}", reg.number),
1097        RegisterType::Output => format!("o{}", reg.number),
1098        _ => format!("out{}", reg.number),
1099    }
1100}
1101
1102fn temp_name(reg: RegisterKey, kind: ShaderKind) -> String {
1103    match (reg.ty, kind) {
1104        (RegisterType::Texture, ShaderKind::Vertex) => format!("a{}", reg.number),
1105        (RegisterType::Predicate, _) => format!("p{}", reg.number),
1106        (RegisterType::TempFloat16, _) => format!("h{}", reg.number),
1107        _ => format!("r{}", reg.number),
1108    }
1109}
1110
1111fn temp_type(reg: RegisterKey) -> &'static str {
1112    match reg.ty {
1113        RegisterType::Predicate => "vec4<bool>",
1114        RegisterType::Texture => "vec4<f32>",
1115        RegisterType::TempFloat16 => "vec4<f32>",
1116        _ => "vec4<f32>",
1117    }
1118}
1119
1120fn zero_value(ty: &str) -> &'static str {
1121    match ty {
1122        "f32" => "0.0",
1123        "bool" => "false",
1124        "vec2<f32>" => "vec2<f32>(0.0)",
1125        "vec3<f32>" => "vec3<f32>(0.0)",
1126        "vec4<bool>" => "vec4<bool>(false)",
1127        _ => "vec4<f32>(0.0)",
1128    }
1129}
1130
1131fn zero_vector(n: usize) -> String {
1132    match n {
1133        1 => "0.0".to_string(),
1134        2 => "vec2<f32>(0.0)".to_string(),
1135        3 => "vec3<f32>(0.0)".to_string(),
1136        _ => "vec4<f32>(0.0)".to_string(),
1137    }
1138}
1139
1140fn one_vector(n: usize) -> String {
1141    match n {
1142        1 => "1.0".to_string(),
1143        2 => "vec2<f32>(1.0)".to_string(),
1144        3 => "vec3<f32>(1.0)".to_string(),
1145        _ => "vec4<f32>(1.0)".to_string(),
1146    }
1147}
1148
1149fn half_vector(n: usize) -> String {
1150    match n {
1151        1 => "0.5".to_string(),
1152        2 => "vec2<f32>(0.5)".to_string(),
1153        3 => "vec3<f32>(0.5)".to_string(),
1154        _ => "vec4<f32>(0.5)".to_string(),
1155    }
1156}
1157
1158fn vector_constructor(values: &[String]) -> String {
1159    match values.len() {
1160        0 => "0.0".to_string(),
1161        1 => values[0].clone(),
1162        2 => format!("vec2<f32>({}, {})", values[0], values[1]),
1163        3 => format!("vec3<f32>({}, {}, {})", values[0], values[1], values[2]),
1164        _ => format!(
1165            "vec4<f32>({}, {}, {}, {})",
1166            values[0], values[1], values[2], values[3]
1167        ),
1168    }
1169}
1170
1171fn wgsl_mask_suffix(mask: u8) -> String {
1172    let m = if mask == 0 { 0xf } else { mask };
1173    if m == 0xf {
1174        String::new()
1175    } else {
1176        let mut s = String::from(".");
1177        for (i, c) in ['x', 'y', 'z', 'w'].iter().enumerate() {
1178            if (m & (1 << i)) != 0 {
1179                s.push(*c);
1180            }
1181        }
1182        s
1183    }
1184}
1185
1186fn wgsl_source_swizzle_suffix(swizzle: [usize; 4], count: usize) -> String {
1187    let count = count.clamp(1, 4);
1188    let identity = [0usize, 1, 2, 3];
1189    if count == 4 && swizzle == identity {
1190        return String::new();
1191    }
1192    let names = ['x', 'y', 'z', 'w'];
1193    let mut s = String::from(".");
1194    for i in 0..count {
1195        s.push(names[swizzle[i]]);
1196    }
1197    s
1198}
1199
1200fn component_suffix(component: usize) -> &'static str {
1201    match component {
1202        0 => ".x",
1203        1 => ".y",
1204        2 => ".z",
1205        3 => ".w",
1206        _ => ".x",
1207    }
1208}
1209
1210fn coerce_expr_width(expr: String, src_width: usize, dst_width: usize) -> String {
1211    let src_width = src_width.clamp(1, 4);
1212    let dst_width = dst_width.clamp(1, 4);
1213    if src_width == dst_width {
1214        return expr;
1215    }
1216    if src_width == 1 {
1217        return match dst_width {
1218            1 => expr,
1219            2 => format!("vec2<f32>({})", expr),
1220            3 => format!("vec3<f32>({})", expr),
1221            _ => format!("vec4<f32>({})", expr),
1222        };
1223    }
1224    if src_width > dst_width {
1225        let suffix = match dst_width {
1226            1 => ".x",
1227            2 => ".xy",
1228            3 => ".xyz",
1229            _ => "",
1230        };
1231        if suffix.is_empty() {
1232            expr
1233        } else {
1234            format!("({}){}", expr, suffix)
1235        }
1236    } else {
1237        match dst_width {
1238            2 => format!("vec2<f32>({}, 0.0)", expr),
1239            3 => format!("vec3<f32>({}, 0.0)", expr),
1240            _ => format!("vec4<f32>({}, 0.0)", expr),
1241        }
1242    }
1243}
1244fn apply_source_modifier(expr: String, modifier: SourceModifier, count: usize) -> String {
1245    match modifier {
1246        SourceModifier::None => expr,
1247        SourceModifier::Negate => format!("-({})", expr),
1248        SourceModifier::Bias => format!("(({}) - {})", expr, half_vector(count)),
1249        SourceModifier::BiasAndNegate => format!("-(({}) - {})", expr, half_vector(count)),
1250        SourceModifier::Sign => format!(
1251            "((({}) - {}) * {})",
1252            expr,
1253            half_vector(count),
1254            one_vector(count)
1255        ),
1256        SourceModifier::SignAndNegate => format!(
1257            "-((({}) - {}) * {})",
1258            expr,
1259            half_vector(count),
1260            one_vector(count)
1261        ),
1262        SourceModifier::Complement => format!("({} - ({}))", one_vector(count), expr),
1263        SourceModifier::X2 => format!("(({}) * 2.0)", expr),
1264        SourceModifier::X2AndNegate => format!("-(({}) * 2.0)", expr),
1265        SourceModifier::DivideByZ => format!("(({}) / ({}).z)", expr, expr),
1266        SourceModifier::DivideByW => format!("(({}) / ({}).w)", expr, expr),
1267        SourceModifier::Abs => format!("abs({})", expr),
1268        SourceModifier::AbsAndNegate => format!("-abs({})", expr),
1269        SourceModifier::Not => format!("!({})", expr),
1270        SourceModifier::Unknown(_) => expr,
1271    }
1272}
1273
1274fn apply_result_modifier(expr: String, modifier: ResultModifier, count: usize) -> String {
1275    if modifier.saturate {
1276        format!(
1277            "clamp({}, {}, {})",
1278            expr,
1279            zero_vector(count),
1280            one_vector(count)
1281        )
1282    } else {
1283        expr
1284    }
1285}
1286
1287fn scalar_bool_expr(expr: String) -> String {
1288    format!("({} != 0.0)", expr)
1289}
1290
1291fn line(out: &mut String, indent: usize, s: &str) {
1292    for _ in 0..indent {
1293        out.push_str("    ");
1294    }
1295    out.push_str(s);
1296    out.push('\n');
1297}
1298
1299fn compare_all_expr(a: String, op: &str, b: String, width: usize) -> String {
1300    if width <= 1 {
1301        format!("{} {} {}", a, op, b)
1302    } else {
1303        format!("all({} {} {})", a, op, b)
1304    }
1305}
1306
1307fn cmp_op(code: u8) -> &'static str {
1308    match code {
1309        1 => ">",
1310        2 => "==",
1311        3 => ">=",
1312        4 => "<",
1313        5 => "!=",
1314        6 => "<=",
1315        _ => "!=",
1316    }
1317}
1318
1319fn sampler_texture_type(ctx: &Context<'_>, sampler: u16) -> SamplerTextureType {
1320    if let Some(c) = sampler_constant(ctx, sampler)
1321        && let Some(t) = &c.type_info
1322    {
1323        return match t.value_type {
1324            ValueType::SamplerCube => SamplerTextureType::Cube,
1325            ValueType::Sampler3D => SamplerTextureType::Volume,
1326            _ => SamplerTextureType::TwoD,
1327        };
1328    }
1329    ctx.sampler_decls
1330        .get(&sampler)
1331        .copied()
1332        .unwrap_or(SamplerTextureType::TwoD)
1333}
1334
1335fn wgsl_texture_type(ty: SamplerTextureType) -> &'static str {
1336    match ty {
1337        SamplerTextureType::Cube => "texture_cube<f32>",
1338        SamplerTextureType::Volume => "texture_3d<f32>",
1339        SamplerTextureType::TwoD | SamplerTextureType::Unknown => "texture_2d<f32>",
1340    }
1341}
1342
1343fn sampler_constant<'a>(ctx: &'a Context<'_>, sampler: u16) -> Option<&'a ConstantInfo> {
1344    ctx.ctab?
1345        .constants
1346        .iter()
1347        .find(|c| c.register_set == RegisterSet::Sampler && c.register_index == sampler)
1348}
1349
1350fn fmt_f32(v: f32) -> String {
1351    if v.is_nan() {
1352        "(0.0 / 0.0)".to_string()
1353    } else if v.is_infinite() {
1354        if v.is_sign_positive() {
1355            "(1.0 / 0.0)".to_string()
1356        } else {
1357            "(-1.0 / 0.0)".to_string()
1358        }
1359    } else {
1360        let mut s = format!("{:.9}", v);
1361        while s.contains('.') && s.ends_with('0') {
1362            s.pop();
1363        }
1364        if s.ends_with('.') {
1365            s.push('0');
1366        }
1367        if !s.contains('.') && !s.contains('e') && !s.contains('E') {
1368            s.push_str(".0");
1369        }
1370        s
1371    }
1372}