bios: HLE as a first-class swappable tier (gbarecomp model) + HLE boot shell-skip

USER PIVOT 2026-07-02 (CLAUDE.md §0 amendment included): psxrecomp now
carries an opt-in High-Level Emulation backend alongside LLE. LLE (the
recompiled BIOS) stays the default, the reference implementation, and the
correctness/timing oracle; every service HLE does not implement falls
through to LLE, so HLE is never load-bearing beyond what it covers.

Mechanism: full_function_emitter.cpp emits a null-by-default hook
g_psx_bios_hle_hook consulted at the top of every psx_dispatch_impl
iteration, on the pre-normalize physical address, BEFORE any backend
(game image / dirty-RAM / static table) claims the target. A handled
call resumes at $ra through the trampoline's existing return/tail
contract. NULL (default) = pure LLE, dispatch-identical. Regenerating
BIOS+game images is required (emitter change).

v1 call-HLE (runtime/src/bios_hle.c): the B0 event family — DeliverEvent
(incl. callback dispatch with the kernel-true $ra 0x1720), OpenEvent,
CloseEvent, TestEvent, EnableEvent, DisableEvent — each ground-truthed
instruction-by-instruction against the SCPH1001 kernel disassembly
(0xBFC11644..0xBFC11A84) and operating on the REAL guest EvCB table
([0x120]/[0x124], stride 0x1C). WaitEvent/threads/pads/card/A0/C0 fall
through to LLE. Cycle charges are documented approximations; LLE remains
the timing oracle. No-stubs stands.

Boot HLE: with the tier on (and no keep-intro opt-out) the hook one-shot
intercepts the first dispatch of the BIOS shell entry (RAM 0x30000 —
LoadRunShell's indirect call always routes through the dispatcher), so
BIOS Main() proceeds straight to SYSTEM.CNF + game EXE load with kernel
state built entirely by the real recompiled kernel init, unpaced (boot
turbo) until game handoff. This REPLACES the fast_boot snapshot
capture/restore; fast_boot=true now aliases the boot-skip alone.

Selection: [runtime] bios_hle / bios_hle_keep_intro (+ settings.toml
bios_hle mirror), PSX_BIOS_HLE / PSX_BIOS_HLE_KEEP_INTRO env overrides,
startup banner bios_backend=/bios_boot=. The PSX_HLE_SCHEDULER spike is
folded in as the tier's standing subsystem replacement: default from
[runtime] hle_scheduler via psx_hle_scheduler_set_default, env wins.

Observability: always-on 16K-entry HLE ring (route LLE-fallthrough /
HLE / boot-skip per record) + hle_dump TCP command (TCP_COMMANDS.md).

VALIDATED on Tomba (build-wt): LLE pass byte-identical behavior (real
intro, card check, save/load OK); HLE pass boots shell-less, TestEvent/
DeliverEvent route HLE, ChangeTh/ReturnFromException/OpenTh/CloseTh and
the card stack fall through to LLE, save/load OK (user-driven).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Matthew Stanley
2026-07-02 16:27:33 -07:00
parent 13c5e0c5b5
commit 582aeccdac
10 changed files with 556 additions and 10 deletions
+34 -1
View File
@@ -106,8 +106,41 @@ faithful core is proven, the goals are accelerated load times (toward 0) and
enhancements (widescreen), where per-game shims/hacks become legitimate
(`ENHANCEMENTS.md`). Per-game hacks remain forbidden during foundation work.
**AMENDMENT 2026-07-02 — HLE is a standing, swappable TIER (the gbarecomp
model), not just a per-landmine carve-out.** This goes further than the
2026-06-29 amendment (which permits HLE only as a targeted subsystem
replacement at an LLE landmine). User-directed pivot: psxrecomp now carries a
first-class High-Level Emulation tier alongside LLE, modeled on
`F:/Projects/gbarecomp/gbarecomp` (`src/runtime/bios_hle.{h,cpp}`, commits
23a57ce + 168e313):
1. **Two selectable backends, LLE default.** A null-by-default hook intercepts
BIOS service dispatch before the recompiled BIOS runs; selection is
per-game config (`[bios] hle = true`) + CLI + env override, with a startup
banner naming the active backend. With HLE off the build is byte-identical
to a build without the tier.
2. **LLE remains the reference implementation and the oracle.** It stays fully
linked, load-bearing, and selectable; every BIOS call the HLE layer does
not implement transparently falls through to the recompiled BIOS, so HLE is
never load-bearing beyond what it covers and never becomes the verification
oracle.
3. **HLE boot is THE boot-skip mechanism.** Skipping BIOS boot = synthesize
the exact post-boot kernel handoff state (kernel tables, vectors, EvCB/TCB,
per-mode state) and jump to the game entry; the recompiled BIOS stays
linked for exception/IRQ dispatch and call fallback. This deprecates the
previous fast-boot mechanism. LLE always plays the real boot.
4. **No-stubs still stands, unchanged.** Every HLE implementation must be a
real, validated implementation of the documented kernel mechanism
(`docs/psx_bios_disasm.txt` / PSX-SPX, Beetle-oracle-checked), operating on
the real guest structures — never a "return the answer the BIOS would have
produced" fake. The discriminator from the 2026-06-29 amendment applies to
every handler.
5. **The HLE layer is an observability surface.** It carries always-on ring
buffers (calls, routes, arguments, results) queryable via the TCP debug
server, per rule 3 and the global ring-buffer rule.
If you find yourself wanting to violate any of the above three
paragraphs **beyond the narrow, oracle-validated amendment just above**,
paragraphs **beyond the two amendments just above**,
**stop and re-read PLAN.md**. Every prior attempt failed by
violating exactly these rules under pressure.
+25
View File
@@ -213,6 +213,31 @@ on a fixed region -> next.
## 5. Status / Log (update every session)
- **2026-07-02 (HLE PIVOT implemented — HLE as a first-class swappable tier, gbarecomp model):**
USER-DIRECTED pivot (supersedes "no HLE" §0; CLAUDE.md amended 2026-07-02, memory
hle_tier_architecture.md). Built the full stack this session: (1) EMITTER —
full_function_emitter.cpp now emits a null-by-default `g_psx_bios_hle_hook` consult at
the top of every psx_dispatch_impl iteration (pre-normalize phys, BEFORE the game/
dirty-RAM/static backends; handled ⇒ resume at $ra; NULL default = pure LLE,
dispatch-identical). (2) RUNTIME TIER — runtime/src/bios_hle.c(+.h): v1 call-HLE =
the B0 event family (DeliverEvent/OpenEvent/CloseEvent/TestEvent/EnableEvent/
DisableEvent) ground-truthed against the SCPH1001 kernel disassembly (Ghidra,
0xBFC11644..0xBFC11A84; EvCB [0x120]/[0x124], stride 0x1C), operating on the real
guest EvCBs, callback delivery via psx_dispatch_call with the kernel-true $ra
0x1720; everything else (WaitEvent/threads/pads/card/A0/C0) falls through to LLE.
(3) BOOT HLE — one-shot shell-entry intercept (RAM 0x30000; LoadRunShell's indirect
call always dispatches): real recompiled kernel init + SYSTEM.CNF + EXE load run
authentically under boot-turbo; only the shell (boot animation) is skipped. The old
fast_boot snapshot restore is REMOVED (fast_boot=true now aliases boot-skip only).
(4) SELECTION — [runtime] bios_hle / bios_hle_keep_intro / hle_scheduler in
config_loader (+ settings.toml bios_hle mirror), PSX_BIOS_HLE / PSX_BIOS_HLE_KEEP_INTRO
env, startup banner bios_backend=/bios_boot=. PSX_HLE_SCHEDULER spike folded in
(default via psx_hle_scheduler_set_default, env wins). (5) OBSERVABILITY — always-on
16K HLE ring (route LLE/HLE/boot-skip) + hle_dump TCP command (TCP_COMMANDS.md).
Regen era-consistent: BIOS + Tomba + MMX6 images (emitter changed). NEXT: Tomba
save+load validation under BOTH backends (user drives); overlay-shard cg-tag refresh
per title; grow the handler set (UnDeliverEvent, RCnt, A0 libc) with kernel-decompile
+ Beetle checks per handler.
- **2026-07-01 (Tomba pause-menu wedge RESOLVED — stale-recompiler shards, guard shipped):**
The post-merge Tomba menu wedge (loaded saves only) was NOT the IRQ-resume class the
prior handoff claimed. Added an always-on exception-EXIT half to irqctx_ring
+17
View File
@@ -143,6 +143,23 @@ re-dispatches the guest's true target. Counters in
---
## `hle_dump` — BIOS-HLE tier call ring (native only)
Always-on ring (`runtime/src/bios_hle.c`, 16K entries) recording every
A0/B0/C0 kernel-vector dispatch the HLE tier's hook observes, plus the boot
shell-skip event. Empty in pure-LLE mode (the tier installs no hook there —
use `bioscall_dump` for LLE-side vector observation).
- `{"cmd":"hle_dump"}` — status: `backend` (`HLE (LLE fallback)` /
`LLE (recompiled BIOS)`), `boot_skip`, `boot_turbo_active`, `total`.
- `{"cmd":"hle_dump","tail":N}` — last N entries: `seq`, `cycle` (guest
cycle), `vec` (0xA0/0xB0/0xC0, or 0x30000 for the boot skip), `fn` ($t1
function number), `a0..a3`, `ra`, `v0` (result when HLE-serviced), `route`
(0 = fell through to LLE, 1 = serviced in HLE, 2 = boot shell-skip).
- Filters: `"fn":N`, `"route":0|1|2`.
---
## Rule when the server can't answer your question
If an inspection need isn't covered by the existing commands, **do not fall back to printf or log files**. Instead:
+14
View File
@@ -129,6 +129,15 @@ static RuntimeConfig parse_runtime_block(const toml::value& cfg, const fs::path&
if (runtime.contains("fast_boot")) {
rt.fast_boot = toml::find<bool>(runtime, "fast_boot");
}
if (runtime.contains("bios_hle")) {
rt.bios_hle = toml::find<bool>(runtime, "bios_hle");
}
if (runtime.contains("bios_hle_keep_intro")) {
rt.bios_hle_keep_intro = toml::find<bool>(runtime, "bios_hle_keep_intro");
}
if (runtime.contains("hle_scheduler")) {
rt.hle_scheduler = toml::find<bool>(runtime, "hle_scheduler");
}
if (runtime.contains("overlay_cache")) {
rt.overlay_cache = toml::find<bool>(runtime, "overlay_cache");
}
@@ -833,6 +842,9 @@ UserSettings load_user_settings(const fs::path& path) {
if (v.contains("fast_boot")) try_get([&]{
s.fast_boot = toml::find<bool>(v, "fast_boot"); s.has_fast_boot = true;
});
if (v.contains("bios_hle")) try_get([&]{
s.bios_hle = toml::find<bool>(v, "bios_hle"); s.has_bios_hle = true;
});
if (v.contains("fullscreen")) try_get([&]{
s.fullscreen = toml::find<bool>(v, "fullscreen"); s.has_fullscreen = true;
});
@@ -983,6 +995,8 @@ bool save_user_settings(const fs::path& path, const UserSettings& s) {
f << "turbo_loads = " << (s.turbo_loads ? "true" : "false") << "\n";
if (s.has_fast_boot)
f << "fast_boot = " << (s.fast_boot ? "true" : "false") << "\n";
if (s.has_bios_hle)
f << "bios_hle = " << (s.bios_hle ? "true" : "false") << "\n";
if (s.has_fullscreen)
f << "fullscreen = " << (s.fullscreen ? "true" : "false") << "\n";
if (s.has_low_latency_input)
+26 -3
View File
@@ -71,11 +71,32 @@ struct RuntimeConfig {
bool has_instant_max_per_frame = false;
int instant_max_per_frame = 0;
// fast_boot: snapshot BIOS state at first game handoff and restore it on
// subsequent launches, skipping BIOS execution entirely. Default off;
// enable per-game in [runtime]. Snapshot is keyed on BIOS SHA256 + entry_pc.
// fast_boot: DEPRECATED alias for the HLE boot shell-skip (see bios_hle
// below). The old mechanism (snapshot BIOS state at first handoff, restore
// on later launches) is gone; fast_boot=true now skips only the BIOS shell
// (the boot animation) via the HLE tier's one-shot shell intercept, with
// kernel init + game EXE load still executed by the real recompiled BIOS
// at host speed. Kept so existing game.toml/settings.toml keep working.
bool fast_boot = false;
// bios_hle: opt-in High-Level Emulation tier for BIOS kernel services
// (CLAUDE.md §0 amendment 2026-07-02, the gbarecomp model). Default off =
// LLE (the recompiled BIOS), which remains the reference implementation
// and the oracle. When on, implemented kernel services are computed
// in-runtime against the real guest kernel structures and every other
// call falls through to LLE. Implies the HLE boot shell-skip unless
// bios_hle_keep_intro. PSX_BIOS_HLE / PSX_BIOS_HLE_KEEP_INTRO env
// override at launch. Runtime: runtime/src/bios_hle.c.
bool bios_hle = false;
bool bios_hle_keep_intro = false;
// hle_scheduler: the HLE tier's standing SUBSYSTEM REPLACEMENT for guest
// thread switching (deterministic TCB scheduler vs the legacy
// non-deterministic host-fiber bridge). Default ON under BOTH BIOS
// backends (CLAUDE.md §0 amendment 2026-06-29 carve-out); PSX_HLE_SCHEDULER
// env wins over this key. Runtime: traps.c psx_hle_scheduler_enabled().
bool hle_scheduler = true;
// overlay_cache: enable the overlay DLL cache + capture (Layer A). Off by
// default. When true the runtime scans cache/<game_id>/ for precompiled
// overlay DLLs (loaded ahead of the dirty-RAM interpreter) and records
@@ -470,6 +491,8 @@ struct UserSettings {
// settings.toml overrides it).
bool has_turbo_loads = false; bool turbo_loads = true;
bool has_fast_boot = false; bool fast_boot = false;
// HLE BIOS tier toggle (see RuntimeConfig::bios_hle). Overrides game.toml.
bool has_bios_hle = false; bool bios_hle = false;
// [video] fullscreen: launch the game window in desktop fullscreen (the
// launcher's "Fullscreen on launch" toggle; the in-game F11 / Alt+Enter
// hotkey still toggles it live). false => windowed (default).
+24 -2
View File
@@ -1217,7 +1217,16 @@ void FullFunctionEmitter::emit_dispatch(
out += "extern void gte_write_data(CPUState* cpu, uint8_t reg, uint32_t val);\n";
out += "extern uint32_t gte_read_data(CPUState* cpu, uint8_t reg);\n";
out += "extern uint32_t g_debug_current_func_addr;\n";
out += "extern void debug_server_trace_dispatch(uint32_t func_addr);\n\n";
out += "extern void debug_server_trace_dispatch(uint32_t func_addr);\n";
out += "/* BIOS HLE tier (CLAUDE.md \xC2\xA7""0 amendment 2026-07-02): null-by-default\n";
out += " * hook consulted at the top of every dispatch iteration, BEFORE any\n";
out += " * backend (game image / dirty-RAM interp / static table) claims the\n";
out += " * target. Installed by the runtime (bios_hle.c) when [runtime] bios_hle\n";
out += " * is on; returns 1 when it fully serviced the target, and the guest\n";
out += " * resumes at $ra exactly as if the kernel routine executed jr $ra.\n";
out += " * NULL (the default) = pure LLE, dispatch identical to a build without\n";
out += " * the tier. */\n";
out += "extern int (*g_psx_bios_hle_hook)(CPUState* cpu, uint32_t phys);\n\n";
out += "#ifdef PSX_HAS_GAME_DISPATCH\n";
out += "extern int psx_game_address_in_text(uint32_t addr);\n";
out += "#endif\n\n";
@@ -1408,6 +1417,19 @@ void FullFunctionEmitter::emit_dispatch(
out += " cpu->pc = 0;\n";
out += fmt::format(" int lo = 0, hi = {} - 1;\n", total_entries);
out += " int found = 0;\n";
out += " /* BIOS HLE tier: consult the hook FIRST, on the pre-normalize\n";
out += " * physical address. It must see (a) the A0/B0/C0 service vectors\n";
out += " * (game thunks jr there with the function number in $t1) and (b)\n";
out += " * the shell entry for HLE boot-skip \xE2\x80\x94 the latter lies inside the\n";
out += " * game-text/dirty-RAM window, so the check cannot sit after those\n";
out += " * backends. Handled (rc 1) \xE2\x87\x92 the service completed against guest\n";
out += " * state and the guest resumes at $ra via the trampoline's normal\n";
out += " * return/tail contract below. rc 0 \xE2\x87\x92 pure LLE fall-through. */\n";
out += " if (g_psx_bios_hle_hook &&\n";
out += " g_psx_bios_hle_hook(cpu, addr & 0x1FFFFFFFu)) {\n";
out += " cpu->pc = cpu->gpr[31];\n";
out += " found = 1;\n";
out += " }\n";
out += "#ifdef PSX_HAS_GAME_DISPATCH\n";
out += " /* Game EXEs can overlap the BIOS shell copy window at\n";
out += " * physical 0x30000-0x5AFFF. If the target belongs to the\n";
@@ -1426,7 +1448,7 @@ void FullFunctionEmitter::emit_dispatch(
out += " extern int fntrace_is_game_started(void);\n";
out += " int game_shell_overlap = fntrace_is_game_started() &&\n";
out += " game_phys >= 0x00030000u && game_phys <= 0x0005AFFFu;\n";
out += " if (psx_game_address_in_text(addr) &&\n";
out += " if (!found && psx_game_address_in_text(addr) &&\n";
out += " (dirty_ram_is_dirty(game_phys) || game_shell_overlap)) {\n";
out += " found = dirty_ram_dispatch(cpu, addr, stop_addr);\n";
out += " }\n";
+103
View File
@@ -0,0 +1,103 @@
/* bios_hle.h — opt-in High-Level Emulation tier for PSX BIOS kernel services.
*
* CLAUDE.md §0 AMENDMENT 2026-07-02 (the gbarecomp model, see
* F:/Projects/gbarecomp/gbarecomp/src/runtime/bios_hle.h): the recompiled real
* BIOS (LLE) is the DEFAULT, the reference implementation, and the
* correctness/timing oracle. HLE is an *alternative backend*, selected
* per-game via `[runtime] bios_hle = true` in game.toml (PSX_BIOS_HLE env
* override). Every kernel service the HLE layer does not implement
* transparently falls through to the recompiled BIOS, so HLE is never
* load-bearing beyond what it covers and never becomes the oracle.
*
* Mechanism: psx_bios_hle_configure() installs (or leaves NULL) the dispatch
* hook g_psx_bios_hle_hook consulted at the top of every psx_dispatch_impl
* iteration (emitted by full_function_emitter.cpp), BEFORE any backend claims
* the target. The hook sees the pre-normalize physical address; when the
* target is a kernel service vector (0xA0/0xB0/0xC0, function number in $t1)
* and the service is implemented, the handler computes its effect directly on
* guest state (the REAL kernel structures in guest RAM — EvCB table etc., per
* docs/psx_bios_disasm.txt + the SCPH1001 kernel disassembly), charges an
* approximate cycle cost, and returns 1: the guest resumes at $ra with no
* BIOS dispatch. NULL hook (default) = pure LLE, byte-identical dispatch.
*
* Boot HLE: with the tier on (and no keep-intro opt-out) the hook also
* intercepts the first dispatch of the BIOS SHELL entry (RAM 0x30000 — the
* boot animation; LoadRunShell's indirect `0x80030000()` call always routes
* through the dispatcher) and returns immediately, so BIOS Main() proceeds
* straight to step 8 (SYSTEM.CNF + game EXE load) with kernel state built
* entirely by the REAL recompiled kernel init. The frontend runs unpaced
* (turbo) until game handoff. This is THE boot-skip mechanism; it deprecates
* the old fast_boot snapshot restore. LLE always plays the real intro.
*
* NO-STUBS STANDS: every handler here is a validated reimplementation of the
* documented kernel mechanism operating on the real guest structures — never
* a "return the answer" fake. Timing is approximate (documented limitation;
* LLE remains the timing oracle).
*
* Observability (CLAUDE.md rule 3 + the global ring-buffer rule): every hook
* decision is recorded in an always-on ring queryable over the TCP debug
* server ("hle_dump"), never armed, never polled.
*/
#ifndef PSXRECOMP_BIOS_HLE_H
#define PSXRECOMP_BIOS_HLE_H
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
struct CPUState;
/* The dispatch hook slot the generated trampoline consults (defined in
* bios_hle.c, emitted-extern by full_function_emitter.cpp). NULL = pure LLE. */
extern int (*g_psx_bios_hle_hook)(struct CPUState* cpu, uint32_t phys);
/* Select the BIOS backend. Call once at bring-up, after config + env
* resolution, before psx_scheduler_run. Two independent axes:
* call_hle — service implemented kernel calls in HLE ([runtime] bios_hle)
* boot_skip — one-shot shell intercept ((bios_hle && !keep_intro) OR the
* deprecated fast_boot alias, which skips boot only)
* The hook is installed when either axis is on; both off = NULL = pure LLE. */
void psx_bios_hle_configure(int call_hle, int boot_skip);
int psx_bios_hle_enabled(void);
int psx_bios_hle_boot_skip_enabled(void);
const char* psx_bios_hle_backend_name(void); /* startup banner */
/* True while the HLE boot-skip is driving the frontend unpaced: boot skip
* enabled and the game entry PC has not yet been dispatched. main.cpp ORs
* this into the turbo predicate so kernel init + EXE load run at host speed. */
int psx_bios_hle_boot_turbo_active(void);
/* ── Always-on HLE ring (queried by debug_server "hle_dump") ─────────────── */
enum {
PSX_HLE_ROUTE_LLE = 0, /* vector call seen, fell through to recompiled BIOS */
PSX_HLE_ROUTE_HLE = 1, /* serviced in HLE */
PSX_HLE_ROUTE_BOOT = 2, /* boot shell-skip fired */
};
typedef struct PsxHleCallEntry {
uint64_t seq;
uint64_t cycle; /* psx_cycle_count at record time */
uint32_t vector; /* 0xA0/0xB0/0xC0, or 0x30000 for the boot skip */
uint32_t fn; /* $t1 function number (0 for boot skip) */
uint32_t a0, a1, a2, a3;
uint32_t ra;
uint32_t v0; /* result for HLE-serviced calls (0 otherwise) */
uint8_t route; /* PSX_HLE_ROUTE_* */
} PsxHleCallEntry;
#define PSX_HLE_RING_CAP 16384 /* power of two */
/* Snapshot accessors: seq is the total number of records ever written; entry
* i (seq-CAP .. seq-1 valid window) returns the ring slot for that sequence. */
uint64_t psx_hle_ring_seq(void);
const PsxHleCallEntry* psx_hle_ring_entry(uint64_t seq_index);
#ifdef __cplusplus
}
#endif
#endif /* PSXRECOMP_BIOS_HLE_H */
+14 -4
View File
@@ -78,10 +78,20 @@ int psx_is_dispatchable(uint32_t pc);
struct CPUState;
void psx_scheduler_run(struct CPUState* cpu);
/* Hidden dev toggle (env PSX_HLE_SCHEDULER, default 1 = HLE). 1 = deterministic
* TCB scheduler (carve-out); 0 = legacy host-fiber bridge (LLE). Read once at
* first call; the mode cannot change mid-run. */
int psx_hle_scheduler_enabled(void);
/* Save-state restore: unwind to psx_scheduler_run and re-dispatch resume_pc
* (the restored CPUState's PC). Call on the scheduler fiber at a block-leader
* boundary, in_exception == 0. Never returns (longjmp). */
void psx_scheduler_resume_at(uint32_t resume_pc);
/* HLE-tier standing subsystem replacement (CLAUDE.md §0 amendments
* 2026-06-29 + 2026-07-02). 1 = deterministic TCB scheduler (default, both
* BIOS backends — the LLE host-fiber bridge it replaces is non-deterministic);
* 0 = legacy host-fiber bridge. Default from [runtime] hle_scheduler via
* psx_hle_scheduler_set_default (call before any dispatch); env
* PSX_HLE_SCHEDULER wins over config. Latched at first query; the mode cannot
* change mid-run. */
void psx_hle_scheduler_set_default(int on);
int psx_hle_scheduler_enabled(void);
#ifdef __cplusplus
}
+2
View File
@@ -154,6 +154,8 @@ set(PSXRECOMP_RUNTIME_SOURCES
${PSXRECOMP_ROOT}/runtime/src/parity_trace.c
${PSXRECOMP_ROOT}/runtime/src/device_trace.c
${PSXRECOMP_ROOT}/runtime/src/boot_state.c
${PSXRECOMP_ROOT}/runtime/src/bios_hle.c
${PSXRECOMP_ROOT}/runtime/src/savestate.c
${PSXRECOMP_ROOT}/runtime/src/cosim_state.c
${PSXRECOMP_ROOT}/runtime/src/cosim.c
${PSXRECOMP_ROOT}/runtime/src/traps.c
+297
View File
@@ -0,0 +1,297 @@
/* bios_hle.c — opt-in High-Level Emulation tier for PSX BIOS kernel services.
*
* See bios_hle.h for the design (LLE default + oracle, per-game opt-in,
* unimplemented services fall through to the recompiled BIOS, boot shell-skip,
* always-on observability ring).
*
* v1 service coverage — the B0 event family, ground-truthed instruction by
* instruction against the SCPH1001 kernel (ROM 0xBFC11644..0xBFC11A84, kernel
* RAM 0x1B44..0x1F8C; cross-checked with docs/psx_bios_disasm.txt B0 table @
* 0x874 and PSX-SPX "Kernel: Events"):
*
* B0:0x07 DeliverEvent(class, spec)
* B0:0x08 OpenEvent(class, spec, mode, func)
* B0:0x09 CloseEvent(event)
* B0:0x0B TestEvent(event)
* B0:0x0C EnableEvent(event)
* B0:0x0D DisableEvent(event)
*
* EvCB table: base pointer at kernel [0x120], size in bytes at [0x124], one
* 0x1C-byte entry per event: +0x00 class, +0x04 status, +0x08 spec, +0x0C
* mode, +0x10 func. Status values: 0 free, 0x1000 disabled, 0x2000 enabled,
* 0x4000 ready/delivered. Mode values: 0x1000 = call callback on delivery,
*0x2000 = mark ready (polled via TestEvent/WaitEvent).
*
* Everything else (WaitEvent — it blocks; threads; pads; the card/CD stack;
* all of A0/C0) falls through to LLE in v1, exactly like gbarecomp's
* machine-state SWIs. The recompiled BIOS stays fully load-bearing for them.
*
* TIMING NOTE: cycle costs charged here are approximations of the kernel
* routine's dynamic instruction count (vector stub + table handler + body).
* LLE accrues the exact count by executing; cycle-exact HLE is a documented
* limitation of the opt-in tier. LLE remains the timing oracle.
*/
#include "bios_hle.h"
#include "cpu_state.h"
#include "psx_cycles.h"
#include <stddef.h>
/* fntrace.c — game-entry handoff latch (drives the boot turbo window). */
extern int fntrace_is_game_started(void);
/* ── hook slot (referenced by the generated dispatch trampoline) ─────────── */
int (*g_psx_bios_hle_hook)(struct CPUState* cpu, uint32_t phys) = NULL;
/* ── mode state ──────────────────────────────────────────────────────────── */
static int s_call_hle_on = 0; /* service kernel calls in HLE */
static int s_boot_skip_on = 0; /* one-shot shell intercept */
static int s_shell_skipped = 0; /* boot skip already fired */
/* SCPH1001 physical addresses. The shell is unpacked to RAM 0x30000 by
* LoadRunShell (ROM 0xBFC06FF0: memcpy(0x80030000, 0xBFC18000, 0x67FF0)) and
* entered via an indirect call, which always routes through the dispatcher. */
#define PSX_SHELL_ENTRY_PHYS 0x00030000u
/* Kernel EvCB table anchors (docs/psx_bios_disasm.txt; kseg1 like the kernel
* itself uses so reads/writes are plain RAM traffic). */
#define KADDR_EVCB_BASE 0xA0000120u
#define KADDR_EVCB_SIZE 0xA0000124u
/* EvCB field offsets / values (SCPH1001 kernel + PSX-SPX). */
#define EVCB_STRIDE 0x1Cu
#define EV_CLASS 0x00u
#define EV_STATUS 0x04u
#define EV_SPEC 0x08u
#define EV_MODE 0x0Cu
#define EV_FUNC 0x10u
#define EVST_FREE 0x0000u
#define EVST_DISABLED 0x1000u
#define EVST_ENABLED 0x2000u
#define EVST_READY 0x4000u
#define EVMD_CALLBACK 0x1000u
#define EVMD_MARK 0x2000u
/* DeliverEvent's callback return site in kernel RAM (the instruction after
* the kernel loop's `jalr v0` at 0x1718) — the exact $ra an LLE delivery
* gives the callback, reused here so the dispatch return contract matches. */
#define KADDR_DELIVER_RET 0x80001720u
/* ── always-on ring ──────────────────────────────────────────────────────── */
static PsxHleCallEntry s_ring[PSX_HLE_RING_CAP];
static uint64_t s_ring_seq = 0;
static void hle_record(uint32_t vector, uint32_t fn, const CPUState* cpu,
uint32_t v0, uint8_t route)
{
PsxHleCallEntry* e = &s_ring[s_ring_seq & (PSX_HLE_RING_CAP - 1)];
e->seq = s_ring_seq;
e->cycle = psx_get_cycle_count();
e->vector = vector;
e->fn = fn;
e->a0 = cpu->gpr[4];
e->a1 = cpu->gpr[5];
e->a2 = cpu->gpr[6];
e->a3 = cpu->gpr[7];
e->ra = cpu->gpr[31];
e->v0 = v0;
e->route = route;
s_ring_seq++;
}
uint64_t psx_hle_ring_seq(void) { return s_ring_seq; }
const PsxHleCallEntry* psx_hle_ring_entry(uint64_t seq_index)
{
if (seq_index >= s_ring_seq) return NULL;
if (s_ring_seq - seq_index > PSX_HLE_RING_CAP) return NULL; /* evicted */
return &s_ring[seq_index & (PSX_HLE_RING_CAP - 1)];
}
/* ── B0 event services (SCPH1001 kernel semantics, on real guest EvCBs) ──── */
/* get_free_EvCB_slot (kernel 0x1D00): first status==0 slot index, else -1. */
static uint32_t ev_free_slot(const CPUState* cpu, uint32_t base, uint32_t n)
{
for (uint32_t i = 0; i < n; i++) {
if (cpu->read_word(base + i * EVCB_STRIDE + EV_STATUS) == EVST_FREE)
return i;
}
return 0xFFFFFFFFu;
}
/* OpenEvent (kernel 0x1D8C): claim a free EvCB, fill it, status=disabled,
* return 0xF1000000|slot; no free slot => 0xFFFFFFFF. */
static void hle_open_event(CPUState* cpu, uint32_t base, uint32_t n)
{
uint32_t slot = ev_free_slot(cpu, base, n);
if (slot == 0xFFFFFFFFu) {
cpu->gpr[2] = 0xFFFFFFFFu;
} else {
uint32_t ev = base + slot * EVCB_STRIDE;
cpu->write_word(ev + EV_CLASS, cpu->gpr[4]);
cpu->write_word(ev + EV_SPEC, cpu->gpr[5]);
cpu->write_word(ev + EV_MODE, cpu->gpr[6]);
cpu->write_word(ev + EV_FUNC, cpu->gpr[7]);
cpu->write_word(ev + EV_STATUS, EVST_DISABLED);
cpu->gpr[2] = 0xF1000000u | slot;
}
psx_advance_cycles(40u + 6u * (slot == 0xFFFFFFFFu ? n : slot + 1u));
}
/* CloseEvent (kernel 0x1E1C): status=free, return 1. */
static void hle_close_event(CPUState* cpu, uint32_t base)
{
uint32_t ev = base + (cpu->gpr[4] & 0xFFFFu) * EVCB_STRIDE;
cpu->write_word(ev + EV_STATUS, EVST_FREE);
cpu->gpr[2] = 1u;
psx_advance_cycles(36u);
}
/* TestEvent (kernel 0x1EC8): ready => re-arm to enabled and return 1, else 0. */
static void hle_test_event(CPUState* cpu, uint32_t base)
{
uint32_t ev = base + (cpu->gpr[4] & 0xFFFFu) * EVCB_STRIDE;
if (cpu->read_word(ev + EV_STATUS) == EVST_READY) {
cpu->write_word(ev + EV_STATUS, EVST_ENABLED);
cpu->gpr[2] = 1u;
} else {
cpu->gpr[2] = 0u;
}
psx_advance_cycles(40u);
}
/* EnableEvent (kernel 0x1F10) / DisableEvent (kernel 0x1F4C): a non-free
* event's status is set to enabled/disabled; return 1 either way. NOTE the
* kernel really does discard a pending READY on EnableEvent — faithful. */
static void hle_set_event_status(CPUState* cpu, uint32_t base, uint32_t status)
{
uint32_t ev = base + (cpu->gpr[4] & 0xFFFFu) * EVCB_STRIDE;
if (cpu->read_word(ev + EV_STATUS) != EVST_FREE)
cpu->write_word(ev + EV_STATUS, status);
cpu->gpr[2] = 1u;
psx_advance_cycles(40u);
}
/* DeliverEvent (kernel 0x1B44): for every ENABLED EvCB matching class+spec,
* mode MARK => status=READY; mode CALLBACK => call the handler exactly as the
* kernel's `jalr v0` does (same $ra, args untouched). */
static void hle_deliver_event(CPUState* cpu, uint32_t base, uint32_t n)
{
uint32_t cls = cpu->gpr[4];
uint32_t spec = cpu->gpr[5];
for (uint32_t i = 0; i < n; i++) {
uint32_t ev = base + i * EVCB_STRIDE;
if (cpu->read_word(ev + EV_STATUS) != EVST_ENABLED) continue;
if (cpu->read_word(ev + EV_CLASS) != cls) continue;
if (cpu->read_word(ev + EV_SPEC) != spec) continue;
uint32_t mode = cpu->read_word(ev + EV_MODE);
if (mode == EVMD_MARK) {
cpu->write_word(ev + EV_STATUS, EVST_READY);
} else if (mode == EVMD_CALLBACK) {
uint32_t func = cpu->read_word(ev + EV_FUNC);
if (func != 0) {
/* Nested guest call, same shape as any compiled call site.
* $ra is the kernel's real post-jalr address so the callback
* returns through the standard dispatch contract; the caller's
* $ra is restored before the hook returns. */
uint32_t saved_ra = cpu->gpr[31];
cpu->gpr[31] = KADDR_DELIVER_RET;
psx_dispatch_call(cpu, func, KADDR_DELIVER_RET);
cpu->gpr[31] = saved_ra;
}
}
/* mode==anything else: the kernel loop skips it — so do we. */
}
/* Return value: the kernel returns with v0 = last comparison scratch; no
* caller may depend on it (libapi types DeliverEvent void). Set 0 for a
* stable, observable value. */
cpu->gpr[2] = 0u;
psx_advance_cycles(36u + 12u * n);
}
/* B0 vector service. Returns 1 iff serviced in HLE. */
static int hle_service_b0(CPUState* cpu, uint32_t fn)
{
/* Kernel executive not initialized yet (InitEvents hasn't run) => the
* EvCB table doesn't exist; everything stays LLE. */
uint32_t base = cpu->read_word(KADDR_EVCB_BASE);
if (base == 0) return 0;
uint32_t n = cpu->read_word(KADDR_EVCB_SIZE) / EVCB_STRIDE;
if (n == 0 || n > 0x1000u) return 0; /* implausible table => LLE */
switch (fn) {
case 0x07u: hle_deliver_event(cpu, base, n); return 1;
case 0x08u: hle_open_event(cpu, base, n); return 1;
case 0x09u: hle_close_event(cpu, base); return 1;
case 0x0Bu: hle_test_event(cpu, base); return 1;
case 0x0Cu: hle_set_event_status(cpu, base, EVST_ENABLED); return 1;
case 0x0Du: hle_set_event_status(cpu, base, EVST_DISABLED); return 1;
default: return 0; /* WaitEvent, threads, pads, card, ... => LLE */
}
}
/* ── boot shell-skip ─────────────────────────────────────────────────────── */
static int hle_boot_shell_skip(CPUState* cpu)
{
if (!s_boot_skip_on || s_shell_skipped || fntrace_is_game_started())
return 0;
s_shell_skipped = 1;
/* Return straight to LoadRunShell's call site: BIOS Main() proceeds to
* step 8 (SYSTEM.CNF + game EXE load) exactly as if the shell had exited
* immediately with no disc-menu interaction. Kernel state at this point
* was built entirely by the real recompiled kernel init. */
cpu->gpr[2] = 0u;
hle_record(PSX_SHELL_ENTRY_PHYS, 0u, cpu, 0u, PSX_HLE_ROUTE_BOOT);
return 1;
}
int psx_bios_hle_boot_turbo_active(void)
{
return s_boot_skip_on && !fntrace_is_game_started();
}
/* ── the hook ────────────────────────────────────────────────────────────── */
static int bios_hle_dispatch(struct CPUState* cpu, uint32_t phys)
{
if (phys == 0xB0u) {
uint32_t fn = cpu->gpr[9]; /* $t1 — the PSY-Q thunk's function number */
int handled = s_call_hle_on ? hle_service_b0(cpu, fn) : 0;
hle_record(0xB0u, fn, cpu, handled ? cpu->gpr[2] : 0u,
handled ? PSX_HLE_ROUTE_HLE : PSX_HLE_ROUTE_LLE);
return handled;
}
if (phys == 0xA0u || phys == 0xC0u) {
/* No A0/C0 services implemented in v1 — observe + fall through. */
hle_record(phys, cpu->gpr[9], cpu, 0u, PSX_HLE_ROUTE_LLE);
return 0;
}
if (phys == PSX_SHELL_ENTRY_PHYS)
return hle_boot_shell_skip(cpu);
return 0;
}
/* ── selection ───────────────────────────────────────────────────────────── */
void psx_bios_hle_configure(int call_hle, int boot_skip)
{
s_call_hle_on = call_hle ? 1 : 0;
s_boot_skip_on = boot_skip ? 1 : 0;
g_psx_bios_hle_hook =
(s_call_hle_on || s_boot_skip_on) ? &bios_hle_dispatch : NULL;
}
int psx_bios_hle_enabled(void) { return s_call_hle_on; }
int psx_bios_hle_boot_skip_enabled(void) { return s_boot_skip_on; }
const char* psx_bios_hle_backend_name(void)
{
return s_call_hle_on ? "HLE (LLE fallback)" : "LLE (recompiled BIOS)";
}