[iterate-3S] Real splash geometry renders: fix ALU/vfetch decode + per-draw NDC transform
The 3O→3R real-render slice ran the guest's real translated VS/PS on real captured vertices at full boot speed, but the --ui window stayed blank. Bifurcated with an env-gated frontbuffer readback + per-vertex NDC dump (both removed): the captured splash quads (RectangleList, k_32_32_FLOAT, 3 verts) were non-zero and sane, so this was a transform/decode chain of bugs, not missing geometry. Four coupled root causes: - GPUBUG-106 (ucode/alu.rs): decode_alu read EVERY field out of w2, but canary's AluInstruction lays dest/write-mask/export/scalar-opcode in w0, the vector opcode + source regs in w2, swizzle/negate/pred in w1. The misread made every *export* ALU decode with vector_write_mask=0 → no oPos/oColor export emitted → the translated VS collapsed every vertex to the clip origin. Rewrote the field map to match ucode.h:2036-2086. - GPUBUG-107 (ucode/fetch.rs + translator.rs): the translator hardcoded R32G32B32A32_FLOAT (4 floats, stride 4); the splash quads are k_32_32_FLOAT (2 floats, stride 2). Over-striding read the next vertex's X into .w → negative W → the rectangle clipped behind the camera. Decode the real VertexFormat + dword stride and emit the matching component read (1/2/3/4 float formats; others reject to the interpreter). - GPUBUG-108 (translator.rs + xenos_interp.wgsl): the vfetch recomputed the buffer base from xenos_consts.fetch[], but that uniform carries the last-published per-frame fetch constant, not this draw's (stale 0x8a000002 vs the real base). The captured window already begins at the fetch base, so index from 0 (vertex i at i*stride) when a real window is present; only the synthetic fallback consults the uniform. - iterate-3S NDC transform (draw_capture.rs + xenos_pipeline.rs + WGSL): the guest VS emits screen-space pixel coords (clip disabled, VTE viewport scale/offset off). Added compute_ndc_xy (mirrors canary GetHostViewportInfo): rescales render-target pixels to [-1,1] clip with the Y-flip for wgpu, plumbed per-draw into DrawConstants and applied in both the translated and interpreter VS. Result (env-gated readback, since removed): the real splash geometry now fills ~50% of the frontbuffer in a clean triangular coverage pattern, real positions from real guest vertices through the real translated shaders (textures are the next stage — sampled color is still the magenta/white texture stub, tex-cache=0). Headless-inert: draw_capture is only built when frame_captures is Some (--ui); the changed decoders feed only the UI translator/metrics. Golden byte-identical (check -n50m --gpu-inline --stable-digest exit 0); 679 workspace tests green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -95,6 +95,8 @@ struct XenosDrawConstants {
|
||||
vertex_count: u32,
|
||||
prim_kind: u32,
|
||||
vertex_base_dwords: u32,
|
||||
ndc_scale: vec2<f32>,
|
||||
ndc_offset: vec2<f32>,
|
||||
};
|
||||
|
||||
struct XenosConstants {
|
||||
@@ -254,6 +256,18 @@ impl EmitCtx {
|
||||
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<f32>(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.color = ocolor;");
|
||||
self.push("return out;");
|
||||
@@ -401,38 +415,72 @@ impl EmitCtx {
|
||||
}
|
||||
|
||||
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-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 (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.
|
||||
// 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.
|
||||
let (comps, stride): (u32, u32) = match vf.format {
|
||||
36 => (1, 1), // k_32_FLOAT
|
||||
37 => (2, 2), // k_32_32_FLOAT
|
||||
57 => (3, 3), // k_32_32_32_FLOAT
|
||||
38 => (4, 4), // k_32_32_32_32_FLOAT
|
||||
_ => return Err(reject::VFETCH_FMT),
|
||||
};
|
||||
// A stride of 0 in the instruction means "use the fetch-constant
|
||||
// stride"; fall back to the tightly packed component count.
|
||||
let stride = if vf.stride != 0 { vf.stride as u32 } else { stride };
|
||||
let fetch_const = (vf.raw[0] >> 5) & 0x1F;
|
||||
let src_reg = vf.src_register & 0x7F;
|
||||
let dst_reg = vf.dest_register & 0x7F;
|
||||
// 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 {
|
||||
if i < comps {
|
||||
format!("bitcast<f32>(gpu_swap(vertex_buffer[addr + {i}u], endian))")
|
||||
} else if i == 3 {
|
||||
"1.0".to_string()
|
||||
} else {
|
||||
"0.0".to_string()
|
||||
}
|
||||
};
|
||||
let read_bound = comps - 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", fetch_const * 2 + 1);
|
||||
self.push(&format!(
|
||||
"{{ let fc0 = xenos_consts.fetch[{fc0_idx}u]; \
|
||||
let fc1 = xenos_consts.fetch[{fc1_idx}u]; \
|
||||
let endian = fc1 & 0x3u; \
|
||||
let abs_base = (fc0 & 0xFFFFFFFCu) >> 2u; \
|
||||
let base = select(abs_base, abs_base - draw_ctx.vertex_base_dwords, \
|
||||
draw_ctx.vertex_base_dwords != 0u && abs_base >= draw_ctx.vertex_base_dwords); \
|
||||
"{{ let endian = {endian_term}; \
|
||||
let vidx = u32(r[{src_reg}u].x); \
|
||||
let addr = base + vidx * 4u; \
|
||||
var base = 0u; \
|
||||
if (draw_ctx.vertex_base_dwords == 0u) {{ \
|
||||
base = (xenos_consts.fetch[{fc0_idx}u] & 0xFFFFFFFCu) >> 2u; \
|
||||
}} \
|
||||
let addr = base + vidx * {stride}u; \
|
||||
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))); \
|
||||
if (addr + {read_bound}u < n) {{ \
|
||||
r[{dst_reg}u] = vec4<f32>({l0}, {l1}, {l2}, {l3}); \
|
||||
}} }}",
|
||||
fc0_idx = fetch_const * 2,
|
||||
fc1_idx = fetch_const * 2 + 1,
|
||||
l0 = lane(0),
|
||||
l1 = lane(1),
|
||||
l2 = lane(2),
|
||||
l3 = lane(3),
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
@@ -582,13 +630,16 @@ 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
|
||||
| (0u32 << 16); // vector_dest = 0
|
||||
// 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 {
|
||||
@@ -604,7 +655,7 @@ mod tests {
|
||||
predicate_condition: false,
|
||||
},
|
||||
],
|
||||
instructions: vec![w0, 0, w2],
|
||||
instructions: vec![w0, w1, w2],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -692,19 +743,17 @@ mod tests {
|
||||
|
||||
#[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);
|
||||
// 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 {
|
||||
@@ -748,6 +797,8 @@ mod tests {
|
||||
src_register: 0,
|
||||
dest_register: 0,
|
||||
dest_write_mask: 0xF,
|
||||
format: 38, // k_32_32_32_32_FLOAT (4 floats)
|
||||
stride: 4,
|
||||
raw: [0; 3],
|
||||
};
|
||||
ctx.emit_vfetch(&vf).expect("emit_vfetch");
|
||||
@@ -772,9 +823,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn unsupported_op_rejected() {
|
||||
let w2 = (29u32) // VOP_MAX_A, not in v1 subset
|
||||
| ((sop::RETAIN_PREV as u32) << 6)
|
||||
| (0xF << 12);
|
||||
// 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,
|
||||
@@ -784,7 +836,7 @@ mod tests {
|
||||
predicated: false,
|
||||
predicate_condition: false,
|
||||
}],
|
||||
instructions: vec![0, 0, w2],
|
||||
instructions: vec![w0, 0, w2],
|
||||
};
|
||||
assert!(matches!(
|
||||
translate(&shader, Stage::Vertex),
|
||||
|
||||
Reference in New Issue
Block a user