Files
psxrecomp/bios/SCPH1001.toml
T
Kevin 3cbe6c1cde kernel-bless: publish install-slot ranges so patched BIOS bodies run native
The BIOS exception handler ran interpreted for the life of every process,
1.22 billion instructions and 44.6% of all interpreted work in Breath of
Fire III, even though it is compiled and even though the stub that
unblessed it sat at exactly the address bios/SCPH1001.toml already
declared as an install slot. Two mechanisms existed and did not compose:
the emitter hooked the declared slot PC, while the runtime's bless
verifier memcmp'd the whole body including that slot, so the body was
MISMATCH forever and native code never reached the hook.

Three parts, none of which pays off alone:

1. Publish the declared ranges to the runtime. The emitter emits a
   PsxKernelPatchRange table next to the kernel body table and routes it
   through the backend descriptor; the bless verifier compares a body in
   segments that skip them. CLEAN now means "every byte outside every
   declared range still matches ROM", which is the claim the native code
   actually depends on.

2. Generalise the slot model to what the patches are. The old model
   assumed a 4-word lui/addiu/jalr/nop stub written over ROM zeros and
   fired on `word != 0`. Measured against both images, only one of five
   observed shapes fits: patchers also plant a `jr` over real ROM
   instructions, rewrite an 11-word prologue and insert an `mfc0 Cause`,
   and NOP out a driver routine. A slot is now a range with a length and
   an explicit `resume` (jalr / fallthrough / none), detected by
   comparing every word against its ROM-baked value.

3. Hand back from the interpreter at the range end. Kernel page 0 is
   permanently dirty, because the handler saves registers there on every
   exception, so straight-line interpretation would run the whole
   function after the patch. The emitter registers the resume PC as a
   continuation key and dirty_ram_interp surfaces there.

A PC inside a declared range must never be a native dispatch key: the
compiled bytes there are not what executes. Seven pre-existing
jal-return continuations violated that and wedged the boot at frame 0 in
an infinite dispatch loop (OpenBIOS RAM 0x357C). The emitter now drops
continuation keys inside a range so dispatch misses through to the
interpreter, and refuses to emit if a range covers a function entry,
which would silently remove a callable function instead.

Both profiles declare their measured ranges, and SCPH1001.toml finally
pins [program.image] sha256 to v2.2 (CRC32 37157331) so a v2.0 dump
refuses to generate rather than producing phantom discovery gaps.

Measured on Breath of Fire III (SLPS-00990, Psy-Q SDK 3.70), headless,
matched frames, PSX_KERNEL_PATCH_RANGES=0 for the OFF side:

                      OpenBIOS            SCPH-1001 v2.2
  bless mismatch      21 -> 0             74 -> 0
  kernel bodies       -87.7%/frame        -97.8%/frame
  all interp insns    -42.6%/frame        -94.2%/frame

The residual kernel-body work is exactly declared-words x entries on
every range, so nothing leaks past the hook. The A0/B0/C0 vector
trampolines are unchanged (+0.2%, noise): the profile excludes them by
design and they are a separate job. Card read traces are byte-identical
across the A/B on both images, all 32 entries, which exercises the two
ranges that replace the kernel's card handler.

Rule 18 holds throughout: the guest's patched instructions still execute
as written on the dirty-RAM interpreter. Only the code around them
changes backend. Nothing is synthesised.

Tests: runtime/tests/test_kernel_patch_ranges.c (new, the verifier's
accept/reject behaviour and the hand-back predicate) and the install-slot
range invariants in recompiler/tests/bios_address_model_test.cpp.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 16:38:34 -04:00

139 lines
6.4 KiB
TOML

# bios/SCPH1001.toml — BIOS build profile for the retail Sony SCPH-1001 image.
#
# One profile per BIOS image. Consumed by `psxrecomp-bios --config`, by
# tools/regen_bios.sh (which reads rom/seeds/out_dir/out_stem from here), and
# by tools/bios_emitter_fingerprint.sh (which hashes this file, the seeds and
# the ROM into the generated/<stem>.emitter.sha staleness stamp).
#
# The .BIN image itself stays untracked (bios/* gitignore rule) — profiles are
# the only tracked contents of this directory.
[program]
name = "Sony SCPH-1001 BIOS"
id = "SCPH-1001"
rom = "bios/SCPH1001.BIN"
load_address = "0xBFC00000"
entry_pc = "0xBFC00000"
text_size = "0x80000"
# Declared identity. The emitter refuses to generate from any other image
# (main_bios.cpp's declared-identity gate), which is the point: the seeds in
# this profile name v2.2 function starts, and generating from a SCPH-1001
# **v2.0** dump (CRC32 55847D8C, 1994-09-22) produced three boot halts and a
# live A0 table with 90 of 161 ROM targets uncompiled. That looked like
# missing upstream seeds and was actually a revision mismatch. An empty pin
# let it through silently.
#
# This is v2.2, 1995-09-18: CRC32 37157331, 512 KiB.
[program.image]
sha256 = "71af94d1e47a68c11e8fdb9f8368040601514a42a5a399cda48c7d3bff1e99d3"
license = "proprietary"
redistributable = false
[recompiler]
seeds = "recompiler/seeds/phase2_ghidra_seeds.json"
out_dir = "generated"
out_stem = "SCPH1001"
strict = true
# The BIOS's boot-time bulk copies of CODE out of ROM into RAM. Single source
# of truth for every relocation window in the recompiler AND in the C it
# emits (the generated dispatch's normalize(), the kernel-bless guard, the
# game/shell-overlap tests). Each entry is a CLAIM that the copy is
# byte-verbatim; the runtime's kernel-bless memcmp enforces it for the
# bless window. All bounds are [lo, hi) with hi EXCLUSIVE.
#
# dispatch_key = "ram": functions in the window are keyed by RAM address
# (normalize folds ROM -> RAM).
# dispatch_key = "rom": functions keep their ROM key (normalize folds the
# RAM alias back to ROM).
[recompiler.address_model]
normalize_mask = "0x1FFFFFFF"
[[recompiler.address_model.copy]]
name = "Kernel Part 2"
rom_lo = "0x1FC10000"
rom_hi = "0x1FC18000"
ram_lo = "0x00000500"
runtime_base = "0x00000500" # KUSEG — the kernel executes uncached-region code cached at 0x500
dispatch_key = "ram"
kernel_bless = true
[[recompiler.address_model.copy]]
name = "Shell"
rom_lo = "0x1FC18000"
rom_hi = "0x1FC43000"
ram_lo = "0x00030000"
runtime_base = "0x80030000" # KSEG0 — LoadRunShell jumps to 0x80030000
dispatch_key = "rom"
kernel_bless = false
# NOTE (do not "fix" without evidence): runtime/src/bios_hle.c documents the
# actual LoadRunShell copy as 0x67FF0 bytes (ROM 0xBFC18000 -> RAM 0x30000..
# 0x97FF0). Every pre-profile window constant covered only 0x2B000 of that,
# and dirty_ram_interp.c tracks live RAM in [0x5B000,0x8F000) as an open
# class-B bug. Widening this window is a REAL BEHAVIOR CHANGE requiring its
# own evidence-backed commit — it is not a parameterization cleanup.
# Kernel-RAM RANGES the BIOS and the game's Psy-Q libapi patchers overwrite at
# runtime (CLAUDE.md rule 18; docs/dynamic_handler_install.md). The emitter
# plants a compare-against-ROM hook at each range start so the guest's patched
# instructions execute; the runtime excludes the range from the kernel-bless
# memcmp and resumes native at the range end, so only the patched words
# interpret.
#
# `resume` says how the compiled body picks up again: "jalr" for the classic
# 4-word lui/addiu/jalr/nop stub (the call returns to ram_addr+0x10),
# "fallthrough" when the patched words ARE the function, "none" when the patch
# jumps out and never comes back.
#
# Measured on a live boot of Breath of Fire III (SLPS-00990, Psy-Q SDK 3.70)
# against the ROM source, 30 s in, identical at t+8 s — the patchers are
# _patch_gte / _patch_card / _patch_card2 / _patch_pad. Every SDK title runs
# these, so the ranges are a property of the BIOS revision and libapi release,
# not of one game. Re-measure per SDK version before trusting the numbers:
# the schema tolerates declaring the union.
[[recompiler.install_slots]]
ram_addr = "0x00000C88" # exception handler: register-save prologue
len = "0x30" # shifted two words + an inserted mfc0 v0,Cause
resume = "fallthrough" # the patched words are the function itself
[[recompiler.install_slots]]
ram_addr = "0x00000CF0" # SIO data-byte handler dispatch slot
len = "0x10" # lui/addiu/jalr/nop over ROM zeros
resume = "jalr" # the stub's call returns to 0x0D00
[[recompiler.install_slots]]
ram_addr = "0x00004964" # pad driver clear routine, 11 words NOP'd
len = "0x2C" # (RemovePatchPad / ChgclrPAD family)
resume = "fallthrough"
[[recompiler.install_slots]]
ram_addr = "0x00004D98" # _patch_card2 payload: jalr into boot EXE
len = "0x14" # 0x8017EB6C; the ra landing word is NOP'd too,
resume = "fallthrough" # so the body resumes past it, not at +0x10
[[recompiler.install_slots]]
ram_addr = "0x00006444" # _patch_card payload: the kernel's card
len = "0x10" # handler replaced by jr into boot EXE
resume = "none" # 0x8017EAA0 — a jr never returns here
# NOT declared: RAM 0x0500, one word of kernel_trampoline_0 cleared to zero
# (ROM `lui t2, 0`). It differs on every boot, but what writes it is not yet
# identified, and a range declared on a guess would tell the bless verifier to
# stop checking a word it should be checking. Rule 14: unknown is acceptable,
# guessing is not. Undeclared means that body keeps interpreting, exactly as
# it does today.
# Per-image anchors couriered into the generated C (psx_bios_image) for the
# runtime HLE tier. Values are FACTS about this image, not tunables: the
# shell entry is where LoadRunShell (ROM 0xBFC06FF0) jumps after its copy;
# deliver_event_ret is the instruction after the kernel DeliverEvent loop's
# `jalr v0` (kernel RAM 0x1718), the $ra a delivered event callback returns
# through. Omit a key on a BIOS that has no such anchor — the runtime then
# treats that HLE feature as structurally unavailable instead of firing on a
# wrong address.
[recompiler.runtime_exports]
shell_entry_phys = "0x00030000"
deliver_event_ret = "0x80001720"