diff --git a/docs/internal/ACCURACY_BURNDOWN.md b/docs/internal/ACCURACY_BURNDOWN.md index 91ee01fc..b3e9a206 100644 --- a/docs/internal/ACCURACY_BURNDOWN.md +++ b/docs/internal/ACCURACY_BURNDOWN.md @@ -178,15 +178,28 @@ 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 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 + 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 + 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 +315,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..5957e876 100644 --- a/docs/internal/FAITHFUL_TIMING_PLAN.md +++ b/docs/internal/FAITHFUL_TIMING_PLAN.md @@ -213,6 +213,77 @@ 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. 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 + 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: + 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, 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. + - 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, 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):** 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, diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 5827de52..41e471b9 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -227,6 +227,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/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/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/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 36e11bf7..0fff114a 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -366,6 +366,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. */ @@ -7210,8 +7211,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 6f63240e..1bac49e0 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,47 @@ 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. + * 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 && + g_host_store_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 +1774,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 +1804,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 +2002,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; } @@ -2298,9 +2340,28 @@ 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 (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/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/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/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/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_gpu_textured_dot_nw_shift_exec.c b/runtime/tests/test_gpu_textured_dot_nw_shift_exec.c index 62294947..e4d5eea7 100644 --- a/runtime/tests/test_gpu_textured_dot_nw_shift_exec.c +++ b/runtime/tests/test_gpu_textured_dot_nw_shift_exec.c @@ -164,6 +164,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 new file mode 100644 index 00000000..8254099a --- /dev/null +++ b/runtime/tests/test_isc_store.c @@ -0,0 +1,234 @@ +/* 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. 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" +#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 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); +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"); + + /* ... 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); + 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; +} 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;