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>
745 lines
29 KiB
Rust
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)
|
|
));
|
|
}
|
|
}
|