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 <[email protected]>
This commit is contained in:
Joseph MattielloandClaude Sonnet 5.5 committed 2026-10-13 18:37:37 -04:00
1 parent d405223ee7
commit d5adfdee61
4 files changed
+428 -38

No files matched your search

+128 -28
View File
@@ -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
{
+45 -2
View File
@@ -5,10 +5,13 @@
#include "vj_frontend_ext.h"
#include <string.h>
/* 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)
{
+50 -7
View File
@@ -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);
+205 -1
View File
@@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
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;
}