From d5adfdee613c570519ae917087ae85fa5b9f31c1 Mon Sep 17 00:00:00 2001 From: Joseph Mattiello Date: Tue, 13 Oct 2026 18:37:37 -0400 Subject: [PATCH] feat(core): frontend BIOS queries, BIOS_STAGED events and a cart-ROM no-content boot Adds VJ_ENV_GET_BIOS_PATH / VJ_ENV_GET_BOOT_TARGET behind a new VJ_CAP_BIOS capability on the private frontend channel (ABI unchanged), so the standalone app can name the exact CD BIOS / cart boot ROM file to run and choose which BIOS a no-content boot enters (CD BIOS, or the cart boot ROM against an empty slot). The core still validates every file and falls back to its own search and the built-in images; RetroArch never answers HELLO, so nothing changes there. VJ_EVT_BIOS_STAGED / VJ_EVT_BIOS_REJECTED say which image was staged. Refs #838 Refs #833 Co-Authored-By: Claude Sonnet 5.5 --- libretro.c | 156 +++++++++++++++++++++++----- src/core/vj_frontend_ext.c | 47 ++++++++- src/core/vj_frontend_ext.h | 57 ++++++++-- test/test_frontend_ext.c | 206 ++++++++++++++++++++++++++++++++++++- 4 files changed, 428 insertions(+), 38 deletions(-) diff --git a/libretro.c b/libretro.c index 85d3cb32..37dc8d84 100644 --- a/libretro.c +++ b/libretro.c @@ -5676,6 +5676,24 @@ static bool has_extension(const char *path, const char *ext) #define CD_BIOS_CRC_RETAIL 0x687068D5u #define CD_BIOS_CRC_DEV 0x55A0669Cu +/* Path of the external BIOS file the last successful try_load_*() accepted, + * for the VJ_EVT_BIOS_STAGED event. Overwritten on every success, read only + * straight after one, so it needs no reset. */ +static char last_bios_path[4096]; + +/* Tell an answering frontend (the standalone app) which BIOS image the core + * is about to run, and where it came from. One never-taken branch under + * RetroArch. src: VJ_BIOS_SRC_*; name: file path or built-in image name. */ +static void bios_staged_event(unsigned role, const uint8_t *img, + unsigned len, unsigned src, const char *name) +{ + if (!VJExtActive()) + return; + VJExtEmit(VJ_EVT_BIOS_STAGED, role, + (unsigned)crc32_calcCheckSum((unsigned char *)img, len), + src, 0, name); +} + /* Try to load a 256 KB CD BIOS image from the given path. * Returns true on success and sets cd_bios_loaded_externally. * A file that exists but fails validation bumps *rejected so the @@ -5712,6 +5730,7 @@ static bool try_load_cd_bios_file(const char *path, int *rejected) return false; } rfclose(f); + snprintf(last_bios_path, sizeof(last_bios_path), "%s", path); /* Known dumps are accepted by checksum, no header check needed. */ crc = (uint32_t)crc32_calcCheckSum(external_cd_bios, 0x40000); @@ -5859,31 +5878,60 @@ static bool load_external_cd_bios(void) return false; } -/* Stage the CD BIOS for the real-BIOS boot path: prefer an external ROM - * file from the system directory (users may carry a different BIOS - * revision), else fall back to an embedded CD BIOS so real-BIOS boot - * works with zero files. Which embedded image is used follows the - * 'CD BIOS Type' option: retail (default) or the developer BIOS, which - * skips some of the retail BIOS's disc checks. +/* Stage the CD BIOS for the real-BIOS boot path. Order: + * 1. the file an answering frontend names (VJ_ENV_GET_BIOS_PATH) -- the + * standalone app's "use this BIOS"; validated like any external file; + * 2. an external ROM file from the system directory (users may carry a + * different BIOS revision); + * 3. an embedded CD BIOS, so real-BIOS boot works with zero files. Which + * embedded image is used follows the 'CD BIOS Type' option: retail + * (default) or the developer BIOS, which skips some of the retail + * BIOS's disc checks. * - * An external file always wins over both — it is the user explicitly - * supplying a revision, so the type selection does not override it. */ + * Under RetroArch step 1 does not exist. An external file always wins over + * the embedded images -- it is the user explicitly supplying a revision, so + * the type selection does not override it. */ static void stage_cd_bios(void) { + char fpath[4096]; + int rejected = 0; + + if (VJExtBiosPath(VJ_BIOS_CD, fpath, sizeof(fpath))) + { + if (try_load_cd_bios_file(fpath, &rejected)) + { + bios_staged_event(VJ_BIOS_CD, external_cd_bios, 0x40000, + VJ_BIOS_SRC_FRONTEND, fpath); + return; + } + LOG_WRN("[CD-BIOS] the frontend-selected CD BIOS %s is unusable " + "(missing, not exactly 262144 bytes, or not a CD BIOS); " + "searching the system directory instead\n", fpath); + VJExtEmit(VJ_EVT_BIOS_REJECTED, VJ_BIOS_CD, 0, 0, 0, fpath); + } + if (load_external_cd_bios()) + { + bios_staged_event(VJ_BIOS_CD, external_cd_bios, 0x40000, + VJ_BIOS_SRC_SYSTEM, last_bios_path); return; + } if (vjs.cdBiosType == CDBIOS_DEV) { memcpy(external_cd_bios, jaguarDevCDBootROM, 0x40000); cd_bios_loaded_externally = true; LOG_INF("[CD-BIOS] using embedded developer CD BIOS\n"); + bios_staged_event(VJ_BIOS_CD, external_cd_bios, 0x40000, + VJ_BIOS_SRC_BUILTIN, "developer CD BIOS"); return; } memcpy(external_cd_bios, jaguarCDBootROM, 0x40000); cd_bios_loaded_externally = true; LOG_INF("[CD-BIOS] using embedded retail CD BIOS\n"); + bios_staged_event(VJ_BIOS_CD, external_cd_bios, 0x40000, + VJ_BIOS_SRC_BUILTIN, "retail CD BIOS"); } static void cart_bios_type_from_path(const char *path) @@ -5992,6 +6040,7 @@ static bool try_load_cart_boot_rom_file(const char *path) path, (unsigned)crc); LOG_INF("[BOOT] external cart boot ROM %s: %s (crc %08x)\n", path, name, (unsigned)crc); + snprintf(last_bios_path, sizeof(last_bios_path), "%s", path); return true; } @@ -6055,15 +6104,40 @@ static void stage_cart_boot_rom(void) RFILE *f; char path[4096]; + /* A file the frontend named (VJ_ENV_GET_BIOS_PATH) beats everything below, + * 'Cart BIOS Type' included: it is an explicit pick of one image. It only + * matters when the boot ROM actually runs (BIOS (Cartridges) = Real, a + * GPU-only cart, or a no-content boot), but staging it is harmless when + * it does not. Anything but exactly 128 KB is refused and we carry on. */ + if (VJExtBiosPath(VJ_BIOS_CART_BOOT, path, sizeof(path))) + { + if (try_load_cart_boot_rom_file(path)) + { + bios_staged_event(VJ_BIOS_CART_BOOT, jagMemSpace + 0xE00000, 0x20000, + VJ_BIOS_SRC_FRONTEND, path); + return; + } + LOG_WRN("[BOOT] the frontend-selected cart boot ROM %s is unusable " + "(missing or not exactly 131072 bytes); using the usual " + "search instead\n", path); + VJExtEmit(VJ_EVT_BIOS_REJECTED, VJ_BIOS_CART_BOOT, 0, 0, 0, path); + } + if (vjs.biosType == BT_CUSTOM) { if (load_external_cart_boot_rom()) + { + bios_staged_event(VJ_BIOS_CART_BOOT, jagMemSpace + 0xE00000, 0x20000, + VJ_BIOS_SRC_SYSTEM, last_bios_path); return; + } LOG_WRN("[BOOT] Custom cart boot ROM selected but no usable file " "found -- falling back to embedded Series K\n"); memcpy(jagMemSpace + 0xE00000, jaguarBootROM, 0x20000); LOG_INF("[BOOT] cart boot ROM: Series K (fallback)\n"); + bios_staged_event(VJ_BIOS_CART_BOOT, jagMemSpace + 0xE00000, 0x20000, + VJ_BIOS_SRC_BUILTIN, "Series K boot ROM"); return; } @@ -6083,6 +6157,8 @@ static void stage_cart_boot_rom(void) { rfclose(f); LOG_INF("[BOOT] using external Model-M boot ROM %s\n", path); + bios_staged_event(VJ_BIOS_CART_BOOT, jagMemSpace + 0xE00000, + 0x20000, VJ_BIOS_SRC_SYSTEM, path); return; } rfclose(f); @@ -6092,6 +6168,10 @@ static void stage_cart_boot_rom(void) memcpy(jagMemSpace + 0xE00000, src, 0x20000); LOG_INF("[BOOT] cart boot ROM: %s\n", (vjs.biosType == BT_M_SERIES) ? "Model M" : "Series K"); + bios_staged_event(VJ_BIOS_CART_BOOT, jagMemSpace + 0xE00000, 0x20000, + VJ_BIOS_SRC_BUILTIN, + (vjs.biosType == BT_M_SERIES) ? "Model M boot ROM" + : "Series K boot ROM"); } /* Fill the entire framebuffer allocation with opaque black. @@ -7065,28 +7145,48 @@ bool retro_load_game(const struct retro_game_info *info) * memcpys the staged BIOS, takes the run address from $800404 and * resets -- so the BIOS comes up on its own insert-disc screen with * zero files present. */ - stage_cd_bios(); + if (VJExtBootTarget() == VJ_BOOT_CART_BOOT_ROM) + { + /* An answering frontend (the standalone app's "Boot" on a cart boot + * ROM) asked for the other no-content boot: the cartridge boot ROM + * against an empty slot -- cd_boot_strategy_none, the pre-#726 + * behaviour. The boot ROM shows its animation and then sits, like a + * Jaguar with nothing in the slot. No CD unit, so no Memory Track + * (it plugs into the CD unit) and jaguar_cd_mode stays false. + * Nothing here runs under RetroArch: it never answers HELLO. */ + vjs.useJaguarBIOS = true; + bootConfig.isCDGame = false; + bootConfig.showBootROM = true; + bootConfig.cdBiosAvailable = false; + bootConfig.strategy = &cd_boot_strategy_none; + LOG_INF("[BOOT] no-content boot: cartridge boot ROM, empty slot " + "(frontend request)\n"); + } + else + { + stage_cd_bios(); - /* The CD unit is attached -- that is WHY the BIOS is running -- so - * this is CD mode with nothing in the tray, not cartridge mode. - * Both flags mirror open_disc_and_resolve_boot(); only the disc is - * missing. Without them a machine sitting in the CD BIOS gets the - * options menu inverted against it (update_option_visibility() keys - * both gates off jaguar_cd_mode, so CD Boot Mode / CD BIOS Type - * would be HIDDEN and the cartridge-BIOS option shown), and the - * Memory Track cart -- which plugs into the CD unit and is present - * whether or not a disc is -- would stay absent until the first - * insert. Safe with no disc: the three disc-identity accessors - * return zero when nothing is mounted (see the savestate chunk), - * and the MT save-buffer paths only touch mtMem. */ - jaguar_cd_mode = true; - jaguarMemTrackInserted = opt_memory_track; + /* The CD unit is attached -- that is WHY the BIOS is running -- so + * this is CD mode with nothing in the tray, not cartridge mode. + * Both flags mirror open_disc_and_resolve_boot(); only the disc is + * missing. Without them a machine sitting in the CD BIOS gets the + * options menu inverted against it (update_option_visibility() keys + * both gates off jaguar_cd_mode, so CD Boot Mode / CD BIOS Type + * would be HIDDEN and the cartridge-BIOS option shown), and the + * Memory Track cart -- which plugs into the CD unit and is present + * whether or not a disc is -- would stay absent until the first + * insert. Safe with no disc: the three disc-identity accessors + * return zero when nothing is mounted (see the savestate chunk), + * and the MT save-buffer paths only touch mtMem. */ + jaguar_cd_mode = true; + jaguarMemTrackInserted = opt_memory_track; - vjs.useJaguarBIOS = true; - bootConfig.isCDGame = false; - bootConfig.showBootROM = true; - bootConfig.cdBiosAvailable = true; - bootConfig.strategy = &cd_boot_strategy_bios; + vjs.useJaguarBIOS = true; + bootConfig.isCDGame = false; + bootConfig.showBootROM = true; + bootConfig.cdBiosAvailable = true; + bootConfig.strategy = &cd_boot_strategy_bios; + } } else { diff --git a/src/core/vj_frontend_ext.c b/src/core/vj_frontend_ext.c index 6aeba9ed..6eba65bd 100644 --- a/src/core/vj_frontend_ext.c +++ b/src/core/vj_frontend_ext.c @@ -5,10 +5,13 @@ #include "vj_frontend_ext.h" +#include + /* File scope, reset in VJExtDeinit(): iOS never dlcloses the core. */ static retro_environment_t vj_ext_env = 0; static int vj_ext_on = 0; static unsigned vj_ext_sent = 0; +static unsigned vj_ext_caps = 0; void VJExtInit(retro_environment_t env_cb, const char *core_version) { @@ -17,6 +20,7 @@ void VJExtInit(retro_environment_t env_cb, const char *core_version) vj_ext_env = 0; vj_ext_on = 0; vj_ext_sent = 0; + vj_ext_caps = 0; if (!env_cb) return; @@ -25,8 +29,9 @@ void VJExtInit(retro_environment_t env_cb, const char *core_version) hello.caps_out = 0; if (env_cb(VJ_ENV_HELLO, &hello) && (hello.caps_out & VJ_CAP_EVENTS)) { - vj_ext_env = env_cb; - vj_ext_on = 1; + vj_ext_env = env_cb; + vj_ext_on = 1; + vj_ext_caps = hello.caps_out; } } @@ -35,6 +40,7 @@ void VJExtDeinit(void) vj_ext_env = 0; vj_ext_on = 0; vj_ext_sent = 0; + vj_ext_caps = 0; } int VJExtActive(void) @@ -42,6 +48,43 @@ int VJExtActive(void) return vj_ext_on; } +int VJExtHasCap(unsigned cap) +{ + return vj_ext_on && (vj_ext_caps & cap) == cap; +} + +int VJExtBiosPath(unsigned role, char *buf, unsigned size) +{ + struct vj_env_bios_path q; + size_t n; + + if (!buf || size == 0) + return 0; + buf[0] = '\0'; + if (!VJExtHasCap(VJ_CAP_BIOS)) + return 0; + q.role = role; + q.path = 0; + if (!vj_ext_env(VJ_ENV_GET_BIOS_PATH, &q) || !q.path || !q.path[0]) + return 0; + n = strlen(q.path); + if (n >= size) + return 0; + memcpy(buf, q.path, n + 1); + return 1; +} + +unsigned VJExtBootTarget(void) +{ + unsigned t = VJ_BOOT_CD_BIOS; + + if (!VJExtHasCap(VJ_CAP_BIOS)) + return VJ_BOOT_CD_BIOS; + if (!vj_ext_env(VJ_ENV_GET_BOOT_TARGET, &t)) + return VJ_BOOT_CD_BIOS; + return (t == VJ_BOOT_CART_BOOT_ROM) ? VJ_BOOT_CART_BOOT_ROM : VJ_BOOT_CD_BIOS; +} + void VJExtEmit(unsigned type, unsigned a, unsigned b, unsigned c, unsigned d, const char *s) { diff --git a/src/core/vj_frontend_ext.h b/src/core/vj_frontend_ext.h index a70f24c3..ff519f38 100644 --- a/src/core/vj_frontend_ext.h +++ b/src/core/vj_frontend_ext.h @@ -14,18 +14,29 @@ * caps_out). RetroArch and every other frontend answers false; the core * then latches the channel off and never calls VJ_ENV_EVENT at all, so * under RetroArch the whole feature costs one branch per would-be event. - * - Synchronous, on the core's thread. VJ_ENV_EVENT is only ever issued - * from inside retro_run() (the emulation thread), and the frontend's - * handler runs inline. Nothing is queued, nothing is called from a - * helper thread. + * - Synchronous, on the core's thread. Everything is issued from the thread + * that called retro_load_game() / retro_run() (the emulation thread), and + * the frontend's handler runs inline. VJ_ENV_EVENT comes from retro_run() + * except for the load-time VJ_EVT_BIOS_* events, which come from inside + * retro_load_game() (or a disk-control insert). Nothing is queued, nothing is called + * from a helper thread. * - Pointers (struct vj_event.s, struct vj_env_hello.core_version) are valid * for the duration of the call only. Copy what you keep. * - Events are informational. Existing log lines are unchanged and remain * the fallback for a frontend that never answered HELLO. * - The frontend's answer to VJ_ENV_EVENT is ignored. * - * Adding an event type or a trailing struct field is compatible; anything - * else bumps VJ_FRONTEND_EXT_ABI. + * - Queries. A frontend that sets VJ_CAP_BIOS may also be asked, while the + * core stages a BIOS (inside retro_load_game() or a disk-control insert, + * never from a helper thread), which BIOS FILE to use (VJ_ENV_GET_BIOS_PATH) and what + * a no-content boot should enter (VJ_ENV_GET_BOOT_TARGET). Both are + * optional: a refusal (false, or an empty answer) leaves the core's own + * logic -- system-folder search, then the built-in image -- exactly as it + * was. The core validates whatever file it is handed (size, header or + * checksum) and falls back the same way if it does not pass. + * + * Adding an event type, a command id, a capability bit or a trailing struct + * field is compatible; anything else bumps VJ_FRONTEND_EXT_ABI. */ #ifndef VJ_FRONTEND_EXT_H #define VJ_FRONTEND_EXT_H @@ -42,9 +53,26 @@ extern "C" { #define VJ_ENV_BASE (0x5600 | RETRO_ENVIRONMENT_PRIVATE) #define VJ_ENV_HELLO (VJ_ENV_BASE + 0) /* data: struct vj_env_hello* */ #define VJ_ENV_EVENT (VJ_ENV_BASE + 1) /* data: const struct vj_event* */ +#define VJ_ENV_GET_BIOS_PATH (VJ_ENV_BASE + 2) /* data: struct vj_env_bios_path*; needs VJ_CAP_BIOS */ +#define VJ_ENV_GET_BOOT_TARGET (VJ_ENV_BASE + 3) /* data: unsigned* (VJ_BOOT_*); needs VJ_CAP_BIOS */ /* Capability bits the frontend may report in caps_out. */ #define VJ_CAP_EVENTS 0x1u /* will accept VJ_ENV_EVENT (always set by an answering frontend) */ +#define VJ_CAP_BIOS 0x2u /* answers VJ_ENV_GET_BIOS_PATH / VJ_ENV_GET_BOOT_TARGET */ + +/* BIOS roles, for VJ_ENV_GET_BIOS_PATH and the VJ_EVT_BIOS_* events. */ +#define VJ_BIOS_CART_BOOT 1u /* the 128 KB console boot ROM at $E00000 */ +#define VJ_BIOS_CD 2u /* the 256 KB Jaguar CD BIOS (mapped at $800000) */ + +/* What a no-content boot (retro_load_game(NULL)) enters. */ +#define VJ_BOOT_CD_BIOS 0u /* the CD BIOS with the CD unit attached (the default) */ +#define VJ_BOOT_CART_BOOT_ROM 1u /* the cartridge boot ROM against an empty slot */ + +struct vj_env_bios_path +{ + unsigned role; /* in: VJ_BIOS_* */ + const char *path; /* out: file to use, valid for the call only; NULL/"" = no preference */ +}; struct vj_env_hello { @@ -59,10 +87,18 @@ enum vj_event_type VJ_EVT_LINK_DOWN, /* s = mode name ("tcp_client", "tcp_server", ...) */ VJ_EVT_LINK_STATS, /* a = hops/s, b = mean hop ms x100, c = slipped hops, d = timeouts */ VJ_EVT_WIRE_SPEED, /* a = multiplier (1 = stock), b = 1 confirmed / 0 back to stock */ - VJ_EVT_CRASH_WATCHDOG /* s = signature ("gpu_pc_escape", "video_stall", ...); a = frame number, b = the + VJ_EVT_CRASH_WATCHDOG, /* s = signature ("gpu_pc_escape", "video_stall", ...); a = frame number, b = the PC / hash / B_COUNT the line reports (signature specific), c,d = extras */ + VJ_EVT_BIOS_STAGED, /* a = VJ_BIOS_* role, b = CRC32 of the image the core will run, c = where it came + from (VJ_BIOS_SRC_*), s = the file path, or the built-in image's name */ + VJ_EVT_BIOS_REJECTED /* a = role, s = the path the frontend named; the core did not accept it (wrong + size or not a BIOS) and fell back to its own search */ }; +#define VJ_BIOS_SRC_BUILTIN 0u /* the image compiled into the core */ +#define VJ_BIOS_SRC_SYSTEM 1u /* found by the core's own search of the system directory */ +#define VJ_BIOS_SRC_FRONTEND 2u /* the file the frontend named (VJ_ENV_GET_BIOS_PATH) */ + struct vj_event { unsigned type; /* enum vj_event_type */ @@ -77,6 +113,13 @@ void VJExtInit(retro_environment_t env_cb, const char *core_version); void VJExtDeinit(void); /* Nonzero once a frontend answered HELLO. */ int VJExtActive(void); +/* True when the answering frontend reported every bit of `cap`. */ +int VJExtHasCap(unsigned cap); +/* Ask the frontend which file to use for `role` (VJ_ENV_GET_BIOS_PATH): copies the path into + * buf and returns 1, or returns 0 (inactive, no VJ_CAP_BIOS, refused, empty, or too long for buf). */ +int VJExtBiosPath(unsigned role, char *buf, unsigned size); +/* What a no-content boot should enter: VJ_BOOT_CD_BIOS unless an answering frontend says otherwise. */ +unsigned VJExtBootTarget(void); /* No-ops (one branch) while inactive. */ void VJExtEmit(unsigned type, unsigned a, unsigned b, unsigned c, unsigned d, const char *s); diff --git a/test/test_frontend_ext.c b/test/test_frontend_ext.c index ff711d6e..bb5ba6ed 100644 --- a/test/test_frontend_ext.c +++ b/test/test_frontend_ext.c @@ -12,6 +12,13 @@ * D) refuse again after B in the same process (the iOS static-state rule: * retro_deinit() drops the latch): silent again. * + * Part 3: the BIOS queries (VJ_CAP_BIOS): which BIOS file to use, and what a + * no-content boot enters. A frontend that refuses HELLO (RetroArch) is never + * asked; one that answers without VJ_CAP_BIOS is never asked either; one that + * names a file gets that image staged (and told so with VJ_EVT_BIOS_STAGED); + * a file the core cannot accept is refused with VJ_EVT_BIOS_REJECTED and the + * core's own search takes over. + * * Part 2: m68k/risc clock-scale parsing. The listed values map exactly as * before; "175" / "175%" are an extra accepted form clamped to 25..400; * everything else the old ladder did not know stays 100. @@ -28,6 +35,8 @@ #include #include #include +#include +#include static int g_fail = 0, g_checks = 0; #define CHECK(cond) \ @@ -48,6 +57,12 @@ static char g_hello_version[64]; static unsigned g_link_up_events; static unsigned g_link_up_device = 99; static char g_link_up_desc[64]; +/* Part 3 */ +static const char *g_bios_path[3]; /* by role; NULL = no preference */ +static unsigned g_boot_target; +static unsigned g_path_calls[3], g_target_calls; +static unsigned g_staged_n[3], g_staged_src[3], g_staged_crc[3], g_rejected_n[3]; +static char g_staged_s[3][512]; static bool env_hook(unsigned cmd, void *data) { @@ -61,12 +76,36 @@ static bool env_hook(unsigned cmd, void *data) } if (!g_answer_hello || !h) return false; - h->caps_out = (g_answer_hello == 1) ? VJ_CAP_EVENTS : 0; + h->caps_out = (g_answer_hello == 1) ? VJ_CAP_EVENTS + : (g_answer_hello == 3) ? (VJ_CAP_EVENTS | VJ_CAP_BIOS) : 0; + return true; + } + if (cmd == VJ_ENV_GET_BIOS_PATH) { + struct vj_env_bios_path *q = (struct vj_env_bios_path *)data; + if (!q || q->role < 1 || q->role > 2) + return false; + g_path_calls[q->role]++; + if (!g_bios_path[q->role]) + return false; + q->path = g_bios_path[q->role]; + return true; + } + if (cmd == VJ_ENV_GET_BOOT_TARGET) { + g_target_calls++; + *(unsigned *)data = g_boot_target; return true; } if (cmd == VJ_ENV_EVENT) { const struct vj_event *e = (const struct vj_event *)data; g_event_calls++; + if (e && e->type == VJ_EVT_BIOS_STAGED && e->a >= 1 && e->a <= 2) { + g_staged_n[e->a]++; + g_staged_src[e->a] = e->c; + g_staged_crc[e->a] = e->b; + snprintf(g_staged_s[e->a], sizeof g_staged_s[e->a], "%s", e->s ? e->s : "(null)"); + } + if (e && e->type == VJ_EVT_BIOS_REJECTED && e->a >= 1 && e->a <= 2) + g_rejected_n[e->a]++; if (e && e->type == VJ_EVT_LINK_UP) { g_link_up_events++; g_link_up_device = e->a; @@ -225,10 +264,175 @@ static void part2(int argc, char **argv) harness_shutdown(&cfg); } + +/* ---- Part 3: BIOS queries --------------------------------------------------- */ +static unsigned crc32_of(const unsigned char *p, size_t n) +{ + unsigned c = 0xFFFFFFFFu; + size_t i; + int k; + for (i = 0; i < n; i++) { + c ^= p[i]; + for (k = 0; k < 8; k++) + c = (c >> 1) ^ (0xEDB88320u & (0u - (c & 1u))); + } + return ~c; +} + +/* Write n bytes of a recognisable pattern; the CD variant carries a plausible run address at $404. */ +static unsigned write_image(const char *path, size_t n, int cd) +{ + unsigned char *b = (unsigned char *)malloc(n); + unsigned crc; + size_t i; + FILE *f; + for (i = 0; i < n; i++) + b[i] = (unsigned char)((i * 7u + 13u) ^ (i >> 8)); + if (cd) { + b[0x404] = 0x00; b[0x405] = 0x80; b[0x406] = 0x20; b[0x407] = 0x00; + } + f = fopen(path, "wb"); + if (f) { fwrite(b, 1, n, f); fclose(f); } + crc = crc32_of(b, n); + free(b); + return crc; +} + +static void reset_part3(void) +{ + memset(g_bios_path, 0, sizeof g_bios_path); + memset(g_path_calls, 0, sizeof g_path_calls); + memset(g_staged_n, 0, sizeof g_staged_n); + memset(g_staged_src, 0xFF, sizeof g_staged_src); + memset(g_staged_crc, 0, sizeof g_staged_crc); + memset(g_rejected_n, 0, sizeof g_rejected_n); + memset(g_staged_s, 0, sizeof g_staged_s); + g_boot_target = 0; + g_target_calls = 0; +} + +static int start_nocontent(harness_config *cfg, int argc, char **argv, int answer) +{ + harness_config fresh = HARNESS_CONFIG_DEFAULT; + *cfg = fresh; + cfg->frames = 1; + cfg->quiet = 1; + g_answer_hello = answer; + g_hello_calls = g_event_calls = 0; + harness_set_env_hook(env_hook); + if (!harness_init_from_args(cfg, argc, argv)) + return 0; + return harness_load_no_content(cfg) ? 1 : 0; +} + +static void part3(int argc, char **argv) +{ + harness_config cfg; + char dir[512], cdp[600], cartp[600], badp[600]; + unsigned cdcrc, cartcrc; + const char *tmp = getenv("TMPDIR"); + + snprintf(dir, sizeof dir, "%s/vjbios_%d", (tmp && tmp[0]) ? tmp : "/tmp", (int)getpid()); + mkdir(dir, 0700); + snprintf(cdp, sizeof cdp, "%s/cd.bin", dir); + snprintf(cartp, sizeof cartp, "%s/boot.rom", dir); + snprintf(badp, sizeof badp, "%s/short.bin", dir); + cdcrc = write_image(cdp, 0x40000, 1); + cartcrc = write_image(cartp, 0x20000, 0); + write_image(badp, 1000, 0); + + /* A: refusing frontend (RetroArch): never asked anything, and no event. */ + reset_part3(); + g_bios_path[2] = cdp; g_bios_path[1] = cartp; g_boot_target = 1; + CHECK(start_nocontent(&cfg, argc, argv, 0)); + run_frames(&cfg, 30); + CHECK(g_path_calls[1] == 0 && g_path_calls[2] == 0 && g_target_calls == 0); + CHECK(g_staged_n[1] == 0 && g_staged_n[2] == 0); + harness_shutdown(&cfg); + + /* B: answers with events but not VJ_CAP_BIOS: still never asked; events say built-in. */ + reset_part3(); + g_bios_path[2] = cdp; g_boot_target = 1; + CHECK(start_nocontent(&cfg, argc, argv, 1)); + run_frames(&cfg, 30); + CHECK(g_path_calls[1] == 0 && g_path_calls[2] == 0 && g_target_calls == 0); + CHECK(g_staged_n[2] == 1 && g_staged_src[2] == VJ_BIOS_SRC_BUILTIN); + CHECK(g_staged_crc[2] == 0x687068D5u); /* the built-in retail CD BIOS */ + harness_shutdown(&cfg); + + /* C: BIOS cap, no preference: the default no-content boot is the CD BIOS. */ + reset_part3(); + CHECK(start_nocontent(&cfg, argc, argv, 3)); + run_frames(&cfg, 30); + CHECK(g_target_calls == 1 && g_path_calls[2] == 1); + CHECK(g_staged_n[2] == 1 && g_staged_src[2] == VJ_BIOS_SRC_BUILTIN && g_staged_n[1] == 1); + harness_shutdown(&cfg); + + /* D: a CD BIOS file named by the frontend is the one staged. */ + reset_part3(); + g_bios_path[2] = cdp; + CHECK(start_nocontent(&cfg, argc, argv, 3)); + run_frames(&cfg, 30); + CHECK(g_staged_n[2] == 1 && g_staged_src[2] == VJ_BIOS_SRC_FRONTEND); + CHECK(g_staged_crc[2] == cdcrc); + CHECK(strcmp(g_staged_s[2], cdp) == 0); + CHECK(g_rejected_n[2] == 0); + harness_shutdown(&cfg); + + /* E: a file the core cannot accept is reported and the built-in image takes over. */ + reset_part3(); + g_bios_path[2] = badp; + CHECK(start_nocontent(&cfg, argc, argv, 3)); + run_frames(&cfg, 30); + CHECK(g_rejected_n[2] == 1); + CHECK(g_staged_n[2] == 1 && g_staged_src[2] == VJ_BIOS_SRC_BUILTIN); + harness_shutdown(&cfg); + reset_part3(); + g_bios_path[2] = "/nonexistent/cd.bin"; + CHECK(start_nocontent(&cfg, argc, argv, 3)); + run_frames(&cfg, 30); + CHECK(g_rejected_n[2] == 1 && g_staged_src[2] == VJ_BIOS_SRC_BUILTIN); + harness_shutdown(&cfg); + + /* F: boot target = cartridge boot ROM: the cart ROM is staged (the named file), the CD BIOS is not. */ + reset_part3(); + g_boot_target = VJ_BOOT_CART_BOOT_ROM; + g_bios_path[1] = cartp; + CHECK(start_nocontent(&cfg, argc, argv, 3)); + run_frames(&cfg, 60); + CHECK(g_staged_n[1] == 1 && g_staged_src[1] == VJ_BIOS_SRC_FRONTEND && g_staged_crc[1] == cartcrc); + CHECK(g_staged_n[2] == 0 && g_path_calls[2] == 0); + CHECK(cfg.current_frame >= 60); + harness_shutdown(&cfg); + + /* G: cart target with no file: the built-in Series K runs. */ + reset_part3(); + g_boot_target = VJ_BOOT_CART_BOOT_ROM; + CHECK(start_nocontent(&cfg, argc, argv, 3)); + run_frames(&cfg, 60); + CHECK(g_staged_n[1] == 1 && g_staged_src[1] == VJ_BIOS_SRC_BUILTIN && g_staged_crc[1] == 0xFB731AAAu); + CHECK(g_staged_n[2] == 0); + harness_shutdown(&cfg); + + /* H: cart content: the boot target is never asked; the cart boot ROM file is (and wins even in HLE). */ + reset_part3(); + g_bios_path[1] = cartp; + CHECK(start(&cfg, argc, argv, 3, NULL)); + run_frames(&cfg, 10); + CHECK(g_target_calls == 0); + CHECK(g_staged_n[1] == 1 && g_staged_src[1] == VJ_BIOS_SRC_FRONTEND && g_staged_crc[1] == cartcrc); + harness_shutdown(&cfg); + + harness_set_env_hook(NULL); + unlink(cdp); unlink(cartp); unlink(badp); + rmdir(dir); +} + int main(int argc, char **argv) { part1(argc, argv); part2(argc, argv); + part3(argc, argv); printf("frontend_ext: %d checks, %d failed\n", g_checks, g_fail); return g_fail ? 1 : 0; }