Files
psxrecomp/runtime/tests/test_bios_module_host.c
T
Alex Vanderveen 60919f5a87 bios: build a retail backend from the player's own dump as a loadable module
Bundled releases link OpenBIOS only; retail-BIOS-derived C never ships.
"Bring your own BIOS" now works without it: when a dump matching a shipped
profile (psx_bios_known_images.h) is selected, the runtime builds that
backend on the player's machine with the overlay toolchain the release
already carries, caches it, and loads it as one more registered backend.

- runtime/include/psx_bios_module.h: the module contract. An overlay-style
  self-contained shared library (no imports from the executable) exporting
  psx_bios_module_{abi,codegen_hash,init,backend}; reaches the runtime only
  through OverlayCallbacks plus a new PsxBiosModuleCallbacks table (traps,
  dirty-RAM/kernel-bless probes, fntrace, log_probe) and pointer aliases of
  the globals the generated dispatcher touches. Its #defines rewrite the
  emitter's own extern lines, so the generated C compiles unchanged.
- runtime/include/psx_bios_module_glue.c.inc: module-side forwarders and the
  four exports, compiled after overlay_dispatch_preamble.c.inc.
- runtime/src/psx_bios_module.c: host side. Cache path
  <exe>/cache/bios/<os-arch>/bm<abi>_<codegen hash>_f<flavor>/<STEM>_<crc>,
  ABI + codegen-hash gated load, callback fill, and a blocking build via
  overlay_toolchain/bios_module_build.py (gcc via compiler.txt or PATH,
  else tcc), then psx_bios_register().
- psx_bios_backend.c: the registry is now a mutable table seeded from the
  CMake-generated psx_bios_builtin_registry by a constructor, with
  psx_bios_register(); every existing walker keeps working.
- main.cpp: validate_bios_for_launch acquires a module (announcing a first
  build) before declaring a mismatch; discovery accepts a buildable dump;
  the launcher verify reports it usable ("ready" / "compiled on first
  launch") with no Generate & rebuild; the BIOS row shows whenever the
  toolchain is present; bundled_only respects module support.
- emitter: the CPS-marker constructor is not emitted in a module build.
- overlay_api.h: OverlayCallbacks gains a struct tag;
  overlay_loader_callbacks() exposes the host table before loader init.
- tools/bios_module_build.py (ships in the toolchain): identity + profile
  SHA-256 gate, staged emitter tree, emit, three-TU compile, ctypes
  self-check, atomic publish. release_stage.py stages psxrecomp-bios, every
  bios/*.toml, the seeds and the builder into overlay_toolchain/.
- Tests: runtime/tests/test_bios_module_host.c against three fake modules
  (good / wrong ABI / wrong hash) on real dlopen; recompiler/tests/
  test_bios_module_build.py builds the bundled OpenBIOS end to end and reads
  the descriptor back; guards extended.

Verified on Twisted Metal 4 (bundled build + staged toolchain, headless):
first launch with --bios SCPH1001.BIN builds and loads the module and boots
on image=SCPH-1001; the second launch loads it from cache.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-30 19:26:29 -04:00

149 lines
7.5 KiB
C

/* test_bios_module_host.c — the host side of loadable BIOS backends
* (runtime/src/psx_bios_module.c) against fake modules built by
* runtime/CMakeLists.txt from tests/bios_module_fake.c.
*
* What it proves, with the real host code and a real dlopen/LoadLibrary:
* 1. A module with the right ABI tag and codegen hash loads, is handed a
* full callback table whose pointers alias the host's globals, and is
* registered AFTER the builtins (which the constructor seeded).
* 2. A module with the wrong ABI tag is refused, and so is one with the
* wrong codegen hash; neither reaches the registry.
* 3. Loading the same module twice registers it once.
* 4. The registry seeded from psx_bios_builtin_registry before main().
*
* Every runtime symbol psx_bios_module.c forwards is stubbed here; nothing
* about the emulation core is exercised. Paths to the three fake modules come
* from compile definitions set by CMake.
*/
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include "cpu_state.h"
#include "overlay_api.h"
#include "psx_bios_backend.h"
#include "psx_bios_module.h"
/* ---- stubs for what the host forwards -------------------------------------- */
void psx_arith_overflow(CPUState *cpu) { (void)cpu; }
void psx_break(CPUState *cpu, uint32_t code, uint32_t pc) { (void)cpu; (void)code; (void)pc; }
void psx_unaligned_access(CPUState *cpu, uint32_t addr, uint32_t pc) { (void)cpu; (void)addr; (void)pc; }
void psx_rfe_escape_check(CPUState *cpu) { (void)cpu; }
int dirty_ram_dispatch(CPUState *cpu, uint32_t addr, uint32_t stop) { (void)cpu; (void)addr; (void)stop; return 0; }
int dirty_ram_is_dirty(uint32_t phys) { (void)phys; return 0; }
int dirty_ram_text_native_ok(uint32_t phys) { (void)phys; return 0; }
int psx_kernel_bless_dispatchable(uint32_t phys) { (void)phys; return 0; }
int psx_game_address_in_text(uint32_t addr) { (void)addr; return 0; }
int fntrace_is_game_started(void) { return 0; }
void fntrace_record(CPUState *cpu, uint32_t target) { (void)cpu; (void)target; }
void debug_server_trace_dispatch(uint32_t f) { (void)f; }
void debug_server_log_probe(uint32_t pc, CPUState *cpu) { (void)pc; (void)cpu; }
void psx_segment_miss_record_kind(uint32_t a, uint32_t h, uint32_t r, uint32_t s, uint32_t f, uint32_t k) { (void)a; (void)h; (void)r; (void)s; (void)f; (void)k; }
uint64_t s_frame_count = 0;
int (*g_psx_bios_hle_hook)(struct CPUState *cpu, uint32_t phys) = 0;
uint64_t g_dispatch_static_hits = 0;
uint32_t g_debug_current_func_addr = 0;
uint64_t g_psx_bail_flattened = 0;
uint32_t g_psx_ram_mask = 0x1FFFFFu;
void psx_bios_hle_configure(uint32_t a, uint32_t b) { (void)a; (void)b; }
int autocompile_toolchain_available(void) { return 0; }
const char *overlay_loader_arch_abi(void) { return "test-arch"; }
static OverlayCallbacks s_cbs;
const OverlayCallbacks *overlay_loader_callbacks(void) { return &s_cbs; }
/* One builtin, standing in for the linked OpenBIOS. */
static const PsxBiosImageInfo s_builtin_image = {
0, 0, 0, 0, 0, 524288u, 0x11111111u, 0u, "", "OPENBIOS", 1,
};
static void builtin_dispatch(struct CPUState *c, uint32_t a) { (void)c; (void)a; }
static void builtin_dispatch_call(struct CPUState *c, uint32_t a, uint32_t r) { (void)c; (void)a; (void)r; }
static const PsxBiosBackend s_builtin = {
&s_builtin_image, builtin_dispatch, builtin_dispatch_call, 0, 0, 0, 0,
};
const PsxBiosBackend *const psx_bios_builtin_registry[] = { &s_builtin };
const uint32_t psx_bios_builtin_registry_count = 1u;
static int s_fail;
#define CHECK(cond, ...) do { if (!(cond)) { s_fail++; printf("FAIL %s:%d: ", __FILE__, __LINE__); printf(__VA_ARGS__); printf("\n"); } } while (0)
#ifndef FAKE_GOOD
#error "FAKE_GOOD / FAKE_BAD_ABI / FAKE_BAD_HASH must name the fake module paths"
#endif
int main(void) {
char err[256];
const PsxBiosBackend *b;
/* 4. Seeded before main() by the constructor. */
CHECK(psx_bios_registry_count == 1, "registry seeded count %u", (unsigned)psx_bios_registry_count);
CHECK(psx_bios_registry[0] == &s_builtin, "registry[0] is the builtin");
CHECK(psx_bios_bundled() == &s_builtin, "bundled() finds the builtin");
CHECK(!psx_bios_has_selectable(), "nothing selectable before a module loads");
/* 2. Wrong ABI tag: refused, registry untouched. */
err[0] = 0;
b = psx_bios_module_load(FAKE_BAD_ABI, err, sizeof(err));
CHECK(b == 0, "bad-ABI module must be refused");
CHECK(strstr(err, "ABI tag") != 0, "bad-ABI reason names the tag: %s", err);
CHECK(psx_bios_registry_count == 1, "bad-ABI module not registered");
err[0] = 0;
b = psx_bios_module_load(FAKE_BAD_HASH, err, sizeof(err));
CHECK(b == 0, "bad-hash module must be refused");
CHECK(strstr(err, "codegen hash") != 0, "bad-hash reason names the hash: %s", err);
CHECK(psx_bios_registry_count == 1, "bad-hash module not registered");
/* 1. The good one loads, inits with the host tables, registers after the builtin. */
err[0] = 0;
b = psx_bios_module_load(FAKE_GOOD, err, sizeof(err));
CHECK(b != 0, "good module loads: %s", err);
if (b) {
CHECK(b->image && strcmp(b->image->image_id, "FAKE-0001") == 0, "descriptor identity");
CHECK(b->image->image_crc32 == 0xDEADBEEFu, "descriptor crc");
CHECK(psx_bios_registry_count == 2, "registered: count %u", (unsigned)psx_bios_registry_count);
CHECK(psx_bios_registry[1] == b, "registered after the builtin");
CHECK(psx_bios_has_selectable(), "a non-bundled backend is now selectable");
CHECK(psx_bios_find("FAKE-0001") == b, "find() by id");
CHECK(psx_bios_activate(b) == 1, "activate");
CHECK(psx_bios_active == b, "active pointer");
CHECK(psx_bios_image.image_crc32 == 0xDEADBEEFu, "published image copied");
/* The module saw the host's own depth counter (aliasing, not a copy). */
{
PsxBiosModuleCallbacks probe;
psx_bios_module_fill_callbacks(&probe);
CHECK(probe.size == sizeof(probe), "table size stamped");
CHECK(probe.dispatch_depth == &g_psx_dispatch_depth, "depth pointer aliases host");
CHECK(probe.hle_hook == &g_psx_bios_hle_hook, "hle hook pointer aliases host");
CHECK(probe.ram_mask == &g_psx_ram_mask, "ram mask pointer aliases host");
}
}
/* 3. Same file again: the descriptor is the same object, registered once. */
{
const PsxBiosBackend *again = psx_bios_module_load(FAKE_GOOD, err, sizeof(err));
CHECK(again == b, "second load returns the same descriptor");
CHECK(psx_bios_registry_count == 2, "second load does not duplicate: %u",
(unsigned)psx_bios_registry_count);
}
/* Cache path shape: one place formats it; assert the pieces are present. */
{
char path[512];
CHECK(psx_bios_module_cache_path("/x", "SCPH1001", 0x37157331u, path, sizeof(path)),
"cache path fits");
CHECK(strstr(path, "/x/cache/bios/test-arch/bm1_") == path, "cache path root: %s", path);
CHECK(strstr(path, "/SCPH1001_37157331.") != 0, "cache path stem+crc: %s", path);
}
/* Identity table: the shipped profiles' images are known; junk is not. */
CHECK(psx_bios_module_known_id(0x37157331u, 524288u) &&
strcmp(psx_bios_module_known_id(0x37157331u, 524288u), "SCPH-1001") == 0,
"SCPH-1001 known");
CHECK(psx_bios_module_known_id(0x12345678u, 524288u) == 0, "unknown crc rejected");
CHECK(psx_bios_module_known_id(0x37157331u, 1u) == 0, "wrong size rejected");
if (s_fail) { printf("%d failure(s)\n", s_fail); return 1; }
printf("bios module host: ok\n");
return 0;
}