From dbe7812a6ae09605ef9b629892707a51febe2de1 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Fri, 3 Jul 2026 20:34:41 -0700 Subject: [PATCH] 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 --- runtime/src/dirty_ram_interp.c | 39 ++++++++++++++++++++++++++-------- runtime/src/memory.c | 16 +++++++++++++- 2 files changed, 45 insertions(+), 10 deletions(-) diff --git a/runtime/src/dirty_ram_interp.c b/runtime/src/dirty_ram_interp.c index 1897e2ec..db74836f 100644 --- a/runtime/src/dirty_ram_interp.c +++ b/runtime/src/dirty_ram_interp.c @@ -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) { diff --git a/runtime/src/memory.c b/runtime/src/memory.c index 802dcb81..7091b114 100644 --- a/runtime/src/memory.c +++ b/runtime/src/memory.c @@ -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