diff --git a/crates/xenia-gpu/src/draw_capture.rs b/crates/xenia-gpu/src/draw_capture.rs index 979dc23..937ca73 100644 --- a/crates/xenia-gpu/src/draw_capture.rs +++ b/crates/xenia-gpu/src/draw_capture.rs @@ -155,29 +155,40 @@ pub fn compute_ndc_xy(rf: &RegisterFile) -> ([f32; 2], [f32; 2]) { 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). + // clip_disable: VS outputs render-target-*pixel* coords (Y-DOWN: pixel + // y=0 is the top row of the render target). Rescale the whole RT extent + // to [-1,1] and FLIP Y so pixel-top → wgpu clip-top (canary's + // huge-host-viewport path; the framebuffer→clip flip is real here). 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]; + // Flip Y: pixel-Y-down → wgpu clip-Y-up. + 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). + // iterate-3AA (DEFECT 1 ROOT): clipping enabled → the VS already emits + // *clip-space* coordinates (Y-UP: +Y is the top of the screen), exactly + // the convention the Xbox 360's D3D9 and wgpu BOTH use for clip space + // (NDC +Y → framebuffer top in each API; the framebuffer Y-direction is + // an internal viewport detail handled identically by both). A clip-space + // position is therefore portable to wgpu with NO Y-flip. The previous + // code unconditionally negated Y (the same flip the screen-space pixel + // path needs), which mirrored the publisher logo vertically: its quad is + // centered (±0.085 around 0) so the *position* stayed centered, but the + // negation swapped top↔bottom vertices while the texture V was unchanged + // → the sampled sub-rect (UV v 0.001→0.090) read bottom-up → "SQUARE + // ENIX" rendered upside down in place. Measured (readback): the red dots + // sit at 43% from the texture top but rendered at 58% from the top + // (= a clean vertical mirror); removing the flip restores them to 43%. + // Identity XY (no flip) maps guest clip-Y-up straight to wgpu clip-Y-up. s = [1.0, 1.0]; o = [0.0, 0.0]; + return (s, o); } - // Flip Y for wgpu (render-target Y-down → clip Y-up). - ([s[0], -s[1]], [o[0], -o[1]]) + (s, o) } /// Encode a [`PrimitiveType`] as the raw Xenos code used across the bridge.