tooling: PSX_FORCE_INTERP — measure the dirty-RAM interp's cycle model

Adds a force-interp knob (dirty_ram_is_dirty reports all RAM above the
kernel window dirty when PSX_FORCE_INTERP=1) so the dispatcher routes
clean compiled game text through the dirty-RAM interpreter — the SAME
path overlays take — with no emitter/dispatch change. This is the
interp-path Δ-ruler enabler: it lets the cyctest isolation loops be
measured native-INTERP vs Beetle.

RESULT — the dirty-RAM interpreter == Beetle EXACT on all 12 ruler#2
components (baseline/alu/load/load2/load_use/div/div_spaced/mult/
gte_rtps/gte_nclip/gte_read_use/ld_div). The interpreter's per-instruction
load-delay + GTE/muldiv interlock is now MEASURED equal to the oracle and
to the compiled backend, not merely shared-by-construction.

Process notes (cost real time, now documented in the cyctest README):
- launch psx-cyctest via PowerShell Start-Process; a bash '&' launch
  fails to boot (pc=0).
- sample cyc_watch / freeze_check at STEADY STATE: an early query (before
  the BIOS boots to the EXE entry) reports dirty_ram_insns=0 / warm-up
  values. This premature-sampling artifact was the whole "dispatch
  paradox" — the interp engages only after the EXE entry is reached.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Matthew Stanley
2026-06-27 16:13:58 -07:00
parent d5977976e5
commit b0391bc778
2 changed files with 46 additions and 3 deletions
+14
View File
@@ -106,8 +106,22 @@ void dirty_ram_mark_executable_range(uint32_t phys, uint32_t len) {
}
}
/* Force-interp mode (tooling): PSX_FORCE_INTERP=1 makes ALL RAM above the kernel
* window report dirty, so every dispatch into game/overlay text routes to the
* dirty-RAM interpreter (the SAME path overlays take) instead of the compiled
* image. Interp-path Δ-ruler enabler: lets the cyctest isolation loops be measured
* native-INTERP vs Beetle. Consulted by every routing site that already calls
* dirty_ram_is_dirty (top dispatch, dirty_ram_dispatch_inner gates, the
* psx_dispatch_game_compiled gates) — no emitter/dispatch change needed. */
static int dirty_ram_force_interp(void) {
static int s = -1;
if (s < 0) { const char* e = getenv("PSX_FORCE_INTERP"); s = (e && e[0] && e[0] != '0'); }
return s;
}
int dirty_ram_is_dirty(uint32_t phys) {
if (phys >= RAM_SIZE) return 0;
if (dirty_ram_force_interp() && phys >= DIRTY_RAM_KERNEL_TRACK_BYTES) return 1;
uint32_t page = phys >> DIRTY_RAM_PAGE_SHIFT;
return (dirty_ram_bitmap[page >> 5] >> (page & 31u)) & 1u;
}
+32 -3
View File
@@ -73,13 +73,42 @@ tracked, so each developer reproduces the disc from a disc they own.
## Measure
```bash
# both backends boot the same disc/cyctest.cue:
# both backends boot the same disc/cyctest.cue (psx-cyctest = the native test-runtime, port 4600):
# beetle: psx-beetle <bios> --disc disc/cyctest.cue --port 4382
# native: <test-runtime> ... --disc disc/cyctest.cue (TODO: test-runtime)
# native: psx-cyctest --no-launcher --game game.toml --bios <bios> --disc disc/cyctest.cue
python measure.py --port 4382 # Beetle oracle: per-iter + per-component delta
python measure.py --port 4500 # native cost model (once the test-runtime exists)
python measure.py --port 4600 # native cost model (compiled backend)
```
IMPORTANT: launch psx-cyctest via PowerShell `Start-Process` (a bash `&` launch
fails to boot — pc=0). cyc_watch / freeze_check sampling must be done AT STEADY
STATE: query AFTER the BIOS has booted to the EXE and the loops are running
(`measure.py`'s own wait handles this) — an early sample reports warm-up values
(Beetle's boot window) or, for the interp path, dirty_ram_insns=0 because the
EXE entry hasn't been reached yet.
## Interp-path measurement (PSX_FORCE_INTERP)
Both backends share one cost model (`psx_cyc_*`, psx_cyc.h), but `measure.py
--port 4600` above measures the COMPILED backend. To measure the dirty-RAM
INTERPRETER on the same loops (isolated interp-vs-Beetle Δ — not by-construction),
launch psx-cyctest with `PSX_FORCE_INTERP=1`:
```powershell
$env:PSX_FORCE_INTERP='1'; Start-Process psx-cyctest.exe -ArgumentList ...
```
`PSX_FORCE_INTERP=1` makes `dirty_ram_is_dirty()` (runtime/src/memory.c) report all
RAM above the kernel window as dirty, so the dispatcher routes the test ROM through
the dirty-RAM interpreter (the SAME path overlays take) instead of the compiled
image — no emitter/dispatch change. Confirm it engaged via freeze_check
`dirty_ram_insns` climbing (hundreds of millions during a measure run).
**Interp == Beetle EXACT on all 12 components (2026-06-27):** baseline/alu/load/
load2/load_use/div/div_spaced/mult/gte_rtps/gte_nclip/gte_read_use/ld_div all match
the oracle, so the interpreter's per-instruction interlock model is MEASURED equal
to the compiled backend and to Beetle, not merely shared-by-construction.
## Beetle ORACLE results (2026-06-26 — the HW cost targets)
Per-iteration cycle delta, and component cost (minus baseline=3):