interp: fix the REAL native-wide CPU collapse - per-instruction ws site scans

Stack-sampled (devkitPro gdb, Tomba2 16:9 attract in the 12fps wedge): the
main thread sat in ws_backdrop_site_kind <- exec_one. Under native-wide,
exec_one classified EVERY interpreted instruction as a potential backdrop
rewrite site; the classifier scans +/-512 bytes of RAM per miss, and its
256-slot direct-mapped cache (any two hot PCs 1KB apart collide) thrashed
on overlay-sized working sets, so hot interpreted loops re-scanned per
executed instruction. Squash mode gates the whole path off, which is why
ws_nw on=0 instantly restored 60fps while every GPU-side theory (mirror
passes, FBO ping-pong, extra prims, present path) failed - the earlier
GL GPU-time numbers were CPU-starvation-inflated timestamp gaps.

Fixes:
- exec_one pre-filters by opcode class first (addu/or/addi/addiu are the
  only rewrite-site forms) - detection no longer runs for ~85% of instrs.
- Both site caches (cull + backdrop) grow to 8192 full-PC-tagged slots.
- NEW g_dirty_ram_code_gen (memory.c): bumped on clean->dirty page
  transitions and every executable-range mark (overlay/EXE loads); cached
  classifications carry the generation, so an overlay reload re-derives
  them. This also fixes the latent staleness bug where both caches could
  serve a kind derived from a PREVIOUS overlay bytes at the same PC.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Matthew Stanley
2026-07-03 20:34:41 -07:00
parent cfa79bbc57
commit dbe7812a6a
2 changed files with 45 additions and 10 deletions
+30 -9
View File
@@ -290,11 +290,21 @@ static inline uint32_t fetch_word(uint32_t phys) {
* function boundaries, so we scan a +/-512-byte window around the site (clamped
* to main RAM) via the shared psx_ws_func_has_screen_cull, cached per-PC so a
* hot render loop pays the scan once. Single-threaded interp => static buffers ok. */
/* Per-PC site caches: FULL-PC tagged, sized for a whole overlay's working set
* (the old 256-slot direct map thrashed on any two hot PCs 1 KB apart — the
* ±512-byte rescan then ran per EXECUTED INSTRUCTION and collapsed native-wide
* 16:9 to ~12fps, entirely CPU-side). Entries carry the dirty-RAM code
* generation (memory.c g_dirty_ram_code_gen) so an overlay reload re-derives
* every classification instead of trusting a stale kind. */
extern uint32_t g_dirty_ram_code_gen;
#define WS_SITE_CACHE_SLOTS 8192u /* 32 KB of code coverage per cache */
static int ws_cull_site(uint32_t pc) {
enum { WIN = 128 }; /* +/- 128 words = +/- 512 bytes */
static struct { uint32_t pc; int8_t flag; } cache[256];
uint32_t slot = (pc >> 2) & 255u;
if (cache[slot].pc == pc) return cache[slot].flag;
static struct { uint32_t pc; uint32_t gen; int8_t flag; } cache[WS_SITE_CACHE_SLOTS];
uint32_t slot = (pc >> 2) & (WS_SITE_CACHE_SLOTS - 1u);
if (cache[slot].pc == pc && cache[slot].gen == g_dirty_ram_code_gen)
return cache[slot].flag;
uint32_t phys = pc & 0x1FFFFFFFu;
uint32_t lo = (phys > (uint32_t)(WIN * 4)) ? phys - (uint32_t)(WIN * 4) : 0u;
uint32_t hi = phys + (uint32_t)(WIN * 4);
@@ -304,7 +314,7 @@ static int ws_cull_site(uint32_t pc) {
for (uint32_t a = lo; a + 4u <= hi && n < (int)(2 * WIN + 1); a += 4u)
words[n++] = fetch_word(a);
int flag = psx_ws_func_has_screen_cull(words, n);
cache[slot].pc = pc; cache[slot].flag = (int8_t)flag;
cache[slot].pc = pc; cache[slot].gen = g_dirty_ram_code_gen; cache[slot].flag = (int8_t)flag;
return flag;
}
@@ -318,9 +328,12 @@ static int ws_cull_site(uint32_t pc) {
* WS_BD_END_WIDEN. Single-threaded interp => static buffers are safe. */
static int ws_backdrop_site_kind(uint32_t pc, int *out_cols) {
enum { WIN = 128 }; /* +/- 128 words = +/- 512 bytes */
static struct { uint32_t pc; int8_t kind; int16_t cols; } cache[256];
uint32_t slot = (pc >> 2) & 255u;
if (cache[slot].pc == pc) { if (out_cols) *out_cols = cache[slot].cols; return cache[slot].kind; }
static struct { uint32_t pc; uint32_t gen; int8_t kind; int16_t cols; } cache[WS_SITE_CACHE_SLOTS];
uint32_t slot = (pc >> 2) & (WS_SITE_CACHE_SLOTS - 1u);
if (cache[slot].pc == pc && cache[slot].gen == g_dirty_ram_code_gen) {
if (out_cols) *out_cols = cache[slot].cols;
return cache[slot].kind;
}
uint32_t phys = pc & 0x1FFFFFFFu;
uint32_t lo = (phys > (uint32_t)(WIN * 4)) ? phys - (uint32_t)(WIN * 4) : 0u;
uint32_t hi = phys + (uint32_t)(WIN * 4 + 4);
@@ -331,7 +344,8 @@ static int ws_backdrop_site_kind(uint32_t pc, int *out_cols) {
words[n++] = fetch_word(a);
int cols = 0;
int kind = psx_ws_backdrop_kind_at(words, n, lo, phys, &cols); /* all physical-space */
cache[slot].pc = pc; cache[slot].kind = (int8_t)kind; cache[slot].cols = (int16_t)cols;
cache[slot].pc = pc; cache[slot].gen = g_dirty_ram_code_gen;
cache[slot].kind = (int8_t)kind; cache[slot].cols = (int16_t)cols;
if (out_cols) *out_cols = cols;
return kind;
}
@@ -1083,7 +1097,14 @@ static int exec_one(CPUState *cpu, uint32_t pc, uint32_t *next_pc_out) {
* complete. Gated on the runtime predicate first => 4:3 pays nothing. Each
* rewrite is recorded to the always-on backdrop ring with the live extent
* (s7), camera-X (scratchpad 0x176) and DL count for `ws_backdrop_ring`. */
if (psx_ws_backdrop_preload()) {
/* Opcode pre-filter FIRST: only addu/or/addi/addiu can be a rewrite site
* (the detector below rewrites exactly one GPR-writing move/addiu). The old
* order ran the ±512-byte site scan for EVERY interpreted instruction; with
* the small direct-mapped cache thrashing, that was the mechanism of the
* native-wide 16:9 CPU collapse (Tomba2 attract 12fps, stack-sampled). */
if (((opc == 0x00u && (fnt == 0x21u || fnt == 0x25u)) ||
opc == 0x09u || opc == 0x08u) &&
psx_ws_backdrop_preload()) {
int wcols = 0;
int bk = ws_backdrop_site_kind(pc, &wcols);
if (bk != WS_BD_NONE) {
+15 -1
View File
@@ -63,10 +63,23 @@ uint8_t *memory_get_scratchpad_ptr(void) { return scratchpad; }
#define DIRTY_RAM_BITMAP_WORDS ((DIRTY_RAM_PAGE_COUNT + 31u) / 32u)
static uint32_t dirty_ram_bitmap[DIRTY_RAM_BITMAP_WORDS];
/* Monotonic generation for RAM-resident CODE changes (kernel install-stub
* writes + DMA/EXE loads that mark executable ranges). Consumers that cache
* per-PC classifications of RAM instructions (the interp's widescreen
* cull/backdrop site caches) compare against this and re-derive after any
* code change — a cached kind must never survive an overlay reload. */
uint32_t g_dirty_ram_code_gen = 1;
static inline void dirty_ram_mark_page(uint32_t phys) {
if (phys >= RAM_SIZE) return;
uint32_t page = phys >> DIRTY_RAM_PAGE_SHIFT;
dirty_ram_bitmap[page >> 5] |= (1u << (page & 31u));
uint32_t bit = 1u << (page & 31u);
/* Generation bumps on the clean->dirty TRANSITION only: kernel-window data
* writes land here on every guest store, and bumping per write would
* invalidate the per-PC site caches continuously. Overlay (re)loads always
* bump via dirty_ram_mark_executable_range. */
if (!(dirty_ram_bitmap[page >> 5] & bit)) g_dirty_ram_code_gen++;
dirty_ram_bitmap[page >> 5] |= bit;
}
static inline void dirty_ram_mark_kernel_write(uint32_t phys) {
@@ -106,6 +119,7 @@ void dirty_ram_mark_executable_range(uint32_t phys, uint32_t len) {
for (uint32_t page = first_page; page <= last_page; page++) {
dirty_ram_bitmap[page >> 5] |= (1u << (page & 31u));
}
g_dirty_ram_code_gen++;
}
/* Force-interp mode (tooling): PSX_FORCE_INTERP=1 makes ALL RAM above the kernel