//! Xenos → WGSL direct translator (P7). //! //! Replaces the runtime uber-shader interpreter (P3b/P3c) for shaders whose //! feature set we cover. Emits a *standalone* WGSL module per shader //! instead of walking a ucode buffer at draw time — pipeline compilation //! happens once, then every subsequent dispatch is a direct `draw()`. //! //! The translator is deliberately narrow: when it encounters an opcode / //! fetch format / CF shape it doesn't know, it returns [`None`] and the //! caller falls back to the interpreter. This keeps the op-coverage work //! incremental — future commits can add one opcode at a time without //! invalidating the scaffolding. //! //! Current coverage (v1): //! * Linear CF: `Exec`/`ExecEnd`, `Alloc`, `Exit`. No loops / branches / //! calls / predicate-gated clauses. //! * ALU vector: `ADD`, `MUL`, `MAX`, `MIN`, `MAD`, `DP4`, `DP3`, //! `DP2_ADD`, `SEQ`, `SGT`, `SGE`, `SNE`, `FRC`, `FLOOR`. //! * ALU scalar: `ADDS`, `MULS`, `MAXS`, `MINS`, `RCP`, `RETAIN_PREV`. //! * Vertex fetch: `R32G32B32A32_FLOAT` only. //! * Texture fetch: 2D via the single `@group(1)` slot (same one P5/M6 //! binds). //! * Exports: VS writes position + interpolator 0 (color); PS writes //! color0. //! //! When a shader exceeds this subset, [`translate`] returns `None` and //! `gpu.shader.translate_reject{reason}` is bumped by the caller. use crate::ucode::alu::{decode_alu, sop, vop, AluInstruction}; use crate::ucode::control_flow::{AllocKind, ControlFlowInstruction}; use crate::ucode::fetch::{decode_fetch, FetchInstruction}; use crate::ucode::ParsedShader; /// Shader stage we're emitting for. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Stage { Vertex, Pixel, } /// Success or refusal from the translator. On refusal, the caller falls /// back to the runtime uber-shader interpreter. #[derive(Debug)] pub enum Translation { /// The emitted WGSL body for *this stage only*. Both VS + PS get /// wrapped into one module via [`combine_stages`]. Ok(String), /// Translator saw an op/pattern it doesn't handle; fallback. Reject(&'static str), } /// Full WGSL module for a (VS, PS) pair ready to hand to /// `wgpu::Device::create_shader_module`. Shares the header across the two /// bodies so bindings, struct declarations, and helpers aren't duplicated. pub fn combine_stages(vs_body: &str, ps_body: &str) -> String { let mut out = String::with_capacity(4096 + vs_body.len() + ps_body.len()); out.push_str(MODULE_HEADER); out.push_str(vs_body); out.push_str(ps_body); out } /// Translate a single shader stage. Returns `None` on any unsupported /// feature with a short reason string that the caller plumbs into the /// `gpu.shader.translate_reject{reason}` metric. pub fn translate(parsed: &ParsedShader, stage: Stage) -> Translation { let mut ctx = EmitCtx::new(stage); // Emit the stage entry function body. if let Err(reason) = ctx.emit_stage_body(parsed) { return Translation::Reject(reason); } Translation::Ok(ctx.finish()) } /// Reject reasons; kept as static &'str for zero-alloc metrics. pub mod reject { pub const VEC_OP_UNSUPPORTED: &str = "vec_op_unsupported"; pub const SCL_OP_UNSUPPORTED: &str = "scl_op_unsupported"; pub const CF_LOOP: &str = "cf_loop"; pub const CF_COND: &str = "cf_cond"; pub const CF_CALL: &str = "cf_call"; pub const CF_UNKNOWN: &str = "cf_unknown"; pub const VFETCH_FMT: &str = "vfetch_fmt"; pub const TFETCH_NON2D: &str = "tfetch_non2d"; pub const INSTR_OOB: &str = "instr_oob"; } /// Shader-module preamble (bindings, helpers, struct defs). The bindings /// mirror the xenos pipeline's `@group(0)` + `@group(1)` layout from P5/M6 /// so we can use **the same bind-group slots** — only the pipeline object /// differs between interpreter mode and translator mode. const MODULE_HEADER: &str = r#" struct XenosDrawConstants { draw_index: u32, vertex_count: u32, prim_kind: u32, vertex_base_dwords: u32, ndc_scale: vec2, ndc_offset: vec2, }; struct XenosConstants { alu: array, 512>, fetch: array, bool_consts: array, loop_consts: array, }; @group(0) @binding(0) var draw_ctx : XenosDrawConstants; @group(0) @binding(1) var xenos_consts : XenosConstants; @group(0) @binding(2) var vs_ucode : array; @group(0) @binding(3) var ps_ucode : array; @group(0) @binding(4) var vertex_buffer : array; @group(1) @binding(0) var xenos_samp : sampler; @group(1) @binding(1) var xenos_tex0 : texture_2d; @group(1) @binding(2) var xenos_tex1 : texture_2d; @group(1) @binding(3) var xenos_tex2 : texture_2d; @group(1) @binding(4) var xenos_tex3 : texture_2d; @group(1) @binding(5) var xenos_tex4 : texture_2d; @group(1) @binding(6) var xenos_tex5 : texture_2d; @group(1) @binding(7) var xenos_tex6 : texture_2d; @group(1) @binding(8) var xenos_tex7 : texture_2d; // iterate-3T: real interpolator passthrough. The Xenos VS exports up to 16 // interpolators (export index 0..15); the PS reads interpolator i from its // general register r[i]. We carry 8 interpolator vec4s (covers Sylpheed's // splash: r0=color, r1=texcoord). `color` retained as an alias of interp0 so // older single-color paths keep working. struct VsOut { @builtin(position) position: vec4, @location(0) interp0: vec4, @location(1) interp1: vec4, @location(2) interp2: vec4, @location(3) interp3: vec4, @location(4) interp4: vec4, @location(5) interp5: vec4, @location(6) interp6: vec4, @location(7) interp7: vec4, }; struct FsOut { @location(0) color0: vec4, }; // Helper: reciprocal guarded against divide-by-zero. fn xe_rcp(x: f32) -> f32 { return select(0.0, 1.0 / x, x != 0.0); } // GPUBUG-102: per-format byte-swap matching canary's `GpuSwapInline` // (xenos.h:1090-1109). Xbox 360 vertex data is big-endian; the host is // little-endian. The fetch constant's `endian` field (low 2 bits of // dword_1) selects: // 0 (kNone) — no swap // 1 (k8in16) — swap bytes within halfwords // 2 (k8in32) — full byte reverse // 3 (k16in32) — swap halfwords fn gpu_swap(value: u32, endian: u32) -> u32 { switch endian { case 1u: { return ((value << 8u) & 0xFF00FF00u) | ((value >> 8u) & 0x00FF00FFu); } case 2u: { return ((value & 0x000000FFu) << 24u) | ((value & 0x0000FF00u) << 8u) | ((value & 0x00FF0000u) >> 8u) | ((value & 0xFF000000u) >> 24u); } case 3u: { return ((value >> 16u) & 0xFFFFu) | (value << 16u); } default: { return value; } } } "#; struct EmitCtx { stage: Stage, out: String, indent: usize, /// GPUBUG-114: dword stride of the most recent *full* vfetch, keyed by /// fetch-const register offset. A vfetch_mini carries stride=0 and reuses /// the address + stride of the preceding full vfetch of the same stream /// (canary ucode.h:733). Without this a mini color attribute indexes by its /// tight dword count instead of the real vertex stride → reads the wrong /// vertex's data (Sylpheed's background fill `0x36660986` read garbage → /// white instead of the intended color). last_full_stride: std::collections::HashMap, } impl EmitCtx { fn new(stage: Stage) -> Self { Self { stage, out: String::with_capacity(2048), indent: 0, last_full_stride: std::collections::HashMap::new(), } } fn finish(self) -> String { self.out } fn push(&mut self, s: &str) { for _ in 0..self.indent { self.out.push_str(" "); } self.out.push_str(s); self.out.push('\n'); } fn emit_stage_body(&mut self, parsed: &ParsedShader) -> Result<(), &'static str> { // Entry function + struct header. match self.stage { Stage::Vertex => { self.push("@vertex"); self.push("fn vs_main(@builtin(vertex_index) vidx: u32) -> VsOut {"); } Stage::Pixel => { self.push("@fragment"); self.push("fn fs_main(in: VsOut) -> FsOut {"); } } self.indent = 1; // Register file + ps chain + export slots. All local `var`s so each // invocation gets its own state; translator-emitted code doesn't // need `var` because we don't share across function calls. self.push("var r: array, 128>;"); self.push("for (var i = 0u; i < 128u; i = i + 1u) { r[i] = vec4(0.0); }"); self.push("var ps: f32 = 0.0;"); match self.stage { Stage::Vertex => { // iterate-3T: host→guest vertex-index remap for primitives the // replay draws non-indexed as a flat triangle list. wgpu has no // QuadList/RectangleList topology, so the host issues 6 vertices // per quad/rect and we map them back to the guest's 4/3 source // vertices here (mirrors `primitive.rs` index rewrite, but in the // VS since the replay path is non-indexed): // QuadList(13): 6 host verts → guest [0,1,2, 0,2,3] // RectangleList(8): 6 host verts → [0,1,2, 2,1,3]; corner 3 // has no backing vertex, so `rect_synth` flags it and // `emit_vfetch` extrapolates its attributes as v0+v2-v1 // (parallelogram completion, matching Xenos rect semantics). // Drawing only the front triangle left a diagonal seam. // Other prims pass through unchanged. self.push("var gvidx: u32 = vidx;"); // Rectangle-list 4th-corner synthesis state, consumed by // `emit_vfetch`. `rect_synth` is false for every other prim. self.push("var rect_base: u32 = 0u;"); self.push("var rect_synth: bool = false;"); self.push("if (draw_ctx.prim_kind == 13u) {"); self.indent += 1; self.push("let q = vidx % 6u; let qbase = (vidx / 6u) * 4u;"); self.push("var lut = array(0u, 1u, 2u, 0u, 2u, 3u);"); self.push("gvidx = qbase + lut[q];"); self.indent -= 1; self.push("} else if (draw_ctx.prim_kind == 8u) {"); self.indent += 1; self.push("let local = vidx % 6u; rect_base = (vidx / 6u) * 3u;"); // Triangles (v0,v1,v2) + (v0,v2,v3) — canary's rect tessellation // (an explicit list, not a strip). v3 (corner 3) is synthesized. self.push("var rlut = array(0u, 1u, 2u, 0u, 2u, 3u);"); self.push("let corner = rlut[local];"); self.push("gvidx = rect_base + corner;"); self.push("rect_synth = corner == 3u;"); self.indent -= 1; self.push("}"); // Seed r0 with vertex index for simple shaders that read it. self.push("r[0] = vec4(f32(gvidx), 0.0, 0.0, 1.0);"); // iterate-3T: real export model. Xenos export index 62 = oPos; // indices 0..15 = interpolators. We hold position + 8 // interpolator vec4s; `emit_export` writes the right slot keyed // on the export index. // // iterate-3AE (WHITE-TRIANGLE ROOT): interpolators a VS does NOT // export must default to ZERO, not white. The old `ointerp[0] = // (1,1,1,1)` was an iterate-3T debug convenience ("so a VS that // only exports position still yields a visible non-zero color") // — but it is a FAKE: it injects white that no guest value backs. // The transition/background draws use the position-only VS // `0xd4c14f46` (one vfetch → oPos; it exports NO color) paired // with PS `0xed732b5a` (`ocolor0 = interp0`). With the white // seed, interp0 stayed (1,1,1,1) → the fullscreen fill rendered // OPAQUE WHITE (the diagonal half-triangle artifact that flashed // before each splash logo and persisted across the dev-logo // transition). Canary shows a black background there because the // un-exported interpolator carries no white. Default to // (0,0,0,0): a position-only VS now contributes nothing visible // under its real (opaque or premultiplied) blend, matching // canary, while every VS that really exports interp0 (the logo // `0x03b7b020`, the `0x36660986` color fill) overwrites this seed // and is unaffected. RGB=0 → black fill; A=0 → premultiplied // overlays stay transparent. self.push("var opos: vec4 = vec4(0.0, 0.0, 0.0, 1.0);"); self.push("var ointerp: array, 8>;"); self.push("for (var i = 0u; i < 8u; i = i + 1u) { ointerp[i] = vec4(0.0, 0.0, 0.0, 0.0); }"); } Stage::Pixel => { // iterate-3T: the PS reads interpolator i from general register // r[i] (Xenos PS input GPR mapping). Seed r0..r7 from the VS's // interpolators so e.g. the logo PS's texcoord (r1) and color // (r0) arrive correctly; tfetch then samples at the real UV. self.push("r[0] = in.interp0;"); self.push("r[1] = in.interp1;"); self.push("r[2] = in.interp2;"); self.push("r[3] = in.interp3;"); self.push("r[4] = in.interp4;"); self.push("r[5] = in.interp5;"); self.push("r[6] = in.interp6;"); self.push("r[7] = in.interp7;"); self.push("var ocolor0: vec4 = in.interp0;"); } } let mut current_alloc = AllocKind::Other; for clause in &parsed.cf { match clause { ControlFlowInstruction::Exec { address, count, sequence, is_end, predicated, .. } => { if *predicated { return Err(reject::CF_COND); } self.emit_exec(parsed, *address, *count, *sequence, current_alloc)?; if *is_end { break; } } ControlFlowInstruction::Alloc { kind, .. } => { current_alloc = *kind; } ControlFlowInstruction::Exit => break, // Non-executing CF clauses: padding (`kNop`) and the // vertex-fetch-done hint (`kMarkVsFetchDone`). Skip them. ControlFlowInstruction::Nop | ControlFlowInstruction::MarkVsFetchDone => {} ControlFlowInstruction::LoopStart { .. } | ControlFlowInstruction::LoopEnd { .. } => return Err(reject::CF_LOOP), ControlFlowInstruction::CondJmp { .. } => return Err(reject::CF_COND), ControlFlowInstruction::CondCall { .. } | ControlFlowInstruction::Return => { return Err(reject::CF_CALL); } ControlFlowInstruction::Unknown { .. } => return Err(reject::CF_UNKNOWN), } } match self.stage { Stage::Vertex => { self.push("var out: VsOut;"); // iterate-3S: guest VS position → host clip space. The guest // emits either clip-space or (screen-space, clip disabled) // render-target-pixel coords; `ndc_scale`/`ndc_offset` (from // canary's GetHostViewportInfo, computed CPU-side per draw) // rescale XY into wgpu clip space with Y already flipped. When // the transform is unset (all-zero scale, procedural fallback) // pass the position through unchanged. self.push("if (draw_ctx.ndc_scale.x != 0.0 || draw_ctx.ndc_scale.y != 0.0) {"); self.indent += 1; self.push("opos = vec4(opos.xy * draw_ctx.ndc_scale + draw_ctx.ndc_offset * opos.w, opos.z, opos.w);"); self.indent -= 1; self.push("}"); self.push("out.position = opos;"); self.push("out.interp0 = ointerp[0];"); self.push("out.interp1 = ointerp[1];"); self.push("out.interp2 = ointerp[2];"); self.push("out.interp3 = ointerp[3];"); self.push("out.interp4 = ointerp[4];"); self.push("out.interp5 = ointerp[5];"); self.push("out.interp6 = ointerp[6];"); self.push("out.interp7 = ointerp[7];"); self.push("return out;"); } Stage::Pixel => { self.push("var out: FsOut;"); // GPUBUG-115: saturate the color export to [0,1], flushing NaN // to 0 — exactly what canary does before writing a UNORM render // target (spirv_shader_translator.cc:3607 "Saturate, flushing // NaN to 0"). The Xenos RB clamps PS output for UNORM targets; // without this an out-of-range guest color (Sylpheed's // background fill exports a huge negative float `-32896.5` as a // fullscreen-clear value) writes garbage/NaN to the sRGB target // → renders white instead of the clamped black canary shows. // `clamp(x,0,1)` returns 0 for NaN under WGSL's clamp semantics. self.push("out.color0 = clamp(ocolor0, vec4(0.0), vec4(1.0));"); self.push("return out;"); } } self.indent = 0; self.push("}"); Ok(()) } fn emit_exec( &mut self, parsed: &ParsedShader, address: u32, count: u32, sequence: u32, current_alloc: AllocKind, ) -> Result<(), &'static str> { for i in 0..(count as usize) { let triple_idx = address as usize + i; let base = triple_idx * 3; if base + 2 >= parsed.instructions.len() { return Err(reject::INSTR_OOB); } let words = [ parsed.instructions[base], parsed.instructions[base + 1], parsed.instructions[base + 2], ]; // sequence: 2 bits per instruction — bit[0]=fetch(1)/ALU(0), // bit[1]=serialize (Xenos `ucode.h:226`). let is_fetch = ((sequence >> (i * 2)) & 1) != 0; if is_fetch { match decode_fetch(words) { FetchInstruction::Vertex(vf) => self.emit_vfetch(&vf)?, FetchInstruction::Texture(tf) => { if tf.dimension != 1 { return Err(reject::TFETCH_NON2D); } self.emit_tfetch(&tf); } FetchInstruction::Unknown { .. } => return Err(reject::VFETCH_FMT), } } else { let alu = decode_alu(words); self.emit_alu(&alu, current_alloc)?; } } Ok(()) } fn emit_alu( &mut self, alu: &AluInstruction, current_alloc: AllocKind, ) -> Result<(), &'static str> { // GPUBUG-100/101: per-operand temp-vs-constant selector (w0 // bits 29-31), 8-bit component-relative swizzle (w1 bytes 0-2), // and 1-bit negate (w1 bits 24-26). Pre-fix all three were // discarded, so every ALU read came back as r[low7] without // any swizzle / negation, dropping every shader's uniforms + // negative operands. // Xenos splits the 512-entry float-constant file into two halves: the // vertex shader addresses c0..255 (physical 0..255), the pixel shader // addresses c0..255 too but the hardware reads them from the UPPER half // (physical 256..511). Our snapshot is the full 512-entry file read // linearly, so a PS constant reference `c_n` must index `alu[256 + n]`. // (RE STEP 95: the movie's YUV→RGB PS reads c254/c255; the game uploads // the coefficients to physical 510/511 = 256+254/255.) let const_base = match self.stage { Stage::Pixel => 256u32, Stage::Vertex => 0, }; let a = src_operand(alu.src_a, alu.src_a_is_temp, alu.src_a_swiz, alu.src_a_negate, const_base); let b = src_operand(alu.src_b, alu.src_b_is_temp, alu.src_b_swiz, alu.src_b_negate, const_base); let c = src_operand(alu.src_c, alu.src_c_is_temp, alu.src_c_swiz, alu.src_c_negate, const_base); if (42..=47).contains(&alu.scalar_opcode) && std::env::var("XENIA_SC_LOG").is_ok() { eprintln!( "SC-OP opc={} sa={} atmp={} asw={:#04x} sb={} btmp={} bsw={:#04x} sc={} ctmp={} csw={:#04x}", alu.scalar_opcode, alu.src_a, alu.src_a_is_temp as u8, alu.src_a_swiz, alu.src_b, alu.src_b_is_temp as u8, alu.src_b_swiz, alu.src_c, alu.src_c_is_temp as u8, alu.src_c_swiz, ); } // Vector pipe. if alu.vector_write_mask != 0 { let expr = vector_expr(alu.vector_opcode, &a, &b, &c) .ok_or(reject::VEC_OP_UNSUPPORTED)?; let dst_reg = alu.vector_dest & 0x7F; if alu.vector_dest_is_export { self.emit_export(dst_reg, current_alloc, &expr, alu.vector_write_mask); } else { self.emit_masked_write(&format!("r[{dst_reg}u]"), &expr, alu.vector_write_mask); } } // Scalar pipe. Most scalar ops use (src_a.x, src_b.x); ps-variants use // the running `ps`. The scalar-constant family (MULSC/ADDSC/SUBSC, // 42..=47) is SPECIAL: it reads one temp register and one float // constant, both addressed through src3 (canary ucode.h // `scalar_const_reg_op_src_temp_reg`): // temp reg = (src3_swiz & 0x3C) | (scalar_opc & 1), component = src3_swiz & 3 // const idx = src3_reg (+256 for the PS constant bank), component = .w // The op is then temp const. Feeding it the plain (src_a.x, // src_b.x) instead computed e.g. r0.x*r0.x (Yb²) for the YUV→RGB luma // scale — a squared luma that crushes shadows so dark regions render as // near-pure chroma (the intro's dark background read purple). The // earlier attempt at this addressing failed only because the constant // bank was still mis-indexed (pre-STEP-95): the coefficients read zero. let (scl_src_a, scl_src_b) = if (42..=47).contains(&alu.scalar_opcode) { let temp_reg = ((alu.src_c_swiz & 0x3C) | (alu.scalar_opcode & 1)) as u32; let temp_comp = ['x', 'y', 'z', 'w'][(alu.src_c_swiz & 0x3) as usize]; let const_idx = alu.src_c as u32 + const_base; ( format!("r[{temp_reg}u].{temp_comp}"), format!("xenos_consts.alu[{const_idx}u].w"), ) } else if alu.scalar_src_is_ps { ("ps".to_string(), format!("{}.x", b)) } else { (format!("{}.x", a), format!("{}.x", b)) }; let expr = match scalar_expr(alu.scalar_opcode, &scl_src_a, &scl_src_b, "ps") { Some(e) => e, None => { if std::env::var("XENIA_BIND_LOG").is_ok() { eprintln!("SCL-UNSUPPORTED opcode={:#04x} ({})", alu.scalar_opcode, alu.scalar_opcode); } return Err(reject::SCL_OP_UNSUPPORTED); } }; self.push(&format!("ps = {expr};")); if alu.scalar_write_mask != 0 { let v = "vec4(ps, ps, ps, ps)"; let dst_reg = alu.scalar_dest & 0x7F; self.emit_masked_write(&format!("r[{dst_reg}u]"), v, alu.scalar_write_mask); } Ok(()) } fn emit_masked_write(&mut self, lhs: &str, rhs: &str, mask: u8) { if mask == 0xF { self.push(&format!("{lhs} = {rhs};")); return; } self.push(&"{".to_string()); self.indent += 1; self.push(&format!("let _prev = {lhs};")); self.push(&format!("let _new = {rhs};")); let mut components = Vec::new(); let letters = ['x', 'y', 'z', 'w']; for (i, c) in letters.iter().enumerate() { if (mask >> i) & 1 == 1 { components.push(format!("_new.{c}")); } else { components.push(format!("_prev.{c}")); } } self.push(&format!( "{lhs} = vec4({}, {}, {}, {});", components[0], components[1], components[2], components[3] )); self.indent -= 1; self.push("}"); } fn emit_export(&mut self, dst_reg: u8, alloc: AllocKind, expr: &str, mask: u8) { // iterate-3T: real Xenos export-index model (replaces the `AllocKind` // heuristic, which collapsed every VS export to a single color slot and // dropped the texcoord interpolator → tfetch sampled (0,0) → flat). // When `export_data` is set the 6-bit vector_dest IS the export index: // VS: 62 = oPos, 63 = oPointSize/edge (ignored), 0..15 = interpolators. // PS: 0..3 = color render targets (we honor RT0). let _ = alloc; match self.stage { Stage::Vertex => { let lhs = if dst_reg == 62 { "opos".to_string() } else if dst_reg <= 15 { // Clamp to the 8 interpolator slots we carry; higher slots // are unused by Sylpheed's splash. let i = (dst_reg as usize).min(7); format!("ointerp[{i}u]") } else { // oPointSize (63) / unknown export slot — discard. return; }; self.emit_masked_write(&lhs, expr, mask); } Stage::Pixel => { // Only RT0 (export index 0) is wired to the single host target. if dst_reg == 0 { self.emit_masked_write("ocolor0", expr, mask); } } } } fn emit_vfetch(&mut self, vf: &crate::ucode::fetch::VertexFetch) -> Result<(), &'static str> { // GPUBUG-107 (iterate-3S): decode the vertex FORMAT + dword STRIDE from // the vfetch instruction instead of hardcoding R32G32B32A32 (4 floats, // stride 4). Sylpheed's splash quads are `k_32_32_FLOAT` (2 floats, // stride 2); over-reading them put the next vertex's X into .w → a // negative W → the whole rectangle clipped behind the camera. We cover // the float vertex formats (the UI / screen-space draws); other formats // reject to the interpreter. // // GPUBUG-102: the fetch constant holds the endian field in dword_1's // low 2 bits; Xbox 360 vertex data is big-endian, so `gpu_swap` undoes // it per component. // (comps, dwords_read) per format. Float formats are 1 dword/component; // iterate-3T adds the packed-16 `k_16_16` (format 6) used for the logo // UV interpolator — 2 components packed into ONE dword. #[derive(PartialEq)] enum Pack { Float, // N f32 lanes, N dwords Norm16x2, // 2× u16 normalized into [0,1], 1 dword (k_16_16) Norm8x4, // 4× u8 normalized into [0,1], 1 dword (k_8_8_8_8) } let (comps, dwords_read, pack): (u32, u32, Pack) = match vf.format { 36 => (1, 1, Pack::Float), // k_32_FLOAT 37 => (2, 2, Pack::Float), // k_32_32_FLOAT 57 => (3, 3, Pack::Float), // k_32_32_32_FLOAT 38 => (4, 4, Pack::Float), // k_32_32_32_32_FLOAT 6 => (4, 1, Pack::Norm8x4), // k_8_8_8_8 (packed RGBA8 — GPUBUG-112) 25 => (2, 1, Pack::Norm16x2), // k_16_16 _ => return Err(reject::VFETCH_FMT), }; // iterate-3X (GPUBUG-110): index the fetch-constant region by the full // `const_index*3 + const_index_sel` mapping (canary `ucode.h:700`), // packed as `const_index*6 + sel*2` dwords. The previous expression // `(vf.raw[0] >> 5) & 0x1F` read the *src_reg* bits, not the const // index — wrong for the endian term and the no-window fallback base. let const_off = vf.const_reg_offset(); // GPUBUG-114: a full vfetch carries the real vertex dword stride; a // vfetch_mini reuses the address + stride of the preceding full vfetch // of the same stream (canary ucode.h:733). Track the last full stride // per fetch-const and inherit it for mini-fetches (stride field == 0). let stride = if vf.is_mini_fetch || vf.stride == 0 { *self .last_full_stride .get(&const_off) .unwrap_or(&dwords_read) } else { self.last_full_stride.insert(const_off, vf.stride as u32); vf.stride as u32 }; // iterate-3T: per-attribute dword offset within the vertex (vfetches // sharing one fetch constant read different attributes). let attr_off = vf.offset; let src_reg = vf.src_register & 0x7F; let dst_reg = vf.dest_register & 0x7F; // is_signed selects [-1,1] vs [0,1] for normalized integer formats. let signed = vf.is_signed; // Build the per-component reads; unread lanes default to 0/0/0/1 so an // XY-only position keeps W=1 (and Z=0). let lane = |i: u32| -> String { match pack { Pack::Float => { if i < comps { format!("bitcast(gpu_swap(vertex_buffer[addr + {i}u], endian))") } else if i == 3 { "1.0".to_string() } else { "0.0".to_string() } } Pack::Norm16x2 => { // One dword holds [u16 lo | u16 hi] after the endian swap. // Component 0 = low halfword, component 1 = high halfword. if i == 0 { if signed { "(max(f32(i32(w16 << 16u) >> 16u) / 32767.0, -1.0))".to_string() } else { "(f32(w16 & 0xFFFFu) / 65535.0)".to_string() } } else if i == 1 { if signed { "(max(f32(i32(w16) >> 16u) / 32767.0, -1.0))".to_string() } else { "(f32(w16 >> 16u) / 65535.0)".to_string() } } else if i == 3 { "1.0".to_string() } else { "0.0".to_string() } } Pack::Norm8x4 => { // One dword holds 4× u8 (canary spirv_shader_translator_fetch // k_8_8_8_8: comp0@bit0, comp1@bit8, comp2@bit16, comp3@bit24) // after the endian swap. All four channels present → normalize // to [0,1]. GPUBUG-112: this is the logo/background vertex // COLOR (RGBA8), previously misdecoded as k_16_16 (2 chans, // B forced 0) → white texture × (R,G,0) = yellow. let sh = i * 8; if signed { format!( "(max(f32(i32(w16 << {l}u) >> 24u) / 127.0, -1.0))", l = 24 - sh ) } else { format!("(f32((w16 >> {sh}u) & 0xFFu) / 255.0)") } } } }; let read_bound = dwords_read - 1; // GPUBUG-108 (iterate-3S): for the captured-geometry path the CPU // uploads a vertex window that begins EXACTLY at the fetch base, so the // base within `vertex_buffer` is 0 and vertex i sits at `i * stride`. // The previous `abs_base - vertex_base_dwords` rebase recomputed the // base from `xenos_consts.fetch[]`, but that uniform carries the // *last-published* (per-frame) fetch constant, not this draw's — for // the splash it was stale (0x8a000002 vs the real 0x0adf… base), so the // rebase produced a huge out-of-window address, the bounds guard // failed, and every vertex kept its seed (vertex_index, 0, 0, 1) → // every quad collapsed to ~one pixel at the origin. Index from 0 when a // real window is present (`vertex_base_dwords != 0`); only the // synthetic/no-window fallback consults the uniform fetch constant. let endian_term = format!("xenos_consts.fetch[{}u] & 0x3u", const_off + 1); // For packed formats (k_16_16, k_8_8_8_8) we read one dword into `w16` // (post endian-swap) and the `lane()` exprs above unpack the channels. let w16_decl = if pack == Pack::Norm16x2 || pack == Pack::Norm8x4 { "let w16 = gpu_swap(vertex_buffer[addr], endian); " } else { "" }; let l0 = lane(0); let l1 = lane(1); let l2 = lane(2); let l3 = lane(3); // One decode of `addr` → the attribute vec4. Reused verbatim for the // normal single-vertex read and for each of the three source vertices // of a synthesized rectangle corner (the lane exprs reference `addr` // and `w16` by name, so each `{ let addr = …; }` sub-scope re-decodes). let read_into = |dst: &str, idx_expr: &str| -> String { format!( "{{ let addr = base + ({idx_expr}) * {stride}u + {attr_off}u; \ if (addr + {read_bound}u < n) {{ {w16_decl}{dst} = vec4({l0}, {l1}, {l2}, {l3}); }} }}" ) }; // RectangleList 4th corner: no backing vertex, so extrapolate the // attribute as v0 + v2 - v1 (parallelogram completion, matching canary). // With the (v0,v1,v2)+(v0,v2,v3) tessellation the synthesized corner is // diagonal to v1 (e.g. TL,TR,BR given → BL = TL+BR-TR). Applies // uniformly to position and every interpolator. `rect_synth`/`rect_base` // come from the VS preamble; false/0 for every non-rect draw. let t0 = read_into("t0", "rect_base + 0u"); let t1 = read_into("t1", "rect_base + 1u"); let t2 = read_into("t2", "rect_base + 2u"); let normal = read_into(&format!("r[{dst_reg}u]"), &format!("u32(r[{src_reg}u].x)")); self.push(&format!( "{{ let endian = {endian_term}; \ var base = 0u; \ if (draw_ctx.vertex_base_dwords == 0u) {{ \ base = (xenos_consts.fetch[{fc0_idx}u] & 0xFFFFFFFCu) >> 2u; \ }} \ let n = arrayLength(&vertex_buffer); \ if (rect_synth) {{ \ var t0 = vec4(0.0, 0.0, 0.0, 1.0); var t1 = t0; var t2 = t0; \ {t0} {t1} {t2} \ r[{dst_reg}u] = t0 + t2 - t1; \ }} else {{ {normal} }} }}", fc0_idx = const_off, )); Ok(()) } fn emit_tfetch(&mut self, tf: &crate::ucode::fetch::TextureFetch) { // Sample the texture bound to this fetch's constant slot; UV = // r[src].xy. The UI binds up to 8 planes (one per fetch-constant slot), // so a multi-plane shader (the intro video's YUV Y/U/V) reads the right // texture per fetch. Slots >= 8 fall back to slot 0 (matches the // interpreter's `default` arm). let src_reg = tf.src_register & 0x7F; let dst_reg = tf.dest_register & 0x7F; let slot = if (tf.fetch_const as usize) < 8 { tf.fetch_const as usize } else { 0 }; // Honor the 12-bit destination swizzle (3 bits/component). Only the // selected lanes are written; `keep` codes preserve the prior value. // This is what lets the YUV video's three fetches (all → r1, each in a // different lane) coexist instead of clobbering each other. let letters = ['x', 'y', 'z', 'w']; let mut comps: Vec = Vec::with_capacity(4); let mut any_write = false; for i in 0..4u16 { let code = (tf.dest_swizzle >> (i * 3)) & 0x7; let expr = match code { 0..=3 => { any_write = true; format!("_tex.{}", letters[code as usize]) } 4 => { any_write = true; "0.0".to_string() } 5 => { any_write = true; "1.0".to_string() } // 6/7 = keep: preserve the destination's current component. _ => format!("r[{dst_reg}u].{}", letters[i as usize]), }; comps.push(expr); } if any_write { self.push("{"); self.indent += 1; self.push(&format!( "let _tex = textureSampleLevel(xenos_tex{slot}, xenos_samp, r[{src_reg}u].xy, 0.0);" )); self.push(&format!( "r[{dst_reg}u] = vec4({}, {}, {}, {});", comps[0], comps[1], comps[2], comps[3] )); self.indent -= 1; self.push("}"); } } } /// Emit the WGSL expression that reads an ALU source operand with /// swizzle + negate applied (no abs — see GPUBUG-100 deferred). Mirrors /// the interpreter shader's `read_src` + `apply_swizzle` + the negate /// half of `apply_modifiers`. The 8-bit `swizzle` is component-relative /// per canary `AluInstruction::GetSwizzledComponentIndex`: for output /// component i, source component is `((swiz >> (2*i)) + i) & 3`. /// Identity swizzle is `0x00`. GPUBUG-100 / GPUBUG-101. fn src_operand(src_byte: u8, is_temp: bool, swizzle: u8, negate: bool, const_base: u32) -> String { let base = if is_temp { format!("r[{}u]", (src_byte & 0x3F) as u32) } else { format!("xenos_consts.alu[{}u]", src_byte as u32 + const_base) }; let s = swizzle as u32; let lane = |i: u32| -> char { let c = (((s >> (2 * i)) + i) & 3) as usize; ['x', 'y', 'z', 'w'][c] }; // Identity swizzle (0x00) maps to .xyzw — emit a bare expression. let swizzled = if swizzle == 0 { base } else { let lx = lane(0); let ly = lane(1); let lz = lane(2); let lw = lane(3); format!("vec4({base}.{lx}, {base}.{ly}, {base}.{lz}, {base}.{lw})") }; if negate { format!("(-{swizzled})") } else { swizzled } } fn vector_expr(op: u8, a: &str, b: &str, c: &str) -> Option { // Semantics mirror the runtime interpreter's `exec_vector_op` // (`shaders/xenos_interp.wgsl`), which in turn mirrors canary's // `AluVectorOpcode` (ucode.h:1001+). Side-effecting ops (kill*, setp_push) // need per-invocation state the AOT emitter doesn't track yet → still // `None` (interpreter fallback). let cmp4 = |op: &str| { format!( "vec4(select(0.0,1.0,{a}.x{op}{b}.x), select(0.0,1.0,{a}.y{op}{b}.y), select(0.0,1.0,{a}.z{op}{b}.z), select(0.0,1.0,{a}.w{op}{b}.w))" ) }; // CND* : per-lane select(c, b, a 0). let cnd4 = |op: &str| { format!( "vec4(select({c}.x,{b}.x,{a}.x{op}0.0), select({c}.y,{b}.y,{a}.y{op}0.0), select({c}.z,{b}.z,{a}.z{op}0.0), select({c}.w,{b}.w,{a}.w{op}0.0))" ) }; let s = match op { vop::ADD => format!("({a} + {b})"), vop::MUL => format!("({a} * {b})"), vop::MAX => format!("max({a}, {b})"), vop::MIN => format!("min({a}, {b})"), vop::MAD => format!("({a} * {b} + {c})"), vop::DOT4 => format!("vec4(dot({a}, {b}))"), vop::DOT3 => format!("vec4(dot({a}.xyz, {b}.xyz))"), vop::DOT2_ADD => format!("vec4({a}.x * {b}.x + {a}.y * {b}.y + {c}.x)"), vop::SEQ => cmp4("=="), vop::SGT => cmp4(">"), vop::SGE => cmp4(">="), vop::SNE => cmp4("!="), vop::CND_EQ => cnd4("=="), vop::CND_GE => cnd4(">="), vop::CND_GT => cnd4(">"), vop::FRC => format!("fract({a})"), vop::TRUNC => format!("trunc({a})"), vop::FLOOR => format!("floor({a})"), vop::MAX4 => format!("vec4(max(max({a}.x,{a}.y), max({a}.z,{a}.w)))"), // dst = (1, src0.y*src1.y, src0.z, src1.w) (canary kDst) vop::DST => format!("vec4(1.0, {a}.y * {b}.y, {a}.z, {b}.w)"), _ => return None, }; Some(s) } fn scalar_expr(op: u8, a: &str, b: &str, prev: &str) -> Option { // Semantics mirror the runtime interpreter's `exec_scalar_op` // (`shaders/xenos_interp.wgsl`) / canary's `AluScalarOpcode` // (ucode.h:1001+). Side-effecting ops (setp*, kills*, maxas*) need // per-invocation predicate/kill/address state the AOT emitter doesn't // track yet → still `None` (interpreter fallback). let s = match op { sop::ADDS => format!("({a} + {b})"), sop::ADDS_PREV => format!("({a} + {prev})"), sop::MULS => format!("({a} * {b})"), sop::MULS_PREV => format!("({a} * {prev})"), // muls_prev2 / LIT emulation (canary kMulsPrev2): guard against // -FLT_MAX / non-finite ps & b, and b <= 0. sop::MULS_PREV2 => format!( "select({a} * {prev}, -3.4028235e38, {prev} == -3.4028235e38 || !(\ {prev} == {prev}) || abs({prev}) > 3.4028235e38 || !({b} == {b}) || \ abs({b}) > 3.4028235e38 || {b} <= 0.0)" ), sop::MAXS => format!("max({a}, {b})"), sop::MINS => format!("min({a}, {b})"), sop::SEQS => format!("select(0.0, 1.0, {a} == 0.0)"), sop::SGTS => format!("select(0.0, 1.0, {a} > 0.0)"), sop::SGES => format!("select(0.0, 1.0, {a} >= 0.0)"), sop::SNES => format!("select(0.0, 1.0, {a} != 0.0)"), sop::FRCS => format!("fract({a})"), sop::TRUNCS => format!("trunc({a})"), sop::FLOORS => format!("floor({a})"), sop::SUBS => format!("({a} - {b})"), sop::SUBS_PREV => format!("({a} - {prev})"), sop::EXP => format!("exp2({a})"), sop::LOG | sop::LOGC => format!("select(log2({a}), 0.0, {a} == 1.0)"), sop::RCP | sop::RCPC | sop::RCPF => format!("xe_rcp({a})"), sop::RSQ | sop::RSQC | sop::RSQF => { format!("select(0.0, inverseSqrt({a}), {a} > 0.0)") } sop::SQRT => format!("select(0.0, sqrt({a}), {a} >= 0.0)"), sop::SIN => format!("sin({a})"), sop::COS => format!("cos({a})"), // Scalar-constant family (canary `kMulsc0..kSubsc1`): a two-source // scalar op `ps = src0.x OP src1.x`. Our `a`/`b` are already src0.x / // src1.x (see the `scl_src_a`/`scl_src_b` setup in the interpreter and // above), so the `c0`/`c1` variants collapse to the same arithmetic — // the constant-bank distinction is resolved during operand read. The // intro video's YUV→RGB pixel shader builds its matrix-multiply from // these (multiply-by-coefficient + add-offset). sop::MULSC0 | sop::MULSC1 => format!("({a} * {b})"), sop::ADDSC0 | sop::ADDSC1 => format!("({a} + {b})"), sop::SUBSC0 | sop::SUBSC1 => format!("({a} - {b})"), sop::RETAIN_PREV => prev.to_string(), _ => return None, }; Some(s) } #[cfg(test)] mod tests { use super::*; use crate::ucode::alu::{sop, vop}; use crate::ucode::control_flow::ControlFlowInstruction; /// iterate-3T: the real publisher-logo VS (`vs_key 0x03b7b020`, captured /// from the live boot) must now TRANSLATE — pre-3T it rejected with /// `vfetch_fmt` because (a) the `k_16_16` color stream (format 6) was /// unsupported and (b) the export-index model (62=oPos, 0/1=interpolators) /// was a wrong AllocKind heuristic. This locks in the format-6 + per- /// attribute-offset + export-index work so the UV interpolator reaches the /// pixel shader (texcoord in r1) instead of collapsing to a single color. #[test] fn real_logo_vs_translates_with_interpolators() { let ucode: [u32; 30] = [ 0x70153003, 0x00001200, 0xC2000000, 0x00001006, 0x00001200, 0xC4000000, 0x00002007, 0x00002200, 0x00000000, 0x2DF82000, 0x00393A88, 0x00000006, 0x05F81000, 0x4006060A, 0x00000306, 0x05F80000, 0x40253FC8, 0x00000406, 0xC80F803E, 0x00000000, 0xC2020200, 0xC8038001, 0x00B0B000, 0xC2000000, 0xC80F8000, 0x00000000, 0xC2010100, 0x00000000, 0x00000000, 0x00000000, ]; let p = crate::ucode::parse_shader(&ucode); let body = match translate(&p, Stage::Vertex) { Translation::Ok(b) => b, Translation::Reject(r) => panic!("logo VS rejected: {r}"), }; // Position must come from the export-index-62 path (`opos`) and the // UV/color interpolators must be exported as distinct slots. assert!(body.contains("opos ="), "no position export: {body}"); assert!(body.contains("ointerp[0u]"), "no interp0 export: {body}"); assert!(body.contains("ointerp[1u]"), "no interp1 export: {body}"); // The k_16_16 attribute must unpack via the packed-16 helper. assert!(body.contains("w16"), "no packed-16 unpack for k_16_16: {body}"); } /// The logo pixel shader (`ps_key 0x03b79001`) samples its texture at the /// interpolated texcoord register r1 — which the PS now seeds from the VS /// interpolator `in.interp1` (Xenos PS-input-GPR mapping). Verifies the UV /// chain so tfetch samples the real UV instead of (0,0). #[test] fn ps_seeds_interpolators_into_registers() { // A trivial PS that just exports — we only assert the preamble wiring. let p = crate::ucode::ParsedShader { cf: vec![ControlFlowInstruction::Exit], instructions: vec![], }; let body = match translate(&p, Stage::Pixel) { Translation::Ok(b) => b, Translation::Reject(r) => panic!("trivial PS rejected: {r}"), }; assert!(body.contains("r[1] = in.interp1;"), "PS must seed r1 from interp1: {body}"); } fn synthetic_trivial_shader() -> ParsedShader { // Single Exec clause: ALU add r0 = r0 + r0; scalar_op = RETAIN_PREV // with full write-mask on vector, zero on scalar. Alloc(Position) // precedes so the ALU's export (if it were one) would target oPos. // GPUBUG-106 canary layout: dest/mask/scalar_opc in w0; vector_opc + // src_sel in w2. All three operands temps → r0. let w0 = (0u32) // vector_dest = 0 | (0xFu32 << 16) // vector_write_mask = 0xF | ((sop::RETAIN_PREV as u32) << 26); // scalar_opc let w1 = 0u32; let w2 = ((vop::ADD as u32) << 24) // vector_opc | (1u32 << 31) // src1_sel = temp | (1u32 << 30) // src2_sel = temp | (1u32 << 29); // src3_sel = temp ParsedShader { cf: vec![ ControlFlowInstruction::Alloc { size: 1, kind: AllocKind::Position, }, ControlFlowInstruction::Exec { address: 0, count: 1, sequence: 0, is_end: true, predicated: false, predicate_condition: false, }, ], instructions: vec![w0, w1, w2], } } #[test] fn trivial_shader_translates() { let shader = synthetic_trivial_shader(); match translate(&shader, Stage::Vertex) { Translation::Ok(body) => { assert!(body.contains("fn vs_main")); assert!(body.contains("r[0u] = (r[0u] + r[0u]);")); assert!(body.contains("return out;")); } Translation::Reject(r) => panic!("rejected: {r}"), } } #[test] fn combined_module_parses_as_wgsl() { let shader = synthetic_trivial_shader(); let vs = match translate(&shader, Stage::Vertex) { Translation::Ok(body) => body, Translation::Reject(r) => panic!("VS rejected: {r}"), }; let ps = match translate(&shader, Stage::Pixel) { Translation::Ok(body) => body, Translation::Reject(r) => panic!("PS rejected: {r}"), }; let module = combine_stages(&vs, &ps); // naga is pinned as a dev-dep in this crate; if this fails the // translator is emitting invalid WGSL. match naga::front::wgsl::parse_str(&module) { Ok(_) => {} Err(e) => panic!( "emitted WGSL failed to parse:\n{}\n--- module ---\n{}", e, module ), } } #[test] fn src_operand_decodes_temp_vs_constant_no_modifiers() { // GPUBUG-101: is_temp=true → r[low6]; is_temp=false → xenos_consts.alu[full]. // Identity swizzle (0x00), no negate → bare base expression. assert_eq!(src_operand(0x00, true, 0x00, false, 0), "r[0u]"); assert_eq!(src_operand(0x05, true, 0x00, false, 0), "r[5u]"); assert_eq!(src_operand(0x3F, true, 0x00, false, 0), "r[63u]"); // For temps, bits 6/7 are reserved (abs/rel) — they don't widen // the register index even if set. Phase D2 will consume them. assert_eq!(src_operand(0x80, true, 0x00, false, 0), "r[0u]"); assert_eq!(src_operand(0xFF, true, 0x00, false, 0), "r[63u]"); // Constants: full 8-bit index. assert_eq!(src_operand(0x00, false, 0x00, false, 0), "xenos_consts.alu[0u]"); assert_eq!(src_operand(0x05, false, 0x00, false, 0), "xenos_consts.alu[5u]"); assert_eq!(src_operand(0xFF, false, 0x00, false, 0), "xenos_consts.alu[255u]"); } #[test] fn src_operand_applies_swizzle_and_negate() { // GPUBUG-100. Component-relative swizzle. swizzle=0x1B reverses // the lanes (.wzyx): for i=0 → ((0x1B >> 0) + 0) & 3 = 3 = w; // for i=1 → ((0x1B >> 2) + 1) & 3 = (6+1)&3 = 3 = w. Hmm — // canary's identity is 0x00 = .xyzw, so .wzyx in component- // relative terms = `s0=3, s1=2, s2=1, s3=0` → bits would be // (3, (2-1)&3=1, (1-2)&3=3, (0-3)&3=1) which combines weirdly. // We just verify the mechanics by precomputing a known case: // swizzle=0x00 (identity) outputs .xyzw — matched by no-swizzle // branch. Negate wraps in `(-…)`. assert_eq!(src_operand(0x05, true, 0x00, true, 0), "(-r[5u])"); // swizzle=0xFF → for each i, ((0xFF >> (2i)) + i) & 3: // i=0: (3 + 0) & 3 = 3 → w // i=1: ((0x3F) + 1) & 3 = (63+1)&3 = 0 → x // i=2: ((0x0F) + 2) & 3 = (15+2)&3 = 1 → y // i=3: ((0x03) + 3) & 3 = (3+3)&3 = 2 → z // Output: .wxyz assert_eq!( src_operand(0x05, true, 0xFF, false, 0), "vec4(r[5u].w, r[5u].x, r[5u].y, r[5u].z)" ); // Combined: negate of constant with .wxyz swizzle. assert_eq!( src_operand(0x07, false, 0xFF, true, 0), "(-vec4(xenos_consts.alu[7u].w, xenos_consts.alu[7u].x, xenos_consts.alu[7u].y, xenos_consts.alu[7u].z))" ); } #[test] fn shader_using_c0_emits_xenos_consts_read() { // ALU: r0 = c0 + r0. GPUBUG-106 canary layout. src_a = src1 (w2 // 16:23), src_b = src2 (w2 8:15). src1_sel (w2 bit31) = 0 → c0; // src2_sel (w2 bit30) = 1 → r0. let w0 = (0u32) // vector_dest = 0 | (0xFu32 << 16) // vector_write_mask | ((sop::RETAIN_PREV as u32) << 26); // scalar_opc let w2 = ((vop::ADD as u32) << 24) // vector_opc | (0u32 << 16) // src1_reg = 0 → c0 | (0u32 << 8) // src2_reg = 0 → r0 | (0u32 << 31) // src1_sel = 0 (constant) | (1u32 << 30); // src2_sel = 1 (temp) let shader = ParsedShader { cf: vec![ ControlFlowInstruction::Alloc { size: 1, kind: AllocKind::Position, }, ControlFlowInstruction::Exec { address: 0, count: 1, sequence: 0, is_end: true, predicated: false, predicate_condition: false, }, ], instructions: vec![w0, 0, w2], }; match translate(&shader, Stage::Vertex) { Translation::Ok(body) => { assert!( body.contains("xenos_consts.alu[0u]"), "expected c0 operand, got: {body}" ); assert!( body.contains("r[0u]"), "expected r0 temp operand, got: {body}" ); } Translation::Reject(r) => panic!("rejected: {r}"), } } #[test] fn vfetch_emit_includes_gpu_swap_helper_call() { // GPUBUG-102: emit_vfetch should reference `gpu_swap(...)` for // each lane. Ensures the per-format endian byte-swap is wired // into the AOT path. let mut ctx = EmitCtx::new(Stage::Vertex); let vf = crate::ucode::fetch::VertexFetch { fetch_const: 0, const_index_sel: 0, src_register: 0, dest_register: 0, dest_write_mask: 0xF, format: 38, // k_32_32_32_32_FLOAT (4 floats) stride: 4, offset: 0, is_signed: false, is_normalized: true, is_mini_fetch: false, raw: [0; 3], }; ctx.emit_vfetch(&vf).expect("emit_vfetch"); let body = ctx.finish(); assert!(body.contains("gpu_swap("), "emitted vfetch body: {body}"); } fn vf(format: u8, stride: u8, offset: u32, mini: bool) -> crate::ucode::fetch::VertexFetch { crate::ucode::fetch::VertexFetch { fetch_const: 0, const_index_sel: 0, src_register: 0, dest_register: 0, dest_write_mask: 0xF, format, stride, offset, is_signed: false, is_normalized: true, is_mini_fetch: mini, raw: [0; 3], } } #[test] fn vfetch_k8888_unpacks_four_channels() { // GPUBUG-112: VertexFormat 6 = k_8_8_8_8 (4× u8 normalized, 1 dword), // NOT k_16_16. All four channels (R,G,B,A) must be unpacked so a // vertex COLOR keeps its blue channel (white texture × white color = // white, not yellow). let mut ctx = EmitCtx::new(Stage::Vertex); ctx.emit_vfetch(&vf(6, 6, 3, false)).expect("emit"); let body = ctx.finish(); // Four /255.0 channel reads from one packed dword `w16`. assert!(body.contains("let w16 ="), "needs packed dword: {body}"); assert_eq!(body.matches("/ 255.0").count(), 4, "four 8-bit channels: {body}"); } #[test] fn vfetch_mini_inherits_full_stride() { // GPUBUG-114: a vfetch_mini (stride field 0) inherits the stride of the // preceding full vfetch of the same stream (canary ucode.h:733). Emit a // full fetch (stride 7) then a mini fetch and assert the mini indexes by // stride 7, not its tight dword count. let mut ctx = EmitCtx::new(Stage::Vertex); ctx.emit_vfetch(&vf(57, 7, 0, false)).expect("full"); // k_32_32_32_FLOAT ctx.emit_vfetch(&vf(38, 0, 3, true)).expect("mini"); // k_32_32_32_32_FLOAT, mini let body = ctx.finish(); assert!(body.contains("vidx * 7u + 3u"), "mini must inherit stride 7: {body}"); assert!(!body.contains("vidx * 4u"), "mini must not use tight stride 4: {body}"); } #[test] fn ps_color_export_is_saturated() { // GPUBUG-115: the PS color export must be clamped to [0,1] (canary // saturates before UNORM RT write) so an out-of-range guest color // doesn't write garbage/white to the sRGB target. let p = crate::ucode::ParsedShader { cf: vec![ControlFlowInstruction::Exit], instructions: vec![], }; let body = match translate(&p, Stage::Pixel) { Translation::Ok(b) => b, Translation::Reject(r) => panic!("PS rejected: {r}"), }; assert!( body.contains("clamp(ocolor0, vec4(0.0), vec4(1.0))"), "PS must saturate color export: {body}" ); } #[test] fn loop_clause_rejected() { let shader = ParsedShader { cf: vec![ControlFlowInstruction::LoopStart { address: 0, loop_id: 0, }], instructions: vec![], }; assert!(matches!( translate(&shader, Stage::Vertex), Translation::Reject(reject::CF_LOOP) )); } #[test] fn unsupported_op_rejected() { // GPUBUG-106 layout: vector_write_mask in w0 (16:19), vector_opc in // w2 (24:28). MAX_A (29) is outside the supported subset → reject. let w0 = (0xFu32 << 16) | ((sop::RETAIN_PREV as u32) << 26); let w2 = (29u32) << 24; // VOP_MAX_A let shader = ParsedShader { cf: vec![ControlFlowInstruction::Exec { address: 0, count: 1, sequence: 0, is_end: true, predicated: false, predicate_condition: false, }], instructions: vec![w0, 0, w2], }; assert!(matches!( translate(&shader, Stage::Vertex), Translation::Reject(reject::VEC_OP_UNSUPPORTED) )); } }