Files
psxrecomp/runtime/tests/test_segment_miss_wiring.py
Shokunin 94be8f6386 recompiler, runtime: compile BIOS segment variants; SCPH-1001 enters its kernel at 0xA0000500
SCPH-1001's reset code enters its relocated kernel through the uncached
alias (lui $a0,0xA000; addiu $a0,$a0,0x500; jr $a0 at ROM 0xBFC0044C).
Beetle charges those four instructions as uncached fetches, 20 cycles; the
BIOS dispatch normalized 0xA0000500 to the kernel body compiled for runtime
0x00000500, which charges cached fetches, 11. That is the -9 cycles left at
SCPH-1001's kernel entry since #429 (docs/SEGMENT_AWARE_CODE.md §3.3).

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 segment variant (§5.4): 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 the seed's segment. Every PC a variant computes from its
own PCs is in that segment (links, fetch tags and the uncached test, IRQ
resume, branch, J and fallthrough targets, store and exception PCs), a KSEG1
variant charges a fetch per instruction, and a jump-table case in another
segment is dispatched instead of continuing locally. Variants and their
continuations get exact-PC rows in a segment_variant_table. A variant seed
that names no emitted function entry stops the build, and so does one in a
segment that does not map physical memory (0x20000000-0x7FFFFFFF, KSEG2).

The dispatch still finds the word by its normalized key (and applies the
kernel-bless guard); a new psx_bios_hit_body() then picks the body for the
exact PC. A variant compiled for that PC runs. Any other PC in another
segment than the one the word's home body was compiled for still runs the
home body, as before, but is now recorded as a BIOS segment miss in the
segment-miss ring (kind "bios", with the home PC), so a new alias entry is
loud instead of silent (§5.5). psx_bios_key_home_pc(), emitted from the
address model, gives each key's home runtime PC: its copy window's runtime
address, or the ROM run in place. The ring, TCP segment_misses and the exit
report carry each entry's kind; tools/collect_game_misses.py lists a BIOS
segment miss for the BIOS seeds instead of writing it into the game's.

SCPH-1001's seeds gain 0xA0000500 (kernel_entry_kseg1): a 4-instruction
KSEG1 variant of kernel_trampoline_0. Every BIOS's dispatch C gains the two
helpers and calls psx_bios_hit_body() where it called the home body. A BIOS
without variant seeds (OpenBIOS, SCPH-101, SCPH-5552) keeps its full C;
SCPH-1001's full C gains the variant body and its dispatch C the one-row
segment_variant_table.

New bios_segment_variants test: OpenBIOS with KSEG1 variants of kernel
0x12BC (closure 0x6F8 by jal and 0x70C by fall-through) and 0x13F4 (0x1424
by a conditional branch only) and a KSEG0 variant of kernel memset 0x540 (a
KUSEG jump table), plus a static check of SCPH-1001's seed. It compiles the
emitted lookup, with and without variants, and queries every variant row,
every home key's home PC (checked against the PC each function body was
compiled for) and every other segment's alias of it. Seeds in a segment
that does not map physical memory stop the build, and a runtime PC in its
window's own segment stays a discovery seed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 00:24:07 -07:00

88 lines
4.5 KiB
Python

"""Segment misses are recorded at dispatch and reach every surface.
docs/SEGMENT_AWARE_CODE.md §5.5 (§10 decision 2): a PC in the EXE's text whose
physical word has a compiled row only in another segment is a segment miss. It
runs interpreted through dirty_ram_dispatch_inner's clean-text-miss path and is
recorded, so the run names each (segment, entry) that needs a seed.
The classification and the ring are unit-tested (test_psx_segment_miss.c,
psx_segment_miss_note included). What a unit test cannot see is the wiring
inside the runtime, which only runs in a full build: deleting the dispatch
hook, the TCP command or a report field would leave every other test green
and make segment misses silent again. This guard pins that wiring and the
`segment_misses` row shape, whose `kind` tells a game segment miss from a
BIOS one (the compiled BIOS ran a window's home body for another segment's
alias PC; bios_segment_variants compiles and queries that emitted check).
"""
from pathlib import Path
import re
ROOT = Path(__file__).resolve().parents[2]
INTERP = (ROOT / 'runtime/src/dirty_ram_interp.c').read_text(encoding='utf-8')
DEBUG = (ROOT / 'runtime/src/debug_server.c').read_text(encoding='utf-8')
CRASH = (ROOT / 'runtime/src/crash_trace.c').read_text(encoding='utf-8')
CMAKE = (ROOT / 'runtime/runtime.cmake').read_text(encoding='utf-8')
def body(source, signature):
start = source.index(signature)
depth, pos = 0, source.index('{', start)
while True:
if source[pos] == '{':
depth += 1
elif source[pos] == '}':
depth -= 1
if depth == 0:
return source[start:pos]
pos += 1
def main():
assert 'runtime/src/psx_segment_miss.c' in CMAKE, 'the runtime does not link psx_segment_miss.c'
# The hook: every clean game-text miss goes to psx_segment_miss_note with
# the full PC, inside the game-dispatch block that sets the flag.
inner = body(INTERP, 'static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, '
'uint32_t stop_addr) {')
block = inner[inner.index('#ifdef PSX_HAS_GAME_DISPATCH'):]
block = block[:block.index('#endif\n\n /* B-2')]
assert re.search(r'clean_game_text_miss = psx_game_address_in_text\(addr\) \? 1 : 0;', block), \
'a compiled-dispatch miss in game text no longer sets clean_game_text_miss'
hook = re.search(r'if \(clean_game_text_miss\)\s*\(void\)psx_segment_miss_note\(addr, '
r'psx_game_is_function_entry,\s*cpu->gpr\[31\], cpu->gpr\[29\],', block)
assert hook, 'dirty_ram_dispatch_inner no longer notes segment misses on a clean-text miss'
assert block.index('psx_dispatch_game_compiled(cpu, addr)') < hook.start(), \
'the segment-miss note must follow the compiled-dispatch attempt'
# TCP: the command, and the counters on dispatch_stats and ping.
assert re.search(r'\{\s*"segment_misses",\s*handle_segment_misses\s*\}', DEBUG), \
'TCP segment_misses is not registered'
seg = body(DEBUG, 'static void handle_segment_misses(int id, const char *json)')
# The summary row shape (the seed tools read `pc` and `kind`: a "bios"
# row belongs in the BIOS seeds, not the game's).
assert '\\"summary\\":[' in seg and \
('{\\"pc\\":\\"0x%08X\\",\\"home\\":\\"0x%08X\\",\\"count\\":%llu,"\n'
' "\\"kind\\":\\"%s\\"}"') in seg and \
'psx_segment_miss_kind_name(kinds[i])' in seg, \
'segment_misses summary rows changed shape'
assert '\\"kind\\":\\"%s\\"}' in seg and 'psx_segment_miss_kind_name(e.kind)' in seg, \
'segment_misses tail entries lost their kind'
stats = body(DEBUG, 'static void handle_dispatch_stats(int id, const char *json)')
assert 'segment_miss_total' in stats and 'psx_segment_miss_total()' in stats
assert 'segment_miss_unique' in stats and 'psx_segment_miss_unique()' in stats
ping = body(DEBUG, 'static void handle_ping(int id, const char *json)')
assert 'segment_miss_total' in ping and 'psx_segment_miss_total()' in ping, \
'ping no longer surfaces segment misses'
# Exit report (psx_last_run_report.json).
dump = body(CRASH, 'void psx_crash_trace_dump(const char *reason, void *seh_info) {')
assert '\\"segment_misses\\"' in dump and 'psx_segment_miss_summary(' in dump, \
'the exit report lost its segment_misses section'
assert 'psx_segment_miss_kind_name(kinds[i])' in dump, \
'the exit report lost the segment-miss kind'
print('segment-miss wiring guards passed')
if __name__ == '__main__':
main()