Files
psxrecomp/runtime/tests/test_mirror_pc_preserved_guards.py
Shokunin d67f0f9396 recompiler: compile a KUSEG-linked EXE for its link segment; exact dispatch
A PS-X EXE header may carry KUSEG addresses (Alien Resurrection SLUS-00633,
Kula World SCES-01000); the BIOS then runs the program at KUSEG PCs, so its
links, EPCs and I-cache tags are KUSEG. The parser folded such a header to
KSEG0, so the whole program compiled for a segment it never runs in, and
the dispatch lookup matched any segment by physical address
(docs/SEGMENT_AWARE_CODE.md §3.2, §5.3, §5.5).

- EXE parser: keeps the header's addresses. link_segment() is the load
  address's segment; the load address and entry must share it and lie in
  the 8 MiB RAM window of KUSEG, KSEG0 or KSEG1 (KSEG1 compiles as an
  uncached home segment). phys_load_address()/contains_phys() do byte
  addressing; the BSS overlap check and the text_size bound are physical.
- function_analysis: the three JAL scans take the target's top bits from
  the delay slot's PC instead of assuming KSEG0 (unchanged for KSEG0).
- Seeds are range-checked by physical address. A seed in the link segment
  is an entry (0x000100E4 was silently dropped before). A seed for the same
  bytes in another segment asks for a segment variant (§5.4): it is listed
  in a WARNING, never folded into the home body. Seeds outside the image are
  counted. The producer_range, cross_call_allow and hosted_interior
  directives have no variant form: a foreign spelling is refused with the
  link-segment one instead of a bare "invalid" error.
- Config code sites that are matched exactly (widescreen and cull sites,
  mod hooks, hot/data-shard funcs, the VSync-query function and its
  event-horizon return PCs, persisted options) and name the image's bytes
  in another segment are refused with the spelling that matches, since they
  would now match nothing. Kinds matched by physical address (byte patches,
  the full-word-guarded cull keep/angle/aspect-cone and signed-X-bound
  sites, dome call sites) reach the same bytes in any spelling and are not
  refused. Overlay compiles are exempt until PR E.
- tools/collect_game_misses.py wrote every dirty-RAM miss as a KSEG0 seed,
  which a KUSEG-linked image now reports instead of compiling. It writes
  them in the link segment (--segment, --game-toml, or a segmented text
  range; it refuses to guess from a physical one) and adds the
  segment_misses rows at their full PC.
- Dispatch source moves to game_dispatch_emitter.cpp. The lookup indexes
  the physical word, then requires row.addr == addr; a second row for one
  word is an error until variants share the table. psx_game_address_in_text
  stays physical, so a segment miss takes the clean-text-miss path (the
  previous commit records it).

For a KSEG0 title the game C is byte-identical; its dispatch file changes
only in psx_game_find_entry (the exact-PC compare) and its comment.

Tests:
- segment_aware_codegen closes link-segment, fetch-tag-segment,
  irq-resume-segment, resume-pc-segment, store-pc-segment,
  home-seed-accepted, alias-fetch-coherence and segment-miss; they stay as
  regression guards, and resume-pc-segment now also checks the dispatch
  rows' keys and resume PCs.
- kuseg_dispatch_lookup: KUSEG rows, names and baked PCs; aliases miss while
  text stays physical; variant seeds reported; a function reached only by
  jal and a SaveState-style continuation are discovered in a KUSEG image
  (kills the two discovery scans reverted to KSEG0; the third, the
  invalid-JAL data heuristic, reads the same bits either way); mixed-segment
  headers rejected; a KSEG1 image charged per instruction; foreign config
  sites refused; the RAM-mirror contract as before.
- kuseg_dispatch_lookup also compiles the lookup's binary-search form (a
  table spanning 2 MiB or more) and queries every segment's alias of each
  row; refuses a KSEG0 event-horizon site and KSEG0 seeds directives with
  their KUSEG spelling; accepts a KSEG0 signed_x_bound site and applies it.
- emitter_runtime_pc_test: the dispatch rows follow runtime_pc() and keep
  the compile identity in func_ names (kills B's two surviving mutants: 76
  and 12 failing checks); two rows on one physical word are an error.
- collect_game_misses (new, tools/tests): link-segment seeds from each
  source, segment misses at their full PC, no guessing, no duplicates,
  against a fake server using the runtime's own summary row format.
- reachable_discovery_test: the parser keeps KUSEG, accepts KSEG1, rejects
  mixed segments and non-RAM load addresses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 21:57:34 -07:00

82 lines
4.1 KiB
Python

"""A PC in a retail RAM mirror keeps its own address (8 MB mod off or on).
Retail DRAM is 2 MiB mirrored four times across the 8 MiB decode window. Code
running at 0x807xxxxx executes the bytes at 0x801xxxxx, but its PC, the $ra a
jal writes and the EPC an interrupt saves are 0x807xxxxx on hardware. Folding
the PC (the 8 MB branch once rewrote `addr` at the top of dirty_ram_dispatch
and in the emitted game lookups) makes compiled 0x801xxxxx bodies run for it
and hands the guest folded return addresses.
The contract (runtime/include/psx_memory.h): CODE identity is the segment-
stripped PC, never mirror-folded; BYTE identity (fetch, dirty/overlay page
checks) folds through the live geometry. The behavioural half lives in
recompiler/tests/test_kuseg_dispatch_lookup.py (mirror PCs never resolve to a
compiled body, in either geometry, against the real psx_ram_geometry.c) and
runtime/tests/test_psx_ram_runtime_map.c (mirror bytes fold only on retail).
This guard pins the interpreter side, which cannot run outside the runtime.
"""
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')
MEMORY = (ROOT / 'runtime/src/memory.c').read_text(encoding='utf-8')
EMITTER = (ROOT / 'recompiler/src/full_function_emitter.cpp').read_text(encoding='utf-8')
GAME = (ROOT / 'recompiler/src/game_dispatch_emitter.cpp').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():
# No code-address canonicalizer exists anywhere: nothing can fold a PC.
for path in list((ROOT / 'runtime/src').glob('*.c*')) + \
list((ROOT / 'runtime/include').glob('*.h')) + \
list((ROOT / 'recompiler/src').glob('*.cpp')):
text = path.read_text(encoding='utf-8', errors='replace')
assert 'canon_code_addr' not in text, f'{path.name}: PC canonicalizer is back'
# dirty_ram_dispatch runs at exactly the PC it was handed.
outer = body(INTERP, 'int dirty_ram_dispatch(CPUState* cpu, uint32_t addr, uint32_t stop_addr) {')
assert not re.search(r'\baddr\s*=[^=]', outer), 'dirty_ram_dispatch rewrites addr'
assert not re.search(r'\bstop_addr\s*=[^=]', outer), 'dirty_ram_dispatch rewrites stop_addr'
assert 'dirty_ram_dispatch_inner(cpu, addr, stop_addr)' in outer
# Inside, code identity and byte identity are separate names.
inner = body(INTERP, 'static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_addr) {')
assert 'uint32_t phys = addr & 0x1FFFFFFFu;' in inner
assert 'const uint32_t ram_phys = psx_ram_map_read(phys);' in inner
assert 'uint32_t pc = addr;' in inner, 'the interpreted PC must start at addr'
assert 'phys_is_overlay_region(ram_phys)' in inner
assert 'phys_is_overlay_flow_region(ram_phys)' in inner
assert 'dirty_ram_mark_executable_range(ram_phys, 4u)' in inner
local = body(INTERP, 'static int is_local_dirty_target(uint32_t target) {')
assert 'psx_ram_map_read(target)' in local
fetch = body(INTERP, 'static inline uint32_t fetch_word(uint32_t phys) {')
assert 'phys = psx_ram_map_read(phys);' in fetch, 'fetch must read the folded bytes'
dirty = body(MEMORY, 'int dirty_ram_is_dirty(uint32_t phys) {')
assert dirty.index('phys = psx_ram_map_read(phys);') < dirty.index('if (phys >= RAM_LIVE) return 0;')
# The emitted dispatchers hand the unmodified PC to every lookup.
assert 'found = dirty_ram_dispatch(cpu, addr, stop_addr);' in EMITTER
assert 'uint32_t game_phys = addr & 0x1FFFFFFFu;' in EMITTER
find = GAME[GAME.index('static const PsxGameDispatchEntry* psx_game_find_entry'):]
find = find[:find.index('ds << "}\\n\\n";')]
assert 'const uint32_t want = addr & 0x1FFFFFFFu;' in find
assert 'psx_ram' not in find, 'game lookup must not fold mirror PCs'
print('mirror PC preserved guards: PASS')
if __name__ == '__main__':
main()