test(cd): fail test_cd_bios_boot when the CD transfer wedged (#799)

test_cd_bios_boot printed [PASS] on a run where cd_seek_wedge fired: its
criteria (pc_in_ram, not_loop, not_thrash, ram_payload) are all satisfied by
a 68K that keeps running its CD service loop while the transfer is dead.

Ask the watchdog instead of grepping its log: crash_detect.c now keeps a
sticky count of cd_seek_wedge threshold crossings (counted before the log
throttle, reset with CrashDetectReset) behind CrashDetectCDSeekWedgeFires()
and CrashDetectCDSeekWedgeStreak(), exported via the existing CrashDetect*
test ABI. The harness reads them per disc, forces virtualjaguar_crash_detect
on, prints a [CD-WEDGE] reason, and FAILs the disc. A core without the
symbols FAILs rather than passing blind.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
Joseph MattielloandClaude Sonnet 5.5 committed 2026-10-07 00:41:50 -04:00
1 parent 93e4097f48
commit 0983d22c2d
5 files changed
+76 -5

No files matched your search

+2
View File
@@ -183,6 +183,8 @@ heartbeat every 600 frames). Cost enabled: one indirect call + ~256-px hash/fram
by clearing BUTCH bit 0 (Myst, Primal Rage, BrainDead 13 …) or I2CNTRL bit 2 (Philia); those
idle drives used to fire it (#741). The line prints `butch_int=`/`i2s_ctrl=`. Pinned by
`test/test_crash_detect_cd_wedge` (CI) + `test/tools/cd_seek_wedge_regress.sh` (private discs).
`test/test_cd_bios_boot` FAILs a disc when the watchdog fired (`CrashDetectCDSeekWedgeFires()`,
sticky, `[CD-WEDGE]` line) -- its PC checks alone pass on a dead transfer (#799).
Triaging "X crashes/hangs/black screen": the RetroArch log shows the signature — no save state
or input recording needed. **Add new signatures here when you find a recurring failure mode not
+17
View File
@@ -144,6 +144,10 @@ static unsigned fb_same_hash_frames;
static uint32_t last_cd_fifo_drains;
static unsigned cd_seek_wedge_frames;
/* Sticky: how many times cd_seek_wedge has crossed its threshold since the
* last CrashDetectReset(). Counted before the log throttle, so a test can
* ask the watchdog directly instead of grepping the log (issue #799). */
static unsigned cd_seek_wedge_fires;
static unsigned next_heartbeat_frame;
@@ -379,6 +383,7 @@ void CrashDetectReset(void)
fb_same_hash_frames = 0;
last_cd_fifo_drains = 0;
cd_seek_wedge_frames = 0;
cd_seek_wedge_fires = 0;
next_heartbeat_frame = HEARTBEAT_FRAMES;
hb_prev_hires_hits = 0;
hb_prev_hires_miss_value = 0;
@@ -473,6 +478,16 @@ int CrashDetectCDSeekWedgeFrame(uint32_t seek_starts, uint32_t seek_dones,
&& (i2s_ctrl & CD_I2S_FIFO_ENABLE) != 0;
}
unsigned CrashDetectCDSeekWedgeFires(void)
{
return cd_seek_wedge_fires;
}
unsigned CrashDetectCDSeekWedgeStreak(void)
{
return cd_seek_wedge_frames;
}
void CrashDetectFrameTick(const uint32_t *fb, unsigned w, unsigned h)
{
uint32_t cur_gpu_pc;
@@ -621,6 +636,8 @@ void CrashDetectFrameTick(const uint32_t *fb, unsigned w, unsigned h)
cd_butch_int, cd_i2s_ctrl))
{
cd_seek_wedge_frames++;
if (cd_seek_wedge_frames == WEDGE_FRAMES_CD_SEEK)
cd_seek_wedge_fires++;
if (cd_seek_wedge_frames == WEDGE_FRAMES_CD_SEEK
&& may_log(&last_log_cd_seek_wedge))
{
+10
View File
@@ -48,6 +48,16 @@ int CrashDetectCDSeekWedgeFrame(uint32_t seek_starts, uint32_t seek_dones,
int processor_running, uint8_t butch_int,
uint8_t i2s_ctrl);
/* Watchdog state for tests (issue #799). A harness's own pass criteria
* (PC in RAM, not looping) are all satisfied by a 68K that keeps running
* its CD service loop while the transfer is dead, so they cannot see a
* cd_seek_wedge; the watchdog can. Fires = how many times the wedge
* crossed its threshold since CrashDetectReset() (sticky, not subject to
* the log throttle, 0 while the watchdog is disabled); Streak = frames the
* wedge predicate has held in a row right now. */
unsigned CrashDetectCDSeekWedgeFires(void);
unsigned CrashDetectCDSeekWedgeStreak(void);
/* In-frame hang signature (issue #740). The blitter runs synchronously
* inside one register write, so a garbage B_COUNT freezes the host inside
* retro_run and the per-frame checks never get a turn. Called at blit
+41 -1
View File
@@ -83,6 +83,9 @@ static bool cd_environment(unsigned cmd, void *data)
return true;
}
if (strcmp(var->key, "virtualjaguar_cd_boot_mode") == 0) { var->value = "bios"; return true; }
/* The wedge verdict below reads the watchdog, so it must be on
* whatever the core's default becomes. */
if (strcmp(var->key, "virtualjaguar_crash_detect") == 0) { var->value = "enabled"; return true; }
var->value = NULL;
return false;
}
@@ -134,6 +137,15 @@ struct cd_disc_result {
/* CD subsystem activity captured at end-of-run. */
struct cd_diag_snapshot diag;
/* Crash-watchdog verdict on the CD transfer (issue #799). The PC checks
* above cannot see a dead transfer: a 68K that keeps running its CD
* service loop satisfies every one of them. So the watchdog's own
* cd_seek_wedge state is the oracle, read through the exported accessors
* rather than by grepping its log line. */
bool wedge_accessor_missing;
unsigned cd_wedge_fires; /* times cd_seek_wedge crossed 300 frames */
unsigned cd_wedge_streak; /* frames the predicate holds at end-of-run */
};
static bool cd_load_game(const char *path)
@@ -293,6 +305,21 @@ static void cd_run_one_disc(const char *path, unsigned frames,
/* Capture CD subsystem snapshot before unload (counters reset on next reset). */
cd_diag_capture(C.handle, &out->diag);
/* Ask the watchdog whether the transfer wedged. A core without these
* symbols (no TEST_EXPORTS) cannot give a verdict -- fail, never pass. */
{
unsigned (*p_fires)(void);
unsigned (*p_streak)(void);
p_fires = (unsigned (*)(void))dlsym(C.handle, "CrashDetectCDSeekWedgeFires");
p_streak = (unsigned (*)(void))dlsym(C.handle, "CrashDetectCDSeekWedgeStreak");
if (p_fires && p_streak) {
out->cd_wedge_fires = p_fires();
out->cd_wedge_streak = p_streak();
} else {
out->wedge_accessor_missing = true;
}
}
if (!hist.unique_overflow && hist.unique_count <= 32) {
size_t i;
fprintf(stderr, " [PC-SET] %zu unique PCs:", hist.unique_count);
@@ -472,7 +499,8 @@ TEST(boot_all_discovered_discs_real_bios)
}
ok = r->pc_stayed_in_ram && r->not_self_looping &&
r->not_thrashing && r->ram_has_payload;
r->not_thrashing && r->ram_has_payload &&
!r->wedge_accessor_missing && r->cd_wedge_fires == 0;
status_word = ok ? "PASS" : "FAIL";
if (!ok) fail++; else pass++;
@@ -485,6 +513,18 @@ TEST(boot_all_discovered_discs_real_bios)
r->unique_pc_count,
r->unique_pc_overflow ? "+" : "",
r->final_pc);
/* The reason a run that looks alive above is still a FAIL (#799). */
if (r->cd_wedge_fires > 0)
fprintf(stderr,
" [CD-WEDGE] %s : CD transfer wedged (cd_seek_wedge fired "
"%u time(s), %u frames held at end-of-run) -- the PC checks "
"cannot see a dead transfer\n",
label, r->cd_wedge_fires, r->cd_wedge_streak);
if (r->wedge_accessor_missing)
fprintf(stderr,
" [CD-WEDGE] %s : CrashDetectCDSeekWedgeFires not exported "
"(build with TEST_EXPORTS=1); cannot judge the transfer\n",
label);
fprintf(stderr,
" [DIAG] %s : gpu_pc=$%06X irq0=%u irq3=%u "
"butchExec=%u fifoIRQs=%u dsaIRQs=%u fifoReads=%u "
+6 -4
View File
@@ -5,10 +5,12 @@
# Before #741 the watchdog counted any frame with frozen FIFO drains, and
# fired on every one of these titles while they went on to run (15 fires,
# BUTCH low byte $02 each time: the game had cleared the master interrupt
# enable to signal completion). Asserts on the log line itself, not the
# harness [PASS]: test_cd_bios_boot also reports [PASS] on a real
# IMASK-stuck wedge, so [PASS] proves nothing here. The other side (a real
# wedge must still fire) is pinned without a disc by
# enable to signal completion). Asserts on the log line as well as the
# harness verdict. Since #799 test_cd_bios_boot itself reports FAIL when the
# watchdog's cd_seek_wedge fired (it used to print [PASS] on a real
# IMASK-stuck wedge), so the [PASS] requirement below already covers a false
# fire; the grep stays as an independent check on the log. The other side (a
# real wedge must still fire) is pinned without a disc by
# test/test_crash_detect_cd_wedge.
#
# Needs the private corpus; SKIPs (exit 0) without it.