mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
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:
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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 */
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)";
|
||||
}
|
||||
Reference in New Issue
Block a user