diff --git a/CLAUDE.md b/CLAUDE.md index c59ab432..c9bdb425 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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. diff --git a/FAITHFUL_TIMING_PLAN.md b/FAITHFUL_TIMING_PLAN.md index cb2c52fa..24bee13a 100644 --- a/FAITHFUL_TIMING_PLAN.md +++ b/FAITHFUL_TIMING_PLAN.md @@ -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 diff --git a/TCP_COMMANDS.md b/TCP_COMMANDS.md index 6a268dc4..ecc20694 100644 --- a/TCP_COMMANDS.md +++ b/TCP_COMMANDS.md @@ -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: diff --git a/recompiler/src/config_loader.cpp b/recompiler/src/config_loader.cpp index c2c39bbb..1bb05108 100644 --- a/recompiler/src/config_loader.cpp +++ b/recompiler/src/config_loader.cpp @@ -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(runtime, "fast_boot"); } + if (runtime.contains("bios_hle")) { + rt.bios_hle = toml::find(runtime, "bios_hle"); + } + if (runtime.contains("bios_hle_keep_intro")) { + rt.bios_hle_keep_intro = toml::find(runtime, "bios_hle_keep_intro"); + } + if (runtime.contains("hle_scheduler")) { + rt.hle_scheduler = toml::find(runtime, "hle_scheduler"); + } if (runtime.contains("overlay_cache")) { rt.overlay_cache = toml::find(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(v, "fast_boot"); s.has_fast_boot = true; }); + if (v.contains("bios_hle")) try_get([&]{ + s.bios_hle = toml::find(v, "bios_hle"); s.has_bios_hle = true; + }); if (v.contains("fullscreen")) try_get([&]{ s.fullscreen = toml::find(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) diff --git a/recompiler/src/config_loader.h b/recompiler/src/config_loader.h index 3bfcad10..d62976a1 100644 --- a/recompiler/src/config_loader.h +++ b/recompiler/src/config_loader.h @@ -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// 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). diff --git a/recompiler/src/full_function_emitter.cpp b/recompiler/src/full_function_emitter.cpp index effa10e0..181a38f1 100644 --- a/recompiler/src/full_function_emitter.cpp +++ b/recompiler/src/full_function_emitter.cpp @@ -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"; diff --git a/runtime/include/bios_hle.h b/runtime/include/bios_hle.h new file mode 100644 index 00000000..1f7fd427 --- /dev/null +++ b/runtime/include/bios_hle.h @@ -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 + +#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 */ diff --git a/runtime/include/psx_scheduler.h b/runtime/include/psx_scheduler.h index c7666e2d..9b4bc76e 100644 --- a/runtime/include/psx_scheduler.h +++ b/runtime/include/psx_scheduler.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 } diff --git a/runtime/runtime.cmake b/runtime/runtime.cmake index e2902936..2a30187a 100644 --- a/runtime/runtime.cmake +++ b/runtime/runtime.cmake @@ -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 diff --git a/runtime/src/bios_hle.c b/runtime/src/bios_hle.c new file mode 100644 index 00000000..16a631f7 --- /dev/null +++ b/runtime/src/bios_hle.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 + +/* 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)"; +}