From 621a92d49bb206a8bf97902e8165a5a73487146b Mon Sep 17 00:00:00 2001 From: Shokunin <30949000+tetrisgm@users.noreply.github.com> Date: Tue, 29 Sep 2026 17:12:55 -0700 Subject: [PATCH 1/3] runtime: apply cache-isolated (IsC) stores to the caches, as Beetle does memory.c dropped every store made while SR.IsC was set. Beetle's PS_CPU::WriteMemory sends them to the caches instead (mednafen/psx/cpu.cpp:482-512), before any address decode: - I-cache enabled (BIU bit 11) with a tag-test, invalidate or lock mode bit: the store rewrites the tag and valid bits of its line. This is how FlushCache (A 44h) and the boot cache init invalidate the I-cache. - D-cache enabled with lock mode clear ((BIU & 0x81) == 0x80): the store lands in the scratchpad at addr & 0x3FF. - Nothing reaches the bus, the BIU register at 0xFFFE0130 included. psx_icache_isc_store transcribes the tag write; memory.c's three store chokepoints call it (and the scratchpad write) first, so compiled code, overlay shards and both interpreters share it. DMA writes are not CPU stores and now bypass IsC, as in Beetle. A render pass applies the same effect (its checkpoint restores tags and scratchpad), and the overlay shadow record fails closed on an isolated store, which its replay cannot redo. isc_store_test drives the real memory.c and psx_icache.c: 15 of its checks fail on master. Co-Authored-By: Claude Opus 5.5 --- runtime/CMakeLists.txt | 23 +++ runtime/include/lockstep.h | 5 + runtime/include/psx_icache.h | 16 ++ runtime/src/dirty_ram_interp.c | 10 +- runtime/src/memory.c | 57 +++++- runtime/src/psx_icache.c | 40 ++++ runtime/tests/psx_ram_runtime_map_stubs.c | 7 + runtime/tests/test_isc_store.c | 217 ++++++++++++++++++++++ 8 files changed, 363 insertions(+), 12 deletions(-) create mode 100644 runtime/tests/test_isc_store.c diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 4357e20f..6f601d99 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -199,6 +199,29 @@ if(BUILD_TESTING) -flto -Wl,--gc-sections -Wno-lto-type-mismatch) target_link_libraries(psx_ram_runtime_map_test PRIVATE ZLIB::ZLIB) add_test(NAME psx_ram_runtime_map_test COMMAND psx_ram_runtime_map_test) + + # Cache-isolated (SR.IsC) CPU stores through the REAL memory.c store + # paths and the REAL psx_icache.c tag model, against Beetle's + # WriteMemory IsC branch: FlushCache invalidation, tag-test valid bits, + # the D-cache scratchpad write, the BIU and DMA exclusions. Same link + # doubles as psx_ram_runtime_map_test, minus the I-cache ones. + add_executable(isc_store_test + tests/test_isc_store.c + tests/psx_ram_runtime_map_stubs.c + src/memory.c + src/dma.c + src/boot_state.c + src/psx_icache.c + src/psx_ram_geometry.c) + target_include_directories(isc_store_test PRIVATE include src) + target_compile_definitions(isc_store_test PRIVATE + PSX_ENABLE_BLOCK_CYCLES=1 PSX_STUBS_REAL_ICACHE=1) + target_compile_options(isc_store_test PRIVATE + -O2 -flto -ffunction-sections -fdata-sections -Wno-lto-type-mismatch) + target_link_options(isc_store_test PRIVATE -flto -Wno-lto-type-mismatch + $,-Wl$-dead_strip,-Wl$--gc-sections>) + target_link_libraries(isc_store_test PRIVATE ZLIB::ZLIB) + add_test(NAME isc_store_test COMMAND isc_store_test) endif() add_executable(host_keymap_test diff --git a/runtime/include/lockstep.h b/runtime/include/lockstep.h index 775634b0..56e0b9e9 100644 --- a/runtime/include/lockstep.h +++ b/runtime/include/lockstep.h @@ -38,6 +38,11 @@ int ls_shadow_replay_end(uint32_t *ops, int *mismatch_kind, uint32_t *pc, uint32_t *addr, uint32_t *expected, uint32_t *actual); void ls_shadow_abort(void); +/* A recorded effect the replay cannot reproduce fails the record closed (the + * comparison is skipped, as for a device touch). memory.c calls it for a + * cache-isolated store: its I-cache effect depends on BIU state, and the + * replay neither writes the BIU nor stores. No-op outside a shadow record. */ +void ls_shadow_record_unreplayable(void); /* Memory chokepoint hooks, called from the psx_read_ and psx_write_ funcs. */ uint32_t ls_read_hook(uint32_t addr, int size, uint32_t real_val); diff --git a/runtime/include/psx_icache.h b/runtime/include/psx_icache.h index e1d1b722..e45b9381 100644 --- a/runtime/include/psx_icache.h +++ b/runtime/include/psx_icache.h @@ -14,6 +14,22 @@ void psx_icache_reset(void); void psx_icache_fetch(CPUState *cpu, uint32_t addr); void psx_icache_fetch_miss(CPUState *cpu, uint32_t addr); +/* BIU / cache control (0xFFFE0130) bits the cache model reads; names and values + * as Beetle's cpu.cpp BIU_* defines. */ +#define PSX_BIU_ICACHE_ENABLE 0x800u /* BIU_ENABLE_ICACHE_S1 */ +#define PSX_BIU_DCACHE_ENABLE 0x080u /* BIU_ENABLE_DCACHE */ +#define PSX_BIU_TAG_TEST 0x004u /* BIU_TAG_TEST_MODE */ +#define PSX_BIU_INVALIDATE 0x002u /* BIU_INVALIDATE_MODE */ +#define PSX_BIU_LOCK 0x001u /* BIU_LOCK_MODE */ + +/* A CPU store made while SR.IsC is set, applied to the I-cache model: the + * I-cache half of Beetle PS_CPU::WriteMemory's IsC branch. With the I-cache + * enabled and a tag-test, invalidate or lock mode bit set, the store rewrites + * the tag and valid bits of the four words of its line; that is how FlushCache + * (A 44h) invalidates the cache. `biu` is the current 0xFFFE0130 value and + * `value` the stored register value (only its low byte lanes matter). */ +void psx_icache_isc_store(uint32_t biu, uint32_t addr, uint32_t value); + /* Keep the interpreter's steady-state tag hit inside its translation unit. * Misses use the shared slow path, preserving exact cache evolution/timing. */ static inline void psx_icache_fetch_interp(CPUState *cpu, uint32_t addr) { diff --git a/runtime/src/dirty_ram_interp.c b/runtime/src/dirty_ram_interp.c index ef6158b7..be586613 100644 --- a/runtime/src/dirty_ram_interp.c +++ b/runtime/src/dirty_ram_interp.c @@ -3489,6 +3489,7 @@ enum { LS_TRACE_CAP = 65536 }; static ls_op_t s_ls_trace[LS_TRACE_CAP]; static int s_ls_trace_n = 0, s_ls_trace_idx = 0; static int s_ls_overflow = 0, s_ls_mismatch = 0; +static int s_ls_unreplayable = 0; /* whole-call record saw a non-replayable effect */ static int s_ls_replay_done = 0; /* trace exhausted: stop replay (benign count mismatch) */ static uint32_t s_ls_cur_pc = 0; /* pc of the instruction being replayed */ static int s_ls_shadow_owner = 0; /* 0 none, 1 record, 2 replay */ @@ -3557,6 +3558,7 @@ int ls_shadow_record_begin(void) { s_ls_trace_n = 0; s_ls_trace_idx = 0; s_ls_overflow = 0; + s_ls_unreplayable = 0; s_ls_mismatch = 0; s_ls_m_kind = 0; s_ls_replay_done = 0; @@ -3572,12 +3574,16 @@ int ls_shadow_record_end(uint32_t *ops, int *saw_exception) { s_ls_shadow_owner = 0; if (ops) *ops = (uint32_t)s_ls_trace_n; if (saw_exception) *saw_exception = s_ls_shadow_saw_exception; - return !s_ls_overflow; + return !s_ls_overflow && !s_ls_unreplayable; +} + +void ls_shadow_record_unreplayable(void) { + if (s_ls_shadow_owner == 1) s_ls_unreplayable = 1; } int ls_shadow_replay_begin(void) { if (s_ls_shadow_owner || g_ls_mode != 0 || g_ls_replay_active || - s_ls_overflow) + s_ls_overflow || s_ls_unreplayable) return 0; s_ls_trace_idx = 0; s_ls_mismatch = 0; diff --git a/runtime/src/memory.c b/runtime/src/memory.c index 6f63240e..280b1f9b 100644 --- a/runtime/src/memory.c +++ b/runtime/src/memory.c @@ -25,6 +25,7 @@ #include "dirty_ram_interp.h" #include "guest_tty.h" #include "psx_cycles.h" +#include "psx_icache.h" #include "psx_memory.h" #include "render_pass.h" #include "render_pass_plan.h" @@ -962,9 +963,8 @@ uint64_t g_kseg2_ignored_writes; static uint32_t cache_ctrl; /* Pointer to cpu->cop0[12] (SR). Set once at init. - * Used by write functions to check the IsC (Isolate Cache) bit. - * When IsC is set, RAM/scratchpad writes are silently dropped — the - * real R3000A sends them to the data cache only. */ + * Used by write functions to check the IsC (Isolate Cache) bit: while it is + * set, a CPU store goes to the caches and never reaches the bus (isc_store). */ static const uint32_t *sr_ptr; /* Interrupt controller — non-static so hardware subsystems can set I_STAT bits. */ @@ -1717,6 +1717,42 @@ static inline void d44_note(uint32_t phys, uint32_t old, uint32_t val) { * counted instead. */ uint64_t g_render_pass_dropped_writes[RENDER_PASS_DROP_CLASSES]; +/* SR.IsC (Isolate Cache, bit 16). While it is set, a CPU store goes to the + * caches, never to the bus. This is Beetle PS_CPU::WriteMemory's IsC branch + * (mednafen/psx/cpu.cpp:482-512), which runs before any address decode: + * - I-cache enabled (BIU bit 11) with a tag-test, invalidate or lock mode + * bit: the store rewrites the tag and valid bits of its line + * (psx_icache_isc_store). FlushCache (A 44h) invalidates the I-cache this + * way. + * - I-cache enabled, no mode bit: Beetle writes an instruction word, which + * the model does not hold. + * - D-cache enabled and lock mode clear ((BIU & 0x81) == 0x80): the store + * lands in the scratchpad, the D-cache array, at addr & 0x3FF, whatever + * region addr is in. + * Nothing else happens: an isolated store to RAM, MMIO or the BIU register + * itself is dropped (Beetle routes 0xFFFE0130 through MemRW, libretro.cpp:1053, + * which only non-isolated stores reach). DMA is not a CPU store; Beetle's DMA + * writes RAM directly, so IsC does not apply while a transfer moves data. */ +static inline int cpu_store_isolated(void) { + return sr_ptr && (*sr_ptr & 0x10000u) && g_dma_exec_depth == 0; +} + +static void isc_store(uint32_t addr, uint32_t val, uint32_t width) { + /* The overlay shadow replay cannot redo this (lockstep.h). */ + if (g_ls_mode == 1) ls_shadow_record_unreplayable(); + psx_icache_isc_store(cache_ctrl, addr, val); + if ((cache_ctrl & (PSX_BIU_DCACHE_ENABLE | PSX_BIU_LOCK)) == + PSX_BIU_DCACHE_ENABLE) { + uint32_t off = addr & 0x3FFu; + uint32_t old = 0; + for (uint32_t i = 0; i < width; i++) + old |= (uint32_t)scratchpad[(off + i) & 0x3FFu] << (8u * i); + debug_server_trace_write_check(0x1F800000u + off, old, val, (uint8_t)width); + for (uint32_t i = 0; i < width; i++) + scratchpad[(off + i) & 0x3FFu] = (uint8_t)(val >> (8u * i)); + } +} + static void render_pass_mmio_write(uint32_t phys, uint32_t val, uint32_t width) { if (width == 4) mmio_write32(phys, val); @@ -1733,7 +1769,10 @@ static void render_pass_store(uint32_t addr, uint32_t val, uint32_t width) { t.ram_size = psx_ram_live_bytes(); /* live geometry, as psx_ram_map_write */ t.scratchpad = scratchpad; t.scratchpad_size = SCRATCHPAD_SIZE; - t.isolate_cache = (sr_ptr && (*sr_ptr & 0x10000u)) ? 1 : 0; + t.isolate_cache = cpu_store_isolated(); + /* Same cache effect as outside a pass; the pass checkpoint restores the + * I-cache tags and the scratchpad when it ends. */ + if (t.isolate_cache) isc_store(addr, val, width); t.mmio_write = render_pass_mmio_write; cls = render_pass_store_to(&t, addr, val, width); if (cls >= 0) g_render_pass_dropped_writes[cls]++; @@ -1760,15 +1799,13 @@ static void psx_write_word_raw(uint32_t addr, uint32_t val) { /* (pgxp) plain-store shadow invalidation retired: the PGXP engine * validates tracked words against the actual packet word on read, so an * overwritten word can never be believed (docs/ENHANCEMENTS.md G1). */ + /* IsC first, before any decode (cpu_store_isolated). */ + if (cpu_store_isolated()) { isc_store(addr, val, 4); return; } /* KSEG2 cache control — before physical translation. */ if (addr == 0xFFFE0130u) { cache_ctrl = val; return; } /* KSEG2 guard — see psx_read_word_raw. */ if (addr >= 0xC0000000u) { g_kseg2_ignored_writes++; return; } - /* IsC (Isolate Cache): when set, writes go to D-cache only. - * We have no cache model, so silently discard RAM/scratchpad writes. */ - if (sr_ptr && (*sr_ptr & 0x10000u)) return; - uint32_t phys = psx_phys_addr_store(addr); /* The generated BIOS mirrors its exception trampoline to 0x80000000 during @@ -1960,7 +1997,7 @@ void psx_write_half(uint32_t addr, uint16_t val) { } static void psx_write_half_raw(uint32_t addr, uint16_t val) { g_guest_store_count++; - if (sr_ptr && (*sr_ptr & 0x10000u)) return; + if (cpu_store_isolated()) { isc_store(addr, val, 2); return; } /* KSEG2 guard — see psx_read_word_raw. */ if (addr >= 0xC0000000u) { g_kseg2_ignored_writes++; return; } @@ -2300,7 +2337,7 @@ void psx_write_byte(uint32_t addr, uint8_t val) { } static void psx_write_byte_raw(uint32_t addr, uint8_t val) { g_guest_store_count++; - if (sr_ptr && (*sr_ptr & 0x10000u)) return; + if (cpu_store_isolated()) { isc_store(addr, val, 1); return; } /* KSEG2 guard — see psx_read_word_raw. */ if (addr >= 0xC0000000u) { g_kseg2_ignored_writes++; return; } diff --git a/runtime/src/psx_icache.c b/runtime/src/psx_icache.c index 732e354c..840e508d 100644 --- a/runtime/src/psx_icache.c +++ b/runtime/src/psx_icache.c @@ -108,6 +108,46 @@ void psx_icache_fetch_miss(CPUState* cpu, uint32_t addr) { #endif } +/* IsC store -> I-cache, transcribed from Beetle PS_CPU::WriteMemory + * (mednafen/psx/cpu.cpp:484-501): + * + * if(BIU & BIU_ENABLE_ICACHE_S1) { + * if(BIU & (BIU_TAG_TEST_MODE | BIU_INVALIDATE_MODE | BIU_LOCK_MODE)) { + * valid_bits = (BIU & BIU_TAG_TEST_MODE) ? ((value << ((address & 0x3) * 8)) & 0x0F) : 0x00; + * ICI = &ICache[(address & 0xFF0) >> 2]; + * for(i = 0; i < 4; i++) + * ICI[i].TV = ((valid_bits & (1U << i)) ? 0x00 : 0x02) | (address & 0xFFFFFFF0) | (i << 2); + * } else + * ICache[(address & 0xFFC) >> 2].Data = value << ((address & 0x3) * 8); + * } + * + * The else arm writes an instruction word and leaves TV alone. This model keeps + * no instruction words (compiled code supplies them), so it has nothing to do. + * Beetle does not model SR.SwC (swapped caches), so neither does this. The + * store keeps the full virtual address as the tag, as Beetle's refill does. */ +void psx_icache_isc_store(uint32_t biu, uint32_t addr, uint32_t value) { +#ifdef PSX_ENABLE_BLOCK_CYCLES + /* Same gate as a refill: lockstep replay must not move the shared tags; + * the whole-call shadow evolves its private view. */ + if (g_ls_replay_active && s_icache_shadow_state != 2) return; + if (g_psx_icache_active < 0) g_psx_icache_active = psx_icache_enabled(); + if (!g_psx_icache_active) return; + if (!(biu & PSX_BIU_ICACHE_ENABLE)) return; + if (!(biu & (PSX_BIU_TAG_TEST | PSX_BIU_INVALIDATE | PSX_BIU_LOCK))) return; + uint32_t valid = (biu & PSX_BIU_TAG_TEST) + ? ((value << ((addr & 0x3u) * 8u)) & 0xFu) : 0u; + uint32_t line = addr & 0xFFFFFFF0u; + uint32_t bidx = (addr & 0xFF0u) >> 2; + for (uint32_t i = 0; i < 4u; i++) + g_psx_icache_tv[bidx + i] = + ((valid & (1u << i)) ? 0x0u : 0x2u) | line | (i << 2); +#else + (void)biu; + (void)addr; + (void)value; +#endif +} + void psx_icache_fetch(CPUState* cpu, uint32_t addr) { psx_icache_fetch_miss(cpu, addr); } diff --git a/runtime/tests/psx_ram_runtime_map_stubs.c b/runtime/tests/psx_ram_runtime_map_stubs.c index a3f8bd41..cc4893c6 100644 --- a/runtime/tests/psx_ram_runtime_map_stubs.c +++ b/runtime/tests/psx_ram_runtime_map_stubs.c @@ -26,7 +26,13 @@ void (*g_overlay_flush_pending_cycles)(void); uint32_t g_psx_cyc_batch; uint32_t g_psx_cyc_batch_limit; uint32_t *g_psx_cyc_local_acc; +#ifndef PSX_STUBS_REAL_ICACHE +/* The I-cache model (psx_icache.c). isc_store_test links the real one. */ uint32_t g_psx_icache_tv[1024]; +void psx_icache_isc_store(uint32_t biu, uint32_t addr, uint32_t value) { + (void)biu; (void)addr; (void)value; __builtin_trap(); /* SR.IsC is never set here */ +} +#endif uint64_t psx_cycle_count; uint64_t s_frame_count; uint64_t psx_next_service_cycle; @@ -48,6 +54,7 @@ void audio_trace_event(uint16_t k, uint32_t a, uint32_t b) { (void)k; (void)a; ( void parity_trace_note_write(uint32_t a, uint32_t w, uint32_t pc) { (void)a; (void)w; (void)pc; } uint32_t ls_read_hook(uint32_t a, int s, uint32_t v) { (void)a; (void)s; return v; } void ls_write_hook(uint32_t a, int s, uint32_t v) { (void)a; (void)s; (void)v; } +void ls_shadow_record_unreplayable(void) {} int fntrace_is_game_started(void) { return 0; } void overlay_loader_note_code_write(void) {} void overlay_loader_resync_validation_after_restore(void) {} diff --git a/runtime/tests/test_isc_store.c b/runtime/tests/test_isc_store.c new file mode 100644 index 00000000..f30a2b37 --- /dev/null +++ b/runtime/tests/test_isc_store.c @@ -0,0 +1,217 @@ +/* Cache-isolated (SR.IsC) CPU stores, end to end through the REAL memory.c + * store paths and the REAL psx_icache.c tag model, against Beetle's + * PS_CPU::WriteMemory IsC branch (mednafen/psx/cpu.cpp:482-512): + * + * if(BIU & BIU_ENABLE_ICACHE_S1) { + * if(BIU & (TAG_TEST | INVALIDATE | LOCK)) -> rewrite the line's tag + valid bits + * else -> write an I-cache data word + * } + * if((BIU & 0x081) == 0x080) -> write the scratchpad at addr & 0x3FF + * + * and nothing reaches the bus, the BIU register at 0xFFFE0130 included. + * + * Load-bearing claims (each fails on a memory.c that drops isolated stores): + * 1. The boot FlushCache loop (BIU 0x804, sw zero to every line) invalidates + * a warm line: the next fetch pays a refill again (7 cycles at a line + * start) instead of hitting. Invalidate and lock mode do the same. + * 2. Tag-test mode loads the valid bits from the stored byte lane: a line + * can be marked valid without a refill, and a byte/half store off lane 0 + * loads no valid bits. + * 3. Data mode (BIU 0x800) and a disabled I-cache leave the tags alone. + * 4. With the D-cache on and lock mode off, the store lands in the + * scratchpad at addr & 0x3FF; RAM never changes. + * 5. An isolated store to 0xFFFE0130 does not change the BIU. + * 6. A DMA write is not a CPU store: it reaches RAM while IsC is set and + * leaves the tags alone. + * 7. A non-isolated store never touches the I-cache. + * 8. Lockstep replay does not move the shared tags. */ +#include "cpu_state.h" +#include "psx_icache.h" +#include "psx_memory.h" + +#include +#include +#include + +extern void psx_write_word(uint32_t addr, uint32_t val); +extern void psx_write_half(uint32_t addr, uint16_t val); +extern void psx_write_byte(uint32_t addr, uint8_t val); +extern uint32_t psx_read_word(uint32_t addr); +extern uint8_t *memory_get_ram_ptr(void); +extern uint8_t *memory_get_scratchpad_ptr(void); +extern void memory_set_sr_ptr(const uint32_t *p); +extern int g_dma_exec_depth; +extern uint64_t psx_cycle_count; + +/* BIU values the kernel and the tests use (Beetle cpu.cpp BIU_* bits). */ +#define BIU_FLUSH_TAGS 0x804u /* I-cache on + tag-test (boot / FlushCache) */ +#define BIU_FLUSH_DATA 0x800u /* I-cache on, no mode bit */ +#define BIU_DEFAULT 0x1E988u /* the kernel's run-time value */ +#define SR_ISC 0x10000u + +static uint32_t sr; +static CPUState cpu; +static int failures; + +static void check(int cond, const char *label) { + if (!cond) { + fprintf(stderr, "FAIL: %s\n", label); + failures++; + } +} + +static uint64_t fetch_cost(uint32_t pc) { + uint64_t before = psx_cycle_count; + psx_icache_fetch(&cpu, pc); + return psx_cycle_count - before; +} + +/* Refill the line holding pc and confirm it now hits. */ +static void warm(uint32_t pc) { + (void)fetch_cost(pc & ~0xFu); + check(fetch_cost(pc) == 0u, "warm line hits"); +} + +static void set_biu(uint32_t v) { + sr = 0; + psx_write_word(0xFFFE0130u, v); +} + +static uint32_t ram_word(uint32_t off) { + uint32_t v; + memcpy(&v, memory_get_ram_ptr() + off, sizeof v); + return v; +} + +static void flush_loop(uint32_t biu, uint32_t value) { + set_biu(biu); + sr = SR_ISC; + for (uint32_t a = 0; a < 0x1000u; a += 0x10u) psx_write_word(a, value); + sr = 0; +} + +int main(void) { + psx_ram_apply_size_request(); + memory_set_sr_ptr(&sr); + psx_icache_reset(); + g_psx_icache_active = 1; + + /* 1. FlushCache invalidates; the stores never reach RAM. */ + memcpy(memory_get_ram_ptr() + 0x500u, "\x11\x22\x33\x44", 4); + warm(0x80000500u); + flush_loop(BIU_FLUSH_TAGS, 0u); + check(fetch_cost(0x80000500u) == 7u, "tag-test flush: warm line refills (7)"); + check(fetch_cost(0x80000504u) == 0u, "tag-test flush: refilled line hits"); + check(ram_word(0x500u) == 0x44332211u, "isolated stores leave RAM alone"); + + warm(0x80000510u); + flush_loop(0x802u, 0xFFFFFFFFu); + check(fetch_cost(0x80000510u) == 7u, "invalidate mode: no valid bits, refill"); + warm(0x80000520u); + flush_loop(0x801u, 0xFFFFFFFFu); + check(fetch_cost(0x80000520u) == 7u, "lock mode: no valid bits, refill"); + + /* 2. Tag-test valid bits come from the stored byte lane. */ + psx_icache_reset(); + g_psx_icache_active = 1; + set_biu(BIU_FLUSH_TAGS); + sr = SR_ISC; + psx_write_word(0x80000600u, 0x5u); /* words 0 and 2 valid */ + sr = 0; + check(g_psx_icache_tv[0x600u >> 2] == 0x80000600u, "tag word 0 valid"); + check(g_psx_icache_tv[(0x600u >> 2) + 1] == (0x80000604u | 2u), "tag word 1 invalid"); + check(fetch_cost(0x80000600u) == 0u, "valid bit set by store: hit"); + check(fetch_cost(0x80000608u) == 0u, "valid bit set by store: hit (word 2)"); + check(fetch_cost(0x80000604u) == 6u, "invalid word: refill from word 1 (6)"); + + warm(0x80000700u); + sr = SR_ISC; + psx_write_byte(0x80000701u, 0xFFu); /* (0xFF << 8) & 0xF == 0 */ + sr = 0; + check(fetch_cost(0x80000700u) == 7u, "byte off lane 0: no valid bits"); + warm(0x80000740u); + sr = SR_ISC; + psx_write_half(0x80000742u, 0xFFFFu); /* (0xFFFF << 16) & 0xF == 0 */ + sr = 0; + check(fetch_cost(0x80000740u) == 7u, "half off lane 0: no valid bits"); + sr = SR_ISC; + psx_write_byte(0x80000780u, 0x03u); /* words 0 and 1 valid */ + sr = 0; + check(fetch_cost(0x80000784u) == 0u, "byte lane 0 loads valid bits"); + + /* 3. Data mode and a disabled I-cache leave the tags alone. */ + warm(0x80000800u); + flush_loop(BIU_FLUSH_DATA, 0u); + check(fetch_cost(0x80000800u) == 0u, "data mode keeps the tags"); + /* (The tag itself, not a fetch: fetch costs with the cache disabled are + * the separate BIU bit 11 item, ACCURACY_BURNDOWN axis 4.) */ + warm(0x80000810u); + flush_loop(0x004u, 0u); + check(g_psx_icache_tv[0x810u >> 2] == 0x80000810u, "I-cache disabled keeps the tags"); + + /* 4. D-cache on, lock off: the store lands in the scratchpad. */ + memset(memory_get_scratchpad_ptr(), 0, 1024); + set_biu(BIU_DEFAULT); + sr = SR_ISC; + psx_write_word(0x00000104u, 0xDEADBEEFu); + psx_write_byte(0xA0001203u, 0x7Eu); + psx_write_half(0x1F800012u, 0xBEEFu); + sr = 0; + { + const uint8_t *sp = memory_get_scratchpad_ptr(); + check(memcmp(sp + 0x104u, "\xEF\xBE\xAD\xDE", 4) == 0, "D-cache word -> scratchpad[addr & 0x3FF]"); + check(sp[0x203u] == 0x7Eu, "D-cache byte -> scratchpad[addr & 0x3FF]"); + check(sp[0x012u] == 0xEFu && sp[0x013u] == 0xBEu, "D-cache half -> scratchpad"); + check(ram_word(0x104u) == 0u, "D-cache store leaves RAM alone"); + } + set_biu(BIU_DEFAULT | 0x1u); /* lock mode: no scratchpad write */ + sr = SR_ISC; + psx_write_word(0x00000200u, 0x01020304u); + sr = 0; + check(memory_get_scratchpad_ptr()[0x200u] == 0u, "lock mode keeps the scratchpad"); + set_biu(BIU_FLUSH_TAGS); /* D-cache off */ + sr = SR_ISC; + psx_write_word(0x00000208u, 0x01020304u); + sr = 0; + check(memory_get_scratchpad_ptr()[0x208u] == 0u, "D-cache off keeps the scratchpad"); + + /* 5. An isolated store to the BIU register does not reach it. */ + set_biu(BIU_FLUSH_TAGS); + sr = SR_ISC; + psx_write_word(0xFFFE0130u, 0u); + sr = 0; + check(psx_read_word(0xFFFE0130u) == BIU_FLUSH_TAGS, "isolated store keeps the BIU"); + + /* 6. DMA is not isolated. */ + warm(0x80000900u); + set_biu(BIU_FLUSH_TAGS); + sr = SR_ISC; + g_dma_exec_depth = 1; + psx_write_word(0x80000900u, 0x12345678u); + g_dma_exec_depth = 0; + sr = 0; + check(ram_word(0x900u) == 0x12345678u, "DMA write reaches RAM under IsC"); + check(fetch_cost(0x80000900u) == 0u, "DMA write keeps the tags"); + + /* 7. Without IsC a store is a bus store; the I-cache does not snoop. */ + warm(0x80000A00u); + psx_write_word(0x80000A00u, 0u); + check(ram_word(0xA00u) == 0u && fetch_cost(0x80000A00u) == 0u, + "plain store: RAM written, tags kept"); + + /* 8. Lockstep replay does not move the shared tags. */ + warm(0x80000B00u); + g_ls_replay_active = 1; + sr = SR_ISC; + psx_write_word(0x00000B00u, 0u); + sr = 0; + g_ls_replay_active = 0; + check(fetch_cost(0x80000B00u) == 0u, "lockstep replay keeps the tags"); + + if (failures) { + fprintf(stderr, "isc_store_test: %d failure(s)\n", failures); + return 1; + } + puts("PASS: isolated stores follow Beetle's WriteMemory IsC branch"); + return 0; +} From 6de75122a3ce8300871217aeacb1c7e03010a295 Mon Sep 17 00:00:00 2001 From: Shokunin <30949000+tetrisgm@users.noreply.github.com> Date: Tue, 29 Sep 2026 17:33:35 -0700 Subject: [PATCH 2/3] docs: record the Beetle gate for IsC stores and tick the burndown item MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FAITHFUL_TIMING_PLAN §5 (2026-09-29): with #429 on master, the LLE boot now matches Beetle at every anchor to the shell on OpenBIOS (was -126) and sits at -9 on SCPH-1001 (was -454), the KSEG1 kernel entry PR D closes. Also measured without #429, the rulers (unchanged), R4's game entry (+342 default boot, +18 LLE boot, 0 dispatch misses) and the unchanged codegen hash. ACCURACY_BURNDOWN axis 4: IsC item ticked; the SWL/SWR corner cases are a new open item; the P2 scratchpad-under-IsC note points at Beetle's rule. Co-Authored-By: Claude Opus 5.5 --- docs/internal/ACCURACY_BURNDOWN.md | 33 +++++++++---- docs/internal/FAITHFUL_TIMING_PLAN.md | 67 +++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 10 deletions(-) diff --git a/docs/internal/ACCURACY_BURNDOWN.md b/docs/internal/ACCURACY_BURNDOWN.md index 91ee01fc..d1c7e93c 100644 --- a/docs/internal/ACCURACY_BURNDOWN.md +++ b/docs/internal/ACCURACY_BURNDOWN.md @@ -178,15 +178,26 @@ Status: PARTIAL. Status: MODERATE-STRONG (regions games use). - [ ] KUSEG/KSEG0/KSEG1 mirroring, scratchpad, cache-isolation (IsC) — psx-spx. -- [ ] IsC stores do not reach the I-cache model. memory.c drops every store made while - SR.IsC is set. Beetle's WriteMemory rewrites the tag and valid bits of the addressed - line when the I-cache is enabled and BIU has a tag-test, invalidate or lock mode bit - set; that is how FlushCache (A 44h) invalidates the cache. Natively the cached - kernel handlers keep hitting after a flush where Beetle refills them. Measured in the - axis-2 land gate: −42 cycles after each of three OpenBIOS flushes and −445 after four - SCPH-1001 flushes, all before the shell. A local prototype of Beetle's write (a few lines in the three - `psx_write_*_raw` IsC branches) makes both LLE boots match Beetle at every anchor to - the shell, apart from SCPH-1001's −9 KSEG1 kernel entry. +- [x] **IsC stores reach the caches, as in Beetle (2026-09-29).** memory.c dropped + every store made while SR.IsC was set. Beetle's WriteMemory + (mednafen/psx/cpu.cpp:482-512) handles them before any address decode. With the + I-cache on and a tag-test, invalidate or lock mode bit in BIU, the store rewrites + its line's tag and valid bits; FlushCache (A 44h) invalidates the cache this way. + With the D-cache on and lock mode off, the store lands in the scratchpad at + addr & 0x3FF. Nothing reaches the bus, not even the BIU register. Now + `psx_icache_isc_store` plus memory.c's `isc_store`, at the top of the three + `psx_write_*_raw` chokepoints that every backend stores through. DMA is not isolated. + Beetle land gate (LLE boot to the shell, both BIOS images): OpenBIOS 0 at every + anchor (was −126 from the first flushes on); SCPH-1001 −9 at every anchor from the + kernel entry on (was −454), which is the KSEG1 entry of SEGMENT_AWARE_CODE.md §3.3. + Rulers #1 and #2 are unchanged. Covered by ctest `isc_store_test`. Details: + FAITHFUL_TIMING_PLAN §5, 2026-09-29. +- [ ] IsC SWL/SWR: the emitters and both interpreters run SWL/SWR as a + read-modify-write, so an isolated one reaches `isc_store` as one aligned word. + Beetle's WriteMemory gets the partial width at the unaligned address. The results + differ in two cases. In tag-test mode, an SWR at addr & 3 ≠ 0 sets no valid bits in + Beetle; natively it takes lane 0 of the RAM word. With the D-cache on, native + also rewrites the scratchpad bytes the store leaves alone. No known code does either. - [ ] BIU bit 11 (I-cache disable, 0xFFFE0130) does not reach the fetch model: memory.c stores it, psx_icache.c and the interp ignore it. Beetle charges +4 per fetch while the cache is disabled (CPU_SetBIU). This matters only for RAM code run @@ -302,7 +313,9 @@ overlap the cycle axis or each other (different files), so they parallelize. - **`accuracy/axis4_memory_mmio.md`.** GOOD: I_STAT/I_MASK/DICR ack semantics CORRECT. P1: RAM mirror wrong (we gate `<2MB`; HW aliases 2MB DRAM across an 8MB window 4× — mirror accesses silently read 0 / drop writes). P2: `IsC` over-broadly drops scratchpad - writes (scratchpad is the D-cache, must stay addressable). P3: level-IRQ relatch; P4: + writes (scratchpad is the D-cache, must stay addressable). (P2 resolved 2026-09-29 to + Beetle's rule: an isolated store reaches the scratchpad only while BIU has the + D-cache on and lock mode off, at addr & 0x3FF; see axis 4.) P3: level-IRQ relatch; P4: per-segment addr masking; P5: open-bus high bits on I_STAT readback. Validate: MMIO trace diff + RAM-mirror sentinel probe. diff --git a/docs/internal/FAITHFUL_TIMING_PLAN.md b/docs/internal/FAITHFUL_TIMING_PLAN.md index 81b392d1..e6adc3ee 100644 --- a/docs/internal/FAITHFUL_TIMING_PLAN.md +++ b/docs/internal/FAITHFUL_TIMING_PLAN.md @@ -213,6 +213,73 @@ on a fixed region -> next. ## 5. Status / Log (update every session) +- **2026-09-29 (cache-isolated stores reach the caches, as in Beetle):** + memory.c dropped every store made while SR.IsC was set, so the I-cache model + never saw FlushCache (A 44h) or the boot cache init. Beetle's + PS_CPU::WriteMemory (mednafen/psx/cpu.cpp:482-512) handles an isolated store + before any address decode: + - With the I-cache on (BIU bit 11) and a tag-test, invalidate or lock mode bit, + the store rewrites the tag and valid bits of its line's four words. In + tag-test mode the valid bits come from the stored byte lane; otherwise none + are set. + - With the I-cache on and no mode bit, Beetle writes an instruction word. The + tag model holds no words, so nothing changes. + - With the D-cache on and lock mode off ((BIU & 0x81) == 0x80), the store + lands in the scratchpad at addr & 0x3FF, whatever the address. + - Nothing reaches the bus, the BIU register at 0xFFFE0130 included: Beetle + routes it through MemRW, which only non-isolated stores reach. Beetle does + not model SR.SwC. + - DMA is not a CPU store. Beetle's DMA writes RAM directly, so native DMA now + bypasses IsC instead of being dropped. + - Implementation: `psx_icache_isc_store` (psx_icache.c) is the tag write, and + memory.c's `isc_store` runs at the top of the three `psx_write_*_raw` + chokepoints. Static code, overlay shards and both interpreters all store + through those chokepoints. A render pass applies the same effect, and its + checkpoint restores tags and scratchpad afterwards. The overlay shadow + record (PSX_OVERLAY_DIFF) fails closed on an isolated store, because its + replay cannot redo it. Lockstep replay leaves the shared tags alone, as it + does for refills. + - LAND GATE (live psx-beetle, same images and method as the #429 gate below). + The LLE boot to the shell was run on four native builds. "master" is + 1ec24e9d (#429 merged), "pre" is 9a8737d5 (master just before #429), and + each was also run with this fix. Deltas are native − Beetle: + - OpenBIOS, 15 anchors from `_boot` to shell main: master is 0 up to the + first flushes and −126 from initEvents on; master + fix is 0 at all 15. + pre goes from −2,165,591 to −2,165,478 at the shell entry with the fix. + The remaining gap is #429's. + - SCPH-1001, 14 anchors from cache init to the shell entry: master is −9 + from kernel init 0x598 and −438/−454 from the boot functions on. master + + fix is −9 at every anchor from 0x598 on. pre goes from −7,049,462 to + −7,049,017 at the shell entry. + - The −9 is the retail kernel entry through its KSEG1 alias + (SEGMENT_AWARE_CODE.md §3.3, PR D). It is the only residual left before + the shell. + - Every hit, counted separately: FlushCache (5 hits on each BIOS), the A0 + gate (12 OpenBIOS, 17 SCPH-1001) and the B0 gate (147 SCPH-1001). On + master the gap grew after each flush: −42 per OpenBIOS flush, and −52, + −149, −149, −95 on SCPH-1001. With the fix it stays at 0 (OpenBIOS) + and −9 (SCPH-1001) on every hit. + - Ruler #1 [0x80001C5C→0x80001CA4], SCPH-1001, 64 passes: all four builds + equal Beetle pass for pass (53 × 56, 9 × 77, 2 × 84). + - Ruler #2 (15-loop ROM, OpenBIOS): all four builds equal Beetle on all 14 + components, on an HLE boot and on an LLE boot. + - R4 (SLUS-00797, OpenBIOS): with the default boot (recompiled LLE kernel, + HLE boot-skip), the game entry 0x8007D8F4 moves from 100,589,404 to + 100,589,746 (+342). With the LLE boot, it moves from 268,764,292 to + 268,764,310 (+18); the CD-ROM waits absorb most of it. Two boots of each + build gave the same numbers. R4 reached a race with 0 dispatch misses, and + R4's 10 developer tests pass. + - Every title changes timing, toward Beetle: each boot-time and in-game cache + flush now costs the refills Beetle charges. The change is runtime-only. The + codegen hash is unchanged (04ed5e51), generated BIOS and game C are + byte-identical, and overlay caches stay valid. Savestates and the netplay + digest already carry the tags, so their formats do not change. + - Test: ctest `isc_store_test` drives the real memory.c and psx_icache.c + stores. It covers flush invalidation, tag-test valid bits, data mode, the + scratchpad write, the BIU and the DMA exclusions, and lockstep replay. + 15 of its checks fail on master. + - Left: IsC SWL/SWR differ from Beetle in two corner cases (ACCURACY_BURNDOWN + axis 4). The BIU bit 11 fetch cost is still open. - **2026-09-29 (uncached KSEG1 fetch charged per instruction, both emitters):** Beetle ReadInstruction never fills a line for a fetch at 0xA0000000 or above: each one costs +4 and clears the load give-back. The interp fetches at every PC, From 825a89a9c5e371e5aa789698e8a157c270a3be73 Mon Sep 17 00:00:00 2001 From: Shokunin <30949000+tetrisgm@users.noreply.github.com> Date: Tue, 29 Sep 2026 17:55:26 -0700 Subject: [PATCH 3/3] runtime: keep host stores out of IsC, as DMA is Only CPU stores are cache-isolated. Mods, the FMV skip, debug pokes, the bg2d ring fill and the exception sentinel write guest memory from the host; they now go through psx_host_write_*, which bypasses isc_store the way DMA does, so a host store inside a FlushCache window reaches memory instead of rewriting an I-cache tag. The render-pass checkpoint restores the new depth counter like the DMA depth. isc_store_test covers it (a lane-0 host store in tag-test mode would otherwise mark a cold line valid). Docs: the SCPH-1001 residual starts at kernel init 0x598, and without #429 the fix removes 113 of the 126 OpenBIOS flush cycles. Co-Authored-By: Claude Opus 5.5 --- docs/internal/ACCURACY_BURNDOWN.md | 8 ++++-- docs/internal/FAITHFUL_TIMING_PLAN.md | 14 ++++++---- runtime/include/psx_memory.h | 8 ++++++ runtime/src/debug_server.c | 4 +-- runtime/src/gpu.c | 4 +-- runtime/src/interrupts.c | 2 +- runtime/src/main.cpp | 5 ++-- runtime/src/memory.c | 28 +++++++++++++++++-- runtime/src/mod_runtime.cpp | 14 +++++----- runtime/src/render_pass.c | 8 +++++- .../test_gpu_textured_dot_nw_shift_exec.c | 4 +++ runtime/tests/test_isc_store.c | 19 ++++++++++++- runtime/tests/test_mod_runtime.cpp | 13 +++++++++ runtime/tests/test_render_pass_abort.c | 7 ++++- runtime/tests/test_render_pass_sandbox.c | 1 + 15 files changed, 112 insertions(+), 27 deletions(-) diff --git a/docs/internal/ACCURACY_BURNDOWN.md b/docs/internal/ACCURACY_BURNDOWN.md index d1c7e93c..b3e9a206 100644 --- a/docs/internal/ACCURACY_BURNDOWN.md +++ b/docs/internal/ACCURACY_BURNDOWN.md @@ -186,10 +186,12 @@ Status: MODERATE-STRONG (regions games use). With the D-cache on and lock mode off, the store lands in the scratchpad at addr & 0x3FF. Nothing reaches the bus, not even the BIU register. Now `psx_icache_isc_store` plus memory.c's `isc_store`, at the top of the three - `psx_write_*_raw` chokepoints that every backend stores through. DMA is not isolated. + `psx_write_*_raw` chokepoints that every backend stores through. DMA and host + stores (`psx_host_write_*`: mods, FMV skip, debug pokes) are not isolated. Beetle land gate (LLE boot to the shell, both BIOS images): OpenBIOS 0 at every - anchor (was −126 from the first flushes on); SCPH-1001 −9 at every anchor from the - kernel entry on (was −454), which is the KSEG1 entry of SEGMENT_AWARE_CODE.md §3.3. + anchor (was −126 from the first flushes on); SCPH-1001 −9 at every anchor from + kernel init 0x598 on (was −454), which is the KSEG1 entry of SEGMENT_AWARE_CODE.md + §3.3. Rulers #1 and #2 are unchanged. Covered by ctest `isc_store_test`. Details: FAITHFUL_TIMING_PLAN §5, 2026-09-29. - [ ] IsC SWL/SWR: the emitters and both interpreters run SWL/SWR as a diff --git a/docs/internal/FAITHFUL_TIMING_PLAN.md b/docs/internal/FAITHFUL_TIMING_PLAN.md index e6adc3ee..5957e876 100644 --- a/docs/internal/FAITHFUL_TIMING_PLAN.md +++ b/docs/internal/FAITHFUL_TIMING_PLAN.md @@ -230,7 +230,9 @@ on a fixed region -> next. routes it through MemRW, which only non-isolated stores reach. Beetle does not model SR.SwC. - DMA is not a CPU store. Beetle's DMA writes RAM directly, so native DMA now - bypasses IsC instead of being dropped. + bypasses IsC instead of being dropped. Host stores (mods, FMV skip, debug + pokes, enhancement fills) are not CPU stores either: they go through + `psx_host_write_*`, which bypasses IsC the same way. - Implementation: `psx_icache_isc_store` (psx_icache.c) is the tag write, and memory.c's `isc_store` runs at the top of the three `psx_write_*_raw` chokepoints. Static code, overlay shards and both interpreters all store @@ -245,12 +247,14 @@ on a fixed region -> next. each was also run with this fix. Deltas are native − Beetle: - OpenBIOS, 15 anchors from `_boot` to shell main: master is 0 up to the first flushes and −126 from initEvents on; master + fix is 0 at all 15. - pre goes from −2,165,591 to −2,165,478 at the shell entry with the fix. + pre goes from −2,165,591 to −2,165,478 at the shell entry with the fix: + 113 of master's 126 flush cycles (36, 40 and 37 per flush, against 42). The remaining gap is #429's. - SCPH-1001, 14 anchors from cache init to the shell entry: master is −9 from kernel init 0x598 and −438/−454 from the boot functions on. master + fix is −9 at every anchor from 0x598 on. pre goes from −7,049,462 to - −7,049,017 at the shell entry. + −7,049,017 at the shell entry, the same 445 cycles the fix removes on + master. - The −9 is the retail kernel entry through its KSEG1 alias (SEGMENT_AWARE_CODE.md §3.3, PR D). It is the only residual left before the shell. @@ -276,8 +280,8 @@ on a fixed region -> next. digest already carry the tags, so their formats do not change. - Test: ctest `isc_store_test` drives the real memory.c and psx_icache.c stores. It covers flush invalidation, tag-test valid bits, data mode, the - scratchpad write, the BIU and the DMA exclusions, and lockstep replay. - 15 of its checks fail on master. + scratchpad write, the BIU, DMA and host-store exclusions, and lockstep + replay. 17 of its checks fail on master. - Left: IsC SWL/SWR differ from Beetle in two corner cases (ACCURACY_BURNDOWN axis 4). The BIU bit 11 fetch cost is still open. - **2026-09-29 (uncached KSEG1 fetch charged per instruction, both emitters):** diff --git a/runtime/include/psx_memory.h b/runtime/include/psx_memory.h index ec47a6cf..07c9a7ae 100644 --- a/runtime/include/psx_memory.h +++ b/runtime/include/psx_memory.h @@ -86,6 +86,14 @@ static inline int psx_ram_resolve(uint32_t address, uint32_t width, return 1; } +/* Stores made by the host rather than the guest CPU (mods, FMV skip, debug + * pokes, enhancement fills). They take the psx_write_* path but are never + * cache-isolated: while SR.IsC is set they still reach memory, as DMA does, + * instead of acting on the caches as a CPU store would (memory.c isc_store). */ +void psx_host_write_word(uint32_t addr, uint32_t val); +void psx_host_write_half(uint32_t addr, uint16_t val); +void psx_host_write_byte(uint32_t addr, uint8_t val); + #ifdef __cplusplus } #endif diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index b8ff2ff7..e1615ad2 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -2666,7 +2666,7 @@ void debug_server_cyc_observe(uint32_t block_leader_phys) { if (a2) { uint32_t fl = psx_read_word(a2 + 184u); if (fl & 0x80u) - psx_write_word(a2 + 184u, fl & ~0x80u); + psx_host_write_word(a2 + 184u, fl & ~0x80u); } } } @@ -5231,7 +5231,7 @@ static void handle_write_ram(int id, const char *json) } uint32_t addr = hex_to_u32(addr_str); uint8_t val = (uint8_t)hex_to_u32(val_str); - psx_write_byte(addr, val); + psx_host_write_byte(addr, val); send_ok(id); } diff --git a/runtime/src/gpu.c b/runtime/src/gpu.c index b2857352..6bc11762 100644 --- a/runtime/src/gpu.c +++ b/runtime/src/gpu.c @@ -1339,7 +1339,7 @@ static void bg2d_clear_column(int layer, int ringcol, int worldY) { uint32_t cell = ringbase + (uint32_t)((ringcol & (int)(g_bg2d_ring_cols - 1u)) * 2) + (uint32_t)((ringrow & 0x1f) * (int)(g_bg2d_ring_cols * 2u)); - psx_write_half(cell, 0); + psx_host_write_half(cell, 0); ringrow = (ringrow + 1) & 0x1f; } } @@ -1420,7 +1420,7 @@ static int bg2d_fill_column(int layer, int worldX, int worldY, int scrollX, int uint16_t tile = psx_read_half(metaBase + (uint32_t)metaIdx * 0x200u + (uint32_t)(trowInMeta * 0x20) + (uint32_t)(tcolInMeta * 2)); if (write) { - psx_write_half(cell, tile); + psx_host_write_half(cell, tile); } else if (cmp_total) { (*cmp_total)++; if (psx_read_half(cell) != tile && cmp_bad) (*cmp_bad)++; diff --git a/runtime/src/interrupts.c b/runtime/src/interrupts.c index 43b7525e..0d5e9170 100644 --- a/runtime/src/interrupts.c +++ b/runtime/src/interrupts.c @@ -1634,7 +1634,7 @@ irq_deliver_eval: g_exc_escape_reason = PSX_EXC_ESCAPE_NONE; /* set at the actual RFE/SYSCALL return */ } else { uint32_t sentinel = PSX_EXC_SENTINEL_PC; - cpu->write_word(sentinel, 0x00000000u); /* NOP, read by the handler's BD check */ + psx_host_write_word(sentinel, 0x00000000u); /* NOP, read by the handler's BD check */ cpu->cop0[COP0_EPC] = sentinel; g_exception_real_epc = sentinel; g_exc_escape_reason = PSX_EXC_ESCAPE_LEGACY_SENTINEL; diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 6dc2e998..fc7ae669 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -363,6 +363,7 @@ extern "C" uint16_t psx_read_half(uint32_t addr); extern "C" void psx_write_half(uint32_t addr, uint16_t val); extern "C" uint8_t psx_read_byte(uint32_t addr); extern "C" void psx_write_byte(uint32_t addr, uint8_t val); +extern "C" void psx_host_write_half(uint32_t addr, uint16_t val); /* Guest-side data-read wrappers: same as psx_read_* but charge PS1 main-RAM * read wait states (R3000A has no D-cache). Wired to cpu->read_* below so the * timing applies to recompiled + interpreted guest loads, not debug/device reads. */ @@ -7168,8 +7169,8 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) { if (g_fmv_skip_total_table) { /* End the active movie via its own frame-count teardown. */ uint8_t mid = psx_read_byte(g_fmv_skip_movie_id); - psx_write_half(g_fmv_skip_total_table + (uint32_t)mid * 2u, - (uint16_t)g_fmv_skip_end_total); + psx_host_write_half(g_fmv_skip_total_table + (uint32_t)mid * 2u, + (uint16_t)g_fmv_skip_end_total); } else { /* Generic fallback: hold START (PSX pad word is active-low; START * is bit 3) so the game's FMV handler aborts the movie itself. */ diff --git a/runtime/src/memory.c b/runtime/src/memory.c index 280b1f9b..1bac49e0 100644 --- a/runtime/src/memory.c +++ b/runtime/src/memory.c @@ -1732,9 +1732,14 @@ uint64_t g_render_pass_dropped_writes[RENDER_PASS_DROP_CLASSES]; * Nothing else happens: an isolated store to RAM, MMIO or the BIU register * itself is dropped (Beetle routes 0xFFFE0130 through MemRW, libretro.cpp:1053, * which only non-isolated stores reach). DMA is not a CPU store; Beetle's DMA - * writes RAM directly, so IsC does not apply while a transfer moves data. */ + * writes RAM directly, so IsC does not apply while a transfer moves data. + * Neither is a host store (psx_host_write_*: mods, FMV skip, debug pokes, + * enhancement fills); like DMA it reaches memory whatever SR says. */ +int g_host_store_depth; /* >0 inside psx_host_write_* */ + static inline int cpu_store_isolated(void) { - return sr_ptr && (*sr_ptr & 0x10000u) && g_dma_exec_depth == 0; + return sr_ptr && (*sr_ptr & 0x10000u) && g_dma_exec_depth == 0 && + g_host_store_depth == 0; } static void isc_store(uint32_t addr, uint32_t val, uint32_t width) { @@ -2335,6 +2340,25 @@ void psx_write_byte(uint32_t addr, uint8_t val) { psx_write_byte_raw(addr, val); s_ls_op_active = 0; } + +/* Host stores: the same paths as a guest store, but never cache-isolated + * (cpu_store_isolated). */ +void psx_host_write_word(uint32_t addr, uint32_t val) { + g_host_store_depth++; + psx_write_word(addr, val); + g_host_store_depth--; +} +void psx_host_write_half(uint32_t addr, uint16_t val) { + g_host_store_depth++; + psx_write_half(addr, val); + g_host_store_depth--; +} +void psx_host_write_byte(uint32_t addr, uint8_t val) { + g_host_store_depth++; + psx_write_byte(addr, val); + g_host_store_depth--; +} + static void psx_write_byte_raw(uint32_t addr, uint8_t val) { g_guest_store_count++; if (cpu_store_isolated()) { isc_store(addr, val, 1); return; } diff --git a/runtime/src/mod_runtime.cpp b/runtime/src/mod_runtime.cpp index d5600377..564f0b25 100644 --- a/runtime/src/mod_runtime.cpp +++ b/runtime/src/mod_runtime.cpp @@ -208,8 +208,8 @@ void set_error(const std::string& error) { void apply_main_write(const ModResolution::Write& write) { if (write.fields.empty()) { for (size_t i = 0; i < write.replacement.size(); ++i) - psx_write_byte((uint32_t)write.location + (uint32_t)i, - write.replacement[i]); + psx_host_write_byte((uint32_t)write.location + (uint32_t)i, + write.replacement[i]); dirty_ram_mark_executable_range( (uint32_t)write.location & 0x1FFFFFFFu, (uint32_t)write.replacement.size()); @@ -217,7 +217,7 @@ void apply_main_write(const ModResolution::Write& write) { } for (const ModResolution::Write::Field& field : write.fields) { for (size_t i = 0; i < field.replacement.size(); ++i) - psx_write_byte( + psx_host_write_byte( (uint32_t)write.location + (uint32_t)field.offset + (uint32_t)i, field.replacement[i]); @@ -1485,7 +1485,7 @@ extern "C" uint8_t psx_mod_read_byte(uint32_t address) { } extern "C" void psx_mod_write_byte(uint32_t address, uint8_t value) { - psx_write_byte(address, value); + psx_host_write_byte(address, value); } extern "C" uint16_t psx_mod_read_half(uint32_t address) { @@ -1493,7 +1493,7 @@ extern "C" uint16_t psx_mod_read_half(uint32_t address) { } extern "C" void psx_mod_write_half(uint32_t address, uint16_t value) { - psx_write_half(address, value); + psx_host_write_half(address, value); } extern "C" uint32_t psx_mod_read_word(uint32_t address) { @@ -1501,11 +1501,11 @@ extern "C" uint32_t psx_mod_read_word(uint32_t address) { } extern "C" void psx_mod_write_word(uint32_t address, uint32_t value) { - psx_write_word(address, value); + psx_host_write_word(address, value); } extern "C" void psx_mod_write_code_word(uint32_t address, uint32_t value) { - psx_write_word(address, value); + psx_host_write_word(address, value); dirty_ram_mark_executable_range(address & 0x1FFFFFFFu, 4u); } diff --git a/runtime/src/render_pass.c b/runtime/src/render_pass.c index 891f0d2c..6f849051 100644 --- a/runtime/src/render_pass.c +++ b/runtime/src/render_pass.c @@ -64,6 +64,7 @@ extern void (*g_overlay_flush_pending_cycles)(void); /* Host nesting that runtime frames set on entry and put back on exit. */ extern int g_call_unit_depth; /* overlay_loader_call_native */ extern int g_dma_exec_depth; /* dma.c kick / async writes */ +extern int g_host_store_depth; /* memory.c psx_host_write_* */ extern int g_dma_cur_ch; extern uint32_t g_dma_cur_madr; extern uint32_t g_dma_cur_bcr; @@ -99,7 +100,7 @@ extern void mdec_snapshot_write(uint8_t *p); * pass that returns normally all of it is already balanced. */ typedef struct RenderPassNesting { int call_unit_depth; - int dma_exec_depth, dma_cur_ch; + int dma_exec_depth, dma_cur_ch, host_store_depth; uint32_t dma_cur_madr, dma_cur_bcr, dma_initiator_pc; int dirty_interp_active, exec_phase, precise_mode; uint32_t dirty_safe_resume_pc; @@ -112,6 +113,7 @@ typedef struct RenderPassNesting { static void nesting_save(RenderPassNesting *n) { n->call_unit_depth = g_call_unit_depth; n->dma_exec_depth = g_dma_exec_depth; + n->host_store_depth = g_host_store_depth; n->dma_cur_ch = g_dma_cur_ch; n->dma_cur_madr = g_dma_cur_madr; n->dma_cur_bcr = g_dma_cur_bcr; @@ -128,6 +130,7 @@ static void nesting_save(RenderPassNesting *n) { static void nesting_restore(const RenderPassNesting *n) { g_call_unit_depth = n->call_unit_depth; g_dma_exec_depth = n->dma_exec_depth; + g_host_store_depth = n->host_store_depth; g_dma_cur_ch = n->dma_cur_ch; g_dma_cur_madr = n->dma_cur_madr; g_dma_cur_bcr = n->dma_cur_bcr; @@ -150,6 +153,7 @@ static int nesting_balanced(const RenderPassNesting *n) { nesting_save(&now); return now.call_unit_depth == n->call_unit_depth && now.dma_exec_depth == n->dma_exec_depth && + now.host_store_depth == n->host_store_depth && now.dirty_interp_active == n->dirty_interp_active && now.exec_phase == n->exec_phase && now.precise_mode == n->precise_mode && @@ -182,6 +186,8 @@ static int nesting_describe(const RenderPassNesting *n, int bb_defer_ck, RP_NOTE(now.ov_flush != n->ov_flush, "shard cycle hook"); RP_NOTE(now.dma_exec_depth != n->dma_exec_depth, "DMA depth %+d", now.dma_exec_depth - n->dma_exec_depth); + RP_NOTE(now.host_store_depth != n->host_store_depth, "host store depth %+d", + now.host_store_depth - n->host_store_depth); RP_NOTE(now.dirty_interp_active != n->dirty_interp_active, "interpreter flag"); RP_NOTE(now.precise_mode != n->precise_mode, "precise flag"); RP_NOTE(now.exec_phase != n->exec_phase, "exec phase %d->%d", diff --git a/runtime/tests/test_gpu_textured_dot_nw_shift_exec.c b/runtime/tests/test_gpu_textured_dot_nw_shift_exec.c index 4986df9f..30a4cade 100644 --- a/runtime/tests/test_gpu_textured_dot_nw_shift_exec.c +++ b/runtime/tests/test_gpu_textured_dot_nw_shift_exec.c @@ -146,6 +146,10 @@ void psx_write_half(uint32_t addr, uint16_t val) { (void)val; } +void psx_host_write_half(uint32_t addr, uint16_t val) { + psx_write_half(addr, val); +} + int mdec_recently_active(uint32_t within_frames) { (void)within_frames; return 0; diff --git a/runtime/tests/test_isc_store.c b/runtime/tests/test_isc_store.c index f30a2b37..8254099a 100644 --- a/runtime/tests/test_isc_store.c +++ b/runtime/tests/test_isc_store.c @@ -22,7 +22,8 @@ * scratchpad at addr & 0x3FF; RAM never changes. * 5. An isolated store to 0xFFFE0130 does not change the BIU. * 6. A DMA write is not a CPU store: it reaches RAM while IsC is set and - * leaves the tags alone. + * leaves the tags alone. Neither is a host store (psx_host_write_*: mods, + * FMV skip, debug pokes), even one that would load valid bits. * 7. A non-isolated store never touches the I-cache. * 8. Lockstep replay does not move the shared tags. */ #include "cpu_state.h" @@ -36,6 +37,7 @@ extern void psx_write_word(uint32_t addr, uint32_t val); extern void psx_write_half(uint32_t addr, uint16_t val); extern void psx_write_byte(uint32_t addr, uint8_t val); +extern int g_host_store_depth; extern uint32_t psx_read_word(uint32_t addr); extern uint8_t *memory_get_ram_ptr(void); extern uint8_t *memory_get_scratchpad_ptr(void); @@ -193,6 +195,21 @@ int main(void) { check(ram_word(0x900u) == 0x12345678u, "DMA write reaches RAM under IsC"); check(fetch_cost(0x80000900u) == 0u, "DMA write keeps the tags"); + /* ... nor is a host store. Tag-test mode with lane-0 value 0xF would mark + * a cold line valid if the store were taken as isolated. */ + psx_icache_reset(); + g_psx_icache_active = 1; + set_biu(BIU_FLUSH_TAGS); + sr = SR_ISC; + psx_host_write_half(0x80000944u, 0xBEEFu); + psx_host_write_byte(0x80000946u, 0x5Au); + psx_host_write_word(0x80000940u, 0x0000000Fu); /* last: its tags stand */ + sr = 0; + check(ram_word(0x940u) == 0x0000000Fu, "host word reaches RAM under IsC"); + check(ram_word(0x944u) == 0x005ABEEFu, "host half/byte reach RAM under IsC"); + check(fetch_cost(0x80000940u) == 7u, "host store loads no valid bits"); + check(g_host_store_depth == 0, "host store depth unwinds"); + /* 7. Without IsC a store is a bus store; the I-cache does not snoop. */ warm(0x80000A00u); psx_write_word(0x80000A00u, 0u); diff --git a/runtime/tests/test_mod_runtime.cpp b/runtime/tests/test_mod_runtime.cpp index 49791598..d60d201d 100644 --- a/runtime/tests/test_mod_runtime.cpp +++ b/runtime/tests/test_mod_runtime.cpp @@ -58,6 +58,19 @@ extern "C" void psx_write_word(uint32_t address, uint32_t value) { ram[offset + 3] = (uint8_t)(value >> 24); } +/* Mod writes are host stores (memory.c psx_host_write_*). */ +extern "C" void psx_host_write_byte(uint32_t address, uint8_t value) { + psx_write_byte(address, value); +} + +extern "C" void psx_host_write_half(uint32_t address, uint16_t value) { + psx_write_half(address, value); +} + +extern "C" void psx_host_write_word(uint32_t address, uint32_t value) { + psx_write_word(address, value); +} + extern "C" uint32_t psx_mod_memory_alloc(uint32_t, uint32_t) { return 0; } extern "C" uint32_t psx_mod_gpu_dma_memory_alloc(uint32_t, uint32_t) { return 0; diff --git a/runtime/tests/test_render_pass_abort.c b/runtime/tests/test_render_pass_abort.c index f9c57435..55668e07 100644 --- a/runtime/tests/test_render_pass_abort.c +++ b/runtime/tests/test_render_pass_abort.c @@ -42,6 +42,7 @@ uint64_t g_render_pass_dropped_writes[RENDER_PASS_DROP_CLASSES]; void (*g_overlay_flush_pending_cycles)(void) = NULL; int g_call_unit_depth = 0; int g_dma_exec_depth = 0; +int g_host_store_depth = 0; int g_dma_cur_ch = -1; uint32_t g_dma_cur_madr = 0, g_dma_cur_bcr = 0, g_dma_initiator_pc = 0; int g_dirty_interp_active = 0; @@ -201,6 +202,7 @@ static void guest_frame(CPUState *cpu, const Frames *f, int level) { g_overlay_flush_pending_cycles = shard_flush; g_exec_phase = 2; g_dma_exec_depth++; /* dma.c kick */ + g_host_store_depth++; /* psx_host_write_* */ g_dma_cur_ch = 2; g_dirty_interp_active = 1; /* dirty_ram_dispatch */ g_precise_mode = 1; /* psx_run_precise */ @@ -224,6 +226,7 @@ static void guest_frame(CPUState *cpu, const Frames *f, int level) { g_precise_mode = prev_precise; g_dirty_interp_active = prev_active; g_dma_exec_depth--; + g_host_store_depth--; g_dma_cur_ch = -1; g_exec_phase = prev_phase; shard_flush(); @@ -246,7 +249,7 @@ static int leaky_pass_fn(struct CPUState *cpu, void *user, uint32_t alpha_q16) { } typedef struct Live { - int bb_defer, unit, dma, dma_ch, active, phase, precise, dispatch; + int bb_defer, unit, dma, host, dma_ch, active, phase, precise, dispatch; int ov_depth; uint32_t ov_ip, batch, resume; void (*flush)(void); @@ -259,6 +262,7 @@ static void snap(Live *l) { l->bb_defer = g_psx_cyc_bb_defer; l->unit = g_call_unit_depth; l->dma = g_dma_exec_depth; + l->host = g_host_store_depth; l->dma_ch = g_dma_cur_ch; l->active = g_dirty_interp_active; l->phase = g_exec_phase; @@ -285,6 +289,7 @@ static void check_live(const Live *a, const char *when) { SAME(bb_defer, "g_psx_cyc_bb_defer"); SAME(unit, "g_call_unit_depth"); SAME(dma, "g_dma_exec_depth"); + SAME(host, "g_host_store_depth"); SAME(dma_ch, "g_dma_cur_ch"); SAME(active, "g_dirty_interp_active"); SAME(phase, "g_exec_phase"); diff --git a/runtime/tests/test_render_pass_sandbox.c b/runtime/tests/test_render_pass_sandbox.c index 16a3a057..e5f80bba 100644 --- a/runtime/tests/test_render_pass_sandbox.c +++ b/runtime/tests/test_render_pass_sandbox.c @@ -56,6 +56,7 @@ uint64_t g_render_pass_dropped_writes[RENDER_PASS_DROP_CLASSES]; void (*g_overlay_flush_pending_cycles)(void) = NULL; int g_call_unit_depth = 0; int g_dma_exec_depth = 0; +int g_host_store_depth = 0; int g_dma_cur_ch = -1; uint32_t g_dma_cur_madr = 0, g_dma_cur_bcr = 0, g_dma_initiator_pc = 0; int g_dirty_interp_active = 0;