From 227cb93500ea2bf067edd377f149069a18ebfbd7 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Fri, 2 Oct 2026 12:35:39 -0700 Subject: [PATCH] PGXP: shift the SXY FIFO shadows on an SXYP write Co-Authored-By: Claude Opus 5.5 (1M context) --- runtime/src/gte.cpp | 20 ++++++++------------ runtime/src/pgxp.cpp | 26 +++++++++++++++++++++++--- runtime/tests/test_pgxp.cpp | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 15 deletions(-) diff --git a/runtime/src/gte.cpp b/runtime/src/gte.cpp index e586d8d8..bde90940 100644 --- a/runtime/src/gte.cpp +++ b/runtime/src/gte.cpp @@ -2138,10 +2138,10 @@ extern "C" void gte_write_data(CPUState* cpu, uint8_t reg, uint32_t val) { cpu->gte_data[29] = packed; } break; - /* Guest writes to the SXY FIFO drop the affected register shadows: - * we never model FIFO side effects on shadows — the shifted regs 12/13 - * now describe words their shadows no longer match, and validation - * drops those on next use. */ + /* Guest writes to the SXY FIFO: regs 12/13 overwrite their shadow, a + * reg 14 write overwrites SXY2 and its SXYP mirror, and a reg 15 + * write is the FIFO push, which pgxp_gte_reg_written applies to the + * shadows exactly as the registers move. */ case 12: case 13: cpu->gte_data[reg] = val; if (!PSXRecomp::GTE::s_gte_replay_sandbox) @@ -2150,20 +2150,16 @@ extern "C" void gte_write_data(CPUState* cpu, uint8_t reg, uint32_t val) { case 14: cpu->gte_data[14] = val; cpu->gte_data[15] = val; - if (!PSXRecomp::GTE::s_gte_replay_sandbox) { - pgxp_gte_reg_written(14, val); - pgxp_gte_reg_written(15, val); - } + if (!PSXRecomp::GTE::s_gte_replay_sandbox) + pgxp_gte_reg_written(14, val); /* resets the SXYP mirror too */ break; case 15: cpu->gte_data[12] = cpu->gte_data[13]; cpu->gte_data[13] = cpu->gte_data[14]; cpu->gte_data[14] = val; cpu->gte_data[15] = val; - if (!PSXRecomp::GTE::s_gte_replay_sandbox) { - pgxp_gte_reg_written(14, val); - pgxp_gte_reg_written(15, val); - } + if (!PSXRecomp::GTE::s_gte_replay_sandbox) + pgxp_gte_reg_written(15, val); /* FIFO push on the shadows */ break; case 28: { cpu->gte_data[9] = (val & 0x1Fu) << 7; diff --git a/runtime/src/pgxp.cpp b/runtime/src/pgxp.cpp index cee86da0..55b1bb29 100644 --- a/runtime/src/pgxp.cpp +++ b/runtime/src/pgxp.cpp @@ -206,6 +206,13 @@ static inline int32_t f_simm(uint32_t i) { return (int32_t)(int16_t)(i & 0xFFF /* Memory-mode hooks: loads / stores */ /* ------------------------------------------------------------------------- */ +/* SXY2 and SXYP are one register seen at two addresses: whichever the hook + * just filled, the other describes the same word. */ +static inline void gte_sxy2_mirror(uint32_t reg) { + if (reg == 14) s_gte[15] = s_gte[14]; + else if (reg == 15) s_gte[14] = s_gte[15]; +} + extern "C" void psx_pgxp_load(struct CPUState *cpu, uint32_t instr, uint32_t addr, uint32_t value) { (void)cpu; @@ -323,6 +330,7 @@ extern "C" void psx_pgxp_cop2(struct CPUState *cpu, uint32_t instr, } else { pv_reset(dst, value); } + gte_sxy2_mirror(f_rt(instr)); return; } case 0x3A: { /* SWC2: [addr] <- gte[rt] */ @@ -360,9 +368,9 @@ extern "C" void psx_pgxp_cop2(struct CPUState *cpu, uint32_t instr, } else { pv_reset(dst, value); } - /* An SXYP write (rd==15) shifts the hardware FIFO; we do not - * model it — regs 12..14 now describe words they no longer - * match, and validation drops them on next use. */ + /* An SXYP write (rd==15) already shifted the FIFO shadows + * (pgxp_gte_reg_written, from the GTE register write). */ + gte_sxy2_mirror(f_rd(instr)); return; } case 0x02: { /* CFC2: control regs carry no @@ -727,8 +735,20 @@ extern "C" void pgxp_gte_reg_written(int reg, uint32_t value) { * machine state back, so its writes must not stick to the shadows). */ if (s_suppress != 0) return; if (reg < 0 || reg > 31) return; + if (reg == 15) { + /* SXYP is a FIFO push: SXY1 moves to SXY0, SXY2 to SXY1, the value + * lands in SXY2 (psx-spx GTE "SXYP"). Engines draw quads from a + * projected-vertex table this way - load three corners, NCLIP, push + * the fourth through SXYP, NCLIP, store SXY0..2 into the packet - so + * the shadows shift with the registers. The MTC2 / LWC2 hook then + * copies the source shadow into the pushed slot. */ + s_gte[12] = s_gte[13]; + s_gte[13] = s_gte[14]; + reg = 14; + } pv_kill(&s_gte[reg]); pv_reset(&s_gte[reg], value); + gte_sxy2_mirror(reg); /* SXY2 and SXYP: one register */ } /* ------------------------------------------------------------------------- */ diff --git a/runtime/tests/test_pgxp.cpp b/runtime/tests/test_pgxp.cpp index 8de86d08..5001ff03 100644 --- a/runtime/tests/test_pgxp.cpp +++ b/runtime/tests/test_pgxp.cpp @@ -259,6 +259,38 @@ int main(void) { } pgxp_set_cpu_mode(0); + /* --- quad from a projected-vertex table (Spider-Man 0x8007C5D4): + * lwc2 SXY0..2 <- table; NCLIP; lwc2 SXYP <- table; NCLIP; + * swc2 SXY0..2 -> packet + * The SXYP write pushes the FIFO; the stored words are corners 1..3. --- */ + { + const uint32_t TABLE = 0x80180100u, PKT = 0x80180200u; + uint32_t words[4]; + for (uint32_t i = 0; i < 4; i++) { + words[i] = ((80u + i) << 16) | (160u + i); + pgxp_gte_push_sxy((int32_t)((160 + i) << 16) | 0x8000, + (int32_t)((80 + i) << 16) | 0x4000, + (uint16_t)(100 + i), words[i]); + psx_pgxp_cop2(nullptr, SWC2(14), words[i], TABLE + i * 4u); + } + for (uint32_t i = 0; i < 3; i++) { /* lwc2 SXY0..2 */ + pgxp_gte_reg_written((int)(12 + i), words[i]); + psx_pgxp_cop2(nullptr, enc_i(0x32, 1, 12 + i, 0), words[i], + TABLE + i * 4u); + } + pgxp_gte_reg_written(15, words[3]); /* lwc2 SXYP: push */ + psx_pgxp_cop2(nullptr, enc_i(0x32, 1, 15, 0), words[3], TABLE + 12u); + for (uint32_t i = 0; i < 3; i++) + psx_pgxp_cop2(nullptr, SWC2(12 + i), words[i + 1], PKT + i * 4u); + for (uint32_t i = 0; i < 3; i++) { + int32_t x, y; uint16_t z; + CHECK(lookup(PKT + i * 4u, words[i + 1], 161 + (int32_t)i, + 81 + (int32_t)i, &x, &y, &z) == PGXP_SRC_DATAFLOW); + CHECK(x == ((int32_t)((161 + i) << 16) | 0x8000)); + CHECK(z == 101 + i); + } + } + /* --- in-place ops read their source before the destination is reset --- */ pgxp_set_cpu_mode(1); produce_at(ADDR_A);