mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
Merge pull request #435 from RetroPortingToolKit/fix/isc-icache-invalidate
Apply cache-isolated (IsC) stores to the caches, as Beetle does
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
$<IF:$<PLATFORM_ID:Darwin>,-Wl$<COMMA>-dead_strip,-Wl$<COMMA>--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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
+2
-2
@@ -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)++;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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. */
|
||||
|
||||
+71
-10
@@ -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; }
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) {}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user