mirror of
https://github.com/libretro/virtualjaguar-libretro.git
synced 2026-11-07 04:36:00 -10:00
* fix(cd): skip the session-2 track pregap when extracting the CD boot stub
The boot-stub reader seeks to the start of the session-2 track's REGION, and a
track region begins with its pregap. The Atari boot header sits at the start of
the user data (INDEX 01), so when the pregap is non-zero we read pregap silence,
the magic check at +0x42 fails, and retro_load_game() returns false.
dataLBA - startLBA is the pregap length in sectors in BOTH layouts, so one
expression covers them:
trackFileBase = fileOffset + (dataLBA - startLBA) * sectorSize
- multi-file CUE: the parser stores the track's INDEX 01 offset within its own
BIN as (dataLBA - startLBA), and fileOffset is 0 because startLBA maps to the
file start. Previously hardcoded to 0, ignoring the pregap.
- single-file CDI / single-BIN CUE: ParseCDI sets dataLBA = startLBA + pregap
and fileOffset is the region start.
Why it stayed hidden: nothing available exercised it with a non-zero skip.
test/tools/cue2cdi emits pregap == 0 on the session-2 track, and all eleven
CUEs in the test corpus have INDEX 01 00:00:00 on their first session-2 track.
DiscJuggler does store it -- 150 sectors on the session-2 track of the ROM-set
Baldies image.
Verified in both directions:
multi-file, pregap=150 synthetic disc: real Baldies with 150 sectors of
silence prepended to track 02's BIN and declared as
INDEX 00 / INDEX 01 00:02:00.
before: Magic mismatch -> load failed
after: offset $0 -> $56220, boot stub injected,
[PASS] final_pc=$05FE82 -- byte-identical
evidence to the unmodified disc
multi-file, pregap=0 real Baldies cue: offset $0 (unchanged), [PASS]
single-file, pregap=0 cue2cdi baldies.cdi: $AB0270 (unchanged), [PASS]
single-file, pregap=150 ROM-set baldies.cdi: $B06490 -> $B5C6B0
This does NOT fix #230. The ROM-set image still fails, on a separate and
still-unexplained 4336-byte shift measured across all nine of its tracks (see
the issue). The pregap defect is real on its own merits; fixing it is just not
sufficient to load that image.
CD boot matrix: all 22 rows GAME_CODE 1/1 in both modes, no change from
develop. C89 lint clean.
Refs #230
Co-Authored-By: Claude Opus 5 <[email protected]>
* test(cd): cover the session-2 pregap skip with a synthetic disc
The pregap fix had no regression test: no CUE in the (gitignored) commercial
corpus declares a pregap on its first session-2 track, and cue2cdi emits
pregap == 0 there, so nothing available exercises a non-zero skip. A test that
depends on those discs would also skip in CI, where they do not exist.
So the test synthesises both discs itself -- no commercial data, runs anywhere:
nopregap track 02 BIN = header block , INDEX 01 00:00:00
pregap track 02 BIN = 150 silent sectors + it , INDEX 00 00:00:00
INDEX 01 00:02:00
Both declare the same user data, so both must load. The boot header is built
in "swapped" space (magic at +0x42, load addr at +0x62, length at +0x66,
payload at +0x6A) and byte-swapped into Jaguar I2S order, matching what the
extractor un-swaps. The payload is $400 bytes of "bra.s *" so the injected
stub parks the 68K instead of executing random bytes.
Confirmed to catch the regression, not just pass:
with the fix rc=0, 1 passed
with develop's cdintf.c rc=1, "Magic mismatch at +0x42", 0 passed 1 failed
Wired into `make test` alongside the other CD tests.
Co-Authored-By: Claude Opus 5 <[email protected]>
---------
Co-authored-by: Claude Opus 5 <[email protected]>
255 lines
8.3 KiB
C
255 lines
8.3 KiB
C
/*
|
|
* test_cd_pregap.c -- session-2 track pregap handling in the CD boot-stub
|
|
* extractor.
|
|
*
|
|
* The boot-stub reader seeks to the start of the session-2 track's REGION,
|
|
* which begins with the track's pregap. The Atari boot header lives at the
|
|
* start of the USER DATA (INDEX 01), so a non-zero pregap has to be skipped
|
|
* or the reader sees pregap silence and the magic check at +0x42 fails.
|
|
*
|
|
* This regressed nothing for a long time because nothing available exercised
|
|
* it with a non-zero skip: test/tools/cue2cdi emits pregap == 0 on the
|
|
* session-2 track, and every CUE in the (gitignored) commercial corpus has
|
|
* INDEX 01 00:00:00 on its first session-2 track. DiscJuggler does store the
|
|
* pregap in-file.
|
|
*
|
|
* So this test SYNTHESISES both discs -- no commercial data, runs anywhere:
|
|
*
|
|
* nopregap track 02 BIN = header block , INDEX 01 00:00:00
|
|
* pregap track 02 BIN = 150 silent sectors + it , INDEX 00 00:00:00
|
|
* INDEX 01 00:02:00
|
|
*
|
|
* Both declare the same user data, so both MUST extract a byte-identical boot
|
|
* stub. Before the fix the "pregap" disc fails to load at all.
|
|
*
|
|
* Build:
|
|
* make -j4 && make test/test_cd_pregap
|
|
* Run:
|
|
* DYLD_LIBRARY_PATH=. test/test_cd_pregap
|
|
*/
|
|
|
|
#include "cd_assertions.h"
|
|
#include "../libretro-common/include/libretro.h"
|
|
|
|
#include <unistd.h>
|
|
#include <sys/stat.h>
|
|
|
|
static struct vj_core C;
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* Synthetic disc construction */
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
#define SECTOR_BYTES 2352u
|
|
#define PREGAP_SECTORS 150u /* 00:02:00 */
|
|
#define STUB_LOAD_ADDR 0x004000u
|
|
#define STUB_LENGTH 0x400u
|
|
#define HDR_MAGIC_OFF 0x42u
|
|
#define HDR_LOAD_OFF 0x62u
|
|
#define HDR_LEN_OFF 0x66u
|
|
#define HDR_PAYLOAD 0x6Au
|
|
#define TRACK2_DATA (HDR_PAYLOAD + STUB_LENGTH + 0x100u)
|
|
|
|
static const char BOOT_MAGIC[32] = "ATARI APPROVED DATA HEADER ATRI ";
|
|
|
|
/* Build the session-2 user data exactly as the extractor expects it, in
|
|
* "swapped" space, then byte-swap each 16-bit pair to get the on-disc bytes
|
|
* (Jaguar I2S order -- the extractor un-swaps before reading the header). */
|
|
static void build_track2_userdata(uint8_t *out, size_t outLen)
|
|
{
|
|
uint8_t *sw;
|
|
size_t i;
|
|
|
|
sw = (uint8_t *)calloc(1, outLen);
|
|
if (!sw) return;
|
|
|
|
memcpy(sw + HDR_MAGIC_OFF, BOOT_MAGIC, sizeof(BOOT_MAGIC));
|
|
|
|
sw[HDR_LOAD_OFF + 0] = (uint8_t)(STUB_LOAD_ADDR >> 24);
|
|
sw[HDR_LOAD_OFF + 1] = (uint8_t)(STUB_LOAD_ADDR >> 16);
|
|
sw[HDR_LOAD_OFF + 2] = (uint8_t)(STUB_LOAD_ADDR >> 8);
|
|
sw[HDR_LOAD_OFF + 3] = (uint8_t)(STUB_LOAD_ADDR);
|
|
|
|
sw[HDR_LEN_OFF + 0] = (uint8_t)(STUB_LENGTH >> 24);
|
|
sw[HDR_LEN_OFF + 1] = (uint8_t)(STUB_LENGTH >> 16);
|
|
sw[HDR_LEN_OFF + 2] = (uint8_t)(STUB_LENGTH >> 8);
|
|
sw[HDR_LEN_OFF + 3] = (uint8_t)(STUB_LENGTH);
|
|
|
|
/* Payload: 68K "bra.s *" ($60FE) repeated -- a valid, harmless stub that
|
|
* parks the CPU instead of executing random bytes. */
|
|
for (i = HDR_PAYLOAD; i + 1 < HDR_PAYLOAD + STUB_LENGTH && i + 1 < outLen; i += 2) {
|
|
sw[i] = 0x60;
|
|
sw[i + 1] = 0xFE;
|
|
}
|
|
|
|
for (i = 0; i + 1 < outLen; i += 2) {
|
|
out[i] = sw[i + 1];
|
|
out[i + 1] = sw[i];
|
|
}
|
|
free(sw);
|
|
}
|
|
|
|
static bool write_file(const char *path, const uint8_t *data, size_t len)
|
|
{
|
|
FILE *f = fopen(path, "wb");
|
|
size_t n;
|
|
if (!f) return false;
|
|
n = len ? fwrite(data, 1, len, f) : 0;
|
|
fclose(f);
|
|
return n == len;
|
|
}
|
|
|
|
/* Writes <dir>/<name>.cue + track BINs. withPregap selects whether the
|
|
* session-2 track carries a 150-sector pregap (declared AND stored). */
|
|
static bool make_disc(const char *dir, bool withPregap, char *cueOut, size_t cueOutLen)
|
|
{
|
|
char path[1024];
|
|
uint8_t *t1, *t2;
|
|
size_t t1Len, t2Len, dataOff;
|
|
FILE *cue;
|
|
bool ok = false;
|
|
|
|
t1Len = 4u * SECTOR_BYTES; /* session 1 filler */
|
|
dataOff = withPregap ? (PREGAP_SECTORS * SECTOR_BYTES) : 0u;
|
|
t2Len = dataOff + TRACK2_DATA;
|
|
|
|
t1 = (uint8_t *)calloc(1, t1Len);
|
|
t2 = (uint8_t *)calloc(1, t2Len);
|
|
if (!t1 || !t2) { free(t1); free(t2); return false; }
|
|
|
|
build_track2_userdata(t2 + dataOff, TRACK2_DATA);
|
|
|
|
snprintf(path, sizeof(path), "%s/track01.bin", dir);
|
|
if (!write_file(path, t1, t1Len)) goto done;
|
|
snprintf(path, sizeof(path), "%s/track02.bin", dir);
|
|
if (!write_file(path, t2, t2Len)) goto done;
|
|
|
|
snprintf(cueOut, cueOutLen, "%s/disc.cue", dir);
|
|
cue = fopen(cueOut, "wb");
|
|
if (!cue) goto done;
|
|
fprintf(cue,
|
|
"REM SESSION 01\n"
|
|
"FILE \"track01.bin\" BINARY\n"
|
|
" TRACK 01 AUDIO\n"
|
|
" INDEX 01 00:00:00\n"
|
|
"REM SESSION 02\n"
|
|
"FILE \"track02.bin\" BINARY\n"
|
|
" TRACK 02 AUDIO\n");
|
|
if (withPregap)
|
|
fprintf(cue, " INDEX 00 00:00:00\n INDEX 01 00:02:00\n");
|
|
else
|
|
fprintf(cue, " INDEX 01 00:00:00\n");
|
|
fclose(cue);
|
|
ok = true;
|
|
|
|
done:
|
|
free(t1);
|
|
free(t2);
|
|
return ok;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
static bool cd_load_game(const char *path)
|
|
{
|
|
struct retro_game_info info;
|
|
bool (*p_retro_load_game)(const struct retro_game_info *);
|
|
memset(&info, 0, sizeof(info));
|
|
info.path = path;
|
|
p_retro_load_game = (bool (*)(const struct retro_game_info *))
|
|
dlsym(C.handle, "retro_load_game");
|
|
if (!p_retro_load_game) return false;
|
|
return p_retro_load_game(&info);
|
|
}
|
|
|
|
static void cd_unload_game(void)
|
|
{
|
|
void (*p)(void) = (void (*)(void))dlsym(C.handle, "retro_unload_game");
|
|
if (p) p();
|
|
}
|
|
|
|
static bool env_cb(unsigned cmd, void *data)
|
|
{
|
|
if (cmd == RETRO_ENVIRONMENT_GET_VARIABLE) {
|
|
struct retro_variable *var = (struct retro_variable *)data;
|
|
if (strcmp(var->key, "virtualjaguar_cd_boot_mode") == 0) { var->value = "hle"; return true; }
|
|
return false;
|
|
}
|
|
if (cmd == RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY) {
|
|
*(const char **)data = ".";
|
|
return true;
|
|
}
|
|
if (cmd == RETRO_ENVIRONMENT_SET_PIXEL_FORMAT) return true;
|
|
return false;
|
|
}
|
|
|
|
static void vcb(const void *d, unsigned w, unsigned h, size_t p)
|
|
{ (void)d; (void)w; (void)h; (void)p; }
|
|
static void acb(int16_t l, int16_t r) { (void)l; (void)r; }
|
|
static size_t abcb(const int16_t *d, size_t f) { (void)d; return f; }
|
|
static void ipcb(void) {}
|
|
static int16_t iscb(unsigned p, unsigned d, unsigned i, unsigned id)
|
|
{ (void)p; (void)d; (void)i; (void)id; return 0; }
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
|
|
TEST(session2_pregap_is_skipped)
|
|
{
|
|
char base[512];
|
|
char subA[1024], subB[1024], cueA[1024], cueB[1024];
|
|
|
|
/* Deterministic per-process scratch dir: mkdtemp() is hidden behind
|
|
* different feature-test macros on Darwin vs glibc, and this needs no
|
|
* uniqueness beyond the running process. */
|
|
snprintf(base, sizeof(base), "/tmp/vj_cd_pregap_%ld", (long)getpid());
|
|
mkdir(base, 0755);
|
|
|
|
snprintf(subA, sizeof(subA), "%s/nopregap", base);
|
|
snprintf(subB, sizeof(subB), "%s/pregap", base);
|
|
ASSERT_TRUE(mkdir(subA, 0755) == 0);
|
|
ASSERT_TRUE(mkdir(subB, 0755) == 0);
|
|
|
|
ASSERT_TRUE(make_disc(subA, false, cueA, sizeof(cueA)));
|
|
ASSERT_TRUE(make_disc(subB, true, cueB, sizeof(cueB)));
|
|
|
|
/* Control: pregap == 0. Must load (it did before the fix too). */
|
|
ASSERT_TRUE(cd_load_game(cueA));
|
|
cd_unload_game();
|
|
|
|
/* Regression: same user data behind a declared 150-sector pregap.
|
|
* Before the fix the reader seeks to the region start, sees silence,
|
|
* and the +0x42 magic check fails -> retro_load_game() returns false. */
|
|
ASSERT_TRUE(cd_load_game(cueB));
|
|
cd_unload_game();
|
|
}
|
|
|
|
int main(int argc, char *argv[])
|
|
{
|
|
(void)argc; (void)argv;
|
|
|
|
TEST_INIT("CD session-2 pregap");
|
|
|
|
if (!vj_core_load(&C)) {
|
|
fprintf(stderr, "FATAL: failed to load core\n");
|
|
return 1;
|
|
}
|
|
|
|
C.retro_set_environment(env_cb);
|
|
C.retro_set_video_refresh(vcb);
|
|
C.retro_set_audio_sample(acb);
|
|
C.retro_set_audio_sample_batch(abcb);
|
|
C.retro_set_input_poll(ipcb);
|
|
C.retro_set_input_state(iscb);
|
|
C.retro_init();
|
|
|
|
RUN_TEST(session2_pregap_is_skipped);
|
|
|
|
{
|
|
void (*p)(void) = (void (*)(void))dlsym(C.handle, "retro_deinit");
|
|
if (p) p();
|
|
}
|
|
if (C.handle) dlclose(C.handle);
|
|
|
|
return TEST_REPORT();
|
|
}
|