Files
psxrecomp/runtime/include/lockstep.h
T
Shokunin 621a92d49b runtime: apply cache-isolated (IsC) stores to the caches, as Beetle does
memory.c dropped every store made while SR.IsC was set. Beetle's
PS_CPU::WriteMemory sends them to the caches instead
(mednafen/psx/cpu.cpp:482-512), before any address decode:

- I-cache enabled (BIU bit 11) with a tag-test, invalidate or lock mode
  bit: the store rewrites the tag and valid bits of its line. This is how
  FlushCache (A 44h) and the boot cache init invalidate the I-cache.
- D-cache enabled with lock mode clear ((BIU & 0x81) == 0x80): the store
  lands in the scratchpad at addr & 0x3FF.
- Nothing reaches the bus, the BIU register at 0xFFFE0130 included.

psx_icache_isc_store transcribes the tag write; memory.c's three store
chokepoints call it (and the scratchpad write) first, so compiled code,
overlay shards and both interpreters share it. DMA writes are not CPU
stores and now bypass IsC, as in Beetle. A render pass applies the same
effect (its checkpoint restores tags and scratchpad), and the overlay
shadow record fails closed on an isolated store, which its replay
cannot redo.

isc_store_test drives the real memory.c and psx_icache.c: 15 of its
checks fail on master.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 17:12:55 -07:00

88 lines
4.1 KiB
C

/* lockstep.h — #2 lockstep "unit-test interp" comparator.
*
* Per-basic-block differential check: the compiled block runs (RECORD mode,
* its memory ops captured), then the SAME block is re-interpreted from the
* same entry state (REPLAY mode, memory ops fed from the recorded trace so the
* replay is side-effect-free and MMIO-safe). If the interp replay reads a
* different address, writes a different value, or ends with different
* registers, that block's compiled output is wrong -> the exact codegen defect.
* If NO block ever diverges, the regression is timing/IRQ-phase, not codegen.
*
* Dev tooling only: the per-block hook is debug_server_cyc_observe, which is
* PSX_NO_DEBUG_TOOLS-stripped in release builds. Window-gated (frame range) so
* it's off (full speed) outside the region of interest.
*/
#pragma once
#include <stdint.h>
#include "cpu_state.h"
#ifdef __cplusplus
extern "C" {
#endif
/* 0=off, 1=record (compiled block running), 2=replay (interp re-run). */
extern int g_ls_mode;
/* 1 while replaying: cycle/icache accounting no-ops so the replay never
* perturbs real global timing state. */
extern int g_ls_replay_active;
/* Side-effect-free whole-call trace used by the overlay differential gate.
* RECORD executes the interpreter authoritatively and records every memory
* operation. REPLAY feeds the recorded reads to native code and verifies
* writes without touching RAM, scratchpad, or MMIO a second time. These calls
* are deliberately single-owner/non-nestable; a false return fails the
* comparison closed. */
int ls_shadow_record_begin(void);
int ls_shadow_record_end(uint32_t *ops, int *saw_exception);
int ls_shadow_replay_begin(void);
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);
void ls_write_hook(uint32_t addr, int size, uint32_t val);
/* Per-basic-block-leader entry, called from debug_server_cyc_observe. */
void ls_at_leader(uint32_t leader_phys, CPUState *cpu);
/* Dispatch-segment comparator: wraps one clean psx_dispatch_game_compiled()
* invocation and replays the same callable through exec_one until it reaches
* the same returned cpu->pc. This measures the native CPS unit, not just one
* basic block. */
void ls_func_enter(uint32_t entry_pc, CPUState *cpu);
void ls_func_exit(uint32_t entry_pc, CPUState *cpu, int handled);
void ls_func_set_window(uint32_t frame_lo, uint32_t frame_hi);
void ls_func_set_record_only(int on);
/* Called at real IRQ exception entry. Function-scope comparisons skip segments
* that delivered an interrupt, keeping codegen/tooling checks separate from
* event-phase behavior. */
void ls_note_exception_entry(void);
/* Suppress lockstep memory recording while runtime/debug observers read guest
* RAM for diagnostics. These reads are not guest instructions and must not
* become part of the compiled-vs-interp trace. */
void ls_suppress_begin(void);
void ls_suppress_end(void);
/* Arm the comparator over a guest-frame window [lo,hi]. hi==0 => disabled. */
void ls_set_window(uint32_t frame_lo, uint32_t frame_hi);
/* Diagnostic: record blocks but skip the inline replay (to test whether the
* inline replay, not recording, is what perturbs the run). */
void ls_set_record_only(int on);
/* Drain the first-divergence record as JSON into buf. Returns bytes written. */
int ls_get_diverge_json(char *buf, int buflen);
int ls_get_func_json(char *buf, int buflen);
#ifdef __cplusplus
}
#endif