Files
psxrecomp/runtime/include/kernel_patch_ranges.h
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

59 lines
2.5 KiB
C

/* kernel_patch_ranges.h — the pure half of the kernel patch-range mechanism.
*
* The guest legitimately overwrites words inside compiled BIOS kernel bodies
* at boot: the Psy-Q libapi patchers (_patch_gte / _patch_card / _patch_card2
* / _patch_pad) and the BIOS's own install stubs. Those ranges are declared
* per image by [[recompiler.install_slots]] and emitted next to the kernel
* body table (psx_bios_image.h, PsxKernelPatchRange).
*
* These two functions are the whole decision, kept free of runtime globals so
* they can be tested directly (runtime/tests/test_kernel_patch_ranges.c).
* memory.c wraps them with its snapshotted window constants; dirty_ram_interp.c
* reads the second one through that wrapper.
*
* Rule 18: the patched words still execute, on the dirty-RAM interpreter,
* exactly as the guest wrote them. Nothing here synthesises behaviour — it
* only decides which backend runs which words.
*/
#ifndef PSX_KERNEL_PATCH_RANGES_H
#define PSX_KERNEL_PATCH_RANGES_H
#include <stdint.h>
#include "psx_bios_image.h"
#ifdef __cplusplus
extern "C" {
#endif
/* Compare live kernel RAM [lo, hi) against its ROM source, SKIPPING every
* declared patch range that intersects it. Returns 0 when every compared
* segment matches (memcmp's "equal" sense), non-zero otherwise.
*
* ram/rom point at the two images; `ram_lo` is the RAM address that
* rom[rom_off] corresponds to, so a byte at RAM address a is ram[a] and
* rom[rom_off + (a - ram_lo)]. `ranges` must be sorted by lo and
* non-overlapping (BiosAddressModel::from_config validates and sorts, so a
* malformed profile cannot reach here). `skips_out`, when non-null, is
* incremented once per skipped segment.
*
* A body with NO intersecting range degenerates to a single memcmp of the
* whole extent — the original behaviour, unchanged. */
int psx_kernel_patch_cmp(const PsxKernelPatchRange *ranges, uint32_t n,
const uint8_t *ram, const uint8_t *rom,
uint32_t ram_lo, uint32_t rom_off,
uint32_t lo, uint32_t hi,
uint64_t *skips_out);
/* Does a declared range END at this RAM address? The emitter registered each
* range's hi as a continuation key, so the interpreter hands straight-line
* flow back to static dispatch there and only the patched words interpret. */
int psx_kernel_patch_ends_at(const PsxKernelPatchRange *ranges, uint32_t n,
uint32_t phys);
#ifdef __cplusplus
}
#endif
#endif /* PSX_KERNEL_PATCH_RANGES_H */