From 9bdee32f7007ac948b0a70d5d94cfa56022c1bcc Mon Sep 17 00:00:00 2001 From: jpolo1224 Date: Mon, 3 Aug 2026 07:53:55 -0400 Subject: [PATCH] GP1(08) mirrors video mode into GPUSTAT bits 14/16-22; recompute on state load; gp1-mode-mirror gate --- psx/dev/gpu.c | 37 ++++++++++++++++++++++++++ tests/gpu_renderer_parity.c | 53 +++++++++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+) diff --git a/psx/dev/gpu.c b/psx/dev/gpu.c index 0d40d02..aa76888 100644 --- a/psx/dev/gpu.c +++ b/psx/dev/gpu.c @@ -3351,6 +3351,35 @@ void psx_gpu_write32(psx_gpu_t* gpu, uint32_t offset, uint32_t value) { } break; case 0x08: gpu->display_mode = value & 0xffffff; + + /* + Mirror the mode into GPUSTAT, per psx-spx: + + GP1(08).0-1 hres1 -> GPUSTAT.17-18 + GP1(08).2 vres -> GPUSTAT.19 + GP1(08).3 PAL/NTSC -> GPUSTAT.20 + GP1(08).4 24bpp -> GPUSTAT.21 + GP1(08).5 interlace -> GPUSTAT.22 + GP1(08).6 hres2 -> GPUSTAT.16 + GP1(08).7 reverse -> GPUSTAT.14 + + These were never written, so a game reading its video mode back saw + every field as 0 — 256x240, 15bpp, progressive, NTSC — regardless of + what it had set. A readback lying about live state, the same defect + class as GPUINFO(5) and GTE ORGB. For a 512-wide title (Crash runs + GP1(08)=0x02) the readback claimed a 256-wide screen: an engine that + sizes its own culling viewport from GPUSTAT drops exactly the outer + flank geometry, which is the measured Crash wedge — the game submits + no triangles over those pixels while the GPU stream stays well-formed. + Confirmed in the device dump header: display_mode=000002 (hres=512) + alongside stat=8000001e (mode bits all zero). + Gate: gp1-mode-mirror in tests/gpu_renderer_parity.c. + */ + gpu->gpustat = (gpu->gpustat & ~0x007f4000u) + | ((value & 0x3fu) << 17) + | ((value & 0x40u) << 10) + | ((value & 0x80u) << 7); + GPU_HW_DEBUG( "gp1-display-mode value=%08x display_mode=0x%08x video_standard=%s", value, @@ -3803,6 +3832,14 @@ int psx_gpu_load_state(psx_gpu_t* gpu, psx_state_reader_t* r) { gpu->gpuread = psx_sr_u32(r); gpu->gpustat = psx_sr_u32(r); + /* States saved before the GP1(08) -> GPUSTAT mirror existed carry zeros in the mode + bits, and a game rarely re-sends GP1(08) after boot — recompute the mirror from the + restored display_mode so a loaded state can't reintroduce the lying readback. */ + gpu->gpustat = (gpu->gpustat & ~0x007f4000u) + | ((gpu->display_mode & 0x3fu) << 17) + | ((gpu->display_mode & 0x40u) << 10) + | ((gpu->display_mode & 0x80u) << 7); + gpu->draw_x1 = psx_sr_u32(r); gpu->draw_y1 = psx_sr_u32(r); gpu->draw_x2 = psx_sr_u32(r); diff --git a/tests/gpu_renderer_parity.c b/tests/gpu_renderer_parity.c index bf9f381..25e70e3 100644 --- a/tests/gpu_renderer_parity.c +++ b/tests/gpu_renderer_parity.c @@ -1395,6 +1395,55 @@ static int gp0_check_pixels(const char* name, const char* what, psx_gpu_t* gpu, set (negative fields) must echo verbatim rather than smearing sign bits over the word. The sweeps below include every such value. */ +/* + GP1(08) -> GPUSTAT mirror. psx-spx: mode bits 0-5 land in GPUSTAT 17-22, bit 6 (hres2) + in 16, bit 7 (reverse) in 14. These were never written at all, so a game reading its + video mode back saw 256x240/15bpp/progressive/NTSC forever — for Crash (512-wide, + GP1(08)=0x02) the readback claimed a screen half the real width, and an engine that + sizes its culling viewport from GPUSTAT drops exactly the outer flank geometry. + Exhaustive over all 256 mode bytes, and the texpage/mask bits E1/E6 own must survive. +*/ +static int run_gp1_mode_mirror_case(void) { + const char* name = "gp1-mode-mirror"; + psx_gpu_t* gpu = make_gpu(); + int failed = 0; + + /* Seed the E1/E6-owned low bits so a clobber is visible. */ + psx_gpu_write32(gpu, 0, 0xe1000000 | 0x2ff); + psx_gpu_write32(gpu, 0, 0xe6000003); + + for (uint32_t m = 0; m < 0x100 && !failed; m++) { + psx_gpu_write32(gpu, 4, 0x08000000 | m); + + uint32_t stat = psx_gpu_read32(gpu, 4); + uint32_t want = ((m & 0x3fu) << 17) | ((m & 0x40u) << 10) | ((m & 0x80u) << 7); + + if ((stat & 0x007f4000u) != want) { + fprintf(stderr, + "GPU_PARITY failed case=%s reason=mode-mirror mode=%02x stat=%08x " + "mirror=%06x want=%06x\n", + name, m, stat, stat & 0x007f4000u, want); + failed = 1; + } + + /* E1's texpage bits 0-10 and E6's mask bits 11-12 must be untouched. */ + if ((stat & 0x1fffu) != 0x1aff) { + fprintf(stderr, + "GPU_PARITY failed case=%s reason=low-bits-clobbered mode=%02x " + "stat=%08x low=%04x want=1aff\n", + name, m, stat, stat & 0x1fffu); + failed = 1; + } + } + + psx_gpu_destroy(gpu); + + if (!failed) + printf("GPU_PARITY passed case=%s\n", name); + + return failed; +} + static int run_gpuinfo_roundtrip_case(void) { const char* name = "gpuinfo-roundtrip"; psx_gpu_t* gpu = make_gpu(); @@ -2301,6 +2350,10 @@ int main(void) { differential pair share. */ failed |= run_gpuinfo_roundtrip_case(); + /* GPUSTAT must tell the game the video mode it actually set — a zeroed mirror told + every title it was on a 256-wide screen. */ + failed |= run_gp1_mode_mirror_case(); + /* Turning accurate_mask_bit on must not make textured content disappear — the setting is inert until a game sends GP0(E6), and a sprite into a clean buffer draws in full under every E6. The mask cases above pin the rules per pixel; this asks whether the