[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:
MechaCat02
2026-06-18 16:35:01 +02:00
parent 6d8a2817a3
commit 80fbff8bd1
7 changed files with 308 additions and 85 deletions

View File

@@ -59,6 +59,102 @@ pub struct DrawCapture {
/// the UI falls back to its procedural geometry for this draw (honest:
/// nothing faked, just "couldn't source real vertices").
pub has_real_vertices: bool,
/// iterate-3S: per-draw NDC transform derived from the guest viewport /
/// clip / VTE registers (mirrors canary `GetHostViewportInfo`). The host VS
/// converts the guest-VS position to wgpu clip space via
/// `clip.xy = pos.xy * ndc_scale + ndc_offset * pos.w`. The Y component
/// already carries the render-target → wgpu Y-flip (negated).
pub ndc_scale: [f32; 2],
pub ndc_offset: [f32; 2],
}
/// iterate-3S: compute the guest→host NDC XY transform for a draw, mirroring
/// canary's `draw_util.cc::GetHostViewportInfo` (the XY half). The Xbox 360 VS
/// emits a clip-space position which the HW then scales/offsets by the viewport
/// (`PA_CL_VPORT_*`, gated by `PA_CL_VTE_CNTL`) into render-target pixels, OR,
/// when clipping is disabled (`PA_CL_CLIP_CNTL.clip_disable`), the VS emits
/// render-target-pixel coordinates directly (the screen-space UI / clear case —
/// this is what Sylpheed's splash quads do). Either way we must rescale into the
/// host's [-1,1] clip space and flip Y (render-target Y-down → wgpu Y-up).
///
/// Returns `(ndc_scale[2], ndc_offset[2])` such that
/// `host_clip.xy = guest_pos.xy * ndc_scale + ndc_offset * guest_pos.w`.
/// The Y entries are pre-negated to flip into wgpu's Y-up clip space.
pub fn compute_ndc_xy(rf: &RegisterFile) -> ([f32; 2], [f32; 2]) {
const PA_CL_CLIP_CNTL: u32 = 0x2204;
const PA_SU_SC_MODE_CNTL: u32 = 0x2205;
const PA_CL_VTE_CNTL: u32 = 0x2206;
const PA_SU_VTX_CNTL: u32 = 0x2302;
const PA_CL_VPORT_XSCALE: u32 = 0x210F;
const PA_CL_VPORT_XOFFSET: u32 = 0x2110;
const PA_CL_VPORT_YSCALE: u32 = 0x2111;
const PA_CL_VPORT_YOFFSET: u32 = 0x2112;
const PA_SC_WINDOW_OFFSET: u32 = 0x2080;
const PA_SC_WINDOW_SCISSOR_BR: u32 = 0x2082;
const RB_SURFACE_INFO: u32 = 0x2000;
let clip_cntl = rf.read(PA_CL_CLIP_CNTL);
let vte = rf.read(PA_CL_VTE_CNTL);
let su_sc_mode = rf.read(PA_SU_SC_MODE_CNTL);
let su_vtx = rf.read(PA_SU_VTX_CNTL);
let fbits = |r: u32| f32::from_bits(rf.read(r));
// VTE enable bits (xenos.h PA_CL_VTE_CNTL): bit0 vport_x_scale_ena,
// bit1 vport_x_offset_ena, bit2 vport_y_scale_ena, bit3 vport_y_offset_ena.
let scale_x = if vte & (1 << 0) != 0 { fbits(PA_CL_VPORT_XSCALE) } else { 1.0 };
let off_x = if vte & (1 << 1) != 0 { fbits(PA_CL_VPORT_XOFFSET) } else { 0.0 };
let scale_y = if vte & (1 << 2) != 0 { fbits(PA_CL_VPORT_YSCALE) } else { 1.0 };
let off_y = if vte & (1 << 3) != 0 { fbits(PA_CL_VPORT_YOFFSET) } else { 0.0 };
// Render-target extent in guest pixels: clamp to the texture max (2048),
// sourced from the window scissor BR (matches canary `x_max`/`y_max`).
let br = rf.read(PA_SC_WINDOW_SCISSOR_BR);
let x_max = ((br & 0x7FFF).max(1)).min(2048) as f32;
let y_max = (((br >> 16) & 0x7FFF).max(1)).min(2048) as f32;
let _ = RB_SURFACE_INFO;
// Half-pixel + window offsets added in render-target pixels.
let mut add_x = 0.0f32;
let mut add_y = 0.0f32;
if su_sc_mode & (1 << 16) != 0 {
let wo = rf.read(PA_SC_WINDOW_OFFSET);
// 15-bit signed each (x: [14:0], y: [30:16]).
let sx = (((wo & 0x7FFF) << 1) as i32) >> 1;
let sy = ((((wo >> 16) & 0x7FFF) << 1) as i32) >> 1;
add_x += sx as f32;
add_y += sy as f32;
}
if su_vtx & 1 == 0 {
// pix_center == kD3DZero → +0.5 half-pixel offset.
add_x += 0.5;
add_y += 0.5;
}
let (s, o);
if clip_cntl & (1 << 16) != 0 {
// clip_disable: VS outputs render-target-pixel coords. Rescale the
// whole RT extent to [-1,1] (canary's huge-host-viewport path).
let px2ndc_x = 2.0 / x_max;
let px2ndc_y = 2.0 / y_max;
let sx = scale_x * px2ndc_x;
let ox = (off_x - x_max * 0.5 + add_x) * px2ndc_x;
let sy = scale_y * px2ndc_y;
let oy = (off_y - y_max * 0.5 + add_y) * px2ndc_y;
s = [sx, sy];
o = [ox, oy];
} else {
// Clipping enabled: the VS already emits clip space; the viewport
// scale/offset map clip→pixels. Convert to the host clip directly:
// host_ndc = guest_ndc (scale ~ 1) but still apply the abs-scale based
// remap canary uses. For the common enabled case the guest already
// outputs [-1,1] so scale=1/offset=0 except sign of Y. We approximate
// with identity XY + Y-flip (sufficient for non-screen-space draws;
// refined alongside depth in a follow-up).
s = [1.0, 1.0];
o = [0.0, 0.0];
}
// Flip Y for wgpu (render-target Y-down → clip Y-up).
([s[0], -s[1]], [o[0], -o[1]])
}
/// Encode a [`PrimitiveType`] as the raw Xenos code used across the bridge.
@@ -179,6 +275,7 @@ pub fn build(
Some((d, base)) => (d, base, true),
None => (Vec::new(), 0, false),
};
let (ndc_scale, ndc_offset) = compute_ndc_xy(rf);
DrawCapture {
draw_index,
prim_code: prim_code(primitive),
@@ -188,5 +285,7 @@ pub fn build(
vertex_dwords,
window_base_dwords,
has_real_vertices: has_real,
ndc_scale,
ndc_offset,
}
}