diff --git a/docs/agent/testing.md b/docs/agent/testing.md index 3f9c5ade..70bcfee3 100644 --- a/docs/agent/testing.md +++ b/docs/agent/testing.md @@ -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 diff --git a/src/core/crash_detect.c b/src/core/crash_detect.c index 9170b02c..d214aa12 100644 --- a/src/core/crash_detect.c +++ b/src/core/crash_detect.c @@ -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)) { diff --git a/src/core/crash_detect.h b/src/core/crash_detect.h index 0c984840..06930a99 100644 --- a/src/core/crash_detect.h +++ b/src/core/crash_detect.h @@ -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 diff --git a/test/test_cd_bios_boot.c b/test/test_cd_bios_boot.c index 50933951..320bc939 100644 --- a/test/test_cd_bios_boot.c +++ b/test/test_cd_bios_boot.c @@ -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 " diff --git a/test/tools/cd_seek_wedge_regress.sh b/test/tools/cd_seek_wedge_regress.sh index 58efe91c..aac211ce 100755 --- a/test/tools/cd_seek_wedge_regress.sh +++ b/test/tools/cd_seek_wedge_regress.sh @@ -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.