mirror of
https://github.com/libretro/virtualjaguar-libretro.git
synced 2026-11-07 12:47:58 -10:00
Every committed CHD fixture was PREGAP 0, so the virtual-pregap path #755 fixed had no coverage. synth_jagcd_vpregap.chd has three session-2 tracks with 2/3/5-sector INDEX 00 runs that the pinned chdman records as VAUDIO pregaps counted inside FRAMES (the Myst (USA) layout). Data sectors carry per-track, per-sector tags so a misplaced read fails on content. test_cd_chd checks each track's TOC MSF, that every pregap sector reads silence, and that every INDEX 01 sector reads its own tag. It passes on develop and fails on f28ade2^1, reading silence at track 3's INDEX 01. jagcd-chd-check must exit 2 on the fixture, in make test and in CI. Refs #774 Co-Authored-By: Claude Opus 5.5 <[email protected]>
428 lines
13 KiB
C
428 lines
13 KiB
C
/*
|
|
* test_cd_chd.c -- CHD session gate (issue #322) and CHSE track-position
|
|
* conflation (issue #476)
|
|
*
|
|
* good : test/roms/synth_jagcd.chd (CHSE, two sessions) must load
|
|
* with session-2 INDEX 01 at LBA 11404 (4-sector session 1
|
|
* + synthesized 11400-sector gap, PREGAP 0).
|
|
* nosession : test/roms/synth_jagcd_nosession.chd must be refused by
|
|
* ParseCHD (log contains "no session metadata (CHSE)"),
|
|
* not merely fail later at HLE extract.
|
|
* multi : test/roms/synth_jagcd_multi.chd has THREE tracks, with
|
|
* session 1 carrying two of them (track 1, track 2) and
|
|
* session 2 starting at track 3. CHD's CHSE metadata is
|
|
* indexed among CHSE entries themselves (index 0 = the 1st
|
|
* CHSE entry anywhere in the file, 1 = the 2nd), not by
|
|
* track position -- a parser that queries CHSE at the same
|
|
* index as its track loop counter only gets the right
|
|
* answer when session 1 has exactly one track. This fixture
|
|
* pins track 2 to session 1 (not session 2) and the boot
|
|
* stub to track 3.
|
|
* vpregap : test/roms/synth_jagcd_vpregap.chd has three session-2
|
|
* tracks whose INDEX 00 runs (2, 3 and 5 sectors) chdman
|
|
* records as VAUDIO virtual pregaps COUNTED INSIDE the
|
|
* track's FRAMES (issue #774, the #754/#755 case). Before
|
|
* #755 ParseCHD added PREGAP to FRAMES a second time (every
|
|
* later track shifted by the sum of the earlier pregaps) and
|
|
* read INDEX 01 from the head of the pregap. Every tagged
|
|
* sector names its own track and sector, so a read from the
|
|
* wrong place fails on content, not just on silence.
|
|
* Source: test/tools/gen_synth_jagcd_vpregap.py.
|
|
*
|
|
* Fixtures are committed uncompressed CD CHDs. If one is missing, SKIP
|
|
* rather than fail a checkout that forgot to git-add the files. There
|
|
* is no in-test rebuild path (CI/Homebrew chdman cannot write CHSE).
|
|
*
|
|
* Build: make TEST_EXPORTS=1 test/test_cd_chd
|
|
*/
|
|
|
|
#include "cd_assertions.h"
|
|
#include "../libretro-common/include/libretro.h"
|
|
|
|
#include <stdarg.h>
|
|
#include <unistd.h>
|
|
#include <sys/stat.h>
|
|
|
|
static struct vj_core C;
|
|
static char g_log[8192];
|
|
|
|
static bool file_exists(const char *path)
|
|
{
|
|
struct stat st;
|
|
return stat(path, &st) == 0 && st.st_size > 0;
|
|
}
|
|
|
|
static void test_log(enum retro_log_level level, const char *fmt, ...)
|
|
{
|
|
va_list ap;
|
|
size_t n;
|
|
int wrote;
|
|
|
|
(void)level;
|
|
n = strlen(g_log);
|
|
if (n + 1 >= sizeof(g_log))
|
|
return;
|
|
va_start(ap, fmt);
|
|
wrote = vsnprintf(g_log + n, sizeof(g_log) - n, fmt, ap);
|
|
va_end(ap);
|
|
(void)wrote;
|
|
}
|
|
|
|
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;
|
|
g_log[0] = '\0';
|
|
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 uint32_t cd_num_sessions(void)
|
|
{
|
|
uint32_t (*p)(void) = (uint32_t (*)(void))dlsym(C.handle, "CDIntfGetNumSessions");
|
|
if (!p) return 0;
|
|
return p();
|
|
}
|
|
|
|
static uint32_t cd_session2_lba(void)
|
|
{
|
|
uint32_t (*p)(void) = (uint32_t (*)(void))dlsym(C.handle, "CDIntfGetSession2FirstTrackLBA");
|
|
if (!p) return 0;
|
|
return p();
|
|
}
|
|
|
|
static uint32_t cd_num_tracks(void)
|
|
{
|
|
uint32_t (*p)(void) = (uint32_t (*)(void))dlsym(C.handle, "CDIntfGetNumTracks");
|
|
if (!p) return 0;
|
|
return p();
|
|
}
|
|
|
|
static uint8_t cd_track_session(uint32_t track)
|
|
{
|
|
uint8_t (*p)(uint32_t) = (uint8_t (*)(uint32_t))dlsym(C.handle, "CDIntfGetTrackSession");
|
|
if (!p) return 0;
|
|
return p(track);
|
|
}
|
|
|
|
static bool cd_read_block(uint32_t lba, uint8_t *buf)
|
|
{
|
|
bool (*p)(uint32_t, uint8_t *) =
|
|
(bool (*)(uint32_t, uint8_t *))dlsym(C.handle, "CDIntfReadBlock");
|
|
if (!p) return false;
|
|
return p(lba, buf);
|
|
}
|
|
|
|
static bool cd_last_virtual_pregap(void)
|
|
{
|
|
bool (*p)(void) = (bool (*)(void))dlsym(C.handle, "CDIntfLastReadWasVirtualPregap");
|
|
if (!p) return false;
|
|
return p();
|
|
}
|
|
|
|
static void cd_clear_virtual_pregap(void)
|
|
{
|
|
void (*p)(void) = (void (*)(void))dlsym(C.handle, "CDIntfClearLastReadVirtualPregap");
|
|
if (p) p();
|
|
}
|
|
|
|
static bool cd_extract_stub(uint8_t *buf, uint32_t bufsz, uint32_t *addr, uint32_t *len)
|
|
{
|
|
bool (*p)(uint8_t *, uint32_t, uint32_t *, uint32_t *) =
|
|
(bool (*)(uint8_t *, uint32_t, uint32_t *, uint32_t *))
|
|
dlsym(C.handle, "CDIntfExtractBootStub");
|
|
if (!p) return false;
|
|
return p(buf, bufsz, addr, len);
|
|
}
|
|
|
|
static void swap16(uint8_t *buf, size_t n)
|
|
{
|
|
size_t i;
|
|
uint8_t t;
|
|
for (i = 0; i + 1 < n; i += 2) {
|
|
t = buf[i];
|
|
buf[i] = buf[i + 1];
|
|
buf[i + 1] = t;
|
|
}
|
|
}
|
|
|
|
static bool env_cb(unsigned cmd, void *data)
|
|
{
|
|
if (cmd == RETRO_ENVIRONMENT_GET_LOG_INTERFACE) {
|
|
struct retro_log_callback *cb = (struct retro_log_callback *)data;
|
|
cb->log = test_log;
|
|
return true;
|
|
}
|
|
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(chd_without_chse_is_refused)
|
|
{
|
|
const char *fix = "test/roms/synth_jagcd_nosession.chd";
|
|
if (!file_exists(fix)) {
|
|
SKIP_TEST(chd_without_chse_is_refused, "missing test/roms/synth_jagcd_nosession.chd");
|
|
return;
|
|
}
|
|
ASSERT_TRUE(!cd_load_game(fix));
|
|
ASSERT_TRUE(strstr(g_log, "no session metadata (CHSE)") != NULL);
|
|
cd_unload_game();
|
|
}
|
|
|
|
TEST(chd_with_chse_has_two_sessions)
|
|
{
|
|
const char *fix = "test/roms/synth_jagcd.chd";
|
|
if (!file_exists(fix)) {
|
|
SKIP_TEST(chd_with_chse_has_two_sessions, "missing test/roms/synth_jagcd.chd");
|
|
return;
|
|
}
|
|
ASSERT_TRUE(cd_load_game(fix));
|
|
ASSERT_EQ_U32(cd_num_sessions(), 2u);
|
|
ASSERT_EQ_U32(cd_num_tracks(), 2u);
|
|
ASSERT_EQ(cd_track_session(1), 1);
|
|
ASSERT_EQ(cd_track_session(2), 2);
|
|
/* 4-sector session 1 + synthesized 11400-sector gap, PREGAP 0. */
|
|
ASSERT_EQ_U32(cd_session2_lba(), 11404u);
|
|
cd_unload_game();
|
|
}
|
|
|
|
TEST(chd_gap_and_session1_are_silence)
|
|
{
|
|
const char *fix = "test/roms/synth_jagcd.chd";
|
|
uint8_t buf[2352];
|
|
size_t i;
|
|
int nonzero;
|
|
|
|
if (!file_exists(fix)) {
|
|
SKIP_TEST(chd_gap_and_session1_are_silence, "missing test/roms/synth_jagcd.chd");
|
|
return;
|
|
}
|
|
ASSERT_TRUE(cd_load_game(fix));
|
|
|
|
ASSERT_TRUE(cd_read_block(0, buf));
|
|
nonzero = 0;
|
|
for (i = 0; i < sizeof(buf); i++) {
|
|
if (buf[i]) { nonzero = 1; break; }
|
|
}
|
|
ASSERT_FALSE(nonzero);
|
|
|
|
cd_clear_virtual_pregap();
|
|
ASSERT_TRUE(cd_read_block(100, buf));
|
|
ASSERT_TRUE(cd_last_virtual_pregap());
|
|
nonzero = 0;
|
|
for (i = 0; i < sizeof(buf); i++) {
|
|
if (buf[i]) { nonzero = 1; break; }
|
|
}
|
|
ASSERT_FALSE(nonzero);
|
|
|
|
cd_unload_game();
|
|
}
|
|
|
|
TEST(chd_session2_header_and_extract)
|
|
{
|
|
const char *fix = "test/roms/synth_jagcd.chd";
|
|
static const char magic[32] = "ATARI APPROVED DATA HEADER ATRI ";
|
|
uint8_t raw[2352];
|
|
uint8_t stub[0x800];
|
|
uint32_t addr, len;
|
|
|
|
if (!file_exists(fix)) {
|
|
SKIP_TEST(chd_session2_header_and_extract, "missing test/roms/synth_jagcd.chd");
|
|
return;
|
|
}
|
|
ASSERT_TRUE(cd_load_game(fix));
|
|
|
|
ASSERT_TRUE(cd_read_block(11404u, raw));
|
|
/* ReadBlock returns CUE/BIN I2S order; unswap to host/header space. */
|
|
swap16(raw, sizeof(raw));
|
|
ASSERT_TRUE(memcmp(raw + 0x42, magic, 32) == 0);
|
|
|
|
addr = 0;
|
|
len = 0;
|
|
ASSERT_TRUE(cd_extract_stub(stub, sizeof(stub), &addr, &len));
|
|
ASSERT_TRUE(len > 0);
|
|
ASSERT_TRUE(len <= sizeof(stub));
|
|
|
|
cd_unload_game();
|
|
}
|
|
|
|
TEST(chd_multi_track_session1_is_not_flipped_early)
|
|
{
|
|
const char *fix = "test/roms/synth_jagcd_multi.chd";
|
|
uint32_t addr, len;
|
|
|
|
if (!file_exists(fix)) {
|
|
SKIP_TEST(chd_multi_track_session1_is_not_flipped_early,
|
|
"missing test/roms/synth_jagcd_multi.chd");
|
|
return;
|
|
}
|
|
ASSERT_TRUE(cd_load_game(fix));
|
|
ASSERT_EQ_U32(cd_num_sessions(), 2u);
|
|
ASSERT_EQ_U32(cd_num_tracks(), 3u);
|
|
|
|
/* The regression: track 2 must stay session 1 (CHSE's 2nd entry
|
|
* belongs to track 3, not track 2 -- see the file header comment). */
|
|
ASSERT_EQ(cd_track_session(1), 1);
|
|
ASSERT_EQ(cd_track_session(2), 1);
|
|
ASSERT_EQ(cd_track_session(3), 2);
|
|
|
|
/* track1 (4) + track2 (4) + synthesized 11400-sector gap, PREGAP 0. */
|
|
ASSERT_EQ_U32(cd_session2_lba(), 11408u);
|
|
|
|
{
|
|
uint8_t stub[0x800];
|
|
addr = 0;
|
|
len = 0;
|
|
ASSERT_TRUE(cd_extract_stub(stub, sizeof(stub), &addr, &len));
|
|
ASSERT_EQ_U32(addr, 0x00080000u);
|
|
ASSERT_EQ_U32(len, 0x100u);
|
|
}
|
|
|
|
cd_unload_game();
|
|
}
|
|
|
|
/* Track layout of synth_jagcd_vpregap.chd, in disc LBAs: track 1 (4) +
|
|
* synthesized 11400-sector session gap + track 2 (4) puts track 3's
|
|
* pregap at 11408. Each later track starts where the previous one's
|
|
* FRAMES (pregap + data) end. */
|
|
#define VPG_DATA_SECTORS 4u
|
|
static const struct {
|
|
uint32_t track, startLBA, pregap;
|
|
} vpg_tracks[] = {
|
|
{ 3u, 11408u, 2u },
|
|
{ 4u, 11414u, 3u },
|
|
{ 5u, 11421u, 5u }
|
|
};
|
|
|
|
/* Mirrors tag_sector() in test/tools/gen_synth_jagcd_vpregap.py. */
|
|
static void vpg_tag_sector(uint32_t track, uint32_t sector, uint8_t *out)
|
|
{
|
|
char tag[17];
|
|
size_t i;
|
|
|
|
snprintf(tag, sizeof(tag), "T%02uS%02u vpregap ",
|
|
(unsigned)track, (unsigned)sector);
|
|
for (i = 0; i < 2352u; i += 16u)
|
|
memcpy(out + i, tag, 16u);
|
|
}
|
|
|
|
TEST(chd_virtual_pregaps_land_on_index01)
|
|
{
|
|
const char *fix = "test/roms/synth_jagcd_vpregap.chd";
|
|
uint8_t got[2352];
|
|
uint8_t want[2352];
|
|
uint8_t zero[2352];
|
|
uint32_t t, s, dataLBA, absLBA;
|
|
uint8_t (*p_track_info)(uint32_t, uint32_t);
|
|
|
|
if (!file_exists(fix)) {
|
|
SKIP_TEST(chd_virtual_pregaps_land_on_index01,
|
|
"missing test/roms/synth_jagcd_vpregap.chd");
|
|
return;
|
|
}
|
|
p_track_info = (uint8_t (*)(uint32_t, uint32_t))
|
|
dlsym(C.handle, "CDIntfGetTrackInfo");
|
|
ASSERT_TRUE(p_track_info != NULL);
|
|
memset(zero, 0, sizeof(zero));
|
|
|
|
ASSERT_TRUE(cd_load_game(fix));
|
|
ASSERT_EQ_U32(cd_num_sessions(), 2u);
|
|
ASSERT_EQ_U32(cd_num_tracks(), 5u);
|
|
|
|
for (t = 0; t < sizeof(vpg_tracks) / sizeof(vpg_tracks[0]); t++) {
|
|
dataLBA = vpg_tracks[t].startLBA + vpg_tracks[t].pregap;
|
|
ASSERT_EQ(cd_track_session(vpg_tracks[t].track), 2);
|
|
|
|
/* TOC: INDEX 01 as absolute MSF (150-frame lead-in). */
|
|
absLBA = dataLBA + 150u;
|
|
ASSERT_EQ_U32(p_track_info(vpg_tracks[t].track, 0), absLBA / 4500u);
|
|
ASSERT_EQ_U32(p_track_info(vpg_tracks[t].track, 1), (absLBA / 75u) % 60u);
|
|
ASSERT_EQ_U32(p_track_info(vpg_tracks[t].track, 2), absLBA % 75u);
|
|
|
|
/* Pregap reads silence, first through last sector. */
|
|
for (s = 0; s < vpg_tracks[t].pregap; s++) {
|
|
ASSERT_TRUE(cd_read_block(vpg_tracks[t].startLBA + s, got));
|
|
ASSERT_TRUE(memcmp(got, zero, sizeof(got)) == 0);
|
|
}
|
|
|
|
/* INDEX 01 onward is this track's own data, sector for sector,
|
|
* through the last one (catches a wrong track length too). */
|
|
for (s = 0; s < VPG_DATA_SECTORS; s++) {
|
|
ASSERT_TRUE(cd_read_block(dataLBA + s, got));
|
|
vpg_tag_sector(vpg_tracks[t].track, s, want);
|
|
if (memcmp(got, want, sizeof(got)) != 0)
|
|
fprintf(stderr, " track %u sector %u at LBA %u read \"%.16s\"\n",
|
|
(unsigned)vpg_tracks[t].track, (unsigned)s,
|
|
(unsigned)(dataLBA + s), (const char *)got);
|
|
ASSERT_TRUE(memcmp(got, want, sizeof(got)) == 0);
|
|
}
|
|
}
|
|
|
|
cd_unload_game();
|
|
}
|
|
|
|
int main(int argc, char *argv[])
|
|
{
|
|
(void)argc; (void)argv;
|
|
|
|
TEST_INIT("CD CHD session gate");
|
|
|
|
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(chd_without_chse_is_refused);
|
|
RUN_TEST(chd_with_chse_has_two_sessions);
|
|
RUN_TEST(chd_gap_and_session1_are_silence);
|
|
RUN_TEST(chd_session2_header_and_extract);
|
|
RUN_TEST(chd_multi_track_session1_is_not_flipped_early);
|
|
RUN_TEST(chd_virtual_pregaps_land_on_index01);
|
|
|
|
{
|
|
void (*p)(void) = (void (*)(void))dlsym(C.handle, "retro_deinit");
|
|
if (p) p();
|
|
}
|
|
if (C.handle) dlclose(C.handle);
|
|
|
|
return TEST_REPORT();
|
|
}
|