From f4debc54c13712e67edf093040a73dcc39e093ef Mon Sep 17 00:00:00 2001 From: Brian Degenhardt Date: Sat, 25 Jul 2026 09:17:48 -0700 Subject: [PATCH] EE: index recRAMCopy by guest address, not startpc/4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The stale-overlap walk at the tail of recRecompile snapshots each compiled block's guest bytes into recRAMCopy, then memcmps older overlapping blocks against their own snapshots to catch code that went stale through a write no protection path caught. recRAMCopy is a byte array covering main RAM 1:1, but both the compare and the snapshot indexed it at `startpc / 4`, packing every block's snapshot 4:1 into the low quarter of the buffer. Two blocks whose guest starts differ by N bytes then land only N/4 bytes apart in the snapshot, so overlapping blocks scribble over each other's snapshots and the compare can never match: each compile recClears the other and both recompile forever. Final Fantasy X (SLUS-20312) has such a pair at 0x002B9F48 / 0x002B9F5C. Measured over 300 frames from an in-game savestate, EE thread: before 56,546,855,007 insns 14,591,559,685 cycles 6 cache resets after 5,885,593,412 insns 1,368,467,226 cycles 0 cache resets 9.6x fewer instructions, 10.7x fewer cycles. The EE thread had been spending ~95% of itself inside the recompiler (vixl, register allocation, vtlb_AddLoadStoreInfo, icache flushes) and only ~5% executing JIT code, burning 51 MB of the 58 MB EE code cache every ~48 frames and taking a full cache reset behind it. The /4 was correct in 2009, when recRAMCopy was u32*. It became wrong in d6de2e394 (2010), which swapped the storage to SpatialArrayReserve whose operator[] yields u8&, and it survived every later refactor because both index sites carried it consistently. Upstream x86 iR5900.cpp:2691/2701 still has it. The failure is conservative — it over-clears, never misses a genuinely stale block — which is why it stayed invisible for this long as a pure performance bug. Also clamp the compare length so a block straddling the top of main RAM cannot overread: correct indexing now actually reaches the top of the buffer, where the /4 could never go. recompiler_tests 1432/1432, including the three EeRecSmc overlap-walk tests that pin this path. eerunner --stepdiff on FFX is unchanged before and after (the pre-existing divergence at 0x002ce6d4 reproduces identically on both). --- pcsx2/arm64/iR5900-arm64.cpp | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/pcsx2/arm64/iR5900-arm64.cpp b/pcsx2/arm64/iR5900-arm64.cpp index 2cbdd96a65..89627cdfe0 100644 --- a/pcsx2/arm64/iR5900-arm64.cpp +++ b/pcsx2/arm64/iR5900-arm64.cpp @@ -4123,8 +4123,16 @@ StartRecomp: if ((oldBlock->startpc + oldBlock->size * 4) <= HWADDR(startpc)) break; - if (memcmp(&recRAMCopy[oldBlock->startpc / 4], PSM(oldBlock->startpc), - oldBlock->size * 4)) + // recRAMCopy is a byte array covering guest main RAM 1:1 — index + // it by guest address. Do NOT reintroduce the `/ 4` upstream x86 + // still carries (iR5900.cpp:2691/2701): it dates from when + // recRAMCopy was u32* (dropped in 2010), and it packs the + // snapshots 4:1 so overlapping blocks overwrite each other's and + // the compare never matches — each then recClears the other + // forever. Clamp keeps a block straddling the top of RAM in bounds. + const u32 cmplen = std::min(oldBlock->size * 4, + Ps2MemSize::MainRam - oldBlock->startpc); + if (memcmp(&recRAMCopy[oldBlock->startpc], PSM(oldBlock->startpc), cmplen)) { recClear(startpc, (pc - startpc) / 4); s_pCurBlockEx = recBlocks.Get(HWADDR(startpc)); @@ -4133,7 +4141,7 @@ StartRecomp: } } - memcpy(&recRAMCopy[HWADDR(startpc) / 4], PSM(startpc), pc - startpc); + memcpy(&recRAMCopy[HWADDR(startpc)], PSM(startpc), pc - startpc); } if (g_branch == 2)