bios emitter: test cache-line starts on the runtime PC

The cached line-leader rule charges a fetch at every 16-byte line start of
the runtime PC. The BIOS emitter tested bits[3:0] of the ROM address instead
and assumed every copy window preserves them. OpenBIOS copies its kernel from
ROM 0x1FC1E4D4 to RAM 0x500, which shifts them by 4. Each kernel line
crossing was charged one instruction early, where it is a hit, and the real
crossing went uncharged. 1,152 kernel instruction sites differed from the
interpreter.

The test now uses the runtime PC that relocate_ra already computes. In the
OpenBIOS kernel, 823 misplaced fetch sites go and 773 are added at runtime
line starts. Retail profiles are unaffected: SCPH-1001's copy windows are
16-byte aligned and SCPH-101/5552 declare none. The uncached_fetch_charge
ctest now also replays every cached OpenBIOS run from a cold cache against
the interpreter's fetch path and the Beetle transcription.

R4 (OpenBIOS, recompiled kernel, HLE boot) reaches the game entry 6,013
guest cycles later than with the uncached fix alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Shokunin
2026-09-29 00:24:03 -07:00
parent 7342e6bcd1
commit c981b3a2d7
4 changed files with 39 additions and 12 deletions
+8
View File
@@ -136,6 +136,14 @@ observe added to the recompiler so ANY block leader is anchorable on both backen
(compiled == interp fetch path == Beetle transcription, per instruction, on
OpenBIOS) and `uncached_fetch_codegen_test` (game emitter). OPEN: full LLE boot
(`bios_hle = false`) parity against live Beetle (no oracle binary on the Mac).
- [x] **BIOS emitter tests cache-line starts on the runtime PC (2026-09-29).** The
line-leader test used the ROM address, assuming every copy window preserves
bits[3:0]. OpenBIOS copies its kernel from ROM 0x1FC1E4D4 to RAM 0x500, so every
kernel line crossing was charged one instruction early (a hit) and the real
crossing went uncharged: 1,152 kernel instruction sites differed from the interp.
Retail profiles are unaffected (SCPH-1001's windows are 16-byte aligned;
SCPH-101/5552 declare none). Covered by ctest `uncached_fetch_charge` (cached runs
compared from a cold cache).
- [ ] **HW test-ROM ruler (#2)** — Amidog CPU/GTE timing ROMs for hand-crafted
single-COMPONENT isolation (div-only, load-only loops) that organic BIOS code
can't give (the prologue combines div+loads in one block). Strongest validator.
+14 -3
View File
@@ -224,9 +224,10 @@ on a fixed region -> next.
psx_icache.c also uses. The A0/B0/C0 call-vector stubs charge one fetch per
executed word.
- Cached code keeps the leader rule. R4's regenerated game C (50 shards and the
dispatch table) is byte-identical, as are the 26 R4 overlay shards compiled in
both runs.
- The OpenBIOS diff is insertions only: 5,477 fetches plus 5 stub fetches.
dispatch table) is byte-identical. So is every R4 overlay shard compiled in
more than one run (26 of the 45 captured; runs capture different sets).
- For the uncached fix, the OpenBIOS diff is insertions only: 5,477 fetches
plus 5 stub fetches.
- The codegen hash changes because the emitter sources do, and
psx_instr_cost.h is now in the hash list. Every title reshards its overlay
cache once.
@@ -238,6 +239,16 @@ on a fixed region -> next.
game entry 0x8007D8F4 moves from guest cycle 97,718,389 to 100,583,391
(+2,865,002, about 5.1 frames). The count was the same on two baseline
boots. R4 reached a race with 0 dispatch misses.
- Same day, second fix in the BIOS emitter: the cached line-start test now
uses the runtime PC instead of the ROM address. OpenBIOS copies its kernel
from ROM 0x1FC1E4D4 to RAM 0x500, which shifts bits[3:0] by 4. Each kernel
line crossing was charged one instruction early (a hit), and the real
crossing went uncharged: 1,152 kernel sites differed from the interp.
In the OpenBIOS kernel, 823 misplaced fetch sites go and 773 are added at
runtime line starts. Retail profiles are unaffected: SCPH-1001's windows
are 16-byte aligned and SCPH-101/5552 declare none. With both fixes, the
R4 game entry is at 100,589,404 (+2,871,015 over master; +6,013 from this
fix). R4 reached a race with 0 dispatch misses.
- Closes `bios-kseg1-fetch-charge` in the segment-aware ledger (PR #419).
- OPEN: full LLE boot (`bios_hle = false`) cycle parity against live Beetle.
There is no oracle binary on the Mac.
+8 -5
View File
@@ -772,10 +772,13 @@ bool FullFunctionEmitter::emit_function(
// to the dirty-RAM interp (cpu->pc) and Beetle.
// - CACHED runtime PC: emitted only at cache-line LEADERS: a block leader (any
// branch/dispatch entry — a possibly-cold cache entry; cross-function targets
// are inserted into block_leaders above) OR a 16-byte-line start (addr&0xC==0,
// a sequential line crossing). Intra-line followers reached by fall-through are
// guaranteed hits (the leader refilled the line to its end) → no call (+0). The
// relocation preserves bits[3:0], so the line-leader test is space-independent.
// are inserted into block_leaders above) OR a 16-byte-line start of the RUNTIME
// PC (pc&0xC==0, a sequential line crossing). Intra-line followers reached by
// fall-through are guaranteed hits (the leader refilled the line to its end) →
// no call (+0). The line test must use the runtime PC: a copy window need not
// preserve bits[3:0]. OpenBIOS copies its kernel from ROM 0x1FC1E4D4 to RAM
// 0x500, so a ROM-address test put every line crossing of the kernel one
// instruction early (a hit) and left the real crossing uncharged.
// - UNCACHED runtime PC (psx_fetch_uncached: the ROM run in place at KSEG1): no
// fetch ever fills a line, so there is no follower to elide. Every instruction
// pays +4 and clears the load give-back, exactly as the interpreter (a fetch at
@@ -785,7 +788,7 @@ bool FullFunctionEmitter::emit_function(
if (!per_insn_cycles) return;
const uint32_t pc = relocate_ra(rom_addr);
if (!psx_fetch_uncached(pc) &&
!(block_leaders.count(rom_addr) || (rom_addr & 0xCu) == 0)) return;
!(block_leaders.count(rom_addr) || (pc & 0xCu) == 0)) return;
out += fmt::format("#ifdef PSX_ENABLE_BLOCK_CYCLES\n psx_icache_fetch(cpu, 0x{:08X}u);\n#endif\n",
pc);
};
@@ -20,9 +20,14 @@ instruction's (cycles, give-back cleared) pair must be the same both ways:
model psx_icache.c (both entry points) equals a transcription of Beetle
ReadInstruction on synthetic sequences: uncached repeat, cached cold
then warm, KSEG0 -> KUSEG alias refill, partial refill.
bios psxrecomp-bios on bios/OpenBIOS.toml. Every straight-line KSEG1
run (the ROM in place) is replayed, and every KSEG1 instruction
must carry a fetch naming its own runtime PC.
bios psxrecomp-bios on bios/OpenBIOS.toml. Every straight-line run from
a label is replayed from a cold cache: KSEG1 runs (ROM in place)
and cached runs (kernel at 0x500+, shell at 0x80030000+) alike.
Every KSEG1 instruction must carry a fetch naming its own runtime
PC. Cached runs pin the line-start test to the runtime PC:
OpenBIOS copies its kernel from ROM 0x1FC1E4D4 to RAM 0x500, so a
ROM-address test charged each kernel line crossing one
instruction early (a hit) and missed the real one.
stubs The A0/B0/C0 call-vector native stubs charge one fetch per
executed word, so a KSEG1 call pays like the interpreter.
@@ -347,7 +352,7 @@ def main():
if not run["from_label"]:
first = next((i for i, (_, t) in enumerate(insns) if t is not None), len(insns))
insns = [x for i, x in enumerate(insns) if i >= first or uncached(x[0])]
if not insns or not all(uncached(pc) for pc, _ in insns):
if not insns:
continue
for pc, tag in insns:
if uncached(pc):