Files
psxrecomp/docs/SEGMENT_AWARE_CODE.md

87 KiB
Raw Permalink Blame History

Segment-aware code

Status: design proposal (2026-09-28). The four owner decisions in §10 were settled on 2026-09-29; each follows this document's recommendation. Based on master. It was first stacked on RetroPortingToolKit/psxrecomp#417 (fix/overlay-segment-alias), which merged on 2026-09-29 together with #418 (mult/div deadlines) and #420 (store-PC forwarding, ABI v24). Rollout PR A merged as #429 on 2026-09-29 (§8). This change adds only this document and an acceptance test; it does not change behaviour. Rollout PR B (refactor/emitter-runtime-pc, stacked on this design) implements §5.2, rollout PR C (feat/kuseg-linked-exe, stacked on B) implements §5.3 and §5.5, rollout PR D (feat/segment-variants, stacked on C) implements §5.4, the exact return checks of §5.5 and the KSEG1 variants of §5.6, and rollout PR E (feat/overlay-segment-keys, stacked on D) implements §5.7.

Acceptance test: recompiler/tests/test_segment_aware_codegen.py (ctest segment_aware_codegen). Synthetic EXE: tools/segment_testrom/gen_segment_exe.py.

1. Summary

A PS1 PC carries a segment:

  • KUSEG 0x0xxxxxxx
  • KSEG0 0x8xxxxxxx (cached)
  • KSEG1 0xAxxxxxxx (uncached)

The three map the same physical RAM. The CPU keeps the segment in:

  • every link value;
  • every EPC;
  • every I-cache tag.

KSEG1 also changes what each fetch costs. Compiled code bakes a single segment into all of these, so it is only correct for PCs in that segment.

#417 made overlay shards fail closed for other segments. Three problems remain:

  1. KUSEG-linked EXEs. Static game code for these executables runs at KUSEG PCs through KSEG0-baked bodies (Kula World SCES-01000, Alien Resurrection SLUS-00633).
  2. Overlay code at KUSEG or KSEG1. Overlay code that genuinely executes at KUSEG or KSEG1 is interpreted forever, because nothing records or compiles its segment.
  3. Uncached fetch charging. Compiled KSEG1 code charged the uncached +4 fetch only at cache-line leaders; Beetle and the interpreter charge it for every instruction. In the BIOS ROM, 5,477 of the 9,592 KSEG1 instruction sites in OpenBIOS were undercharged. PR A (#429) fixed that in both emitters (§5.6). Game code still has no KSEG1 bodies to charge (§5.4).

Recommendation: per-segment compiled variants, not segment-relative emission.

  • Code identity becomes the full runtime virtual address: segment plus physical address.
  • The emitter takes the segment as a compile-time constant. This generalizes the BIOS emitter's existing relocate_ra.
  • A body exists for each (segment, entry) that is actually executed:
    • static code: the EXE's link segment, plus variants requested with segment-qualified seeds;
    • overlays: the segments the capture observed.
  • A PC with no body for its segment is a loud dispatch miss. In game and overlay code it never runs another segment's body. The compiled BIOS, whose dispatch is keyed by the normalized address, still runs its window's body for such a PC, but records it as a BIOS segment miss (§5.4).
  • Uncached variants charge fetch on every instruction.

For every existing KSEG0 title the emitted code is byte-identical, so the static fast path costs nothing. Segment-relative emission would cost +3.8 % code size for the constants alone, and +27.6 % with exact KSEG1 charging (measured on R4, §6).

2. What the segment changes: the Beetle model

Beetle is the oracle (CLAUDE.md §2). The citations below are to libretro/beetle-psx-libretro mednafen/psx/cpu.c @ a7f0811. The in-tree transcription, runtime/src/psx_icache.c, cites the older in-tree cpu.cpp:534-601; the semantics are the same.

Architectural value Beetle Depends on segment?
Link of jal/jalr/bgezal/bltzal GPR[31] = new_PC, i.e. PC+8 (DO_BRANCH, 1012-1031) yes: PC's segment
j/jal target ((PC+4) & 0xF0000000) + (target << 2) (JAL 1810-1818) preserves PC[31:28]
Branch target PC-relative preserves the segment inside any RAM window
EPC CP0.EPC = PC (−4 in a delay slot) (CPU_Exception, 846-850) yes
I-cache hit ICache[(addr & 0xFFC) >> 2].TV == addr: the full virtual address (744) yes: KUSEG and KSEG0 aliases never share a line
Uncached fetch addr >= 0xA0000000 or BIU bit 11 clear: +4 on every fetch, no fill, clears the load give-back (746-763) yes: KSEG1
Cached miss +3, plus 1 per word refilled to the line end; tags written with the full address (764-831) yes
Data loads and stores addr_mask[addr >> 29]: KSEG0/KSEG1 reduced to physical no

The interpreter (dirty_ram_interp.c) already follows every segment row: it links with pc + 8 (2019), computes j targets from pc + 4 (1905), and fetches at cpu->pc (1554). It does not model the BIU cache-disable case (§9).

Invariant used by this design. Direct control flow never changes segment:

  • j/jal keep PC[31:28];
  • branches stay within ±128 KiB of the PC.

Only indirect transfers switch segment: jr/jalr, exception entry, and the return through EPC. All three already go through dispatch with the full PC. So a body compiled for segment S only ever reaches other S bodies by direct edges.

Oracle deviation (decided 2026-09-29, §10). Beetle's own comment (719-730) says hardware clears bit 31 of a KSEG0 fetch address before the tag compare and the tag write. On hardware, KUSEG and KSEG0 aliases therefore do share lines. I-cache tags follow the oracle, as psx_icache.c does today. The acceptance test pins this: its Beetle model and psx_icache.c must agree. Moving to hardware semantics would be a separate, oracle-backed change. §5.6 keeps the tag computation in one place so that change stays one line. The hardware difference is recorded under ACCURACY_BURNDOWN axis 4 ("KUSEG/KSEG0/KSEG1 mirroring").

3. Where psxrecomp loses the segment today

3.1 Baked PCs in the game and overlay emitter (code_generator.cpp)

Before PR B, every one of these sites wrote the compile address, which is always KSEG0. The emitter functions are named instead of line numbers, which move with every change.

Site Emitter R4 static count
cpu->gpr[31] = 0x…u (jal, jalr, bgezal, bltzal links) and a jalr link in a branch delay slot translate_basic_block, translate_instruction 5,373
psx_icache_fetch(cpu, 0x…u) (fetch tags) translate_basic_block (emit_pre_icache) 61,181
psx_check_interrupts_at(cpu, 0x…u) (resume PC → EPC) emit_interrupt_check, ~20 callers 30,596
cpu->pc = 0x…u; return; (CPS exits, stale-static guard, image-edge tail transfer) translate_basic_block, generate_function, generate_alias_group, emit_stale_static_guard 6,927
g_debug_last_store_pc = 0x…u (before every sb/sh/sw/swl/swr/swc2) translate_instruction 22,936
CPS continuation keys case 0x…u: goto block_… generate_function, generate_alias_group —
Reserved-instruction EPC cpu->cop0[14] = 0x…u translate_basic_block —
psx_slice_block(cpu, 0x…u, …) (the interpreter resumes here) translate_basic_block —
psx_vsync_query_hle_enter(cpu, 0x…u, …): load_accel.c bases fetch tags, store-PC stamps and resume PCs on it generate_function — (R4 configures none)

The R4 figures come from all 50 generated shards: 2,994 functions and 141,335 emitted instruction sites.

The store-PC stamp is guest-affecting, not a debug hook. Its name and the emitted extern comment describe debug attribution, but the runtime reads it in every build:

  • memory.c psx_write_word_raw drops a word store to RAM 0x0-0xF when the stamp equals one of a list of exact PCs. An opt-in Tomba card filter (PSX_TOMB_CARD_EVCB_PROTECT) keys on it the same way.
  • memory.c's GP0 write path compared it with 0xBFC38B1C before PR B (§9 has the re-key), a BIOS store to GP0. On a match it hands gpu_set_gp0_source() a RAM source key taken from $a0. No preprocessor gate covers this, and main.cpp binds debug_cpu_ptr at startup (debug_server_set_cpu), so it also runs in every build. The resulting gp0_cmd_source_addr feeds the opt-in presentation paths (widescreen prim matching, the ws_* code in gpu.c, geometry/texture correction, mod texture keys) and prim-ring diagnostics. The faithful rendering path does not branch on it.
  • The interpreter stamps the full executing PC (dirty_ram_interp.c). A compiled body that stamps a different segment than the one it runs in can make a filter or key match in one execution path and miss in the other.
  • #420 (fix/fingerprint-guest-facts, ABI v24, merged 2026-09-29) makes overlay shards write the host's copy in every build. Before it, overlay stores wrote a private copy, so after an overlay store the filters saw the PC of an older store.

So the stamp is a baked PC like the others in the table, and §5.2 routes it through runtime_pc().

Two other groups carry PCs but are identity keys, not architectural state:

  • debug and identity hooks: debug_server_* (the runtime masks debug_server_cyc_observe's argument to physical), psx_mod_function_entry, psx_datashard_enter and psx_native_bad_entry's owner;
  • .ranges manifests.

The cosim_* hooks (the PSX_COSIM build only) record the executing PC at their checkpoints, as the interpreter does, so PR B routes them like the table.

Before PR C, the dispatch file (main_psx.cpp, psxrecomp_game_main):

  • keyed its table by KSEG0 address and looked it up with the physical address (psx_game_find_entry, want = addr & 0x1FFFFFFF);
  • then set cpu->pc = entry->resume_pc, which is a KSEG0 constant.

A KUSEG or KSEG1 PC therefore entered the KSEG0 body and continued in KSEG0, and test_kuseg_dispatch_lookup.py asserted exactly that alias behaviour. PR C (§5.5) moved the emitter to game_dispatch_emitter.cpp, made the lookup exact and rewrote the test: an alias with no body of its own misses.

3.2 KUSEG-linked EXEs

Before PR C (§5.3), ps1_exe_parser.cpp rewrote the header's KUSEG addresses to KSEG0 in place and recorded no original segment. It also rejected a KSEG1 entry. Three things followed:

  • The whole game compiled for KSEG0 while hardware runs it at KUSEG. Every link it saves on the stack and every EPC was 0x8… instead of 0x0…. Every fetch tag differed from the one the BIOS, the interpreter and Beetle use.
  • Seeds in the game's own segment were silently dropped. main_psx.cpp range-checked seeds against the normalized load_address, so a seed written as 0x000100E4 was ignored. It had to be written 0x800100E4.
  • R4 is linked at KSEG0 and is unaffected.

3.3 Aliases

Before PR C, a KSEG1 or KSEG0 alias of static text resolved to the same body (§3.1). The uncached cases are rare but real: code that ORs 0xA0000000 into a function address to run it uncached, and cache-maintenance trampolines. Those ran with the wrong fetch cost and the wrong links. Since C they miss and run interpreted (§5.5). Since D a segment-qualified seed compiles a body for the second segment (§5.4).

Measured in the BIOS (2026-09-29). SCPH-1001 enters its relocated kernel through the uncached alias 0xA0000500. In #429's Beetle gate (§7.2) Beetle charges the four instructions there as uncached fetches, 20 cycles; native runs the cached body and charges 11. That is the −9 cycles native − Beetle at SCPH-1001's kernel entry and first kernel calls. The BIOS emitter needs a KSEG1 variant of that entry to close it, which PR D (§8) provides: SCPH-1001's seeds ask for 0xA0000500, and the first hit of every SCPH-1001 boot anchor now matches Beetle (§7.2).

3.4 Overlays (after #417)

Segment is lost at every stage:

  • Capture: it records bytes and entry PCs per physical word (g_dirty_ram_{exec,dispatch}_pc_bitmap). It writes every PC back out as PSX_OVERLAY_CODE_SEGMENT | offset (overlay_capture.c 365, 420-446).
  • Interpreter dispatch: it sees the full PC, but drops the segment at dirty_ram_interp.c:2814.
  • Cache names and manifests:
    • filenames and namespaces start from the physical address ({phys}_{crc}, ov_{phys}_…);
    • .ranges F entries are forced to KSEG0 (overlay_loader.c:942);
    • exports must be named func_{KSEG0 entry};
    • candidates are indexed by physical address.
  • Gate: #417's gate (overlay_loader.c:3690) interprets every non-KSEG0 PC.

In R4 that is about 27 dispatches per frame: OpenBIOS enters its RAM patch slots 0x0000281C and 0x0000357C at KUSEG. Nothing ever asks for a KUSEG shard. PR E records the segment at capture and keys shards by it (§5.7).

3.5 Uncached fetch in compiled code

Fixed by #429 (§5.6). This section records the gap as it was on master before that PR.

Both emitters emitted psx_icache_fetch only at cache-line leaders:

  • block leaders, jump-table targets, and addr & 0xC == 0 (the leader test, now code_generator.cpp:1903 and full_function_emitter.cpp:790).

The stated reason is that "intra-line followers reached by fall-through are guaranteed hits". That holds for cached segments only. Every KSEG1 fetch misses and costs +4 (§2).

The BIOS main ROM runs in place at 0xBFC0… (KSEG1). In OpenBIOS, 5,477 of its 9,592 KSEG1 instruction sites had no fetch call. Each was:

  • 4 cycles short;
  • missing the load give-back clear.

The interpreter charges every one. Compiled and interpreted ROM code therefore disagreed, against the "one shared per-instruction cost" rule (CLAUDE.md RULE −1). Ruler #2 and ruler #1 both run in cached RAM, so neither caught it.

4. Options

A. Segment-relative emission

One body per function takes the segment at run time: from a field set by dispatch, or from a parameter. It emits seg | phys at every site in §3.1.

  • Code and speed. Every baked constant becomes an OR with a live register. That is about 127k sites in R4 (22,936 of them store-PC stamps), before continuation keys and switch tables. CPS continuation switches can no longer be case constants; they need a physical-address switch plus a segment check.
  • Exact KSEG1 charging. The body cannot know statically whether it is uncached. So each non-leader instruction (about 80k sites in R4) needs a run-time if (seg >= KSEG1) fetch test. A block-level surcharge is not exact, because each uncached fetch clears the load give-back between instructions.
  • ABI. Every generated function needs the segment source:
    • direct calls must carry it;
    • dispatch must scope it;
    • the overlay DLL ABI (PSX_OVERLAY_ABI_TAG) and mod entry hooks change.
  • Measured cost (§6): +3.8 % __TEXT for the constants, +27.6 % with exact KSEG1 charging.
  • Benefit: one body serves any segment, and no variant set has to be chosen.
  • The emitter bakes the segment as a constant, as it does now, but reads it from a per-compile code_seg instead of assuming KSEG0.
  • A body is compiled for each (segment, entry) the program executes.
  • Uncached variants emit a fetch charge on every instruction; cached ones keep the leader rule.

Costs:

  • Code grows only by the variants actually requested. These are almost always small closures, such as a cache-flush trampoline.
  • The variant set must be known. The segment-miss ring (§5.5) and capture (§5.7) supply that evidence.

Reasons to choose B:

  • It keeps the fast path exactly as it is for the home segment.
  • It keeps each body's cost model static, so it can be checked against Beetle per instruction.
  • It follows the model the BIOS emitter already uses: per-window runtime PCs through BiosAddressModel::runtime_pc.
  • Option A spends 4-28 % of every title's code on a case most titles never hit.

5. Design

5.1 Code identity is the runtime virtual address

A compiled body is identified by seg | phys:

  • func_ names use it: func_00010000 for a KUSEG-linked entry, func_A00100F0 for a KSEG1 variant.
  • Direct C calls and CPS exits then resolve to the same-segment variant with no extra lookup.
  • KSEG0 names do not change.

5.2 Emitter: one runtime_pc() for every baked PC

  • CodeGenerator gets runtime_pc(compile_addr) = code_seg | (compile_addr & 0x1FFFFFFF), modelled on bios_runtime_pc.
  • Every site in §3.1 goes through it, the store-PC stamp included. That is 9 helpers and about 40 format sites, plus 8 store-PC format sites.
  • The helper also covers generate_alias_group bodies and the dispatch emitter's rows (addr, resume_pc).
  • Identity keys (block labels, .ranges) may keep the compile address. The store-PC stamp is not an identity key (§3.1).
  • code_seg defaults to the segment of the image's load address. Until §5.3 the EXE parser folded KUSEG headers to KSEG0, so that was KSEG0 for every title; since PR C it is the link segment. Either way runtime_pc(addr) == addr for every address in the image.
  • Acceptance: regenerating any KSEG0 title is byte-identical. That proves the refactor has zero fast-path cost. Closes nothing in the ledger on its own; enables §5.3-5.6.

Implemented in PR B. CodeGenerator::set_code_segment() and runtime_pc(). Routed through it:

  • links (jal, jalr, bgezal/bltzal, and a jalr in a branch delay slot, which translate_instruction emits) and the call contract's return PC;
  • fetch tags and emit_pre_icache's uncached test (§5.6);
  • interrupt resume PCs and the reserved-instruction EPC;
  • CPS exit PCs, continuation case keys (function and alias bodies) and the alias entry keys;
  • call_by_address and stale-static-guard targets, the slice resume PC and the cosim_* PCs;
  • store-PC stamps, including the widescreen-backdrop and persisted-option store sites;
  • the VSync-query hook's first argument. load_accel.c derives fetch tags, store-PC stamps and resume PCs from it, so it is a PC, not an id. Its hand-timed body charges cached line-leader fetches only, so the hook is not emitted for an uncached code segment: a KSEG1 body runs its own instructions, each charged (§5.6). The dispatch-side psx_vsync_query_hle_try compares the full PC exactly, so another segment never takes it (nothing calls psx_vsync_query_hle_configure today);
  • the PC psx_unknown_dispatch reports from a data stub;
  • dispatch rows (addr, and resume_pc when it is a PC).

func_/block_ names, .ranges, debug_server_log_call_entry, the mod and data-shard hook ids and psx_native_bad_entry's owner keep the compile address. So does debug_server_cyc_observe: the game emitter passes the compile address and the runtime masks it to physical.

Jump-table case values are data words, the PCs the jr reaches, and are emitted as read. In B each case continues in the current body. That is exact only while every table value is in the body's own segment. A variant (§5.4) has tables in another segment than its own, and so does a home or overlay compile whose table holds another segment's PCs of its own bytes (a KSEG0-compiled overlay shard of code that runs at KUSEG). From PR D every game and overlay compile tail-transfers a case whose segment differs from its own (CodeGenerator::foreign_segment_case()); the BIOS emitter applies the same test in its variants.

The BIOS side is StrictTranslator::translate(d, runtime_pc): the store-PC stamp, the syscall EPC and the break and unaligned-access PCs use the runtime PC, while the psx_ldd_ temporaries and terminator_target keep the ROM address. The full-function emitter passes relocate_ra() to every translate() call, including orphaned delay slots and the two calls that read only terminator metadata, and publishes the runtime PC on a fallthrough. The seven memory.c keys are re-keyed in the same commit, and gated so that each still matches only its own SCPH-1001 instruction (§9).

Tests:

  • emitter_runtime_pc_test compiles a small program in CPS, CPS-overlay and legacy mode, through generate_all_functions. With the code segment left at its default, set to KSEG0, set to KUSEG and set to KSEG1, it checks each game-emitter PC class above except the dispatch rows, the identity keys, and that KUSEG output differs from KSEG0 only in constant segments. The program reaches every emission path listed above (the test fails if one stops being emitted), including split conditional branches, alias groups, data stubs, the image edge, the backdrop and persisted-option store sites and a jalr in a delay slot. It also checks that a KSEG1 code segment charges exactly what a KSEG1 image charges. For the translator it checks every PC-bearing form, the deferred loads and both syscall forms (ctest runs it a second time with PSX_CPS=0).
  • The dispatch rows are routed too. In B they were not tested: a real executable compiles in its own segment, where runtime_pc() is the identity. PR C moved the dispatch source to game_dispatch_emitter.cpp, and emitter_runtime_pc_test now emits it with a KUSEG and a KSEG1 code segment: row keys and resume PCs carry the segment, func_ names keep the compile identity.
  • A single-site mutation sweep (revert one route, rebuild, run the unit test in both PSX_CPS modes and the ledger) killed 77 of the 80 routes in B. The three survivors were the two dispatch-row routes, which PR C's test kills, and the continuation key for a call return in the middle of a block, which the CFG analyzer does not produce (it starts a block at every call return; R4's 2,994 functions have none).
  • The ledger gains two regression guards (§7.1).

The BIOS emitter's precedent has holes to close in the same pass:

  • the fallthrough cpu->pc = next_addr (full_function_emitter.cpp);
  • the strict_translator syscall, break and unaligned-access PCs, which use the ROM address;
  • the strict_translator store-PC stamp, which also uses the ROM address. Moving it changes which memory.c keys match: the store filters (RAM 0x0-0xF and the opt-in Tomba EvCB filter) and the GP0 source key (§3.1). Seven of those keys, six store-filter keys and the GP0 key 0xBFC38B1C, are ROM addresses of code that runs relocated, so they must be re-keyed to runtime PCs in the same change (§9).
  • PS1Executable records link_segment = initial_pc & 0xE0000000.
  • It keeps physical helpers for byte addressing instead of rewriting the header.
  • Load address and entry must share the segment:
    • KUSEG and KSEG0 are accepted;
    • KSEG1 is accepted and compiles as an uncached home segment.
  • The game emitter runs with code_seg = link_segment.
  • Seeds are range-checked by physical address, so 0x000100E4 is a valid home seed.

Closes link-segment, fetch-tag-segment, irq-resume-segment, resume-pc-segment, store-pc-segment, home-seed-accepted and alias-fetch-coherence.

Implemented in PR C. The image is analysed and compiled at its link virtual addresses, so code identity is the VA (§5.1) and runtime_pc() is the identity for every home body: a KUSEG-linked EXE gets func_00010000, KUSEG rows, links, fetch tags, resume PCs and store-PC stamps with no other emitter change.

  • PS1Executable::link_segment() is the load address's segment. The header is kept as written; phys_load_address() and contains_phys() do byte addressing, and the BSS overlap check and the text_size bound are physical. The load address and entry must lie in the 8 MiB RAM window of KUSEG, KSEG0 or KSEG1 and share the segment; anything else is rejected with a message that names both.
  • function_analysis.cpp's three JAL scans took their targets' top bits from KSEG0. They now take them from the delay slot's PC, as every other jump decoder does; for a KSEG0 image nothing changes.
  • Seeds are range-checked by physical address. A seed in the link segment is an entry. A seed for the same bytes in another segment is a variant request (§5.4): until PR D it is listed in a WARNING with its home spelling and not compiled, and never folded into the home body. Seeds outside the image are counted. The seeds file's directive lines (producer_range, cross_call_allow, hosted_interior) have no variant form: one that names the image's bytes in another segment is refused with its link-segment spelling.
  • tools/collect_game_misses.py writes each dirty-RAM miss in the link segment (from --segment, --game-toml or a segmented text range; it refuses to guess from a physical one) and each segment_misses row at its full PC, a variant request. Before C it wrote every seed as KSEG0.
  • Config code sites that the emitter or the runtime match exactly, and that name the image's bytes in another segment, are refused with the matching spelling: [widescreen] and [widescreen.cull] sites, mod entry hooks, hot and data-shard functions, the VSync-query function and its event-horizon return PCs (compared with $ra), and persisted option stores. After C such a site would match nothing, with no message. Kinds matched by physical address reach the same bytes in any spelling and are not refused: [[recompiler.patches]] (byte patches), the full-word-guarded [widescreen.cull] keep, angle and aspect-cone sites, [[widescreen.signed_x_bound]], and [widescreen.dome] call sites (the runtime masks them). Overlay compiles are exempt; their segment keys are §5.7.
  • A KSEG1-linked image compiles as an uncached home segment: every instruction is charged its own fetch (#429's rule).
  • Until PR E, a KUSEG-linked title's overlay code runs interpreted. Its static code now enters overlay RAM at KUSEG PCs (direct jal/j keep the caller's segment, and call_by_address takes runtime_pc()), and #417's gate keeps every non-KSEG0 PC off the KSEG0-compiled shards (segment_alias_interp counts them). Before C the folded KSEG0 static code entered them at KSEG0 and ran the shards natively, with KSEG0 links. Since PR E those entries are captured with their segment and compile to KUSEG shards (§5.7).
  • A KSEG0 title that enters its own static text through a KUSEG or KSEG1 alias (§3.3) runs that path interpreted after it is regenerated, recorded as segment misses, until PR D compiles a variant. Before C the alias ran the KSEG0 body. Of the in-tree titles only R4 was checked (0 segment misses).

5.4 Variant requests and closure

  • A seed whose segment differs from the link segment requests a variant, for example 0xA00100F0. This reuses the seeds file; no new configuration surface is needed. Decided 2026-09-29 (§10): segment-qualified seeds are the only request mechanism, with no game.toml table.
  • The recompiler compiles the variant's direct-edge closure in that segment:
    • direct calls;
    • tail jumps;
    • CPS continuation targets.
  • By the invariant in §2, that closure is everything the variant can reach without dispatch.
  • Bodies are emitted once per (segment, entry). Cached variants use the leader rule; KSEG1 variants use §5.6.
  • A jump-table case whose value lies in another segment tail-transfers to dispatch instead of continuing in the variant (§5.2).

Closes segment-variants.

Until D, PR C reports such a seed and compiles nothing for it (§5.3), so the PC stays a recorded segment miss (§5.5).

Implemented in PR D.

  • main_psx.cpp compiles the home image first. plan_segment_variants() (segment_variants.cpp) then maps each variant seed to the home compile's final pieces (CodeGenerator::last_functions(), after the split pass): a seed at a function entry roots that function, a seed at a CPS continuation roots its owner, and an alias entry brings its whole alias group (one shared body). The closure follows direct_successors(): branch and jump targets and not-taken paths that leave a function, call targets, call returns split into another piece and a fall-through past its last block. A seed that names no home dispatch entry stops the build with its home PC. So does a seed in a segment that does not map physical memory (maps_physical(): KUSEG below 0x20000000, KSEG0 and KSEG1 do). 0x20000000-0x7FFFFFFF and KSEG2 are no alias of the image: Beetle's addr_mask leaves them unmasked, so they address no RAM. Such a seed is refused where the seeds file is read and again by the planner, and collect_game_misses.py reports such a segment miss instead of writing it as a seed.
  • Each segment compiles through segment_view(), the image with its header moved into that segment, so the variant's code identity is its VA (§5.1: func_A00100F0, block_A00100FC, psx_alias_body_A…), the generator's code segment is the view's, and runtime_pc() is the identity again. The CFGs are re-derived on the view (the same bytes give the same blocks). The split pre-pass is off: the closure is already made of the home pieces.
  • rebase_codegen_config() moves every exact-match config code site that names the image into the variant's segment, so a variant gets the same hooks and substitutions as its home body (for example hot_funcs, mod entry hooks, widescreen sites). Physically matched kinds are kept as written.
  • The variant's bodies join the home shards and declarations header, its F/R records join .ranges, and its rows join the dispatch table (emit_game_dispatch() takes one unit per compile).
  • A cached variant is the home C with the segment swapped (kuseg_dispatch_lookup compares them); a KSEG1 variant charges a fetch before every instruction (§5.6).
  • A jump-table case whose value is in another segment than its body is not a local case (foreign_segment_case()); the default tail-transfer dispatches it with its full PC. This applies to every game and overlay compile, not only to variants. A variant has such cases whenever it switches on its home segment's PCs. A home or overlay compile has one only when its table holds another segment's PCs of its own bytes, for example a KSEG0-compiled overlay shard of code that runs at KUSEG (a KUSEG-linked title's overlays, or BIOS-installed RAM code). Such a case used to continue in the KSEG0 body with KSEG0 links and fetch tags; now a game-text PC with no row in its segment is a recorded segment miss (§5.5), and #417's gate sends an overlay PC to the interpreter. Both run it at its own PC. R4's game C and its 26 overlay shard sources are unchanged (§8 D); no other title was regenerated here.
  • Overlay compiles take no variant seeds; a variant seed there is reported and not compiled, as before. Overlay code gets its segments another way: capture records the segment of each entry, and each segment's entries compile as their own shard (§5.7, PR E).

BIOS. The BIOS emitter keys its dispatch by the normalized address, so a PC in any segment runs the body compiled for its window's segment. A BIOS seed that is a runtime PC inside a relocated copy window, in another segment than the window runs in, now asks for a variant (main_bios.cpp split_segment_variant_seeds()): the function at those ROM bytes and its direct-edge closure (branch and J/JAL targets that leave it, the call return and fall-through past its end) are emitted again with every runtime PC in the seed's segment (bios_runtime_pc() under g_variant_seg). The variants and their continuations get exact-PC rows in segment_variant_table. A variant seed in a segment that does not map physical memory stops the build; a runtime PC in its window's own segment is no variant request and stays a discovery seed.

The dispatch still finds the word by its normalized key (so the kernel-bless guard still applies); psx_bios_hit_body() then picks the body for the exact PC. A variant compiled for that PC runs. Any other PC runs the word's home body as before, and when it is in another segment than the one that body was compiled for, it is a BIOS segment miss: the home body bakes its own segment's links, EPCs, fetch tags and store PCs, so the run goes into the segment-miss ring with kind bios and the home PC (TCP segment_misses, dispatch_stats, the exit report). The miss is loud, as in game code (§5.5), but the home body still runs. Sending the PC to the dirty-RAM interpreter instead would change what runs, which needs a measured case (there is none), and ROM code has no such path. The fix is a seed in the BIOS profile's seeds; collect_game_misses.py lists such a row instead of writing it into the game's seeds. psx_bios_key_home_pc(), emitted from the address model next to normalize(), gives each key's home PC: its copy window's runtime address, or the ROM run in place. SCPH-1001's seeds ask for 0xA0000500 (§3.3). Every BIOS's dispatch C gains the two helpers; a BIOS without variant seeds keeps its full C.

Measured (§7.2): through the probe's spin (an LLE disc boot through the shell into the game, 198 M cycles on OpenBIOS and 399 M on SCPH-1001) neither BIOS records a BIOS segment miss, so 0xA0000500 is the only alias entry either boot makes. Without SCPH-1001's 0xA0000500 seed the same run records exactly that PC (kind bios, home 0x00000500, frame 0).

5.5 Dispatch is exact; a segment miss is a dispatch miss

  • The table is keyed by the full VA. psx_game_find_entry keeps its physical-word index. The index now points at the first row for that word, and the lookup requires row.addr == addr.
  • Rows for one physical word are adjacent. A title with no variants has one row per word, which is today's shape.
  • resume_pc is per-variant, so cpu->pc = entry->resume_pc is correct by construction.
  • A physical hit with no row for the PC's segment is a segment miss:
    • it is counted and recorded in a TCP-visible ring with the full PC;
    • it takes the existing clean-text-miss path (dirty_ram_interp.c clean_game_text_miss).
  • Resolving it follows the project rule for any dispatch miss: add the segment-qualified seed, then regenerate.
  • It never runs another segment's body.
  • Decided 2026-09-29 (§10): a segment miss in static game code interprets loudly until the title is regenerated. It does not fail fast.

Closes segment-miss. The plan was to make psx_call_contract's segment-masked return check (cpu_state.h, its bail-resolve and $ra compares) exact in the same PR; C deferred it to PR D, below.

Implemented in PR C.

  • game_dispatch_emitter.cpp (moved out of main_psx.cpp) sorts rows by physical address, indexes the physical word and then requires row.addr == addr, in both the indexed and the binary-search lookup. One compile has one code segment, so a second row for one word is a build error until D's variants share the table.
  • psx_game_address_in_text stays physical (byte identity). A segment miss therefore takes dirty_ram_dispatch_inner's clean-text-miss path and is interpreted.
  • The runtime records it (psx_segment_miss.c): when a clean-text miss has no row but another segment's alias does, the full PC, that row, $ra, $sp and the frame go into an always-on ring and a per-PC count. TCP segment_misses returns the summary or {"tail":N}; dispatch_stats carries segment_miss_total/_unique; psx_last_run_report.json has a segment_misses section.
  • psx_call_contract stays segment-masked for now. The BIOS dispatch loop the full-function emitter generates (psx_dispatch_impl and its return-boundary helper) makes the same masked return check in four places, and one call contract must not have two rules. Before D the masked compare can only let a body continue for another segment when a callee returns to an alias of its own call site, which no title is known to do. D makes the five checks exact together.
  • Tests: kuseg_dispatch_lookup compiles the emitted lookup in its indexed form and, for a table spanning 2 MiB or more, its binary-search form, and queries every segment's alias of every row; emitter_runtime_pc_test checks that two rows on one physical word are a build error; psx_segment_miss_test covers the dispatch hook's helper (psx_segment_miss_note), and segment_miss_wiring_test pins its call in dirty_ram_dispatch_inner, the TCP command, the dispatch_stats and ping fields and the exit-report section.

Implemented in PR D.

  • A word may carry a row per compiled segment (the home body and its variants, §5.4), adjacent and in PC order. When any word has more than one row, the lookup finds the word's first row (the index points at it; the binary search finds the lowest row of the word) and then searches that word's rows for the exact PC. A table without variants emits the lookup as before. Two rows for one PC stay a build error.
  • The five return checks compare the full PC: psx_call_contract's bail-resolve PC and $ra check, and the emitted BIOS dispatch loop's return boundary, bail resolve, wild-return test and exception-stack straddle. A guest that returns to another segment's alias of its call site did not return to that body, so the suspended C continuation no longer runs there. B's runtime_pc() and C/D's per-segment bodies make every legitimate link match in full; LLE boots and R4 fingerprints are unchanged by it (§7.2). Ledger guard exact-return-contract.
  • The segment-miss ring carries a kind: game (static game text, run interpreted) or bios (a compiled BIOS window entered in another segment, run through its home body, §5.4). TCP segment_misses rows and the exit report name it.

5.6 Per-instruction fetch charging for uncached code

Landed in PR A, #429 (2026-09-29). The rule: an instruction whose runtime PC is uncached is charged a fetch of its own.

  • Cached code keeps the leader rule, which is exact for it: the ledger's model check proves that against Beetle for the probe block.
  • Both emitters emit psx_icache_fetch(cpu, pc) before every instruction for which psx_fetch_uncached(pc) holds (psx_instr_cost.h, pc >= 0xA0000000). psx_icache.c uses the same predicate: such a fetch never hits, so it clears the load give-back, adds +4 and fills nothing, as Beetle does. The A0/B0/C0 call-vector stubs charge one fetch per executed word.
  • The same PR fixed the BIOS emitter's cached line-start test, which used the ROM address. It now tests the runtime PC (relocate_ra): OpenBIOS copies its kernel from ROM 0x1FC1E4D4 to RAM 0x500, which shifts bits[3:0] by 4, and 1,152 kernel instruction sites were charged one instruction early.
  • The tag compare stays in psx_icache.c. Tags follow Beetle (decided, §10), and this is the one place a later, oracle-backed move to hardware tags (§2) would change.

Where it applies:

  • BIOS emitter: relocate_ra(rom) is uncached for the ROM run in place. Done in #429, which closed bios-kseg1-fetch-charge.
  • Game and overlay emitter: the predicate was already there, but emit_pre_icache tested the compile address, which is always KSEG0. §5.2 routes two things through runtime_pc(): the psx_fetch_uncached() argument and the fetch tag (done in PR B). Routing only the tag would charge a KSEG1 variant at line leaders alone. With both routed, a KSEG1 variant (§5.4) is charged per instruction with no further emitter change; emitter_runtime_pc_test checks that a KSEG1 code segment charges exactly what a KSEG1 image does. PR D adds those variants and closes kseg1-fetch-charge; the ledger checks the KSEG1 probe_run variant against Beetle's fetch model.
  • Interpreter: unchanged; it already charges every fetch.

BIU bit 11 (cache disable) makes every fetch uncached in Beetle. It is a run-time state, not a segment, and is listed under §9.

5.7 Segment-aware overlay capture and cache keys

The bytes are segment-free; execution is not. So the byte identity (region, CRC, per-function code_crc, pair dedup) stays physical, and the segment joins only the entry identity and the compiled artifact key.

Capture

  • g_dirty_ram_dispatch_pc_bitmap keeps its meaning (dispatched in any segment), and region building keeps using it.
  • Add one sibling bitmap per segment: KUSEG, KSEG0 and KSEG1.
  • The siblings are set at the interpreter dispatch that already sets the bit, from the full addr before dirty_ram_interp.c:2814 masks it. The cost is one bit set per interpreted dispatch, not per instruction. Host memory is 3 × RAM/32 (192 KiB retail, 768 KiB in 8 MiB mode).
  • The execution bitmap needs no segment: direct edges keep the entry's segment.

Capture JSON

  • Schema v3 adds "dispatch_entry_segments": {"kuseg": [...], "kseg0": [...], "kseg1": [...]}.
  • If it is absent, entries are KSEG0 only, so every existing v2 capture stays valid.
  • The #417 counter segment_alias_interp becomes the trigger: a miss records its entry into these bitmaps, and the next compile builds the shard.

Compile (compile_overlays.py)

  • One shard per (region, segment with entries).
  • make_psxexe(seg | phys, …) relies on §5.3's parser.
  • _canonical_guest_addr and the | 0x80000000 sites become seg | phys.
  • The static-overlay generator accepts non-KSEG0 variants: it emits the segment in its table and matches exactly, replacing the #417 rejection at 2535.

Cache keys (layout decided 2026-09-29, §10)

  • KSEG0 artifacts keep today's path and names, so no existing cache invalidates.
  • Other segments go in a per-segment subdirectory of the same cache tag: …/cg<N>_<hash>_gc<hash>_f<n>/seg-kuseg/{phys}_{crc}.{dll,so} (and seg-kseg1/).
  • The filename grammar (psx_overlay_cache_name_parse, 8_8 or 8_8_8) does not change.
  • Static namespaces add s<seg >> 29> for non-KSEG0 shards (ov_s0_{phys}_… for KUSEG, ov_s5_… for KSEG1), so static variants can link side by side.
  • Exports are func_{VA} (§5.1), so they are distinct per segment.
  • pair_id hashes the C source, so variants never pair-alias one another.
  • A variant shard stamps its own segment's store PCs (§5.2). This relies on #420 (ABI v24), which forwards overlay stamps to the host's g_debug_last_store_pc in every build, so the memory.c filters see them.

Manifest

  • .ranges gains S <segment>; if it is absent, the segment is KSEG0.
  • F entries carry the full VA and must match S. parse_manifest stops forcing KSEG0 (942).
  • R lines stay physical extents: they validate bytes, not execution.

Loader

  • Candidates keep the physical index and gain a seg field.
  • The #417 gate generalizes from "PC is KSEG0" to "a candidate for this physical address has seg == pc & 0xE0000000".
  • Other aliases still interpret. Rule 18 permits that for runtime-installed code, and it is counted and fed back to capture.
  • overlay_idle_note_is_internal_or_return (2308) compares full VAs.

Docs: update AOT_OVERLAY_PLAN's "canonical KSEG0 entries" contract and AOT_SHARDING's KSEG0 window. OVERLAY_CACHE_V2's per-function key already names guest_entry_vaddr; this design makes that VA include the segment.

Expected R4 effect: the OpenBIOS KUSEG patch slots are captured as KUSEG entries of the kernel page and get a KUSEG shard. segment_alias_interp drops to 0 on a warm cache.

Implemented in PR E.

  • Capture. g_dirty_ram_dispatch_seg_bitmap[3] (dirty_ram_interp.h) holds one bit per word for KUSEG, KSEG0 and KSEG1. dirty_ram_dispatch_inner sets the bit for the full PC next to the any-segment bit. A PC in 0x20000000-0x7FFFFFFF or KSEG2 maps no RAM in Beetle, so it gets no bit. Every site that drops dispatch evidence clears the siblings too (dirty_ram_dispatch_evidence_clear()): boot reset, a store into an executed page, DMA preservation and the autocapture epoch. Snapshot jobs copy the siblings with the rest of their evidence.

  • Schema v3 (overlay_capture.c). Each region adds "dispatch_entry_segments": {"kuseg": [...], "kseg0": [...], "kseg1": [...]}, each PC spelled in its segment. dispatch_entry_pcs keeps its v2 meaning and spelling (entered in any segment, written at KSEG0), so v2 readers keep working. coverage_vault.py unions the record when it merges variants and crops it when it compacts regions. A v2 record's entries are KSEG0 entries.

  • Compile (compile_overlays.py). capture_segment_views() splits a capture into one view per segment with entries. A dispatch entry that no segment list names is a KSEG0 entry, and a v2 capture comes back unchanged. A view is the capture seen from one segment: load_addr and every PC in it are spelled in that segment, its dispatch entries are that segment's alone, and it carries all the execution evidence (it has no segment). Two kinds of record also get a view where they saw no dispatch entry:

    • a record with no dispatch entry in any segment (execution evidence only, which the runtime writes) is one KSEG0 view, exactly as a v2 reader reads it, so its classification, forced interiors, declared entries and prior manifests apply as before;
    • a demand from outside the capture names its segment (declared_view_segments()): a --force-interior PC inside the record (KSEG0, since a physical and a KUSEG PC are the same number) asks for the KSEG0 view, and a game.toml [[overlays]] table asks for the segment its load address, or else each of its entries inside the record, is spelled in.

    Each view is one ordinary compile, region and fragment pass alike:

    • the image is compiled at segment | phys, so the recompiler's §5.3 path names and bakes it in that segment;
    • helpers that name PCs without an image address spell them in the view's segment (the image_segment() context);
    • the shard lands in segment_cache_dir().

    The static generator emits the exact PC in its table: KUSEG, KSEG0 and KSEG1 rows of one word coexist, and its segment gate admits the segments it compiled. Its isolated-fragment pass compiles each (entry, image) demand against its own segment's view of the image, so one entry dispatched in two segments gets a fragment in each. --force-interior PCs are KSEG0 as before, so they force KSEG0 views only. [[overlays]] entries in game.toml match their spelled load address.

  • Config code sites. The recompiler compiles an overlay view with overlay_codegen_config() (segment_variants.cpp): every exact-match site kind that D's rebase_codegen_config() moves, and whose physical address lies in the image in any segment that maps RAM, moves into the view's segment. The config names overlay code by its bytes, and those bytes run in any segment. The runtime keys mod function-entry hooks by physical address, and the interpreter fires them at every entry, whatever its segment, so a KUSEG or KSEG1 shard must emit the same psx_mod_function_entry calls as its KSEG0 sibling. Otherwise a hook fires on a cold cache and not on a warm one. Widescreen, data-shard and persisted-store sites reach every view the same way. A KSEG0 view whose sites are spelled at KSEG0, as every pre-§5.7 overlay compile's are, compiles exactly as before.

  • Cache keys, as decided: KSEG0 shards keep their directory and names, and KUSEG and KSEG1 shards go in seg-kuseg/ and seg-kseg1/ under the same cache tag, with the same {phys}_{crc} names. The candidate-capacity namespace counts the segment directories with their leaf, because the runtime's candidate table is shared. Static namespaces are ov_s0_{phys}_… (KUSEG) and ov_s5_… (KSEG1).

  • Manifest. A KUSEG or KSEG1 manifest starts with S <segment> (the base, 00000000 or A0000000) before its first F. F entries are full VAs in that segment. R records stay KSEG0-spelled byte extents. A KSEG0 manifest is unchanged, with no S. The loader (parse_manifest) and the tool (parse_runtime_shard_manifest) apply the same rule, and both refuse:

    • a manifest whose segment differs from its directory's;
    • an F entry outside the manifest's segment;
    • S after an F, S twice, or an S that is not a RAM segment base.
  • Loader. Each candidate records its segment. The physical indexes stay (the bytes are one identity) and every lookup matches the PC's segment:

    • the entry chain;
    • CPS range ownership;
    • the lazy manifest index;
    • try_load_region;
    • the interpreter's call contract (overlay_loader_call_native).

    The negative caches (the lazy-miss and range-owner memos) are keyed by the full PC. A PC in a segment that maps no RAM never runs a shard. Exports are resolved by their full VA. segment_alias_interp now counts the KUSEG and KSEG1 dispatches the loader leaves to the interpreter (no shard of their segment yet), and the new segment_native counts those a shard of their own segment runs. Both are in overlay_loader_status. A warm cache takes the first to 0, except for dispatches at a PC in a segment that maps no RAM (0x20000000-0x7FFFFFFF, KSEG2): those are counted there too and never get a shard (none occur in R4 or the probe). The overlay_candidates reply gains each candidate's seg and each lazy manifest's entry.

  • Full-VA compares. overlay_idle_note_is_internal_or_return compares the resume PC with $ra exactly, and treats it as internal only for a running shard of its own segment. The shadow diff's two "returned to the caller" tests compare the exact PC; these were the last masked compares against a call's link (§5.5). The diff mode's kernel-window exclusion now tests the physical address, so KSEG0 and KUSEG kernel candidates are treated alike. Before, it tested the full PC, so only a KUSEG PC matched, and #417's gate never let one reach it.

  • Tests:

    • overlay_segment_keys (new) runs capture views, manifests, the cache layout, the real compile_overlays.py driver on a v3 capture, and the real loader on the shards it builds. The driver cases also cover dispatch-less records and forced interiors against their v2 reading, a --static isolated fragment per segment of one entry, a KUSEG/KSEG1 view compiled into a cache that holds KSEG0 manifests and fragments of the same region, and mod function-entry hooks spelled in another segment than the view's;
    • segment_variants_test (extended) checks overlay_codegen_config() in every segment's view;
    • overlay_segment_gate (extended) covers, in the loader harness: per-segment shards, stale bytes, CPS continuations, lazy loads, refused manifests (including through live publication), the native-call contract and the capture wiring;
    • overlay_capture_retry_test (extended) checks the schema v3 record;
    • coverage_vault (extended) covers segment unions and crops;
    • the ledger adds the guard overlay-segment-shards (§7.1).

6. Cost for the static fast path

Option A (segment-relative) Option B (variants), this design
Home-segment code +3.8 % __TEXT; +27.6 % with exact KSEG1 charging 0: byte-identical for KSEG0 titles
Per-instruction work an OR at each baked PC, plus a KSEG1 test at about 80k non-leader sites none added
Dispatch segment scoping on every dispatch one row.addr == addr compare per dispatch; with no variants, the same row count as today
ABI every generated function and the overlay DLL ABI unchanged (it relies on #420's v24 store-PC forwarding, which is independent of this design)
Extra code none the requested variant closures only
KSEG1 fetch fix runtime test everywhere a per-instruction charge only in KSEG1 bodies: 5,477 added call sites in OpenBIOS ROM code, paid only while ROM code runs

How the A numbers were measured

  • R4 shards 00, 17 and 33 (911,454 B __TEXT), compiled with the build's own command (Apple clang 21, arm64, -O3).
  • Rewritten in a scratch copy to load the segment once per function and OR it into every link, fetch-tag, IRQ-resume, CPS-exit and store-PC constant: 946,046 B (+3.8 %).
  • With if (seg >= KSEG1) fetch added before each non-leader instruction: 1,163,390 B (+27.6 %).
  • The first measurement (2026-09-28) left out the 1,261 store-PC stamps in these shards, because it treated them as debug-only. Without them the figures were 940,382 B (+3.2 %) and 1,159,826 B (+27.2 %).
  • This is a size measurement, not a timing one. The per-instruction test sits on the hottest path and is only exact if it runs every time.

Why B's figure is 0

  • It follows by construction, not measurement: a KSEG0-linked title (R4 included) is emitted with code_seg = KSEG0, which is today's output. PR B (§8) proves it by byte-identical regeneration.

What B adds at run time

  • One compare per dispatch.
  • The capture bitmaps (§5.7).
  • The recovered native coverage: the ~27 per-frame interpreted KUSEG alias dispatches in R4 become native.

7. Validation against Beetle

7.1 Acceptance ledger (this PR)

test_segment_aware_codegen.py builds the synthetic KUSEG-linked EXE and runs the real psxrecomp-game and psxrecomp-bios on it (OpenBIOS for the latter, 0.3 s). It checks three things:

  • Emitted PC constants in the bodies that dispatch picks for link-segment PCs.
  • Dispatch behaviour, using the emitted lookup compiled and queried:
    • home PCs;
    • requested variants;
    • unrequested aliases, which must miss.
  • Fetch cycles. The emitted fetch sequence runs through the real runtime/src/psx_icache.c. The result must equal the per-instruction sequence Beetle executes, computed by a Python transcription of ReadInstruction.

Model checks (must always pass)

  • psx_icache.c equals the Beetle transcription on:
    • cold then warm cached runs;
    • a KSEG0 → KUSEG alias refill;
    • repeated KSEG1;
    • partial refill.
  • The leader rule is exact for the cached probe block.
  • A mutation of the model to hardware bit-31 tags, or to a +5 uncached cost, fails.

Gap ledger. KNOWN_GAPS in the test lists each id with the section that closes it. The test passes only when the observed gaps equal the ledger. Ten were open when the ledger landed; PR C closed eight and PR D the last two, so KNOWN_GAPS is empty:

id observed before the closing PR closed by state
link-segment 7 link constants, e.g. 0x80010018 §5.3 closed (C)
fetch-tag-segment 30 fetch tags in KSEG0 §5.3 closed (C)
irq-resume-segment 4 resume PCs in KSEG0 §5.3 closed (C)
resume-pc-segment 10 exit PCs / continuation keys in KSEG0 §5.3 closed (C)
store-pc-segment 10 store-PC stamps in KSEG0, e.g. 0x8001000C §5.3 closed (C)
home-seed-accepted KUSEG seed loaded 0 of 1 §5.3 closed (C)
alias-fetch-coherence interp-then-compiled leaf: 22 cycles vs Beetle 11 (the #417 shape) §5.3 closed (C)
segment-variants 0xA00100F0 resolved to the 0x800100F0 body; after C it missed §5.4 closed (D)
segment-miss unrequested KSEG0/KSEG1 aliases resolve; KUSEG PCs resolve to KSEG0 rows §5.5 closed (C)
kseg1-fetch-charge no KSEG1 body (Beetle: 40 cycles for the 10-instruction run) §5.4 + §5.6 closed (D)

Each implementation PR removes the ids it closes. PR A (#429, §8) was the exception: it landed on master before this test did. This PR dropped its id, bios-kseg1-fetch-charge, when it was rebased onto it. The BIOS check stays as a regression guard: an OpenBIOS KSEG1 instruction without its own fetch reports that id again, as a new gap.

PR B closes no id. It adds two more regression guards, which are REGRESSION_GUARDS in the test:

  • bios-runtime-pc (§5.2): no store-PC stamp, syscall EPC, break or unaligned-access PC, or fallthrough PC in the OpenBIOS output names a ROM address inside a relocated window. Before B, 2,295 distinct PCs did (3,328 PC and site-class pairs, since a sh/sw carries both a stamp and an unaligned-access PC), all in the Kernel ramtext and Shell windows. The output only shows the translations OpenBIOS exercises, and most orphaned-delay-slot paths never inline a PC-bearing instruction there, so the guard also requires every StrictTranslator::translate() call in full_function_emitter.cpp to pass relocate_ra().
  • store-pc-keys-runtime (§9): no memory.c store-PC key lies inside one of SCPH1001.toml's relocated ROM windows. A key for relocated code must be the runtime PC of its ROM store, computed from the profile's windows as BiosAddressModel::runtime_pc() does, and must go through the gated scph1001_relocated_store(); a bare RAM key would also match other BIOSes and game code.

PR C removes its eight ids from KNOWN_GAPS and lists them in REGRESSION_GUARDS, so each reports a new gap if it breaks again. resume-pc-segment now also checks every dispatch row's key and resume PC.

PR E closes no id (§5.7 had none in the ledger) and adds the guard overlay-segment-shards, described below; KNOWN_GAPS stays empty.

PR D moves segment-variants and kseg1-fetch-charge to REGRESSION_GUARDS. With variants in the table, resume-pc-segment requires each row's key and resume PC to be in the segment of the body it enters, and segment-variants also requires every PC a variant body bakes (links, fetch tags, IRQ resume PCs, exits and continuation keys, store PCs) and its name to be in its segment. D adds the guard exact-return-contract (§5.5): the test compiles psx_call_contract and checks that another segment's alias of the call site starts a bail and resolves none, and that the emitted OpenBIOS dispatch loop has its four exact return checks and no masked one.

The overlay half (§5.7) gets its own acceptance case in runtime/tests/test_overlay_segment_gate.py:

  • a KUSEG-compiled fixture shard runs natively for its KUSEG PC;
  • the same shard is interpreted for the KSEG0 and KSEG1 aliases.

PR E implements it there, loaded lazily from seg-kuseg/, entry and CPS continuation alike, together with a shard per segment for one word and the manifests the loader must refuse. recompiler/tests/test_overlay_segment_keys.py runs the same property end to end: the real compile_overlays.py builds KUSEG and KSEG1 shards from a v3 capture, and the real loader runs them. Each run links, stamps and fetches in its segment, and costs what Beetle's fetch model charges. PR E also adds the ledger guard overlay-segment-shards. It compiles the probe's disc-loaded ov_run (§7.2) from a v3 capture, one view per segment, with compile_overlays.py's own helpers and the real recompiler. It requires every PC a shard bakes and its names to be in its segment, the S record, and ov_run's straight run to cost Beetle's cycles: line leaders cached at KUSEG and KSEG0, +4 per fetch at KSEG1.

7.2 Oracle runs (implementation PRs)

The synthetic EXE is also an oracle probe. It writes into a results block at 0x00011000, outside the image:

  • the three link values;
  • the executing-segment link from each of the three probe_run entries;
  • a Timer 2 delta around each of those entries.

Since PR E it then loads an overlay (§5.7): it leaves the critical section, reads its own EXE file back from the disc through the BIOS file calls (B0:32 open, B0:34 read, B0:36 close; the kernel DMAs the sectors) into RAM at 0x000A0000, and calls the copy of the position-independent ov_run there through KUSEG, KSEG0 and KSEG1. It records each run's T2 delta and ov_getpc link at R+0x28-R+0x3C, and the file descriptor and byte count at R+0x40/R+0x44. The overlay phase replaces the old j . with a j of the same size, so every earlier label keeps its address.

It then spins. Procedure:

  • Boot it from a tools/cycle_testrom-style disc (SYSTEM.CNF plus the EXE). Do not sideload it: Beetle's EXE loader forces a KSEG0 start address, so a sideloaded run executes at 0x8001… and every link comes back KSEG0. Under OpenBIOS a sideloaded run never reaches the program at all.
  • Read the block from Beetle, from the interpreter (PSX_FORCE_INTERP=1) and from the compiled build, and compare.
  • Expected values: links 0x00010018, 0x00010024, 0x00010038; segment probes 0x000100FC, 0x800100FC and 0xA00100FC. The KUSEG and KSEG0 T2 deltas are equal, and the KSEG1 delta exceeds them by the uncached fetch surcharge: 26 cycles for the 15 KSEG1 fetches (Beetle's fetch model: 60 uncached against 34 cold cached).

Beetle results (2026-09-29). psx-beetle built on macOS (docs/beetle-macos.md, from #431), disc boot, OpenBIOS and SCPH-1001:

  • The first run found a probe bug: the T2 subtraction sat in the second lw's load delay slot and read the stale register, and the difference was not masked to T2's 16 bits. Every delta was unusable. The generator now waits one nop and masks with andi 0xFFFF. It also starts each call sequence and probe_run on their own I-cache lines; before that, alignment alone made the deltas read 59/59/89.
  • Links and segment probes match the expected values above on both BIOSes.
  • T2 deltas: 56 / 56 / 82 cycles for KUSEG / KSEG0 / KSEG1 on both BIOSes. The KSEG1 surcharge is 26, exactly the fetch model's.
  • A cycle watch on the first layout, before these fixes, timed the pieces directly: 10 / 10 / 15 cycles from probe_run entry to getpc, and 8 / 8 / 10 from getpc back.

Interpreter and compiled columns (PR B, 2026-09-29). Both come from a probe runtime built like tools/cycle_testrom's: disc boot, LLE, with seeded seeded as 0x800100E4. The interpreter column needed a tooling fix: PSX_FORCE_INTERP=1 had stopped affecting clean game text. It only marks pages dirty, and the native-safety checks now decide by the bytes, so B restores it.

links segment probes T2 KUSEG / KSEG0 / KSEG1
Beetle 0x00010018/24/38 0x000100FC, 0x800100FC, 0xA00100FC 56 / 56 / 82
interpreter same same 56 / 56 / 82
compiled (B = base) 0x80010018/24/38 0x800100FC × 3 56 / 20 / 20
  • These results are the same on OpenBIOS and SCPH-1001.
  • A cycle watch shows the same split. From one probe_run entry to the next, Beetle and the interpreter take 90 and 90 cycles, and compiled code takes 90 and 54. From probe_run to getpc, Beetle and the interpreter take 10 / 10 / 15 and compiled code takes 10 / 1 / 1.
  • The compiled column is the expected set of gaps:
    • links in KSEG0 (§3.2, closed by C);
    • fetch tags in KSEG0, so the two alias runs hit the lines the home run filled, where Beetle refills or runs uncached (fetch-tag-segment, alias-fetch-coherence, C);
    • no KSEG1 body (D).
  • The compiled probe output is byte-identical before and after B.

Compiled column after PR C (2026-09-29). The probe runtime is rebuilt with the EXE compiled at KUSEG (game.toml load and entry 0x00010000, and seeded seeded as 0x000100E4, the spelling B could not use). The KSEG0 and KSEG1 runs of probe_run have no body of their own, so they are segment misses and run interpreted.

links segment probes T2 KUSEG / KSEG0 / KSEG1
Beetle 0x00010018/24/38 0x000100FC, 0x800100FC, 0xA00100FC 56 / 56 / 82
interpreter same same 56 / 56 / 82
compiled (C) same same 56 / 56 / 82
  • The same on OpenBIOS and SCPH-1001.
  • Cycle watch: from one probe_run entry to the next, all three take 90 and 90 cycles (compiled took 90 and 54 before C); from probe_run to getpc, all three take 10 / 10 / 15 (compiled took 10 / 1 / 1).
  • The compiled build and the interpreter now agree to the cycle for the whole run: the spin is reached at 198,522,602 cycles on OpenBIOS and 398,724,722 on SCPH-1001 in both (B's compiled build: 198,522,504 and 398,724,624, 98 fewer).
  • The segment-miss ring at the spin holds six entries on both BIOSes, each once: probe_run (0x800100F0, 0xA00100F0), its getpc callee (0x80010124, 0xA0010124) and the continuation after that call (0x800100FC, 0xA00100FC), each with its KUSEG row as home.
  • Apart from segments, the compiled probe C equals B's. The dispatch file differs only in the lookup (§5.5).

Compiled column after PR D (2026-09-29). The probe runtime is rebuilt with the variant seeds 0x800100F0 and 0xA00100F0 added to C's seeds. They compile probe_run in KSEG0 and KSEG1 with its getpc callee and the call's continuation (6 variant rows), so every run of probe_run is native.

links segment probes T2 KUSEG / KSEG0 / KSEG1
Beetle 0x00010018/24/38 0x000100FC, 0x800100FC, 0xA00100FC 56 / 56 / 82
interpreter same same 56 / 56 / 82
compiled (D) same same 56 / 56 / 82
  • The same on OpenBIOS and SCPH-1001.
  • Cycle watch: from one probe_run entry to the next, all three take 90 and 90 cycles; from probe_run to getpc, all three take 10 / 10 / 15.
  • The compiled build and the interpreter reach the spin on the same cycle as in C (198,522,602 on OpenBIOS, 398,724,722 on SCPH-1001).
  • The segment-miss ring at the spin is empty on both BIOSes (C: six entries), with 0 dispatch misses.

LLE boot after PR D (2026-09-29). Native − Beetle at each hit, the same anchors as #429's gate:

  • OpenBIOS: unchanged from C (FLUSH ×5 0 → −126, A0 ×12, shell entry −126).
  • SCPH-1001: the first hit of every anchor is 0 (FLUSH, A0, B0 and C0; it was −9 in A, B and C), and every later hit moves by the same +9: the shell entry is −445 (was −454). That is the IsC gap alone (−52, −149, −149 and −95 at SCPH-1001's four boot flushes, §9).
  • With #435 (IsC) merged locally on top of D: both BIOSes are at 0 at every hit of every anchor up to and including the shell entry (OpenBIOS FLUSH ×5, A0 ×12, shell; SCPH-1001 FLUSH ×5, A0 ×17, B0 ×147, C0 ×7, shell).
  • After the shell entry SCPH-1001 still differs, as it did before D: the C0 hit 1,460 cycles after the shell entry is −836 with #435 (−864 relative to the shell entry without it), and a FLUSH about 14 s after power-on is −66,558,224. These lie outside this gate and are not caused by the segment work.

After PR D's review fixes (2026-09-30). The BIOS dispatch now picks the body through psx_bios_hit_body() and records BIOS segment misses (§5.4); the variant rules refuse seeds outside the physical segments. Rerun on the rebuilt probe runtime:

  • Every LLE anchor hit above is identical to D before the fixes, on both BIOSes, with and without #435 on top.
  • The probe's results, spin cycles and cycle-watch intervals are identical.
  • The segment-miss ring at the probe's spin is empty on both BIOSes: neither boot enters a compiled BIOS window through another segment's alias, apart from SCPH-1001's 0xA0000500, which has its variant.
  • The same build with SCPH-1001's 0xA0000500 seed removed records exactly one BIOS segment miss, 0xA0000500 (home 0x00000500, $ra 0xBFC0683C, frame 0), and its shell entry returns to −454.

Overlay phase after PR E (2026-09-30). The probe runtime is rebuilt with overlay_cache = true in its game.toml. At the spin, the cold run's capture (schema v3) holds one region, 0x800A0000, with its dispatch entry 0x000A0A20 in all three segments. compile_overlays.py builds one shard per segment from it: 000A0000_* at KSEG0, seg-kuseg/, seg-kseg1/. Each is an isolated fragment, because ov_run has no callable boundary.

links (ov_getpc) T2 KUSEG / KSEG0 / KSEG1
Beetle 0x000A0A2C, 0x800A0A2C, 0xA00A0A2C 56 / 56 / 82
interpreter (PSX_FORCE_INTERP=1, overlay native off) same 56 / 56 / 82
compiled, cold cache (overlay interpreted) same 56 / 56 / 82
compiled, warm cache (a shard per segment) same 56 / 56 / 82
  • The same on OpenBIOS and SCPH-1001. The fd is 2, and 0x1000 bytes are read, on every backend.
  • Cycle watch at the copy's ov_run and ov_getpc: from one ov_run entry to the next, every backend takes 90 and 90 cycles; from ov_run to ov_getpc, 10 / 10 / 15. The static probe_run watches equal D's on every backend, Beetle included (first hit 198,522,323 native and 224,958,018 Beetle on OpenBIOS; 398,724,443 and 496,646,350 on SCPH-1001).
  • The three native modes reach the spin on the same cycle: 205,367,484 on OpenBIOS and 400,644,561 on SCPH-1001.
  • overlay_loader_status at the spin: cold segment_alias_interp 2 and segment_native 0 (the KUSEG and KSEG1 entries interpreted); warm 0 and 6 (the KUSEG and KSEG1 runs native, each through its ov_getpc call and continuation). There are 0 dispatch misses and 0 segment misses.

LLE boot after PR E (2026-09-30). Every anchor hit on both BIOSes equals D's (FLUSH, A0, B0, C0 and the shell entry, whose native − Beetle stays −126 on OpenBIOS and −445 on SCPH-1001). This is with the overlay cache enabled, which the D probe runtime did not have.

R4 after PR E (2026-09-30). A cold boot-to-race smoke run with the runtime's own autocapture and autocompile produced two non-KSEG0 shards:

  • seg-kuseg/00002000_82812130: OpenBIOS's patch slots 0x0000281C, 0x00003554 and 0x0000357C;
  • seg-kseg1/0000D000_E54B69C3: kernel RAM code entered uncached at 0xA000DFAC, 0xA000DFC0 and 0xA000DFD4.

Neither region was entered at KSEG0 in that run, so it has no KSEG0 shard. The KSEG0 shards earlier compilers built from these regions' KSEG0-spelled entries did not run in these runs either: #417's gate kept the KUSEG and KSEG1 entries off them. Counters:

  • during that run, before and after the shards arrived: segment_alias_interp 101,373, segment_native 21,355;
  • in a warm run: segment_alias_interp 0 and segment_native 66,508 (at frame 8,857), with 0 dispatch misses and 0 segment misses. The native count grows with the frame the smoke script reaches in wall time: earlier warm runs counted 66,309 and 66,344 (frame 8,792).

Every baked PC in both shards is in its segment, and the KSEG1 shard charges a fetch before each of its instructions.

Other runs

  • §5.6 BIOS fix (PR A, #429): done, passed. LLE boot (bios_hle = false) cycle parity against live Beetle to the shell, OpenBIOS and the owner's SCPH-1001 dump, the same image on both sides. Native − Beetle at the shell entry moves from −2,165,591 to −126 cycles (OpenBIOS) and from −7,049,462 to −454 (SCPH-1001). The residue comes from two known gaps that #429 does not cause or widen: IsC stores that do not invalidate the I-cache model (§9) and SCPH-1001's KSEG1 kernel entry (§3.3). Ruler #1 (SCPH-1001) matches pass for pass on all 64 passes, and ruler #2 matches on all 14 components.
  • KUSEG titles: regenerate Kula World and Alien Resurrection and smoke them. Use a warm/cold A/B with the #417 procedure (seeded, PSX_OVERLAY_AUTOCOMPILE_OFF=1, compare cyc/mc/sp plus an order-independent write sum).
    • Kula World (SCES-01000): done, passed (#671, bce31aff, SCPH-1001, 971 functions). Attract demo without input to frame 10,000, cold cache against warm cache (5 shards: 4 in seg-kuseg/, 1 in seg-kseg1/): every judge column and both locators are identical at every frame. segment_alias_interp 172,863 → 733, segment_native 0 → 59,524, 0 dispatch misses, 0 segment misses. Both demos end with Beetle's timer and score (3603 / 9950, 3017 / 6845). Memory card and the ending FMV run too (macOS arm64).
    • Alien Resurrection: open.
  • KSEG0 titles: byte-identical regeneration (§5.2).

8. Rollout

Small PRs in this order. Each PR updates the ledger and FAITHFUL_TIMING_PLAN's log.

  • A: fix/uncached-fetch-per-insn (§5.6). Merged on 2026-09-29 as #429. Both emitters charge psx_icache_fetch before every instruction whose runtime PC is uncached, and the BIOS emitter's cache-line start test uses the runtime PC. Closed bios-kseg1-fetch-charge; this PR dropped the id when it was rebased onto A (§7.1). Beetle parity (LLE boot to the shell, §7.2): native − Beetle at the shell entry is −126 on OpenBIOS and −454 on SCPH-1001, down from −2,165,591 and −7,049,462. The residuals are the IsC gap (§9) and SCPH-1001's KSEG1 kernel entry (§3.3). The oracle recipe for macOS is docs/beetle-macos.md (#431).
  • B: refactor/emitter-runtime-pc (§5.2). No behaviour change for game code, including the store-PC stamps; proven by byte-identical regeneration of KSEG0 titles. The BIOS-emitter holes in §5.2 do change BIOS output. The store-PC one ships with the re-key of the seven memory.c keys: six store-filter keys and the GP0 source key (§9). Implemented 2026-09-29:
    • R4 regenerates byte-identically: 53 files, including the dispatch table, and all 26 overlay shard sources compiled from a race capture.
    • Every changed BIOS line is a runtime_pc() move: 2,334 lines in OpenBIOS and 12,501 in SCPH-1001. The dispatch tables are unchanged.
    • LLE boot against Beetle matches the previous build at every anchor: shell entry −126 on OpenBIOS and −454 on SCPH-1001, as in A's gate.
    • The probe's total cycles to its spin are unchanged on both BIOSes.
    • R4 fingerprints (12000 frames, cold and warm) agree on every column that does not hash store PCs. mmio (a judge column whose ordered hash includes the store PC) and the pc locator first differ at fingerprint frame 4 and stay different, as rolling hashes do. An ordered recording of PSX_RECORD_FRAME=3 (12,238 entries) holds the whole difference: 102 store PCs moved from ROM to runtime in OpenBIOS's Kernel ramtext window. PSX_RECORD_FRAME=4 (14,474 entries) is identical.
    • In an SCPH-1001 LLE boot, the GP0 source key fires for the same 5,596 commands in both builds.
  • C: feat/kuseg-linked-exe (§5.3, §5.5). Also rewrites the alias assertions in test_kuseg_dispatch_lookup.py: an alias with no body now misses. Closes eight ids: §5.3's seven and segment-miss (§5.5). Implemented 2026-09-29:
    • The EXE parser keeps the header's segment and the image compiles there; dispatch is exact; segment misses are interpreted and recorded (ring, TCP segment_misses, dispatch_stats, exit report). Seeds and config sites are checked by physical address and segment (§5.3).
    • The ledger is at two gaps (segment-variants, kseg1-fetch-charge).
    • Probe: compiled equals Beetle and the interpreter in every result word and every cycle-watch interval, on both BIOSes (§7.2).
    • LLE boot against Beetle is identical to B at every hit of every anchor (shell entry −126 OpenBIOS, −454 SCPH-1001). BIOS C is byte-identical.
    • R4 (KSEG0): the 50 game shards, the declarations header and the .ranges manifest are byte-identical; SLUS_007.97_dispatch.c changes only in psx_game_find_entry (the exact-PC compare and its comment). All 26 overlay shard sources compiled from B's race captures are byte-identical. Fingerprints (12000 frames, cold and warm) are identical to B on every column, mmio and the pc/wr locators included. A smoke run reaches a live race with 0 dispatch misses and 0 segment misses.
    • The codegen hash changes, so every title's overlay cache recompiles once and the runtime refuses every savestate, rewind buffer and boot-state cache made before C (the hash is in the state header). A KUSEG title whose game.toml omits entry_pc now gets the header's KUSEG entry, which also keys its save slots.
  • D: feat/segment-variants (§5.4). Closes segment-variants and kseg1-fetch-charge. Since #429, D only needs the KSEG1 variants: with B's runtime_pc(), #429's per-instruction rule charges them (§5.6), and kseg1-fetch-charge stays open until D lands. Also gives the BIOS emitter a KSEG1 variant of SCPH-1001's kernel entry 0xA0000500, which closes the −9 cycles left at that entry in #429's gate (§3.3), and makes the five segment-masked return checks exact together: psx_call_contract and the four in the generated BIOS dispatch loop (§5.5). Implemented 2026-09-29:
    • Segment-qualified seeds compile the direct-edge closure per segment through a segment view of the image; dispatch keeps a row per compiled segment of a word and looks up the exact PC; jump-table cases in another segment than their body are dispatched, in every game and overlay compile (§5.4, §5.5). BIOS seeds for a copy window's alias compile BIOS variants with exact-PC rows, and a BIOS window entered through another segment's alias with no variant is recorded as a BIOS segment miss (§5.4). Seeds in a segment that does not map physical memory stop the build.
    • The ledger is at zero gaps; new guard exact-return-contract. New tests segment_variants_test and bios_segment_variants; the latter compiles the emitted BIOS hit lookup, with and without variants, and queries every variant row, every home key's home PC and every other segment's alias of it. Two single-site mutation sweeps kill every mutant: 21 sites in D (planner, dispatch lookup, jump-table filter, BIOS variant emission, the five checks) and 21 sites of the review fixes (seed refusal, alias-group declarations, the data-stub row filter, the home jump-table filter, the BIOS branch closure and same-segment seeds, the BIOS hit lookup and its recording, the key home PCs, the ring's kind and the seed tool's filters).
    • Probe: every run of probe_run is native and equals Beetle and the interpreter; the segment-miss ring is empty (§7.2).
    • LLE boot: SCPH-1001's first hits are at 0 and its shell entry at −445; with #435 both BIOSes are at 0 through the shell entry (§7.2). Neither boot records a BIOS segment miss.
    • R4 (KSEG0): the 50 game shards, the declarations header, the .ranges manifest and the dispatch table are byte-identical to C, and so are all 26 overlay shard sources compiled from C's race captures. OpenBIOS's full C is unchanged; its dispatch C changes in the four exact return checks and gains psx_bios_key_home_pc() and psx_bios_hit_body(). Fingerprints (12000 frames, cold and warm) are identical to C on every column, mmio and the pc/wr locators included, and an A/A pair is identical; against R4 master's pin they show the same split as B and C (#429). A smoke run reaches a live race with 0 dispatch misses and 0 segment misses of either kind.
    • The codegen hash changes (eea16175 → bd3a72ac: code_generator.*, full_function_emitter.*, ps1_exe_parser.h and cpu_state.h are hashed), so every title's overlay cache recompiles once and pre-D savestates, rewind buffers and boot-state caches are refused. SCPH-1001's generated C gains the variant and its table. Titles without variant seeds get the same game C, except where a table holds another segment's PCs of the image's bytes (§5.4); the same applies to overlay shards.
  • E: feat/overlay-segment-keys (§5.7). Includes the runtime acceptance case, and the R4 warm-cache check that segment_alias_interp reaches 0. It also returns a KUSEG-linked title's overlay code to native shards, which C sends to the interpreter (§5.3). Implemented 2026-09-30:
    • Capture records each interpreted dispatch's segment (schema v3, dispatch_entry_segments). compile_overlays.py compiles one shard per (region, segment with entries), with KUSEG and KSEG1 shards in seg-kuseg/ and seg-kseg1/ and S in their manifests. The loader runs a shard only for PCs of its segment (§5.7). The ledger stays at zero gaps and gains the guard overlay-segment-shards.

    • Tests: the new overlay_segment_keys, and extended versions of overlay_segment_gate (per-segment shards, stale bytes, CPS continuations, lazy loads, refused manifests, the native-call contract, capture wiring), overlay_capture_retry_test, coverage_vault, compile_overlays_static_split, overlay_init_guard and overlay_retry_c11. A single-site mutation sweep of 47 sites kills 42. Of the five survivors, three are equivalent (a validated memo key, the early reject of unmapped segments, and the region pass's segment context, whose canonical PCs are only compared with each other). The other two sit in the opt-in idle-skip note and the opt-in shadow diff, which no unit test drives.

    • Review fixes (2026-09-30): the static isolated-fragment pass keys its images by segment; a dispatch-less record, a forced interior and a declared [[overlays]] table keep their view; overlay views move config code sites into their segment (overlay_codegen_config()); two segment-routing sites that no test reached (the empty-primary reconciliation's and the prior-manifest merge's cache directory) are now tested. A second sweep of 17 single-site mutants of these fixes kills all 17. After them, the probe's shards and R4's game C, OpenBIOS C and every shard source and manifest compiled from D's and E's captures are byte-identical; a cold R4 smoke run's autocompile builds the same KUSEG and KSEG1 shards, and R4 fingerprints (12000 frames, cold and warm) are IDENTICAL to E's, locators included.

    • Probe: the new overlay phase equals Beetle and the interpreter in every result word and cycle-watch interval, cold and warm, on both BIOSes; warm runs its KUSEG and KSEG1 entries natively (§7.2). LLE boot anchors equal D's at every hit.

    • R4 (KSEG0): the 53 game C files, OpenBIOS's C and all 52 shard sources and manifests compiled from D's race captures are byte-identical to D. The codegen hash is unchanged (bd3a72ac); no hashed file changes. Fingerprints (12000 frames):

      • cold, and warm with D's cache, are IDENTICAL to D, locators included;
      • warm with the new KUSEG and KSEG1 kernel shards is IDENTICAL to warm without them, locators included, and to cold with the 501 VBlank straddles any warm-vs-cold pair shows;
      • against R4 master's pin, the same #429 split as B, C and D.

      Smoke runs reach a live race with 0 dispatch misses and 0 segment misses; warm, segment_alias_interp is 0 (§7.2).

    • No existing cache is invalidated, and savestates are unaffected: the codegen hash, the ABI and the KSEG0 layout are unchanged, and the capture bitmaps are host-only.

9. Adjacent gaps (not in scope, recorded)

  • BIU bit 11 does not reach the fetch model. memory.c stores 0xFFFE0130 but psx_icache.c ignores it. Beetle charges +4 for every fetch while the cache is disabled (CPU_SetBIU, 484-505). This matters only for RAM code run with the cache off.

  • IsC stores are dropped (memory.c:1768). Beetle writes the I-cache tag and valid bits through them (WriteMemory_IsC_misc, 623-641), so a FlushCache in psxrecomp does not invalidate the I-cache model. #429's Beetle gate measured it: native − Beetle drops by 42 cycles at each of OpenBIOS's three boot flushes (−126 at the shell), and by 52 / 149 / 149 / 95 at SCPH-1001's four (−445, plus the −9 of §3.3, gives −454). Native keeps hitting lines that Beetle has invalidated. A local prototype that invalidates the model on IsC stores brings OpenBIOS to 0 through the shell and SCPH-1001 to −9 (the §3.3 kernel entry). It is its own small PR, #435. With PR D's kernel-entry variant and #435 together, both BIOSes are at 0 through the shell entry (§7.2).

  • BIOS store-PC keys are ROM addresses. The strict_translator stamp is the ROM address; the interpreter stamps the runtime PC. Seven memory.c keys lie in SCPH1001's relocated windows (bios/SCPH1001.toml). The runtime PC shown for each is what BiosAddressModel::runtime_pc() returns:

    • Kernel Part 2, which runs at 0x500: the RAM filter key 0xBFC10A00 (0x00000F00) and the Tomba key 0xBFC117E4 (0x00001CE4);
    • Shell, which runs at 0x80030000: the RAM filter keys 0xBFC3EEB4 (0x80056EB4), 0xBFC405E4 (0x800585E4), 0xBFC40788 (0x80058788) and 0xBFC41C50 (0x80059C50), and the GP0 source key 0xBFC38B1C (0x80050B1C, memory.c's GP0 write path).

    For these keys a filter fires, or the GP0 write gets a RAM source key, only while that code runs compiled. If the same code runs interpreted, the store lands and the GP0 source stays the register address. Routing the BIOS stamp through runtime_pc() (§5.2) without re-keying would silently disable all seven. Re-keying them to runtime PCs in the same change also makes the interpreted path agree.

    Re-keyed in PR B. The SCPH-1001 output stamps each of the seven runtime PCs exactly once, where it stamped the ROM key before; the four in-place ROM keys (0xBFC04E90, 0xBFC04EF0, 0xBFC05164, 0xBFC0D634) do not move. The GP0 key is live during boot: frames 1-600 of an SCPH-1001 LLE boot send 46,850 GP0 commands, 5,596 of them keyed, identically before and after.

    A RAM PC alone is ambiguous: another BIOS, or game code once the region is reused, can run a store at the same address, where the old ROM key could only match the compiled SCPH-1001 store. So each of the seven keys goes through scph1001_relocated_store(pc, rom), which matches only while the active image is SCPH-1001 (by CRC) and the RAM word at pc is still the ROM word it was copied from. That keeps each key to the one instruction it named before:

    • OpenBIOS, SCPH-101 and SCPH-5552 never match. SCPH-101 and SCPH-5552 have no address model, so their kernel (byte-identical to SCPH-1001's) and their shells run interpreted and stamp runtime PCs; an ungated RAM key would have started firing on them.
    • Game code at those addresses never matches, on any BIOS.
    • The one widening left is intended: SCPH-1001's own instruction now also matches when it runs interpreted, as it already did compiled.

    The filters themselves diverge from Beetle; PR B keeps them as they were. At the segment probe's spin on SCPH-1001 (disc boot, LLE), Beetle's RAM 0x0-0xB holds 00000003 275A0C80 03400008: the delay-loop scratch word and the second and third words of the exception-vector stub. Native holds zeros, because game start clears the low scratch (memory_clear_low_boot_scratch) and the filters drop the later stores: Beetle's write log shows the in-place ROM store 0xBFC0D634 writing word 0 after game start. That is ACCURACY_BURNDOWN axis 4 ("RAM 0x0-0xF boot scratch"), to be decided with oracle evidence on its own.

  • Syscall EPC in compiled code comes from cpu->pc (traps.c:1040). The BIOS emitter sets it; since PR B it sets the runtime PC. The game emitter does not set it.

    • Confirmed in PR B: under CPS a compiled game body runs with cpu->pc == 0, because the entry switch consumes it.
    • The game emitter also ignores psx_syscall's transfer result. The BIOS emitter returns on it.
    • A game syscall that reaches the BIOS exception handler (anything but Enter/ExitCriticalSection, which psx_syscall handles directly) would therefore record EPC 0 and continue inline.
    • R4 has two syscall sites, both Enter/ExitCriticalSection.
    • The fix is to emit cpu->pc = <runtime_pc>; if (psx_syscall(...)) return;, as the BIOS emitter does. That changes the generated code of every title with a syscall site, so it is its own oracle-backed change, not part of B's byte-identical refactor.

10. Decisions (2026-09-29)

The owner delegated the four open questions and accepted this document's recommendations. They are settled; implementation PRs follow them.

  1. I-cache tags follow Beetle, the oracle. Tags compare the full virtual address. Hardware clears bit 31 of KSEG0 fetches first; that difference is recorded in ACCURACY_BURNDOWN axis 4 (§2). A move to hardware tags would be a separate, oracle-backed change.
  2. Segment misses in static game code interpret loudly until the title is regenerated. They are counted, recorded in the TCP-visible ring and taken down the existing clean-text-miss path (§5.5). There is no fail-fast mode.
  3. Extra segments are requested through segment-qualified seeds (§5.4). There is no game.toml table.
  4. The overlay cache uses per-segment subdirectories (seg-kuseg/, seg-kseg1/) under the existing cache tag (§5.7). The filename grammar does not change.