diff --git a/runtime/src/boot_state.c b/runtime/src/boot_state.c index b4acb4175..ddb399586 100644 --- a/runtime/src/boot_state.c +++ b/runtime/src/boot_state.c @@ -19,6 +19,7 @@ extern uint8_t* memory_get_scratchpad_ptr(void); extern uint32_t dirty_ram_get_bitmap_word(uint32_t word_index); extern uint32_t dirty_ram_get_bitmap_word_count(void); extern void dirty_ram_set_bitmap_words(const uint32_t* words, uint32_t count); +extern void dirty_ram_invalidate_code_caches(void); extern uint32_t i_stat; extern uint32_t i_mask; extern uint64_t psx_cycle_count; @@ -277,6 +278,20 @@ static int apply_section(uint32_t tag, const uint8_t* p, uint32_t len, { extern void psx_kernel_bless_note_range(uint32_t phys, uint32_t l); psx_kernel_bless_note_range(0, RAM_SIZE); + /* Replacing all of RAM can change code identity at any address + * without any page making a clean->dirty transition, so caches + * that key on the RAM code generation would keep serving verdicts + * derived from the PREVIOUS contents. The overlay loader's + * negative cache is the damaging one: it short-circuits dispatch + * before rediscovery, so a stale "no native owner" entry pins an + * address to the wrong implementation for the rest of the run. + * Symptom when this was missing: loading a gameplay state into a + * process that had not itself entered that stage restored RAM + * perfectly and kept running at 60fps, but the player stood + * frozen -- his overlay-resident movement code was never selected + * -- while the rest of the scene animated normally. */ + extern void dirty_ram_invalidate_code_caches(void); + dirty_ram_invalidate_code_caches(); } return 1; case BS_SEC_SPAD: diff --git a/runtime/src/memory.c b/runtime/src/memory.c index 23796bb58..b28b8d525 100644 --- a/runtime/src/memory.c +++ b/runtime/src/memory.c @@ -560,10 +560,36 @@ uint32_t dirty_ram_get_bitmap_word_count(void) { return DIRTY_RAM_BITMAP_WORDS; } +void dirty_ram_invalidate_code_caches(void); + void dirty_ram_set_bitmap_words(const uint32_t* words, uint32_t count) { if (count > DIRTY_RAM_BITMAP_WORDS) count = DIRTY_RAM_BITMAP_WORDS; for (uint32_t i = 0; i < count; i++) dirty_ram_bitmap[i] = words[i]; + /* Wholesale bitmap replacement changes which addresses are RAM-resident + * code, so every cached per-PC classification is now potentially wrong. + * The per-page mark path bumps the generation on its clean->dirty edge; + * this path bypasses it, so bump explicitly. */ + dirty_ram_invalidate_code_caches(); +} + +/* Invalidate every consumer that caches a per-PC classification of RAM + * instructions. Bumping the generation is sufficient by construction: each + * cache stores the generation it was derived under and re-derives on + * mismatch (the interpreter's widescreen site caches, and the overlay + * loader's negative "no native owner" cache, which otherwise short-circuits + * dispatch before it would ever re-discover the correct overlay). + * + * This exists for bulk RAM writes that are NOT ordinary guest stores -- + * savestate restore above all. A savestate replaces all 2 MB at once, so + * code identity can change completely while no page ever makes a + * clean->dirty transition and no executable range is marked. Without this, + * a loaded state keeps executing whatever the host already believed was + * resident: observed as a restored player standing frozen while the rest of + * the scene animated, because the overlay-resident movement code was never + * re-selected. */ +void dirty_ram_invalidate_code_caches(void) { + g_dirty_ram_code_gen++; } /* ---- Inc3: watched overlay pages + per-page generation counters ---------