Files
xenia-rs/crates/xenia-gpu/src/translator.rs
MechaCat02 ec2d955dbd fix(gpu): GPUBUG-102 — apply per-format endian byte-swap to vertex fetch
The vertex fetch constant (canary `xe_gpu_vertex_fetch_t`,
xenos.h:1158-1172) holds an `endian` field (low 2 bits of dword_1)
selecting kNone/k8in16/k8in32/k16in32 swap patterns per
`GpuSwapInline` (xenos.h:1090-1109). Xbox 360 vertex data is stored
big-endian; the host is little-endian. Pre-fix every dword was
bitcast as-is — vertex positions decoded as byte-reversed garbage,
producing clipped or NaN positions in any draw that survived to the
host.

Mechanical changes:
- crates/xenia-gpu/src/translator.rs: AOT `emit_vfetch` reads
  fetch_const dword 1 (endian) and wraps each lane's load in
  `gpu_swap(value, endian)`. New `gpu_swap` helper added to the
  emitted module header.
- crates/xenia-gpu/src/shaders/xenos_interp.wgsl: matching
  `gpu_swap` helper added to the runtime interpreter shader.
  `interpret_vertex_fetch` reads fc1, computes the endian, and wraps
  every format's per-lane load (including 8_8_8_8 and 16_16_FLOAT
  paths). Mirrors the AOT translator's emission.

Verification at -n 100M lockstep:
  swaps:                2 → 2     (gated by Phase E for draws)
  draws:                0 → 0
  packets:              ~60M (within noise)
Tests: +1 (vfetch_emit_includes_gpu_swap_helper_call).

Closes GPUBUG-102 (P0).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 14:18:46 +02:00

745 lines
29 KiB
Rust

//! 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<vec4<f32>, 512>,
fetch: array<u32, 256>,
bool_consts: array<u32, 8>,
loop_consts: array<u32, 32>,
};
@group(0) @binding(0) var<uniform> draw_ctx : XenosDrawConstants;
@group(0) @binding(1) var<storage, read> xenos_consts : XenosConstants;
@group(0) @binding(2) var<storage, read> vs_ucode : array<u32>;
@group(0) @binding(3) var<storage, read> ps_ucode : array<u32>;
@group(0) @binding(4) var<storage, read> vertex_buffer : array<u32>;
@group(1) @binding(0) var xenos_tex : texture_2d<f32>;
@group(1) @binding(1) var xenos_samp : sampler;
struct VsOut {
@builtin(position) position: vec4<f32>,
@location(0) color: vec4<f32>,
};
struct FsOut {
@location(0) color0: vec4<f32>,
};
// 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<private>` because we don't share across function calls.
self.push("var r: array<vec4<f32>, 128>;");
self.push("for (var i = 0u; i < 128u; i = i + 1u) { r[i] = vec4<f32>(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>(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<f32> = vec4<f32>(0.0, 0.0, 0.0, 1.0);");
self.push("var ocolor: vec4<f32> = vec4<f32>(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<f32> = 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<f32>(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<f32>({}, {}, {}, {});",
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<f32>( \
bitcast<f32>(gpu_swap(vertex_buffer[addr + 0u], endian)), \
bitcast<f32>(gpu_swap(vertex_buffer[addr + 1u], endian)), \
bitcast<f32>(gpu_swap(vertex_buffer[addr + 2u], endian)), \
bitcast<f32>(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<f32>({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<String> {
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<f32>(dot({a}, {b}))"),
vop::DOT3 => format!("vec4<f32>(dot({a}.xyz, {b}.xyz))"),
vop::DOT2_ADD => format!(
"vec4<f32>({a}.x * {b}.x + {a}.y * {b}.y + {c}.x)"
),
vop::SEQ => format!(
"vec4<f32>(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<f32>(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<f32>(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<f32>(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<String> {
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<f32>(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<f32>(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)
));
}
}