diff --git a/runtime/src/freeze_dump_policy.c b/runtime/src/freeze_dump_policy.c index 8197dae4..b3ebec64 100644 --- a/runtime/src/freeze_dump_policy.c +++ b/runtime/src/freeze_dump_policy.c @@ -89,3 +89,59 @@ int freeze_dump_format_path(char *out, size_t cap, const char *backend, backend, wall, sequence); return n > 0 && (size_t)n < cap; } + +int freeze_distinct_add(FreezeDistinct *set, uint32_t value) { + if (set->count > FREEZE_WEDGE_SPIN_MAX_FNS) return 1; + for (uint32_t i = 0; i < set->count; i++) + if (set->values[i] == value) return 0; + set->values[set->count++] = value; + return set->count > FREEZE_WEDGE_SPIN_MAX_FNS; +} + +uint32_t freeze_window_fn_diversity(uint64_t lo, uint64_t hi, uint64_t oldest, + int (*get)(uint64_t seq, uint32_t *fn, + int *in_exception)) { + if (lo < oldest) lo = oldest; + FreezeDistinct set = {{0}, 0}; + int any = 0; + for (uint64_t seq = lo; seq < hi; seq++) { + uint32_t fn = 0; + int in_exception = 0; + if (!get(seq, &fn, &in_exception) || in_exception) continue; + any = 1; + if (freeze_distinct_add(&set, fn)) break; + } + return any ? set.count : FREEZE_WEDGE_FN_DIVERSITY_UNKNOWN; +} +uint32_t freeze_wedge_classify(const FreezeWedgeSample *window, uint32_t count, + uint32_t fn_diversity, + int (*mdec_recently_active)(uint32_t frames)) { + if (!window || count < 2u) return 0; + const FreezeWedgeSample *oldest = &window[0]; + const FreezeWedgeSample *newest = &window[count - 1u]; + uint64_t frame_delta = (newest->frame_count >= oldest->frame_count) + ? (newest->frame_count - oldest->frame_count) : 0; + uint64_t excre_delta = (newest->exc_reentry >= oldest->exc_reentry) + ? (newest->exc_reentry - oldest->exc_reentry) : 0; + + if (frame_delta == 0) return FREEZE_WEDGE_HARD; + if (excre_delta / frame_delta > FREEZE_WEDGE_EXC_REENTRY_PER_FRAME_THRESHOLD) + return FREEZE_WEDGE_REENTRY_STORM; + if (frame_delta < FREEZE_WEDGE_SLOW_FRAMES_MAX_DELTA) return FREEZE_WEDGE_SLOW_FRAMES; + + for (uint32_t i = 1; i < count; i++) { + if (window[i].current_func != oldest->current_func || + window[i].last_store_pc != oldest->last_store_pc || + window[i].dirty_ram_insns != oldest->dirty_ram_insns) + return 0; + } + if (fn_diversity != FREEZE_WEDGE_FN_DIVERSITY_UNKNOWN && + fn_diversity > FREEZE_WEDGE_SPIN_MAX_FNS) + return 0; + /* During an FMV the guest parks in BIOS MDEC code with every pinned + * signal legitimately still while frames advance. */ + if (mdec_recently_active && + mdec_recently_active((uint32_t)(frame_delta > 0xFFFFFFFEu ? 0xFFFFFFFEu : frame_delta) + 1u)) + return 0; + return FREEZE_WEDGE_SPIN; +} \ No newline at end of file diff --git a/runtime/src/freeze_dump_policy.h b/runtime/src/freeze_dump_policy.h index 22f2c8e6..aa2c4184 100644 --- a/runtime/src/freeze_dump_policy.h +++ b/runtime/src/freeze_dump_policy.h @@ -39,6 +39,62 @@ int freeze_dump_policy_observe(FreezeDumpPolicy *policy, uint32_t wedge_kind, * bounded number of failed attempts before the policy consumes that slot. */ void freeze_dump_policy_record_result(FreezeDumpPolicy *policy, int written); +/* Wedge kinds reported by freeze_wedge_classify (0 = healthy). */ +#define FREEZE_WEDGE_HARD 1u /* frame_count did not advance */ +#define FREEZE_WEDGE_REENTRY_STORM 2u /* exception re-entries per frame over threshold */ +#define FREEZE_WEDGE_SLOW_FRAMES 3u /* frames advanced below the slow-frames floor */ +#define FREEZE_WEDGE_SPIN 5u /* guest pinned in one loop while frames advance */ + +#define FREEZE_WEDGE_EXC_REENTRY_PER_FRAME_THRESHOLD 20000u /* ~10x chronic 2K/frame */ +#define FREEZE_WEDGE_SLOW_FRAMES_MAX_DELTA 10u /* <5 fps avg over the 2s window */ + +/* One heartbeat sample, as far as wedge classification needs it. */ +typedef struct FreezeWedgeSample { + uint64_t frame_count; + uint64_t exc_reentry; + uint64_t dirty_ram_insns; + uint32_t current_func; + uint32_t last_store_pc; +} FreezeWedgeSample; + +/* A spinning guest enters only its wait loop plus whatever the interrupt + * handlers last stamped; a running game loop enters dozens of functions per + * window (LoD title/field/battle >= 13 per 120 frames, measured 2026-10-12). */ +#define FREEZE_WEDGE_SPIN_MAX_FNS 8u +#define FREEZE_WEDGE_FN_DIVERSITY_UNKNOWN UINT32_MAX + +/* Distinct-value counter that stops counting past its capacity. */ +typedef struct FreezeDistinct { + uint32_t values[FREEZE_WEDGE_SPIN_MAX_FNS + 1u]; + uint32_t count; +} FreezeDistinct; + +/* Add one value; returns nonzero once more than FREEZE_WEDGE_SPIN_MAX_FNS + * distinct values have been seen (the caller can stop scanning). */ +int freeze_distinct_add(FreezeDistinct *set, uint32_t value); + +/* Distinct functions entered outside exceptions over profile samples + * [lo, hi), clamped to oldest (the ring's oldest held sample). get() returns + * 0 for an evicted/unavailable sample. Stops counting past + * FREEZE_WEDGE_SPIN_MAX_FNS; FREEZE_WEDGE_FN_DIVERSITY_UNKNOWN when no + * non-exception sample covers the window. */ +uint32_t freeze_window_fn_diversity(uint64_t lo, uint64_t hi, uint64_t oldest, + int (*get)(uint64_t seq, uint32_t *fn, + int *in_exception)); +/* Classify a window of consecutive samples, oldest first. A spin requires the + * executing function, last store PC and dirty-RAM instruction count to hold + * one value at EVERY sample, AND (when known) at most FREEZE_WEDGE_SPIN_MAX_FNS + * distinct guest functions entered outside exceptions over the window. + * Statically compiled code stamps neither current_func nor dirty_ram_insns, so + * the pinned signature alone also matches a healthy static main loop; the + * function-entry stamp is maintained by all generated code. fn_diversity is + * FREEZE_WEDGE_FN_DIVERSITY_UNKNOWN when no profile samples cover the window. + * mdec_recently_active (may be NULL) vetoes a spin while FMV decoding parks + * the guest in BIOS MDEC code. */ +uint32_t freeze_wedge_classify(const FreezeWedgeSample *window, uint32_t count, + uint32_t fn_diversity, + int (*mdec_recently_active)(uint32_t frames)); + /* Format a unique dump path. The sequence prevents same-second truncation. */ int freeze_dump_format_path(char *out, size_t cap, const char *backend, long long wall, uint32_t sequence); diff --git a/runtime/src/freeze_heartbeat.c b/runtime/src/freeze_heartbeat.c index 8b4d73c6..f3fb810e 100644 --- a/runtime/src/freeze_heartbeat.c +++ b/runtime/src/freeze_heartbeat.c @@ -4,6 +4,7 @@ #include "psx_memory.h" #include "freeze_heartbeat.h" #include "freeze_dump_policy.h" +#include "guest_cycle_profile.h" #include "debug_server.h" #include "crash_trace.h" /* g_psx_fatal_reason */ #include "cpu_state.h" /* g_psx_bail_* call-contract counters */ @@ -125,8 +126,7 @@ static int s_sym_initialized = 0; * activity (boot, FMV decode, save load) doesn't trip it; short enough * that real wedges dump within seconds. */ #define WEDGE_WINDOW_TICKS 20u -#define WEDGE_EXC_REENTRY_PER_FRAME_THRESHOLD 20000u /* ~10x chronic 2K/frame */ -#define WEDGE_SLOW_FRAMES_MAX_DELTA 10u /* <5 fps avg over the 2s window */ +/* Thresholds and the classifier live in freeze_dump_policy.{h,c} (unit-tested). */ /* Per-ring caps for auto-dump. Newest-first window. The old 4-16K caps * spanned well under a second of activity — too short to cross a @@ -163,6 +163,7 @@ typedef struct { uint64_t tcp_stall_ms; uint16_t t1_count; /* Timer1/RCnt1 counter — must advance if the wait is to complete */ uint64_t t1_irq_fired; /* Timer1 IRQ-fire count — flat == RCnt IRQ never fires */ + uint64_t gcp_seq; /* guest_cycle_profile_total() at this tick */ } HbRingEntry; static HbRingEntry s_ring[RING_CAP]; static uint32_t s_ring_head = 0; @@ -755,6 +756,14 @@ void freeze_heartbeat_fatal_dump(const char *reason) { sio_get_mc_max_state(), sio_get_tx_writes()); } +/* Adapter from the guest-cycle profile ring to freeze_window_fn_diversity. */ +static int gcp_sample_fn(uint64_t seq, uint32_t *fn, int *in_exception) { + GcpSample s; + if (!guest_cycle_profile_get(seq, &s)) return 0; + *fn = s.fn; + *in_exception = (s.flags & GCP_FLAG_EXCEPTION) != 0; + return 1; +} static void heartbeat_write(void) { uint64_t cyc = psx_get_cycle_count(); uint64_t frame = s_frame_count; @@ -805,6 +814,7 @@ static void heartbeat_write(void) { { uint16_t _t1c = 0; uint64_t _t1f = 0; timers_get_debug(1, &_t1c, NULL, NULL, &_t1f); re->t1_count = _t1c; re->t1_irq_fired = _t1f; } + re->gcp_seq = guest_cycle_profile_total(); s_ring_head = (s_ring_head + 1) % RING_CAP; if (s_ring_count < RING_CAP) s_ring_count++; @@ -823,43 +833,34 @@ static void heartbeat_write(void) { s_ring_count = 0; s_last_wedge_kind = 0; } else if (s_ring_count >= WEDGE_WINDOW_TICKS) { - /* The just-pushed tick is at (s_ring_head - 1). The oldest in - * our window is WEDGE_WINDOW_TICKS - 1 ticks before it. */ - uint32_t newest_idx = (s_ring_head + RING_CAP - 1u) % RING_CAP; - uint32_t oldest_idx = (s_ring_head + RING_CAP - WEDGE_WINDOW_TICKS) % RING_CAP; - uint64_t newest_frame = s_ring[newest_idx].frame_count; - uint64_t oldest_frame = s_ring[oldest_idx].frame_count; - uint64_t newest_excre = s_ring[newest_idx].exc_reentry; - uint64_t oldest_excre = s_ring[oldest_idx].exc_reentry; - uint64_t frame_delta = (newest_frame >= oldest_frame) - ? (newest_frame - oldest_frame) : 0; - uint64_t excre_delta = (newest_excre >= oldest_excre) - ? (newest_excre - oldest_excre) : 0; - - /* Logical-hang (kind D) signature: the executing function, the last - * store PC, and the retired dirty-RAM instruction count are all - * unchanged across the whole window. Requiring all three pinned makes - * this specific to a guest spin loop — a legitimate long native - * compute would still move last_store_pc, and any interpreted/overlay - * work would advance dirty_insns. Checked only when frames are - * advancing healthily (kinds 1/2/3 take precedence below). */ - int logic_pinned = - (s_ring[newest_idx].current_func == s_ring[oldest_idx].current_func) && - (s_ring[newest_idx].last_store_pc == s_ring[oldest_idx].last_store_pc) && - (s_ring[newest_idx].dirty_ram_insns == s_ring[oldest_idx].dirty_ram_insns); - - if (frame_delta == 0) - wedge_kind = 1; - else if (excre_delta / frame_delta > WEDGE_EXC_REENTRY_PER_FRAME_THRESHOLD) - wedge_kind = 2; - else if (frame_delta < WEDGE_SLOW_FRAMES_MAX_DELTA) - wedge_kind = 3; - else if (logic_pinned && !mdec_recently_active((uint32_t)frame_delta + 1u)) - wedge_kind = 5; /* spin freeze: game wedged while frames advance */ - /* Wave-5 F9: during an FMV the guest parks in BIOS MDEC code with the dirty-RAM - * counters legitimately still while frames advance -- exactly the spin signature. - * Every boot FMV wrote a 47-76 MB dump (1.8 GB per kit in one session). MDEC - * activity inside the window rules the spin classification out. */ + /* The just-pushed tick is at (s_ring_head - 1); the window is the + * WEDGE_WINDOW_TICKS samples ending there, passed oldest first. A + * spin must hold at every sample, not just the window ends: static + * code leaves dirty_ram_insns flat, and a per-frame main loop often + * stores at the same PC when two ticks happen to align. */ + FreezeWedgeSample window[WEDGE_WINDOW_TICKS]; + for (uint32_t i = 0; i < WEDGE_WINDOW_TICKS; i++) { + const HbRingEntry *src = + &s_ring[(s_ring_head + RING_CAP - WEDGE_WINDOW_TICKS + i) % RING_CAP]; + window[i].frame_count = src->frame_count; + window[i].exc_reentry = src->exc_reentry; + window[i].dirty_ram_insns = src->dirty_ram_insns; + window[i].current_func = src->current_func; + window[i].last_store_pc = src->last_store_pc; + } + wedge_kind = freeze_wedge_classify(window, WEDGE_WINDOW_TICKS, + FREEZE_WEDGE_FN_DIVERSITY_UNKNOWN, + mdec_recently_active); + if (wedge_kind == FREEZE_WEDGE_SPIN) { + /* Only a pinned window pays for the profile scan. */ + uint64_t lo = s_ring[(s_ring_head + RING_CAP - WEDGE_WINDOW_TICKS) % RING_CAP].gcp_seq; + uint64_t hi = s_ring[(s_ring_head + RING_CAP - 1u) % RING_CAP].gcp_seq; + wedge_kind = freeze_wedge_classify(window, WEDGE_WINDOW_TICKS, + freeze_window_fn_diversity( + lo, hi, guest_cycle_profile_oldest(), + gcp_sample_fn), + mdec_recently_active); + } } if (!s_wedge_classification_paused && diff --git a/runtime/tests/test_freeze_dump_policy.c b/runtime/tests/test_freeze_dump_policy.c index 89b0013b..02341e87 100644 --- a/runtime/tests/test_freeze_dump_policy.c +++ b/runtime/tests/test_freeze_dump_policy.c @@ -23,6 +23,29 @@ static void observe_healthy(FreezeDumpPolicy *policy, uint32_t ticks) { for (uint32_t i = 0; i < ticks; i++) EXPECT_EQ(observe(policy, 0), 0); } +#define UNK FREEZE_WEDGE_FN_DIVERSITY_UNKNOWN + +static int mdec_idle(uint32_t frames) { (void)frames; return 0; } +static int mdec_busy(uint32_t frames) { (void)frames; return 1; } + +/* 20 samples, frames advancing 9 per tick, guest pinned in one kernel loop. */ +static void spin_window(FreezeWedgeSample *w, uint32_t n) { + for (uint32_t i = 0; i < n; i++) { + w[i].frame_count = 1746u + 9u * i; + w[i].exc_reentry = 2000u + i; + w[i].dirty_ram_insns = 908846u; + w[i].current_func = 0x00000F40u; + w[i].last_store_pc = 0x8003A230u; + } +} + +static int sample_profile(uint64_t seq, uint32_t *fn, int *exception) { + if (seq >= 12u) return 0; + *fn = (uint32_t)seq; + *exception = seq == 2u; + return 1; +} + int main(void) { FreezeDumpPolicy policy = {0}; @@ -114,8 +137,45 @@ int main(void) { EXPECT_EQ(strcmp(path0, path1) != 0, 1); EXPECT_EQ(freeze_dump_format_path(path0, 4, "psx-runtime", 1234, 0), 0); - if (failures != 0) { - fprintf(stderr, "freeze dump policy: %d failure(s)\n", failures); + /* Wedge classification over the whole window. */ + FreezeWedgeSample w[20]; + spin_window(w, 20); + EXPECT_EQ(freeze_wedge_classify(w, 20, UNK, mdec_idle), FREEZE_WEDGE_SPIN); + EXPECT_EQ(freeze_wedge_classify(w, 20, UNK, NULL), FREEZE_WEDGE_SPIN); + /* A busy native window is healthy, while a small wait-loop remains detectable. */ + EXPECT_EQ(freeze_wedge_classify(w, 20, 9u, mdec_idle), 0); + EXPECT_EQ(freeze_wedge_classify(w, 20, 8u, mdec_idle), FREEZE_WEDGE_SPIN); + EXPECT_EQ(freeze_window_fn_diversity(0, 12, 0, sample_profile), 9u); + EXPECT_EQ(freeze_window_fn_diversity(0, 8, 5, sample_profile), 3u); + EXPECT_EQ(freeze_window_fn_diversity(2, 3, 0, sample_profile), UNK); + EXPECT_EQ(freeze_window_fn_diversity(20, 30, 0, sample_profile), UNK); + /* FMV decode parks the guest the same way; MDEC activity vetoes it. */ + EXPECT_EQ(freeze_wedge_classify(w, 20, UNK, mdec_busy), 0); + /* Static-code main loop (LoD 2026-10-12 dump): the window ends agree but + * intermediate ticks store elsewhere. Not a spin. */ + w[3].last_store_pc = 0x800461F0u; + w[11].last_store_pc = 0x800FBD44u; + EXPECT_EQ(freeze_wedge_classify(w, 20, UNK, mdec_idle), 0); + spin_window(w, 20); + w[7].current_func = 0x1FC02918u; + EXPECT_EQ(freeze_wedge_classify(w, 20, UNK, mdec_idle), 0); + spin_window(w, 20); + w[19].dirty_ram_insns += 4u; + EXPECT_EQ(freeze_wedge_classify(w, 20, UNK, mdec_idle), 0); + /* Frame-progress kinds take precedence over the spin test. */ + spin_window(w, 20); + for (uint32_t i = 0; i < 20; i++) w[i].frame_count = 500u; + EXPECT_EQ(freeze_wedge_classify(w, 20, UNK, mdec_busy), FREEZE_WEDGE_HARD); + for (uint32_t i = 0; i < 20; i++) w[i].frame_count = 500u + (i == 19 ? 5u : 0u); + EXPECT_EQ(freeze_wedge_classify(w, 20, UNK, mdec_idle), FREEZE_WEDGE_SLOW_FRAMES); + spin_window(w, 20); + w[19].exc_reentry = w[0].exc_reentry + + (uint64_t)(FREEZE_WEDGE_EXC_REENTRY_PER_FRAME_THRESHOLD + 1u) * 171u; + EXPECT_EQ(freeze_wedge_classify(w, 20, UNK, mdec_idle), FREEZE_WEDGE_REENTRY_STORM); + EXPECT_EQ(freeze_wedge_classify(w, 1, UNK, mdec_idle), 0); + EXPECT_EQ(freeze_wedge_classify(NULL, 20, UNK, mdec_idle), 0); + + if (failures != 0) { fprintf(stderr, "freeze dump policy: %d failure(s)\n", failures); return 1; } puts("freeze dump policy: all checks passed");