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

91 lines
3.6 KiB
C

/* psx_bios_backend.h — the routing seam between the runtime and whichever
* recompiled BIOS is active.
*
* Every build links more than one recompiled BIOS (bundled OpenBIOS plus a
* retail image). Each one exports exactly ONE symbol — its backend descriptor,
* named <STEM>_psx_bios_backend — and everything else it defines is either
* static or stem-prefixed, so the images cannot collide at link time.
*
* The runtime picks a backend at startup (docs/BIOS_SELECTION.md: no explicit
* player choice -> OpenBIOS; an explicit, identity-matching choice -> that
* image) and publishes it as psx_bios_active.
*
* Callers do not go through this struct. The unprefixed psx_dispatch() /
* psx_dispatch_call() the game's generated C and the runtime already call are
* thin forwarders (psx_bios_backend.c) that route to the active backend, and
* psx_bios_image is assigned from it at selection time — so adding a second
* BIOS changed no call sites.
*
* Adding another BIOS later is one more descriptor and one more registry
* entry; it is not another round of symbol collisions.
*/
#ifndef PSX_BIOS_BACKEND_H
#define PSX_BIOS_BACKEND_H
#include <stdint.h>
#include "psx_bios_image.h"
#ifdef __cplusplus
extern "C" {
#endif
struct CPUState;
typedef struct PsxBiosBackend {
/* Identity, kernel-bless window and HLE anchors for this image. Copied
* into the global psx_bios_image when this backend is selected. */
const PsxBiosImageInfo *image;
/* Dispatch entry points for this image. */
void (*dispatch)(struct CPUState *cpu, uint32_t addr);
void (*dispatch_call)(struct CPUState *cpu, uint32_t addr,
uint32_t return_addr);
/* Kernel body-extent table used by the kernel-image bless mechanism
* (memory.c). Published as psx_bios_kernel_bodies/_count on selection.
*
* The native call-stub table is deliberately absent: it is only read by
* the generated dispatch itself, so it stays static there. */
const PsxKernelBody *kernel_bodies;
uint32_t kernel_body_count;
/* Kernel-RAM ranges the guest legitimately patches at runtime, from this
* image's [[recompiler.install_slots]] (psx_bios_image.h). The bless
* verifier skips them and the dirty-RAM interpreter resumes native at
* each range's hi. Null/0 for an image that declares no slots. */
const PsxKernelPatchRange *kernel_patch_ranges;
uint32_t kernel_patch_range_count;
} PsxBiosBackend;
/* The backend in use. Null before psx_bios_select() runs; every forwarder and
* every consumer of psx_bios_image depends on it being set first. */
extern const PsxBiosBackend *psx_bios_active;
/* Backends compiled into this binary, in preference order (bundled OpenBIOS
* first). Emitted by the build as psx_bios_registry.c. */
extern const PsxBiosBackend *const psx_bios_registry[];
extern const uint32_t psx_bios_registry_count;
/* Look up a compiled-in backend by its profile id ("SCPH-1001", "OPENBIOS").
* Null if this build does not carry it. */
const PsxBiosBackend *psx_bios_find(const char *image_id);
/* Select a backend and publish it (also assigns the global psx_bios_image).
* Returns 0 if backend is null. */
int psx_bios_activate(const PsxBiosBackend *backend);
/* The bundled, redistributable backend (image_bundled != 0), or null if this
* build has none. */
const PsxBiosBackend *psx_bios_bundled(void);
/* True if a player-supplied image is meaningful for this build (some linked
* backend is not the bundled one). */
int psx_bios_has_selectable(void);
#ifdef __cplusplus
}
#endif
#endif /* PSX_BIOS_BACKEND_H */