Files
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

153 lines
7.5 KiB
TOML

# bios/OpenBIOS.toml — BIOS build profile for PCSX-Redux OpenBIOS.
#
# OpenBIOS is the PCSX-Redux project's from-scratch, MIT-licensed PS1 BIOS
# (https://github.com/grumpycoders/pcsx-redux, src/mips/openbios). MIT means
# the image is REDISTRIBUTABLE: a game verified against it ships its BIOS
# with the build and the player supplies only a disc (redistributable=true
# below removes the whole BIOS-selection surface at runtime).
#
# Pinned build (bios/openbios.bin + the ELFs the seeds derive from):
# pcsx-redux 55fbf0468345acfbe5512628a3be5c0cfe3f22e7
# uC-sdk 69e06871824e2d62069487a7426ded09090ceb69
# toolchain g++-mipsel-none-elf (GCC 16.1.0, static.grumpycoder.net build)
# build make -C src/mips/openbios PREFIX=mipsel-none-elf FORMAT=elf32-littlemips
# Regenerating seeds after a re-pin (--extra is REQUIRED to keep the 5
# empirically-discovered dispatch-miss seeds; without it they are silently
# dropped and the regenerated set has 641 entries, not 646):
# tools/gen_bios_seeds_from_elf.py --profile bios/OpenBIOS.toml \
# --elf .../openbios.elf --elf .../shell.elf \
# --extra recompiler/seeds/openbios_dispatch_miss.json \
# --out recompiler/seeds/openbios_elf_seeds.json
#
# The address model below was established by direct disassembly of the boot
# path (reset vector -> copy loops), each copy confirmed twice (boot + the
# warm-restart path reloading lengths from ROM constants; the shell length
# additionally confirmed by the blob's own self-relocation constants).
[program]
name = "PCSX-Redux OpenBIOS"
id = "OPENBIOS"
rom = "bios/openbios.bin"
load_address = "0xBFC00000"
entry_pc = "0xBFC00000"
text_size = "0x80000"
[program.image]
sha256 = "fabe498fbf224e4721f12f31b6f5fe0659205e341dc4e5c5f91b9bd1a1011c57"
license = "MIT"
redistributable = true
[recompiler]
seeds = "recompiler/seeds/openbios_elf_seeds.json"
out_dir = "generated"
out_stem = "OpenBIOS"
strict = true
# OpenBIOS's boot-time bulk code copies. Windows are BYTE-PROVEN against
# openbios.elf (readelf -lW program headers give the .data LMA; the embedded
# shell blob was located by content search; both ranges memcmp-verified
# ELF-content == image-content — an earlier hand-disassembly of the copy
# routines mis-read both source offsets by 0xB8/0x2C0-class deltas, which
# emitted every shell/ramtext function from shifted bytes):
# - kernel .data (VMA 0x500, WAX — code+data): LMA 0xBFC1E4D4, FileSiz
# 0x46A0, copied to RAM 0x500 at boot. Byte-verbatim -> kernel_bless.
# - shell blob (objcopy of shell.elf, linked at 0x80030000): embedded in
# .rodata at ROM 0xA500, size 0x13D44, copied to RAM 0x80030000. The
# first-crash a1=0xBFC1E244 was exactly this blob's END pointer mid-copy.
# NOT modeled, by design:
# - the 4x16-byte vector stubs copied to RAM 0x80/0xA0/0xB0/0xC0
# (runtime-written trampolines -> dirty-RAM interp territory, rule 18);
# - the RAM->RAM A0-table move (0x4844 -> 0x200): nothing dispatches TO the
# table, discovery gets entries from ELF symbols, and runtime dispatch
# keys on the actual jump targets.
[recompiler.address_model]
normalize_mask = "0x1FFFFFFF"
[[recompiler.address_model.copy]]
name = "Kernel ramtext"
rom_lo = "0x1FC1E4D4"
rom_hi = "0x1FC22B74"
ram_lo = "0x00000500"
runtime_base = "0x00000500"
dispatch_key = "ram"
kernel_bless = true
[[recompiler.address_model.copy]]
name = "Shell"
rom_lo = "0x1FC0A500"
rom_hi = "0x1FC1E244"
ram_lo = "0x00030000"
runtime_base = "0x80030000"
dispatch_key = "rom"
kernel_bless = false
# Kernel-RAM RANGES the guest's Psy-Q libapi patchers overwrite at runtime
# (CLAUDE.md rule 18; docs/dynamic_handler_install.md). See SCPH1001.toml for
# what `len` and `resume` mean. OpenBIOS declared none until 2026-09-11; the
# consequence was that its exception handler failed the kernel-bless memcmp
# from the first card access onward and interpreted for the life of the
# process — 1.22 billion instructions in one measured title.
#
# Measured on a live boot of Breath of Fire III (SLPS-00990, Psy-Q SDK 3.70)
# against the ROM source, 30 s in. The shapes are the same three the retail
# image shows, because the patchers are the game's, not the BIOS's.
[[recompiler.install_slots]]
ram_addr = "0x000027B4" # exceptionHandler: register-save prologue
len = "0x30" # shifted two words + an inserted mfc0 v0,Cause
resume = "fallthrough"
[[recompiler.install_slots]]
ram_addr = "0x0000281C" # stub into exceptionHandlerCardFastTrack
len = "0x10" # lui/addiu/jalr/nop over ROM zeros; installed
resume = "jalr" # on the first card access, not at boot
[[recompiler.install_slots]]
ram_addr = "0x0000357C" # exceptionHandlerCardFastTrack: the kernel's
len = "0x10" # card handler replaced by jr into boot EXE
resume = "none" # 0x8017EAA0 (_patch_card payload)
# NOT declared: the three pad-table words after readPadHighLevel (0x45FC,
# 0x460C, 0x461C). They are data, not instructions, so they unbless nothing
# and a range there would only widen what the verifier stops checking.
# HLE anchors: only the boot-skip anchor is exported. deliver_event_ret is
# deliberately ABSENT (0) until the B0 event services are validated against
# this kernel's EvCB behavior (they are believed compatible — OpenBIOS
# reimplements the documented kernel ABI, EvCB anchor 0x120/0x124, TCB
# 0x108/0x110 — but believed is not verified). Absent = the kernel-call HLE
# tier is STRUCTURALLY UNAVAILABLE here and config/env cannot force it on.
#
# That refusal is scoped to kernel calls ONLY. The boot-skip is a separate axis
# gated on shell_entry_phys alone and works under pure LLE, so `bios_hle` (and
# the deprecated `fast_boot` alias) skips the boot animation and goes straight
# to the game on THIS image exactly as it does on retail SCPH-1001 — the flag
# means one thing on every BIOS. Both axes are decided in one pure place,
# psx_bios_hle_plan() (runtime/include/bios_hle_plan.h), which exists because
# deriving the boot-skip from the anchor-gated call tier silently took the skip
# away on this image (fixed 2026-07-27).
#
# shell_entry_phys was verified against the generated C (2026-07-23):
# - startShell (ELF symbol, ROM 0xBFC074DC) ends `lui $t9,0x8003; jr $t9`
# (0xBFC07534/38) — a TAIL jump to the RAM shell copy at 0x80030000
# that goes through the dispatch trampoline, where the hook fires on
# the pre-normalize phys 0x00030000;
# - $ra at that point is the jal link 0xBFC06EA8 (startShell restores its
# stack before the tail jump), so the intercept's resume-at-$ra lands
# exactly at the shell call's continuation;
# - that continuation OVERWRITES $v0 immediately (addiu $v0,$zero,8 — a
# boot-progress POST byte), so the intercept's v0=0 is never observed,
# and the boot proceeds unconditionally to the SYSTEM.CNF/EXE load
# (gameMainThunk), exactly the retail LoadRunShell contract the skip
# models. The shell's own animation (waitVSync/flip/convexHull in
# shell.elf) is the only thing skipped.
[recompiler.runtime_exports]
shell_entry_phys = "0x00030000"
# No [runtime] block: a BIOS profile states facts about an image, never runtime
# preferences, and load_bios_config rejects one outright. This file used to end
# with `[runtime] bios_hle = false`, which nothing read — the recompiler parsed
# it into a field with no consumers. Runtime options live in game.toml /
# settings.toml; per-image HLE capability is expressed by which anchors
# [recompiler.runtime_exports] above does and does not export.