//! 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, _pad: u32, }; 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_tex : texture_2d; @group(1) @binding(1) var xenos_samp : sampler; struct VsOut { @builtin(position) position: vec4, @location(0) color: 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, } impl EmitCtx { fn new(stage: Stage) -> Self { Self { stage, out: String::with_capacity(2048), indent: 0, } } 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 => { // Seed r0 with vertex index for simple shaders that read it. self.push("r[0] = vec4(f32(vidx), 0.0, 0.0, 1.0);"); // Synthetic export slots — match the interpreter's layout so // the fallback path and translator path produce the same // visual output on shaders both support. self.push("var opos: vec4 = vec4(0.0, 0.0, 0.0, 1.0);"); self.push("var ocolor: vec4 = vec4(1.0, 1.0, 1.0, 1.0);"); } Stage::Pixel => { // Seed r0.xy with interpolated color lane so trivial shaders // that read r0 still produce something. self.push("r[0] = in.color;"); self.push("var ocolor0: vec4 = in.color;"); } } 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, 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;"); self.push("out.position = opos;"); self.push("out.color = ocolor;"); self.push("return out;"); } Stage::Pixel => { self.push("var out: FsOut;"); self.push("out.color0 = ocolor0;"); 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], ]; let is_fetch = ((sequence >> (i * 2 + 1)) & 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. let a = src_operand(alu.src_a, alu.src_a_is_temp, alu.src_a_swiz, alu.src_a_negate); let b = src_operand(alu.src_b, alu.src_b_is_temp, alu.src_b_swiz, alu.src_b_negate); let c = src_operand(alu.src_c, alu.src_c_is_temp, alu.src_c_swiz, alu.src_c_negate); // 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. Binary ops use (src_a.x, src_b.x); ps-variants use // src_a.x + running ps. `scl_src_a` mirrors the interpreter's // `scalar_src_is_ps` selector. let scl_src_a = if alu.scalar_src_is_ps { "ps".to_string() } else { format!("{}.x", a) }; let scl_src_b = format!("{}.x", b); let expr = scalar_expr(alu.scalar_opcode, &scl_src_a, &scl_src_b, "ps") .ok_or(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) { // Xenos's export "register" indexing within an alloc range is // normally (alloc_base + offset). Since our CF stream doesn't // carry per-export slot offsets cleanly, use `alloc` to pick the // target. let lhs = match (self.stage, alloc) { (Stage::Vertex, AllocKind::Position) => "opos", (Stage::Vertex, AllocKind::Interpolators) => "ocolor", (Stage::Vertex, AllocKind::Colors) => "ocolor", (Stage::Vertex, _) => "ocolor", // fall through — any other alloc (Stage::Pixel, AllocKind::Colors) => "ocolor0", (Stage::Pixel, _) => "ocolor0", }; let _ = dst_reg; // per-slot export indexing reserved for a richer v2 self.emit_masked_write(lhs, expr, mask); } fn emit_vfetch(&mut self, vf: &crate::ucode::fetch::VertexFetch) -> Result<(), &'static str> { // v1: treat all vertex fetches as R32G32B32A32_FLOAT, stride = 4 // dwords. Matches the interpreter's MVP semantics; unlocks more // formats alongside the CPU texture cache's format expansion. // // GPUBUG-102: the fetch constant (xe_gpu_vertex_fetch_t, // xenos.h:1158-1172) holds the endian field in dword_1's low // 2 bits. Vertex data on Xbox 360 is big-endian; the host is // little-endian. Pre-fix, every dword was bitcast as-is → // vertex positions were byte-reversed garbage and any draw // that did reach the host produced clipped / NaN positions. let fetch_const = (vf.raw[0] >> 5) & 0x1F; let src_reg = vf.src_register & 0x7F; let dst_reg = vf.dest_register & 0x7F; self.push(&format!( "{{ let fc0 = xenos_consts.fetch[{fc0_idx}u]; \ let fc1 = xenos_consts.fetch[{fc1_idx}u]; \ let endian = fc1 & 0x3u; \ let base = (fc0 & 0xFFFFFFFCu) >> 2u; \ let vidx = u32(r[{src_reg}u].x); \ let addr = base + vidx * 4u; \ let n = arrayLength(&vertex_buffer); \ if (addr + 3u < n) {{ \ r[{dst_reg}u] = vec4( \ bitcast(gpu_swap(vertex_buffer[addr + 0u], endian)), \ bitcast(gpu_swap(vertex_buffer[addr + 1u], endian)), \ bitcast(gpu_swap(vertex_buffer[addr + 2u], endian)), \ bitcast(gpu_swap(vertex_buffer[addr + 3u], endian))); \ }} }}", fc0_idx = fetch_const * 2, fc1_idx = fetch_const * 2 + 1, )); Ok(()) } fn emit_tfetch(&mut self, tf: &crate::ucode::fetch::TextureFetch) { // v1: sample the single bound texture; UV = r[src].xy. P5's cache // publishes the `fetch_const=0` texture into `@group(1)`; slot // mismatch is a silent magenta for now. let src_reg = tf.src_register & 0x7F; let dst_reg = tf.dest_register & 0x7F; self.push(&format!( "r[{dst_reg}u] = textureSampleLevel(xenos_tex, xenos_samp, r[{src_reg}u].xy, 0.0);" )); } } /// 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) -> String { let base = if is_temp { format!("r[{}u]", (src_byte & 0x3F) as u32) } else { format!("xenos_consts.alu[{}u]", src_byte as u32) }; 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 { 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 => format!( "vec4(select(0.0,1.0,{a}.x=={b}.x), select(0.0,1.0,{a}.y=={b}.y), select(0.0,1.0,{a}.z=={b}.z), select(0.0,1.0,{a}.w=={b}.w))" ), vop::SGT => format!( "vec4(select(0.0,1.0,{a}.x>{b}.x), select(0.0,1.0,{a}.y>{b}.y), select(0.0,1.0,{a}.z>{b}.z), select(0.0,1.0,{a}.w>{b}.w))" ), vop::SGE => format!( "vec4(select(0.0,1.0,{a}.x>={b}.x), select(0.0,1.0,{a}.y>={b}.y), select(0.0,1.0,{a}.z>={b}.z), select(0.0,1.0,{a}.w>={b}.w))" ), vop::SNE => format!( "vec4(select(0.0,1.0,{a}.x!={b}.x), select(0.0,1.0,{a}.y!={b}.y), select(0.0,1.0,{a}.z!={b}.z), select(0.0,1.0,{a}.w!={b}.w))" ), vop::FRC => format!("fract({a})"), vop::FLOOR => format!("floor({a})"), _ => return None, }; Some(s) } fn scalar_expr(op: u8, a: &str, b: &str, prev: &str) -> Option { 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})"), sop::MAXS => format!("max({a}, {b})"), sop::MINS => format!("min({a}, {b})"), sop::RCP => format!("xe_rcp({a})"), 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; 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. // Word-0 bits 29-31 set so all three operands resolve as temps — // matches the prior assertion `r[0u] = (r[0u] + r[0u])`. let w0 = (1u32 << 29) | (1u32 << 30) | (1u32 << 31); let w2 = (vop::ADD as u32) | ((sop::RETAIN_PREV as u32) << 6) | (0xF << 12) // vector_write_mask | (0u32 << 16); // vector_dest = 0 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], } } #[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), "r[0u]"); assert_eq!(src_operand(0x05, true, 0x00, false), "r[5u]"); assert_eq!(src_operand(0x3F, true, 0x00, false), "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), "r[0u]"); assert_eq!(src_operand(0xFF, true, 0x00, false), "r[63u]"); // Constants: full 8-bit index. assert_eq!(src_operand(0x00, false, 0x00, false), "xenos_consts.alu[0u]"); assert_eq!(src_operand(0x05, false, 0x00, false), "xenos_consts.alu[5u]"); assert_eq!(src_operand(0xFF, false, 0x00, false), "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), "(-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), "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), "(-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. src_a (low byte) is constant index 0; // src_b (next byte) is temp index 0. src_a_is_temp=false → // src1_sel-style bit at w0 bit 29 = 0; src_b_is_temp=true → // bit 30 = 1. (src_c left as 0/temp; unused.) let w0 = 0x00u32 // src_a = c0 | (0x00u32 << 8) // src_b = r0 | (0x00u32 << 16) // src_c | (0u32 << 29) // src_a_is_temp = false (constant) | (1u32 << 30); // src_b_is_temp = true (register) let w2 = (vop::ADD as u32) | ((sop::RETAIN_PREV as u32) << 6) | (0xF << 12) | (0u32 << 16); 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, src_register: 0, dest_register: 0, dest_write_mask: 0xF, raw: [0; 3], }; ctx.emit_vfetch(&vf).expect("emit_vfetch"); let body = ctx.finish(); assert!(body.contains("gpu_swap("), "emitted vfetch body: {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() { let w2 = (29u32) // VOP_MAX_A, not in v1 subset | ((sop::RETAIN_PREV as u32) << 6) | (0xF << 12); let shader = ParsedShader { cf: vec![ControlFlowInstruction::Exec { address: 0, count: 1, sequence: 0, is_end: true, predicated: false, predicate_condition: false, }], instructions: vec![0, 0, w2], }; assert!(matches!( translate(&shader, Stage::Vertex), Translation::Reject(reject::VEC_OP_UNSUPPORTED) )); } }