mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
Merge pull request #646 from RetroPortingToolKit/fix/freeze-spin-whole-window
freeze_heartbeat: no spin_freeze false positives on statically compiled code
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user