mirror of
https://github.com/libretro/virtualjaguar-libretro.git
synced 2026-11-13 05:01:35 -10:00
Refuse pre-15886 rips, vendor libchdr, and ship pinned chdman helpers on tagged releases and nightlies so users can convert CUE/BIN themselves. Co-authored-by: Cursor <[email protected]>
487 lines
16 KiB
C
487 lines
16 KiB
C
/*
|
|
* cd_assertions.h - Shared helpers for the CD HLE boot test suite.
|
|
*
|
|
* Provides:
|
|
* - discover_discs() : recursive scan of a disc-image root for cue/iso/cdi
|
|
* - per-frame assertion helpers that operate on a vj_core
|
|
* - SHA1 helpers used by the regression-baseline JSON sidecar
|
|
*
|
|
* Designed to be #included by test_cd_hle_boot.c (single TU; no separate .c).
|
|
*/
|
|
|
|
#ifndef CD_ASSERTIONS_H
|
|
#define CD_ASSERTIONS_H
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <stdint.h>
|
|
#include <stdbool.h>
|
|
#include <string.h>
|
|
#include <strings.h>
|
|
#include <dirent.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/types.h>
|
|
|
|
#include "test_framework.h"
|
|
|
|
#define CD_ASSERT_MAX_DISCS 64
|
|
#define CD_ASSERT_MAX_PATH_LEN 4096
|
|
#define CD_ASSERT_MAX_SCAN_DEPTH 8
|
|
|
|
/* m68k register IDs (mirrors test_hle_bios.c) */
|
|
#ifndef M68K_REG_PC
|
|
#define M68K_REG_D0 0
|
|
#define M68K_REG_D1 1
|
|
#define M68K_REG_A0 8
|
|
#define M68K_REG_A1 9
|
|
#define M68K_REG_PC 16
|
|
#define M68K_REG_SP 18
|
|
#endif
|
|
|
|
struct cd_disc_entry {
|
|
char path[CD_ASSERT_MAX_PATH_LEN];
|
|
char ext[8]; /* lowercase, no leading dot; copied so memmove-safe */
|
|
size_t file_size;
|
|
};
|
|
|
|
struct cd_disc_list {
|
|
struct cd_disc_entry entries[CD_ASSERT_MAX_DISCS];
|
|
size_t count;
|
|
};
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* String helpers */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static inline const char *cd_disc_extension(const char *path)
|
|
{
|
|
const char *dot = strrchr(path, '.');
|
|
if (!dot || dot == path) return "";
|
|
return dot + 1;
|
|
}
|
|
|
|
static inline bool cd_str_iequals(const char *a, const char *b)
|
|
{
|
|
return strcasecmp(a, b) == 0;
|
|
}
|
|
|
|
static inline int cd_disc_priority(const char *ext)
|
|
{
|
|
if (cd_str_iequals(ext, "cue")) return 0;
|
|
if (cd_str_iequals(ext, "iso")) return 1;
|
|
if (cd_str_iequals(ext, "cdi")) return 2;
|
|
if (cd_str_iequals(ext, "chd")) return 3;
|
|
return -1;
|
|
}
|
|
|
|
/* Honors VJ_TEST_CD_EXTS (comma-separated) to filter which extensions to
|
|
* enumerate. Default is "cue" — CDI/ISO are opt-in because:
|
|
* CDI: V3.5 images (incl. third-party baldies.cdi) parse and boot cleanly
|
|
* since the global-offset landmark scan (PR #233). Of the 4 damaged
|
|
* CDI V2 rips, mystdemo and worldtourracing now boot via the V2
|
|
* boot-track repair (cdintf.c CDIRepairV2BootTrack); ironsoldier2
|
|
* and vidgrid lost title-specific boot bytes the repair cannot
|
|
* synthesize and still refuse to load -- see issue #269 for the
|
|
* pending policy decision.
|
|
* ISO: Jaguar boot from ISO is fundamentally degraded (no session 2 audio);
|
|
* useful for read-only sanity but not boot smoke. */
|
|
static inline bool cd_ext_enabled(const char *ext)
|
|
{
|
|
const char *list = getenv("VJ_TEST_CD_EXTS");
|
|
size_t elen;
|
|
const char *p;
|
|
if (!list || !list[0]) list = "cue";
|
|
if (cd_str_iequals(list, "all")) return cd_disc_priority(ext) >= 0;
|
|
|
|
elen = strlen(ext);
|
|
p = list;
|
|
while (*p) {
|
|
const char *q = p;
|
|
size_t segLen;
|
|
while (*q && *q != ',') q++;
|
|
segLen = (size_t)(q - p);
|
|
if (segLen == elen && strncasecmp(p, ext, elen) == 0)
|
|
return true;
|
|
p = (*q == ',') ? q + 1 : q;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static inline bool cd_should_skip_dir(const char *name)
|
|
{
|
|
if (name[0] == '.') return true;
|
|
if (cd_str_iequals(name, "BigPEmu_v121-DEV")) return true;
|
|
/* Conventional BIOS directory marker we use in the corpus. */
|
|
if (strncmp(name, "[BIOS]", 6) == 0) return true;
|
|
return false;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Discovery */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static void cd_disc_list_add(struct cd_disc_list *list,
|
|
const char *path, const char *ext, size_t size)
|
|
{
|
|
struct cd_disc_entry *e;
|
|
size_t i;
|
|
if (list->count >= CD_ASSERT_MAX_DISCS) return;
|
|
e = &list->entries[list->count++];
|
|
snprintf(e->path, sizeof(e->path), "%s", path);
|
|
snprintf(e->ext, sizeof(e->ext), "%s", ext ? ext : "");
|
|
/* lowercase the ext so cd_disc_priority/cd_str_iequals work uniformly */
|
|
for (i = 0; e->ext[i]; i++)
|
|
if (e->ext[i] >= 'A' && e->ext[i] <= 'Z') e->ext[i] += 32;
|
|
e->file_size = size;
|
|
}
|
|
|
|
/* Drop ISO/CDI entries that share a directory with an already-recorded CUE.
|
|
* CUE wins because it carries pregap/track-type metadata that ISO lacks. */
|
|
static void cd_disc_list_dedup(struct cd_disc_list *list)
|
|
{
|
|
size_t i, j;
|
|
for (i = 0; i < list->count; i++) {
|
|
const char *cue_dir_end;
|
|
size_t cue_dir_len;
|
|
if (!cd_str_iequals(list->entries[i].ext, "cue")) continue;
|
|
|
|
cue_dir_end = strrchr(list->entries[i].path, '/');
|
|
if (!cue_dir_end) continue;
|
|
cue_dir_len = cue_dir_end - list->entries[i].path;
|
|
|
|
for (j = 0; j < list->count; ) {
|
|
const char *other_dir_end;
|
|
size_t other_dir_len;
|
|
if (j == i) { j++; continue; }
|
|
if (cd_str_iequals(list->entries[j].ext, "cue")) { j++; continue; }
|
|
other_dir_end = strrchr(list->entries[j].path, '/');
|
|
other_dir_len = other_dir_end ?
|
|
(size_t)(other_dir_end - list->entries[j].path) : 0;
|
|
if (other_dir_len == cue_dir_len &&
|
|
strncmp(list->entries[i].path, list->entries[j].path, cue_dir_len) == 0) {
|
|
/* Remove entry j */
|
|
memmove(&list->entries[j], &list->entries[j + 1],
|
|
(list->count - j - 1) * sizeof(list->entries[0]));
|
|
list->count--;
|
|
if (j < i) i--;
|
|
} else {
|
|
j++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static int cd_disc_compare(const void *a, const void *b)
|
|
{
|
|
const struct cd_disc_entry *ea = (const struct cd_disc_entry *)a;
|
|
const struct cd_disc_entry *eb = (const struct cd_disc_entry *)b;
|
|
int pa = cd_disc_priority(ea->ext);
|
|
int pb = cd_disc_priority(eb->ext);
|
|
if (pa != pb) return pa - pb;
|
|
return strcmp(ea->path, eb->path);
|
|
}
|
|
|
|
static void cd_discover_walk(const char *root, struct cd_disc_list *list, int depth)
|
|
{
|
|
DIR *dir;
|
|
struct dirent *de;
|
|
if (depth > CD_ASSERT_MAX_SCAN_DEPTH) return;
|
|
if (list->count >= CD_ASSERT_MAX_DISCS) return;
|
|
|
|
dir = opendir(root);
|
|
if (!dir) return;
|
|
|
|
while ((de = readdir(dir)) != NULL && list->count < CD_ASSERT_MAX_DISCS) {
|
|
char path[CD_ASSERT_MAX_PATH_LEN];
|
|
struct stat st;
|
|
const char *ext;
|
|
if (cd_should_skip_dir(de->d_name)) continue;
|
|
|
|
snprintf(path, sizeof(path), "%s/%s", root, de->d_name);
|
|
|
|
if (stat(path, &st) != 0) continue;
|
|
|
|
if (S_ISDIR(st.st_mode)) {
|
|
cd_discover_walk(path, list, depth + 1);
|
|
continue;
|
|
}
|
|
if (!S_ISREG(st.st_mode)) continue;
|
|
|
|
ext = cd_disc_extension(de->d_name);
|
|
if (cd_disc_priority(ext) < 0) continue;
|
|
if (!cd_ext_enabled(ext)) continue;
|
|
|
|
cd_disc_list_add(list, path, ext, (size_t)st.st_size);
|
|
}
|
|
|
|
closedir(dir);
|
|
}
|
|
|
|
static void cd_discover_discs(const char *root, struct cd_disc_list *list)
|
|
{
|
|
const char *env_root;
|
|
memset(list, 0, sizeof(*list));
|
|
|
|
/* Honor optional override: VJ_TEST_CD_ROOT */
|
|
env_root = getenv("VJ_TEST_CD_ROOT");
|
|
if (env_root && env_root[0]) root = env_root;
|
|
|
|
cd_discover_walk(root, list, 0);
|
|
cd_disc_list_dedup(list);
|
|
qsort(list->entries, list->count, sizeof(list->entries[0]), cd_disc_compare);
|
|
}
|
|
|
|
/* True if the disc basename contains the substring in VJ_TEST_CD_FOCUS (case-insensitive),
|
|
* or if the env var is unset (no filter). */
|
|
static bool cd_disc_in_focus(const char *path)
|
|
{
|
|
const char *needle;
|
|
size_t nlen, plen, i;
|
|
needle = getenv("VJ_TEST_CD_FOCUS");
|
|
if (!needle || !needle[0]) return true;
|
|
|
|
/* Naive case-insensitive substring search */
|
|
nlen = strlen(needle);
|
|
plen = strlen(path);
|
|
if (nlen > plen) return false;
|
|
for (i = 0; i + nlen <= plen; i++) {
|
|
size_t k = 0;
|
|
while (k < nlen) {
|
|
char a, b;
|
|
a = path[i + k]; if (a >= 'A' && a <= 'Z') a += 32;
|
|
b = needle[k]; if (b >= 'A' && b <= 'Z') b += 32;
|
|
if (a != b) break;
|
|
k++;
|
|
}
|
|
if (k == nlen) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Per-frame assertions */
|
|
/* */
|
|
/* These return true on success and emit a one-line diagnostic on */
|
|
/* failure. They DO NOT abort the test loop — caller decides whether */
|
|
/* a single frame failure terminates the per-disc test. */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
/* Returns 0 = in-range, otherwise the offending PC. Caller decides whether to log. */
|
|
static inline uint32_t cd_pc_oob(struct vj_core *core)
|
|
{
|
|
uint32_t pc;
|
|
if (!core->m68k_get_reg) return 0;
|
|
pc = core->m68k_get_reg(NULL, M68K_REG_PC);
|
|
if (pc < 0x200000) return 0;
|
|
if (pc >= 0xE00000 && pc < 0xE20000) return 0; /* boot ROM */
|
|
if (pc >= 0x800000 && pc < 0x900000) return 0; /* cart / CD BIOS */
|
|
return pc;
|
|
}
|
|
|
|
#define CD_PC_HISTORY_LEN 64
|
|
#define CD_PC_UNIQUE_CAP 256
|
|
|
|
/* Tracks both a sliding window of recent PCs (for "stuck self-loop"
|
|
* detection) and a bounded set of unique PCs seen over the whole run
|
|
* (for "tight 2-instruction retry-loop" detection). */
|
|
struct cd_pc_history {
|
|
uint32_t samples[CD_PC_HISTORY_LEN];
|
|
size_t write_idx;
|
|
size_t filled;
|
|
|
|
uint32_t unique[CD_PC_UNIQUE_CAP];
|
|
size_t unique_count;
|
|
bool unique_overflow; /* set when we exceed the cap (= healthy variety) */
|
|
};
|
|
|
|
static inline void cd_pc_history_push(struct cd_pc_history *h, uint32_t pc)
|
|
{
|
|
size_t i;
|
|
h->samples[h->write_idx] = pc;
|
|
h->write_idx = (h->write_idx + 1) % CD_PC_HISTORY_LEN;
|
|
if (h->filled < CD_PC_HISTORY_LEN) h->filled++;
|
|
|
|
if (h->unique_overflow) return;
|
|
for (i = 0; i < h->unique_count; i++)
|
|
if (h->unique[i] == pc) return;
|
|
if (h->unique_count >= CD_PC_UNIQUE_CAP) {
|
|
h->unique_overflow = true;
|
|
return;
|
|
}
|
|
h->unique[h->unique_count++] = pc;
|
|
}
|
|
|
|
/* True if every PC sample in the recent window is identical (tight self-loop). */
|
|
static inline bool cd_pc_history_is_self_loop(const struct cd_pc_history *h)
|
|
{
|
|
uint32_t first;
|
|
size_t i;
|
|
if (h->filled < CD_PC_HISTORY_LEN) return false;
|
|
first = h->samples[0];
|
|
for (i = 1; i < h->filled; i++)
|
|
if (h->samples[i] != first) return false;
|
|
return true;
|
|
}
|
|
|
|
/* True if the run only ever visited <= max_unique distinct PCs.
|
|
* Catches the "CD_read -> CD_poll -> CD_fifo_disable -> retry" tight loop
|
|
* (Iron Soldier 2 bounces between two PCs the entire run). */
|
|
static inline bool cd_pc_history_is_thrashing(const struct cd_pc_history *h,
|
|
size_t max_unique)
|
|
{
|
|
if (h->unique_overflow) return false;
|
|
return h->unique_count <= max_unique;
|
|
}
|
|
|
|
/* Counts how many bytes in the given RAM range are non-zero. */
|
|
static inline size_t cd_count_nonzero(const uint8_t *ram, uint32_t addr, uint32_t len)
|
|
{
|
|
size_t n = 0;
|
|
uint32_t i;
|
|
for (i = 0; i < len; i++) if (ram[addr + i]) n++;
|
|
return n;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* CD subsystem instrumentation */
|
|
/* */
|
|
/* Reads diagnostic counters exposed by the core via dlsym so a per- */
|
|
/* disc report can include real CD activity (BUTCH ticks, FIFO IRQs, */
|
|
/* GPU IRQ0/IRQ3 firing counts, HLE transfer bytes) — not just 68K PC. */
|
|
/* ------------------------------------------------------------------ */
|
|
struct cd_diag_snapshot {
|
|
uint32_t butchExec;
|
|
uint32_t fifoIRQs;
|
|
uint32_t dsaIRQs;
|
|
uint32_t fifoReads;
|
|
uint32_t seeks;
|
|
uint32_t globalDisabled;
|
|
uint32_t hleBytes;
|
|
uint32_t gpu_irq0_count;
|
|
uint32_t gpu_irq3_count;
|
|
uint32_t gpu_pc;
|
|
};
|
|
|
|
typedef void (*cd_diag_get_counters_fn)(uint32_t *, uint32_t *,
|
|
uint32_t *, uint32_t *,
|
|
uint32_t *, uint32_t *,
|
|
uint32_t *);
|
|
|
|
static inline void cd_diag_capture(void *handle, struct cd_diag_snapshot *out)
|
|
{
|
|
cd_diag_get_counters_fn p_get;
|
|
uint32_t *p_irq0, *p_irq3, *p_gpu_pc;
|
|
uint32_t (*p_get_pc)(void);
|
|
memset(out, 0, sizeof(*out));
|
|
if (!handle) return;
|
|
|
|
p_get = (cd_diag_get_counters_fn)dlsym(handle, "CDROMDiagGetCounters");
|
|
if (p_get) {
|
|
p_get(&out->butchExec, &out->fifoIRQs, &out->dsaIRQs,
|
|
&out->fifoReads, &out->seeks, &out->globalDisabled,
|
|
&out->hleBytes);
|
|
}
|
|
|
|
p_irq0 = (uint32_t *)dlsym(handle, "gpu_irq0_count");
|
|
p_irq3 = (uint32_t *)dlsym(handle, "gpu_irq3_count");
|
|
if (p_irq0) out->gpu_irq0_count = *p_irq0;
|
|
if (p_irq3) out->gpu_irq3_count = *p_irq3;
|
|
|
|
/* gpu_pc is a non-static global (used elsewhere as `extern uint32_t gpu_pc`).
|
|
* Prefer GPUGetPC if exported; fall back to the symbol directly. */
|
|
p_get_pc = (uint32_t (*)(void))dlsym(handle, "GPUGetPC");
|
|
if (p_get_pc) {
|
|
out->gpu_pc = p_get_pc();
|
|
} else {
|
|
p_gpu_pc = (uint32_t *)dlsym(handle, "gpu_pc");
|
|
if (p_gpu_pc) out->gpu_pc = *p_gpu_pc;
|
|
}
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* SHA1 (small, dependency-free, for baseline sidecar) */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
struct cd_sha1_ctx {
|
|
uint32_t state[5];
|
|
uint64_t bits;
|
|
uint8_t buf[64];
|
|
size_t buflen;
|
|
};
|
|
|
|
static inline uint32_t cd_sha1_rol(uint32_t v, unsigned n) { return (v << n) | (v >> (32 - n)); }
|
|
|
|
static void cd_sha1_block(struct cd_sha1_ctx *ctx, const uint8_t *block)
|
|
{
|
|
uint32_t w[80];
|
|
uint32_t a, b, c, d, e;
|
|
int i;
|
|
for (i = 0; i < 16; i++)
|
|
w[i] = ((uint32_t)block[i*4] << 24) | ((uint32_t)block[i*4+1] << 16) |
|
|
((uint32_t)block[i*4+2] << 8) | (uint32_t)block[i*4+3];
|
|
for (i = 16; i < 80; i++)
|
|
w[i] = cd_sha1_rol(w[i-3] ^ w[i-8] ^ w[i-14] ^ w[i-16], 1);
|
|
|
|
a = ctx->state[0]; b = ctx->state[1]; c = ctx->state[2];
|
|
d = ctx->state[3]; e = ctx->state[4];
|
|
for (i = 0; i < 80; i++) {
|
|
uint32_t f, k, t;
|
|
if (i < 20) { f = (b & c) | ((~b) & d); k = 0x5A827999; }
|
|
else if (i < 40) { f = b ^ c ^ d; k = 0x6ED9EBA1; }
|
|
else if (i < 60) { f = (b & c) | (b & d) | (c & d); k = 0x8F1BBCDC; }
|
|
else { f = b ^ c ^ d; k = 0xCA62C1D6; }
|
|
t = cd_sha1_rol(a, 5) + f + e + k + w[i];
|
|
e = d; d = c; c = cd_sha1_rol(b, 30); b = a; a = t;
|
|
}
|
|
ctx->state[0] += a; ctx->state[1] += b; ctx->state[2] += c;
|
|
ctx->state[3] += d; ctx->state[4] += e;
|
|
}
|
|
|
|
static void cd_sha1_init(struct cd_sha1_ctx *ctx)
|
|
{
|
|
ctx->state[0] = 0x67452301; ctx->state[1] = 0xEFCDAB89;
|
|
ctx->state[2] = 0x98BADCFE; ctx->state[3] = 0x10325476;
|
|
ctx->state[4] = 0xC3D2E1F0;
|
|
ctx->bits = 0; ctx->buflen = 0;
|
|
}
|
|
|
|
static void cd_sha1_update(struct cd_sha1_ctx *ctx, const uint8_t *data, size_t len)
|
|
{
|
|
ctx->bits += (uint64_t)len * 8;
|
|
while (len) {
|
|
size_t take;
|
|
take = 64 - ctx->buflen;
|
|
if (take > len) take = len;
|
|
memcpy(ctx->buf + ctx->buflen, data, take);
|
|
ctx->buflen += take; data += take; len -= take;
|
|
if (ctx->buflen == 64) { cd_sha1_block(ctx, ctx->buf); ctx->buflen = 0; }
|
|
}
|
|
}
|
|
|
|
static void cd_sha1_final(struct cd_sha1_ctx *ctx, char out_hex[41])
|
|
{
|
|
static const uint8_t pad[64] = { 0x80 };
|
|
static const char hex[] = "0123456789abcdef";
|
|
uint64_t bits;
|
|
uint8_t length_be[8];
|
|
size_t pad_len;
|
|
int i, j;
|
|
bits = ctx->bits;
|
|
for (i = 0; i < 8; i++) length_be[i] = (uint8_t)(bits >> (56 - 8*i));
|
|
|
|
pad_len = (ctx->buflen < 56) ? (56 - ctx->buflen) : (120 - ctx->buflen);
|
|
cd_sha1_update(ctx, pad, pad_len);
|
|
cd_sha1_update(ctx, length_be, 8);
|
|
|
|
for (i = 0; i < 5; i++) {
|
|
for (j = 0; j < 4; j++) {
|
|
uint8_t v = (uint8_t)(ctx->state[i] >> (24 - j*8));
|
|
out_hex[i*8 + j*2 + 0] = hex[v >> 4];
|
|
out_hex[i*8 + j*2 + 1] = hex[v & 0xF];
|
|
}
|
|
}
|
|
out_hex[40] = '\0';
|
|
}
|
|
|
|
#endif /* CD_ASSERTIONS_H */
|