GP1(08) mirrors video mode into GPUSTAT bits 14/16-22; recompute on state load; gp1-mode-mirror gate

This commit is contained in:
jpolo1224
2026-08-03 07:53:55 -04:00
parent 2238978b4f
commit 9bdee32f70
2 changed files with 90 additions and 0 deletions
+37
View File
@@ -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);
+53
View File
@@ -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