fix(gpu): GPUBUG-101 — decode src1/2/3_sel temp-vs-constant selector

Per canary AluInstruction layout (xenia-canary/src/xenia/gpu/ucode.h:
2078-2086), word-0 bits 29-31 are the per-operand `srcN_sel` flags
selecting temp register (1) vs ALU constant (0); the corresponding
8-bit src byte indexes either:
  - a temp register (bits 5:0 = index, bits 6/7 reserved for
    relative-addressing / abs flags consumed by Phase D2), or
  - an ALU constant (full 8-bit index).

Pre-fix, the WGSL interpreter and AOT translator both masked `& 0x7F`
on the src byte and emitted `r[low7]` regardless of the operand class.
Every shader's WVP matrix / light constant / per-frame uniform read
came back as r[low7] — typically zero — yielding invisible rendering.

Mechanical changes:
- crates/xenia-gpu/src/ucode/alu.rs: decode src_a_is_temp /
  src_b_is_temp / src_c_is_temp from w0 bits 29/30/31. Note that our
  src_a (low byte of w0) is canary's third operand, hence its selector
  is bit 29 (canary src3_sel), not bit 31.
- crates/xenia-gpu/src/shaders/xenos_interp.wgsl: `read_src` now takes
  the is_temp flag; constants index xenos_consts.alu directly.
- crates/xenia-gpu/src/translator.rs: `src_operand` mirrors the
  interpreter — `r[idx]` when temp, `xenos_consts.alu[idx]` when
  constant.

The trivial-shader synthetic test was updated to set the temp flags so
its `r[0u] = (r[0u] + r[0u])` assertion remains valid; without the
flags set, all sources would now resolve as constants.

Bank-selection (cf-level relative addressing for higher banks of the
512 ALU constants) remains a Phase G+ extension — covers c0..c127
in bank 0, which most Sylpheed shaders use directly.

Verification at -n 100M lockstep:
  swaps:                2 → 2     (unchanged — gated by D2/D3/E for draws)
  draws:                0 → 0
  packets:              ~61M (within noise)
Tests: 552 → 554 (+2 translator tests for the temp/constant decode).

Closes GPUBUG-101 (P0).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-05-03 14:10:11 +02:00
parent 1b74db6fa7
commit 78ea81c12a
3 changed files with 139 additions and 15 deletions

View File

@@ -287,9 +287,15 @@ impl EmitCtx {
alu: &AluInstruction,
current_alloc: AllocKind,
) -> Result<(), &'static str> {
let a = format!("r[{}u]", alu.src_a & 0x7F);
let b = format!("r[{}u]", alu.src_b & 0x7F);
let c = format!("r[{}u]", alu.src_c & 0x7F);
// GPUBUG-101: word-0 bits 29-31 select temp vs constant for
// each operand (canary `srcN_sel`); the corresponding src byte
// indexes either a general register (temp) or an ALU constant
// (c#). Pre-fix the translator unconditionally emitted r[low7]
// for both, dropping every shader's uniform read. Mirrors the
// interpreter's `read_src`.
let a = src_operand(alu.src_a, alu.src_a_is_temp);
let b = src_operand(alu.src_b, alu.src_b_is_temp);
let c = src_operand(alu.src_c, alu.src_c_is_temp);
// Vector pipe.
if alu.vector_write_mask != 0 {
@@ -403,6 +409,20 @@ impl EmitCtx {
}
}
/// Emit the WGSL expression that reads an ALU source operand. Per
/// canary `xenia-canary/src/xenia/gpu/ucode.h`, the temp-vs-constant
/// selector is in word-0 bits 29-31 (`srcN_sel`); the src byte is then
/// either a temp descriptor (bits 5:0 = temp index, bits 6/7 reserved
/// for relative/abs flags decoded in Phase D2) or a flat constant
/// index. Mirrors the interpreter shader's `read_src`. GPUBUG-101.
fn src_operand(src_byte: u8, is_temp: bool) -> String {
if is_temp {
format!("r[{}u]", (src_byte & 0x3F) as u32)
} else {
format!("xenos_consts.alu[{}u]", src_byte as u32)
}
}
fn vector_expr(op: u8, a: &str, b: &str, c: &str) -> Option<String> {
let s = match op {
vop::ADD => format!("({a} + {b})"),
@@ -459,6 +479,9 @@ mod tests {
// 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
@@ -478,7 +501,7 @@ mod tests {
predicate_condition: false,
},
],
instructions: vec![0, 0, w2],
instructions: vec![w0, 0, w2],
}
}
@@ -518,6 +541,69 @@ mod tests {
}
}
#[test]
fn src_operand_decodes_temp_vs_constant() {
// GPUBUG-101: is_temp=true → r[low6]; is_temp=false → xenos_consts.alu[full].
assert_eq!(src_operand(0x00, true), "r[0u]");
assert_eq!(src_operand(0x05, true), "r[5u]");
assert_eq!(src_operand(0x3F, true), "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), "r[0u]");
assert_eq!(src_operand(0xFF, true), "r[63u]");
// Constants: full 8-bit index.
assert_eq!(src_operand(0x00, false), "xenos_consts.alu[0u]");
assert_eq!(src_operand(0x05, false), "xenos_consts.alu[5u]");
assert_eq!(src_operand(0xFF, false), "xenos_consts.alu[255u]");
}
#[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 loop_clause_rejected() {
let shader = ParsedShader {