mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-28 02:56:36 -04:00
runtime: cross-fiber longjmp fix + decouple vblank-present from MMIO read
Two distinct bugs were causing OPTIONS-black hard freezes after 12K-65K frames: 1. interrupts.c + traps.c: exception_jmpbuf is a single global. setjmp captures the running fiber, but longjmp may be requested from a different fiber via psx_exception_longjmp / psx_restore_state_escape during chain-handler dispatch. Cross-fiber longjmp restores RSP to the wrong stack while Win32 still tracks the calling fiber, corrupting fiber state until SwitchToFiber eventually hangs. Fix: track g_exception_owner_fiber at setjmp time. New deferred_exception_longjmp checks the current fiber; if it's not the owner, sets a pending flag and SwitchToFiber's back to the owner. psx_change_thread_fiber's post-SwitchToFiber path observes the flag and executes the longjmp on the correct stack. 2. gpu.c: gpu_read_gpustat fired gpu_vblank_tick() after 1000 polls, which invoked sdl_vblank_present (SDL_Delay -> Sleep) from inside an MMIO read code path. That meant recompiled MIPS spent wall time in Sleep deep inside the fiber call tree -- confusing to reason about and a contributor to fiber/longjmp interactions. Fix: synthetic VBLANK from a poll-storm only updates emulation state (LCF + I_STAT). The host-side render+sleep happens only at the proper cycle-paced VBLANK trigger in psx_check_interrupts. Also adds: dirty_block ring slice to the freeze auto-dump, Win32 main- thread stack capture for wedge_kind=1 hard freezes (SuspendThread + StackWalk64 + SymFromAddr), and dbghelp link. Validated: TombaRecomp psx-runtime ran 30+ minutes / 105K+ frames at OPTIONS with no hard freeze. Previous freezes consistently hit at 11900, 20493, and 64596 frames. Diagnostic tooling under tools/_options_session/ that produced this diagnosis is included. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
#define PSXRECOMP_V4_DEBUG_SERVER_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include "cpu_state.h"
|
||||
|
||||
@@ -178,6 +179,27 @@ extern CPUState *debug_cpu_ptr;
|
||||
void debug_server_send_line(const char *json);
|
||||
void debug_server_send_fmt(const char *fmt, ...);
|
||||
|
||||
/* ---- Freeze auto-dump accessors ----
|
||||
*
|
||||
* Called from the freeze_heartbeat thread (NOT from TCP). Each function
|
||||
* writes a JSON array `[entry1,entry2,...]` of the newest `max_count`
|
||||
* entries from its ring directly to the given FILE*. Caller wraps with
|
||||
* the field name (e.g. "wtrace_all":[ ... ]).
|
||||
*
|
||||
* Thread-safety: best-effort. The heartbeat thread fires these only
|
||||
* after detecting a main-thread stall, so concurrent writers are rare,
|
||||
* but a wedged loop that still touches RAM could tear an entry near the
|
||||
* ring head. Forensic value is in the bulk of older entries which are
|
||||
* stable. */
|
||||
void debug_server_freeze_dump_wtrace_all_json(FILE *f, uint32_t max_count);
|
||||
void debug_server_freeze_dump_wtrace_json(FILE *f, uint32_t max_count);
|
||||
void debug_server_freeze_dump_frame_history_json(FILE *f, uint32_t max_count);
|
||||
void debug_server_freeze_dump_sio_pc_json(FILE *f, uint32_t max_count);
|
||||
void debug_server_freeze_dump_thread_trace_json(FILE *f, uint32_t max_count);
|
||||
void debug_server_freeze_dump_restore_trace_json(FILE *f, uint32_t max_count);
|
||||
void debug_server_freeze_dump_fn_entry_json(FILE *f, uint32_t max_count);
|
||||
void debug_server_freeze_dump_dirty_block_json(FILE *f, uint32_t max_count);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -165,7 +165,7 @@ function(psxrecomp_v4_add_runtime_target target)
|
||||
endif()
|
||||
|
||||
if(WIN32 OR MINGW)
|
||||
target_link_libraries(${target} PRIVATE ws2_32)
|
||||
target_link_libraries(${target} PRIVATE ws2_32 dbghelp)
|
||||
endif()
|
||||
|
||||
if(MINGW)
|
||||
|
||||
+436
-8
@@ -163,6 +163,27 @@ static uint32_t s_wtrace_head = 0;
|
||||
static struct { uint32_t lo, hi; } s_wtrace_ranges[WTRACE_MAX_RANGES];
|
||||
static int s_wtrace_range_count = 0;
|
||||
|
||||
/* ---- wtrace_all ring (ALWAYS-ON; no filter; lean fields) ----
|
||||
* Parity with psx-beetle's s_wtrace_all. Every recompiled-code write
|
||||
* to RAM lands here unconditionally, so a probe that connects AFTER
|
||||
* an event still has ~1 second of context to query without needing
|
||||
* to have pre-armed a range. Lean record (no register window) keeps
|
||||
* 262144 * 32 B = 8 MB per backend. */
|
||||
#define WRITE_TRACE_ALL_CAP (1 << 18)
|
||||
typedef struct {
|
||||
uint64_t seq;
|
||||
uint32_t addr;
|
||||
uint32_t new_val;
|
||||
uint32_t pc;
|
||||
uint32_t ra;
|
||||
uint32_t frame;
|
||||
uint8_t w;
|
||||
uint8_t pad[3];
|
||||
} WriteTraceAllEntry;
|
||||
static WriteTraceAllEntry *s_wtrace_all = NULL;
|
||||
static uint64_t s_wtrace_all_seq = 0;
|
||||
static uint32_t s_wtrace_all_head = 0;
|
||||
|
||||
/* Function attribution global — set by psx_dispatch() before each call. */
|
||||
uint32_t g_debug_current_func_addr = 0;
|
||||
|
||||
@@ -4368,8 +4389,25 @@ static void wtrace_record(uint32_t phys, uint32_t old_val, uint32_t new_val, uin
|
||||
s_wtrace_head = (s_wtrace_head + 1) % WRITE_TRACE_CAP;
|
||||
}
|
||||
|
||||
/* Always-on catch-all recorder. Lean record (no register window). */
|
||||
static void wtrace_all_record(uint32_t phys, uint32_t new_val, uint8_t width)
|
||||
{
|
||||
if (!s_wtrace_all) return;
|
||||
WriteTraceAllEntry *e = &s_wtrace_all[s_wtrace_all_head];
|
||||
e->seq = s_wtrace_all_seq++;
|
||||
e->addr = phys;
|
||||
e->new_val = new_val;
|
||||
e->pc = g_debug_last_store_pc;
|
||||
e->ra = debug_cpu_ptr ? debug_cpu_ptr->gpr[31] : 0;
|
||||
e->frame = (uint32_t)s_frame_count;
|
||||
e->w = width;
|
||||
s_wtrace_all_head = (s_wtrace_all_head + 1) % WRITE_TRACE_ALL_CAP;
|
||||
}
|
||||
|
||||
/* Multi-range check called from memory.c write paths.
|
||||
* Iterates up to 8 ranges; records if any match. */
|
||||
* Iterates up to 8 ranges; records if any match.
|
||||
* The always-on catch-all ring is recorded UNCONDITIONALLY first so
|
||||
* late-connecting probes can still see recent writes without arming. */
|
||||
void debug_server_trace_write_check(uint32_t phys, uint32_t old_val,
|
||||
uint32_t new_val, uint8_t width)
|
||||
{
|
||||
@@ -4377,6 +4415,7 @@ void debug_server_trace_write_check(uint32_t phys, uint32_t old_val,
|
||||
(void)phys; (void)old_val; (void)new_val; (void)width;
|
||||
return;
|
||||
#endif
|
||||
wtrace_all_record(phys, new_val, width);
|
||||
if (s_wtrace_range_count == 0) return;
|
||||
for (int i = 0; i < s_wtrace_range_count; i++) {
|
||||
if (phys >= s_wtrace_ranges[i].lo && phys < s_wtrace_ranges[i].hi) {
|
||||
@@ -4459,6 +4498,15 @@ static void handle_wtrace_del(int id, const char *json)
|
||||
send_ok(id);
|
||||
}
|
||||
|
||||
/* Normalized verb: disarm every range in one shot. Parity with
|
||||
* psx-beetle's wtrace_disarm_all. */
|
||||
static void handle_wtrace_disarm_all(int id, const char *json)
|
||||
{
|
||||
(void)json;
|
||||
s_wtrace_range_count = 0;
|
||||
send_ok(id);
|
||||
}
|
||||
|
||||
static void handle_wtrace_ranges(int id, const char *json)
|
||||
{
|
||||
(void)json;
|
||||
@@ -4689,6 +4737,90 @@ static void handle_wtrace_clear(int id, const char *json)
|
||||
send_ok(id);
|
||||
}
|
||||
|
||||
/* ---- Always-on catch-all wtrace handlers (parity with psx-beetle) ---- */
|
||||
|
||||
static void handle_wtrace_all_stats(int id, const char *json)
|
||||
{
|
||||
(void)json;
|
||||
uint64_t total = s_wtrace_all_seq;
|
||||
uint64_t oldest = (total <= WRITE_TRACE_ALL_CAP) ? 0 : total - WRITE_TRACE_ALL_CAP;
|
||||
uint64_t newest = (total > 0) ? total - 1 : 0;
|
||||
send_fmt("{\"id\":%d,\"ok\":true,\"total\":%llu,\"capacity\":%d,"
|
||||
"\"oldest_seq\":%llu,\"newest_seq\":%llu}",
|
||||
id, (unsigned long long)total, WRITE_TRACE_ALL_CAP,
|
||||
(unsigned long long)oldest, (unsigned long long)newest);
|
||||
}
|
||||
|
||||
static void handle_wtrace_all_reset(int id, const char *json)
|
||||
{
|
||||
(void)json;
|
||||
s_wtrace_all_seq = 0;
|
||||
s_wtrace_all_head = 0;
|
||||
if (s_wtrace_all)
|
||||
memset(s_wtrace_all, 0,
|
||||
(size_t)WRITE_TRACE_ALL_CAP * sizeof(WriteTraceAllEntry));
|
||||
send_ok(id);
|
||||
}
|
||||
|
||||
static void handle_wtrace_all_dump(int id, const char *json)
|
||||
{
|
||||
if (!s_wtrace_all) { send_err(id, "wtrace_all not initialized"); return; }
|
||||
|
||||
/* Optional post-hoc address filter (matches wtrace_dump shape). */
|
||||
char lo_str[32], hi_str[32];
|
||||
uint32_t flo = 0, fhi = 0xFFFFFFFFu;
|
||||
if (json_get_str(json, "addr_lo", lo_str, sizeof(lo_str)))
|
||||
flo = hex_to_u32(lo_str) & 0x1FFFFFFFu;
|
||||
if (json_get_str(json, "addr_hi", hi_str, sizeof(hi_str)))
|
||||
fhi = hex_to_u32(hi_str) & 0x1FFFFFFFu;
|
||||
|
||||
uint64_t total = s_wtrace_all_seq;
|
||||
uint32_t avail = (total < WRITE_TRACE_ALL_CAP)
|
||||
? (uint32_t)total : WRITE_TRACE_ALL_CAP;
|
||||
uint32_t start = (total < WRITE_TRACE_ALL_CAP) ? 0 : s_wtrace_all_head;
|
||||
|
||||
int max_out = json_get_int(json, "count", 65536);
|
||||
if (max_out < 1) max_out = 1;
|
||||
if (max_out > WRITE_TRACE_ALL_CAP) max_out = WRITE_TRACE_ALL_CAP;
|
||||
int newest_first = json_get_int(json, "newest", 0) != 0;
|
||||
|
||||
const uint32_t MAX_OUT = (uint32_t)max_out;
|
||||
size_t BUF_SZ = 256u + (size_t)MAX_OUT * 200u;
|
||||
if (BUF_SZ > (size_t)64 * 1024 * 1024) BUF_SZ = (size_t)64 * 1024 * 1024;
|
||||
char *buf = (char *)malloc(BUF_SZ);
|
||||
if (!buf) { send_err(id, "oom"); return; }
|
||||
size_t pos = 0;
|
||||
uint32_t emitted = 0;
|
||||
pos += snprintf(buf + pos, BUF_SZ - pos,
|
||||
"{\"id\":%d,\"ok\":true,\"total\":%llu,\"available\":%u,\"entries\":[",
|
||||
id, (unsigned long long)total, avail);
|
||||
for (uint32_t i = 0; i < avail && emitted < MAX_OUT && pos < BUF_SZ - 256; i++) {
|
||||
uint32_t idx;
|
||||
if (newest_first) {
|
||||
uint32_t newest = (s_wtrace_all_head + WRITE_TRACE_ALL_CAP - 1u)
|
||||
% WRITE_TRACE_ALL_CAP;
|
||||
idx = (newest + WRITE_TRACE_ALL_CAP - (i % WRITE_TRACE_ALL_CAP))
|
||||
% WRITE_TRACE_ALL_CAP;
|
||||
} else {
|
||||
idx = (start + i) % WRITE_TRACE_ALL_CAP;
|
||||
}
|
||||
WriteTraceAllEntry *e = &s_wtrace_all[idx];
|
||||
if (e->addr < flo || e->addr >= fhi) continue;
|
||||
pos += snprintf(buf + pos, BUF_SZ - pos,
|
||||
"%s{\"seq\":%llu,\"addr\":\"0x%08X\","
|
||||
"\"new\":\"0x%08X\",\"pc\":\"0x%08X\","
|
||||
"\"ra\":\"0x%08X\",\"frame\":%u,\"w\":%u}",
|
||||
(emitted == 0) ? "" : ",",
|
||||
(unsigned long long)e->seq,
|
||||
e->addr, e->new_val, e->pc, e->ra,
|
||||
e->frame, (unsigned)e->w);
|
||||
emitted++;
|
||||
}
|
||||
pos += snprintf(buf + pos, BUF_SZ - pos, "],\"emitted\":%u}", emitted);
|
||||
debug_server_send_line(buf);
|
||||
free(buf);
|
||||
}
|
||||
|
||||
static void handle_wtrace_dump(int id, const char *json)
|
||||
{
|
||||
if (!s_wtrace) { send_err(id, "trace not initialized"); return; }
|
||||
@@ -5237,6 +5369,278 @@ static void handle_fn_exit_dump(int id, const char *json) {
|
||||
free(buf);
|
||||
}
|
||||
|
||||
/* ====================================================================
|
||||
* Freeze auto-dump accessors. Called from the freeze_heartbeat thread
|
||||
* after it detects a main-thread stall. Each function writes a JSON
|
||||
* array `[entry1,entry2,...]` of the newest entries directly to FILE*.
|
||||
*
|
||||
* Snapshot the ring's seq/head ONCE on entry so a concurrent writer
|
||||
* (very rare at dump time) cannot make us walk off the end.
|
||||
* ==================================================================== */
|
||||
|
||||
void debug_server_freeze_dump_wtrace_all_json(FILE *f, uint32_t max_count)
|
||||
{
|
||||
if (!f) return;
|
||||
if (!s_wtrace_all) { fputs("[]", f); return; }
|
||||
|
||||
uint64_t total = s_wtrace_all_seq;
|
||||
uint32_t head = s_wtrace_all_head;
|
||||
uint32_t avail = (total < WRITE_TRACE_ALL_CAP)
|
||||
? (uint32_t)total : WRITE_TRACE_ALL_CAP;
|
||||
if (max_count > avail) max_count = avail;
|
||||
if (max_count == 0) { fputs("[]", f); return; }
|
||||
|
||||
/* Walk oldest-first within the newest `max_count` window. */
|
||||
uint32_t start = (total < WRITE_TRACE_ALL_CAP)
|
||||
? (avail - max_count)
|
||||
: (head + (WRITE_TRACE_ALL_CAP - max_count)) % WRITE_TRACE_ALL_CAP;
|
||||
|
||||
fputc('[', f);
|
||||
for (uint32_t i = 0; i < max_count; i++) {
|
||||
uint32_t idx = (start + i) % WRITE_TRACE_ALL_CAP;
|
||||
const WriteTraceAllEntry *e = &s_wtrace_all[idx];
|
||||
fprintf(f,
|
||||
"%s{\"seq\":%llu,\"addr\":\"0x%08X\",\"new\":\"0x%08X\","
|
||||
"\"pc\":\"0x%08X\",\"ra\":\"0x%08X\",\"frame\":%u,\"w\":%u}",
|
||||
i ? "," : "",
|
||||
(unsigned long long)e->seq, e->addr, e->new_val,
|
||||
e->pc, e->ra, e->frame, (unsigned)e->w);
|
||||
}
|
||||
fputc(']', f);
|
||||
}
|
||||
|
||||
void debug_server_freeze_dump_wtrace_json(FILE *f, uint32_t max_count)
|
||||
{
|
||||
if (!f) return;
|
||||
if (!s_wtrace) { fputs("[]", f); return; }
|
||||
|
||||
uint64_t total = s_wtrace_seq;
|
||||
uint32_t head = s_wtrace_head;
|
||||
uint32_t avail = (total < WRITE_TRACE_CAP) ? (uint32_t)total : WRITE_TRACE_CAP;
|
||||
if (max_count > avail) max_count = avail;
|
||||
if (max_count == 0) { fputs("[]", f); return; }
|
||||
|
||||
uint32_t start = (total < WRITE_TRACE_CAP)
|
||||
? (avail - max_count)
|
||||
: (head + (WRITE_TRACE_CAP - max_count)) % WRITE_TRACE_CAP;
|
||||
|
||||
fputc('[', f);
|
||||
for (uint32_t i = 0; i < max_count; i++) {
|
||||
uint32_t idx = (start + i) % WRITE_TRACE_CAP;
|
||||
const WriteTraceEntry *e = &s_wtrace[idx];
|
||||
fprintf(f,
|
||||
"%s{\"seq\":%llu,\"addr\":\"0x%08X\",\"old\":\"0x%08X\","
|
||||
"\"new\":\"0x%08X\",\"ra\":\"0x%08X\",\"func\":\"0x%08X\","
|
||||
"\"pc\":\"0x%08X\",\"cpu_pc\":\"0x%08X\",\"sp\":\"0x%08X\","
|
||||
"\"v0\":\"0x%08X\",\"v1\":\"0x%08X\","
|
||||
"\"a0\":\"0x%08X\",\"a1\":\"0x%08X\","
|
||||
"\"a2\":\"0x%08X\",\"a3\":\"0x%08X\","
|
||||
"\"t0\":\"0x%08X\",\"t1\":\"0x%08X\","
|
||||
"\"frame\":%u,\"w\":%u}",
|
||||
i ? "," : "",
|
||||
(unsigned long long)e->seq, e->addr, e->old_val, e->new_val,
|
||||
e->ra, e->func_addr, e->pc, e->cpu_pc, e->sp,
|
||||
e->v0, e->v1, e->a0, e->a1, e->a2, e->a3, e->t0, e->t1,
|
||||
e->frame, (unsigned)e->width);
|
||||
}
|
||||
fputc(']', f);
|
||||
}
|
||||
|
||||
void debug_server_freeze_dump_frame_history_json(FILE *f, uint32_t max_count)
|
||||
{
|
||||
if (!f) return;
|
||||
if (!s_frame_history) { fputs("[]", f); return; }
|
||||
|
||||
uint64_t total = s_history_count;
|
||||
if (total == 0 || max_count == 0) { fputs("[]", f); return; }
|
||||
uint64_t oldest = (total > FRAME_HISTORY_CAP) ? total - FRAME_HISTORY_CAP : 0;
|
||||
uint64_t start = (total > (uint64_t)max_count)
|
||||
? total - (uint64_t)max_count : 0;
|
||||
if (start < oldest) start = oldest;
|
||||
|
||||
fputc('[', f);
|
||||
int first = 1;
|
||||
for (uint64_t fr = start; fr < total; fr++) {
|
||||
uint32_t idx = (uint32_t)(fr % FRAME_HISTORY_CAP);
|
||||
const PSXFrameRecord *r = &s_frame_history[idx];
|
||||
if (r->frame_number != (uint32_t)fr) continue;
|
||||
fprintf(f,
|
||||
"%s{\"frame\":%u,\"verify\":%d,"
|
||||
"\"sr\":\"0x%08X\",\"cause\":\"0x%08X\",\"epc\":\"0x%08X\","
|
||||
"\"i_stat\":\"0x%08X\",\"i_mask\":\"0x%08X\","
|
||||
"\"pad\":\"0x%04X\",\"sio_stat\":\"0x%04X\",\"sio_ctrl\":\"0x%04X\","
|
||||
"\"dispatch_count\":%u,\"total_dispatches\":%llu,"
|
||||
"\"disp\":{\"x\":%u,\"y\":%u,\"w\":%u,\"h\":%u,\"off\":%u},"
|
||||
"\"last_func\":\"%s\"}",
|
||||
first ? "" : ",",
|
||||
r->frame_number, r->verify_pass,
|
||||
r->cop0_sr, r->cop0_cause, r->cop0_epc,
|
||||
r->i_stat, r->i_mask,
|
||||
(unsigned)r->pad_buttons,
|
||||
(unsigned)r->sio_stat, (unsigned)r->sio_ctrl,
|
||||
r->dispatch_count,
|
||||
(unsigned long long)r->total_dispatches,
|
||||
(unsigned)r->display_area_x, (unsigned)r->display_area_y,
|
||||
(unsigned)r->display_w, (unsigned)r->display_h,
|
||||
(unsigned)r->display_disabled,
|
||||
r->last_func);
|
||||
first = 0;
|
||||
}
|
||||
fputc(']', f);
|
||||
}
|
||||
|
||||
void debug_server_freeze_dump_sio_pc_json(FILE *f, uint32_t max_count)
|
||||
{
|
||||
if (!f) return;
|
||||
uint64_t total = s_sio_pc_trace_seq;
|
||||
uint32_t avail = (total < SIO_PC_TRACE_CAP) ? (uint32_t)total : SIO_PC_TRACE_CAP;
|
||||
if (max_count > avail) max_count = avail;
|
||||
if (max_count == 0) { fputs("[]", f); return; }
|
||||
|
||||
uint64_t start = total - (uint64_t)max_count;
|
||||
|
||||
fputc('[', f);
|
||||
for (uint32_t i = 0; i < max_count; i++) {
|
||||
uint64_t s = start + i;
|
||||
const SioPcTraceEntry *e = &s_sio_pc_trace[s % SIO_PC_TRACE_CAP];
|
||||
fprintf(f,
|
||||
"%s{\"seq\":%llu,\"pc\":\"0x%08X\",\"func\":\"0x%08X\","
|
||||
"\"addr\":\"0x%08X\",\"value\":\"0x%08X\","
|
||||
"\"byte_seq\":%u,\"width\":%u}",
|
||||
i ? "," : "",
|
||||
(unsigned long long)e->seq, e->pc, e->func,
|
||||
e->addr, e->value, e->byte_seq, (unsigned)e->width);
|
||||
}
|
||||
fputc(']', f);
|
||||
}
|
||||
|
||||
void debug_server_freeze_dump_thread_trace_json(FILE *f, uint32_t max_count)
|
||||
{
|
||||
if (!f) return;
|
||||
uint64_t total = s_thread_trace_seq;
|
||||
uint64_t avail = (total < THREAD_TRACE_CAP) ? total : THREAD_TRACE_CAP;
|
||||
if ((uint64_t)max_count > avail) max_count = (uint32_t)avail;
|
||||
if (max_count == 0) { fputs("[]", f); return; }
|
||||
|
||||
uint64_t start = total - (uint64_t)max_count;
|
||||
|
||||
fputc('[', f);
|
||||
for (uint32_t i = 0; i < max_count; i++) {
|
||||
uint64_t s = start + i;
|
||||
const ThreadTraceEntry *e = &s_thread_trace[s % THREAD_TRACE_CAP];
|
||||
fprintf(f,
|
||||
"%s{\"seq\":%llu,\"kind\":%u,\"name\":\"%s\","
|
||||
"\"current_tcb\":\"0x%08X\",\"target_tcb\":\"0x%08X\","
|
||||
"\"current_state\":\"0x%08X\",\"target_state\":\"0x%08X\","
|
||||
"\"target_pc\":\"0x%08X\",\"func\":\"0x%08X\","
|
||||
"\"store_pc\":\"0x%08X\",\"ra\":\"0x%08X\",\"sp\":\"0x%08X\","
|
||||
"\"a0\":\"0x%08X\",\"a1\":\"0x%08X\","
|
||||
"\"sr\":\"0x%08X\",\"epc\":\"0x%08X\","
|
||||
"\"istat\":\"0x%08X\",\"imask\":\"0x%08X\","
|
||||
"\"frame\":%u,\"in_exc\":%u}",
|
||||
i ? "," : "",
|
||||
(unsigned long long)e->seq, e->kind, thread_kind_name(e->kind),
|
||||
e->current_tcb, e->target_tcb,
|
||||
e->current_state, e->target_state,
|
||||
e->target_pc, e->func,
|
||||
e->last_store_pc, e->ra, e->sp,
|
||||
e->a0, e->a1,
|
||||
e->sr, e->epc, e->istat, e->imask,
|
||||
e->frame, (unsigned)e->in_exception);
|
||||
}
|
||||
fputc(']', f);
|
||||
}
|
||||
|
||||
void debug_server_freeze_dump_restore_trace_json(FILE *f, uint32_t max_count)
|
||||
{
|
||||
if (!f) return;
|
||||
uint64_t total = s_restore_trace_seq;
|
||||
uint64_t avail = (total < RESTORE_TRACE_CAP) ? total : RESTORE_TRACE_CAP;
|
||||
if ((uint64_t)max_count > avail) max_count = (uint32_t)avail;
|
||||
if (max_count == 0) { fputs("[]", f); return; }
|
||||
|
||||
uint64_t start = total - (uint64_t)max_count;
|
||||
|
||||
fputc('[', f);
|
||||
for (uint32_t i = 0; i < max_count; i++) {
|
||||
uint64_t s = start + i;
|
||||
const RestoreTraceEntry *e = &s_restore_trace[s % RESTORE_TRACE_CAP];
|
||||
fprintf(f,
|
||||
"%s{\"seq\":%llu,\"kind\":%u,\"name\":\"%s\",\"jmp\":%u,"
|
||||
"\"target\":\"0x%08X\",\"cpu_pc\":\"0x%08X\","
|
||||
"\"func\":\"0x%08X\",\"store_pc\":\"0x%08X\","
|
||||
"\"byte_seq\":%u,\"ra\":\"0x%08X\",\"sp\":\"0x%08X\","
|
||||
"\"sr\":\"0x%08X\",\"epc\":\"0x%08X\","
|
||||
"\"istat\":\"0x%08X\",\"imask\":\"0x%08X\","
|
||||
"\"frame\":%u,\"in_exc\":%u}",
|
||||
i ? "," : "",
|
||||
(unsigned long long)e->seq, e->kind, restore_kind_name(e->kind),
|
||||
e->jmp_val, e->target_pc, e->cpu_pc,
|
||||
e->func, e->last_store_pc, e->byte_seq, e->ra, e->sp,
|
||||
e->sr, e->epc, e->istat, e->imask,
|
||||
e->frame, (unsigned)e->in_exception);
|
||||
}
|
||||
fputc(']', f);
|
||||
}
|
||||
|
||||
void debug_server_freeze_dump_fn_entry_json(FILE *f, uint32_t max_count)
|
||||
{
|
||||
if (!f) return;
|
||||
if (!s_fn_entry) { fputs("[]", f); return; }
|
||||
|
||||
uint64_t total = s_fn_entry_seq;
|
||||
uint64_t oldest = (total > FN_TRACE_CAP) ? total - FN_TRACE_CAP : 0;
|
||||
uint64_t start = (total > (uint64_t)max_count)
|
||||
? total - (uint64_t)max_count : 0;
|
||||
if (start < oldest) start = oldest;
|
||||
if (start >= total) { fputs("[]", f); return; }
|
||||
|
||||
fputc('[', f);
|
||||
int first = 1;
|
||||
for (uint64_t s = start; s < total; s++) {
|
||||
const FnEntryEntry *e = &s_fn_entry[s % FN_TRACE_CAP];
|
||||
if (e->seq != s) continue;
|
||||
fprintf(f,
|
||||
"%s{\"seq\":%llu,\"func\":\"0x%08X\",\"ra\":\"0x%08X\","
|
||||
"\"a0\":\"0x%08X\",\"a1\":\"0x%08X\",\"a2\":\"0x%08X\","
|
||||
"\"a3\":\"0x%08X\",\"t1\":\"0x%08X\",\"frame\":%u}",
|
||||
first ? "" : ",",
|
||||
(unsigned long long)e->seq, e->func_addr, e->ra,
|
||||
e->a0, e->a1, e->a2, e->a3, e->t1, e->frame);
|
||||
first = 0;
|
||||
}
|
||||
fputc(']', f);
|
||||
}
|
||||
|
||||
void debug_server_freeze_dump_dirty_block_json(FILE *f, uint32_t max_count)
|
||||
{
|
||||
if (!f) { return; }
|
||||
|
||||
uint64_t total = g_dirty_ram_block_log_seq;
|
||||
uint64_t avail = (total < (uint64_t)DIRTY_RAM_BLOCK_LOG_CAP)
|
||||
? total : (uint64_t)DIRTY_RAM_BLOCK_LOG_CAP;
|
||||
uint64_t want = ((uint64_t)max_count < avail) ? (uint64_t)max_count : avail;
|
||||
uint64_t start = total - want;
|
||||
|
||||
fputc('[', f);
|
||||
int first = 1;
|
||||
for (uint64_t s = start; s < total; s++) {
|
||||
const DirtyRamBlockLogEntry *e =
|
||||
&g_dirty_ram_block_log[s & (DIRTY_RAM_BLOCK_LOG_CAP - 1u)];
|
||||
if (e->seq != s) { continue; }
|
||||
fprintf(f,
|
||||
"%s{\"seq\":%llu,\"target\":\"0x%08X\",\"ra\":\"0x%08X\","
|
||||
"\"a0\":\"0x%08X\",\"a1\":\"0x%08X\",\"a2\":\"0x%08X\","
|
||||
"\"a3\":\"0x%08X\",\"t0\":\"0x%08X\",\"t1\":\"0x%08X\","
|
||||
"\"sp\":\"0x%08X\",\"frame\":%u}",
|
||||
first ? "" : ",",
|
||||
(unsigned long long)e->seq, e->target, e->ra,
|
||||
e->a0, e->a1, e->a2, e->a3, e->t0, e->t1, e->sp, e->frame);
|
||||
first = 0;
|
||||
}
|
||||
fputc(']', f);
|
||||
}
|
||||
|
||||
typedef void (*CmdHandler)(int id, const char *json);
|
||||
typedef struct { const char *name; CmdHandler handler; } CmdEntry;
|
||||
|
||||
@@ -5305,13 +5709,27 @@ static const CmdEntry s_commands[] = {
|
||||
{ "imask_trace", handle_imask_trace },
|
||||
{ "watch", handle_watch },
|
||||
{ "unwatch", handle_unwatch },
|
||||
{ "wtrace_range", handle_wtrace_range },
|
||||
{ "wtrace_add", handle_wtrace_add },
|
||||
{ "wtrace_del", handle_wtrace_del },
|
||||
{ "wtrace_ranges", handle_wtrace_ranges },
|
||||
{ "wtrace_dump", handle_wtrace_dump },
|
||||
{ "wtrace_clear", handle_wtrace_clear },
|
||||
{ "wtrace_stats", handle_wtrace_stats },
|
||||
/* wtrace — normalized verb set (parity contract with psx-beetle).
|
||||
* One slot per "lo,hi" pair. arm/disarm/disarm_all/reset/ranges/
|
||||
* dump/stats. Both backends accept identical JSON shape. */
|
||||
{ "wtrace_arm", handle_wtrace_add },
|
||||
{ "wtrace_disarm", handle_wtrace_del },
|
||||
{ "wtrace_disarm_all", handle_wtrace_disarm_all },
|
||||
{ "wtrace_reset", handle_wtrace_clear },
|
||||
{ "wtrace_ranges", handle_wtrace_ranges },
|
||||
{ "wtrace_dump", handle_wtrace_dump },
|
||||
{ "wtrace_stats", handle_wtrace_stats },
|
||||
/* Always-on catch-all wtrace ring (parity with psx-beetle). */
|
||||
{ "wtrace_all_dump", handle_wtrace_all_dump },
|
||||
{ "wtrace_all_stats", handle_wtrace_all_stats },
|
||||
{ "wtrace_all_reset", handle_wtrace_all_reset },
|
||||
/* Legacy verbs retained for existing tools/ scripts that haven't
|
||||
* been updated; they dispatch to the same handlers as the
|
||||
* normalized verbs. Will retire once consumers are migrated. */
|
||||
{ "wtrace_range", handle_wtrace_range },
|
||||
{ "wtrace_add", handle_wtrace_add },
|
||||
{ "wtrace_del", handle_wtrace_del },
|
||||
{ "wtrace_clear", handle_wtrace_clear },
|
||||
{ "freeze_check", handle_freeze_check },
|
||||
{ "mmio_dump", handle_mmio_dump },
|
||||
{ "mmio_clear", handle_mmio_clear },
|
||||
@@ -5444,6 +5862,16 @@ void debug_server_init(int port)
|
||||
s_wtrace_seq = 0;
|
||||
s_wtrace_head = 0;
|
||||
|
||||
/* Always-on catch-all wtrace ring (8 MB). Records EVERY RAM write
|
||||
* with lean fields (no register window). Sized for ~1 second of
|
||||
* coverage at typical Tomba write rates. */
|
||||
if (!s_wtrace_all) {
|
||||
s_wtrace_all = (WriteTraceAllEntry *)calloc(WRITE_TRACE_ALL_CAP,
|
||||
sizeof(WriteTraceAllEntry));
|
||||
}
|
||||
s_wtrace_all_seq = 0;
|
||||
s_wtrace_all_head = 0;
|
||||
|
||||
/* Function entry/exit ring buffers (32 MB each, 64 MB total). */
|
||||
if (!s_fn_entry) s_fn_entry = (FnEntryEntry *)calloc(FN_TRACE_CAP, sizeof(FnEntryEntry));
|
||||
if (!s_fn_exit) s_fn_exit = (FnExitEntry *)calloc(FN_EXIT_TRACE_CAP, sizeof(FnExitEntry));
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* freeze_heartbeat.c — see header for rationale. */
|
||||
|
||||
#include "freeze_heartbeat.h"
|
||||
#include "debug_server.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
@@ -9,6 +10,7 @@
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#include <dbghelp.h>
|
||||
#endif
|
||||
|
||||
/* State accessors. All defined in other compilation units; declared here
|
||||
@@ -43,11 +45,42 @@ static char s_backend[32] = "psx-runtime";
|
||||
|
||||
#ifdef _WIN32
|
||||
static HANDLE s_thread = NULL;
|
||||
static HANDLE s_main_thread = NULL; /* DuplicateHandle of main thread */
|
||||
static DWORD s_main_thread_id = 0;
|
||||
static int s_sym_initialized = 0;
|
||||
#endif
|
||||
|
||||
#define HB_FILE "psx_freeze_heartbeat.json"
|
||||
#define HB_FILE "psx_freeze_heartbeat.json"
|
||||
#define HB_INTERVAL_MS 100u
|
||||
|
||||
/* Wedge detection.
|
||||
*
|
||||
* Two failure modes to catch:
|
||||
* A. HARD freeze — main thread is fully wedged; frame_count stops
|
||||
* advancing entirely. Detected by zero frame_delta over the window.
|
||||
* B. SOFT freeze / reentry storm — frame_count keeps advancing
|
||||
* (sometimes at near-normal rate) but the main thread is starved
|
||||
* by exception-handler reentry. Detected by exc_reentry_blocks
|
||||
* delta exceeding a threshold over the window. OPTIONS-black on
|
||||
* psx-runtime is this case: ~58 Hz frame rate, ~45K reentries/sec.
|
||||
*
|
||||
* Window = 20 ticks = 2.0 sec. Long enough that legitimate startup
|
||||
* 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_DELTA_THRESHOLD 10000u /* >5K/sec sustained = wedge */
|
||||
|
||||
/* Per-ring caps for auto-dump. Newest-first window. Sized so the total
|
||||
* dump fits in ~10-15 MB JSON even when every ring is full. */
|
||||
#define DUMP_CAP_WTRACE_ALL 16384u
|
||||
#define DUMP_CAP_WTRACE 8192u
|
||||
#define DUMP_CAP_FRAME_HISTORY 600u
|
||||
#define DUMP_CAP_SIO_PC 8192u
|
||||
#define DUMP_CAP_THREAD_TRACE 4096u
|
||||
#define DUMP_CAP_RESTORE_TRACE 4096u
|
||||
#define DUMP_CAP_FN_ENTRY 4096u
|
||||
#define DUMP_CAP_DIRTY_BLOCK 16384u
|
||||
|
||||
/* Pre-freeze history ring. Each entry = a snapshot taken at one heartbeat
|
||||
* tick (~100 ms). When the runtime freezes, all the "now" values stop
|
||||
* advancing but the past N entries still show what state it was in for
|
||||
@@ -71,6 +104,266 @@ static HbRingEntry s_ring[RING_CAP];
|
||||
static uint32_t s_ring_head = 0;
|
||||
static uint32_t s_ring_count = 0;
|
||||
|
||||
/* Wedge detection state.
|
||||
* s_dump_armed - 1 if a wedge dump is allowed to fire; cleared after
|
||||
* firing, re-armed when the wedge clears (healthy tick). */
|
||||
static int s_dump_armed = 1;
|
||||
static uint32_t s_last_wedge_kind = 0; /* informational, last detected kind */
|
||||
|
||||
#ifdef _WIN32
|
||||
/* Capture the main thread's call stack at the moment of a hard freeze.
|
||||
*
|
||||
* Called from the heartbeat thread when wedge_kind==1 (frame_count not
|
||||
* advancing). Suspends the main thread, walks its stack via StackWalk64,
|
||||
* symbolizes via SymFromAddr, then resumes. Each frame becomes a JSON
|
||||
* object {addr, symbol?, displacement?, module?}.
|
||||
*
|
||||
* Best-effort: if DbgHelp init fails, dump an empty array and continue. */
|
||||
static void freeze_dump_main_stack_json(FILE *f) {
|
||||
if (!f) return;
|
||||
if (!s_main_thread) { fputs("[]", f); return; }
|
||||
|
||||
if (!s_sym_initialized) {
|
||||
SymSetOptions(SYMOPT_LOAD_LINES | SYMOPT_UNDNAME | SYMOPT_DEFERRED_LOADS);
|
||||
if (SymInitialize(GetCurrentProcess(), NULL, TRUE)) {
|
||||
s_sym_initialized = 1;
|
||||
} else {
|
||||
fputs("[]", f);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
DWORD susp_count = SuspendThread(s_main_thread);
|
||||
if (susp_count == (DWORD)-1) { fputs("[]", f); return; }
|
||||
|
||||
CONTEXT ctx;
|
||||
memset(&ctx, 0, sizeof(ctx));
|
||||
ctx.ContextFlags = CONTEXT_FULL;
|
||||
if (!GetThreadContext(s_main_thread, &ctx)) {
|
||||
ResumeThread(s_main_thread);
|
||||
fputs("[]", f);
|
||||
return;
|
||||
}
|
||||
|
||||
STACKFRAME64 frame;
|
||||
memset(&frame, 0, sizeof(frame));
|
||||
#if defined(_M_X64) || defined(__x86_64__)
|
||||
DWORD machine = IMAGE_FILE_MACHINE_AMD64;
|
||||
frame.AddrPC.Offset = ctx.Rip;
|
||||
frame.AddrFrame.Offset = ctx.Rbp;
|
||||
frame.AddrStack.Offset = ctx.Rsp;
|
||||
#else
|
||||
DWORD machine = IMAGE_FILE_MACHINE_I386;
|
||||
frame.AddrPC.Offset = ctx.Eip;
|
||||
frame.AddrFrame.Offset = ctx.Ebp;
|
||||
frame.AddrStack.Offset = ctx.Esp;
|
||||
#endif
|
||||
frame.AddrPC.Mode = AddrModeFlat;
|
||||
frame.AddrFrame.Mode = AddrModeFlat;
|
||||
frame.AddrStack.Mode = AddrModeFlat;
|
||||
|
||||
fputc('[', f);
|
||||
int first = 1;
|
||||
union {
|
||||
SYMBOL_INFO si;
|
||||
char buf[sizeof(SYMBOL_INFO) + 512];
|
||||
} sym_storage;
|
||||
SYMBOL_INFO *sym = &sym_storage.si;
|
||||
|
||||
for (int depth = 0; depth < 128; depth++) {
|
||||
if (!StackWalk64(machine, GetCurrentProcess(), s_main_thread, &frame,
|
||||
&ctx, NULL, SymFunctionTableAccess64, SymGetModuleBase64,
|
||||
NULL)) {
|
||||
break;
|
||||
}
|
||||
DWORD64 addr = frame.AddrPC.Offset;
|
||||
if (!addr) break;
|
||||
|
||||
memset(sym, 0, sizeof(SYMBOL_INFO));
|
||||
sym->SizeOfStruct = sizeof(SYMBOL_INFO);
|
||||
sym->MaxNameLen = 511;
|
||||
DWORD64 displacement = 0;
|
||||
BOOL got_sym = SymFromAddr(GetCurrentProcess(), addr, &displacement, sym);
|
||||
|
||||
IMAGEHLP_MODULE64 mod;
|
||||
memset(&mod, 0, sizeof(mod));
|
||||
mod.SizeOfStruct = sizeof(mod);
|
||||
BOOL got_mod = SymGetModuleInfo64(GetCurrentProcess(), addr, &mod);
|
||||
|
||||
fprintf(f, "%s{\"depth\":%d,\"addr\":\"0x%016llX\"",
|
||||
first ? "" : ",", depth, (unsigned long long)addr);
|
||||
if (got_sym) {
|
||||
/* Escape JSON-special chars in symbol name (best-effort: bracket replace). */
|
||||
char safe[512];
|
||||
size_t k = 0;
|
||||
for (size_t i = 0; sym->Name[i] && k < sizeof(safe) - 2; i++) {
|
||||
char c = sym->Name[i];
|
||||
if (c == '"' || c == '\\') safe[k++] = '_';
|
||||
else if ((unsigned char)c < 0x20) safe[k++] = '_';
|
||||
else safe[k++] = c;
|
||||
}
|
||||
safe[k] = 0;
|
||||
fprintf(f, ",\"symbol\":\"%s\",\"displacement\":%llu",
|
||||
safe, (unsigned long long)displacement);
|
||||
}
|
||||
if (got_mod && mod.ModuleName[0]) {
|
||||
fprintf(f, ",\"module\":\"%s\"", mod.ModuleName);
|
||||
}
|
||||
fputc('}', f);
|
||||
first = 0;
|
||||
}
|
||||
fputc(']', f);
|
||||
|
||||
ResumeThread(s_main_thread);
|
||||
}
|
||||
#endif /* _WIN32 */
|
||||
|
||||
static void freeze_dump_write(long long wall, uint64_t frame, uint64_t cyc,
|
||||
uint64_t exc_reentry, uint32_t cur_fn,
|
||||
uint32_t last_store, uint32_t i_stat_v,
|
||||
uint32_t i_mask_v, int in_exc,
|
||||
uint64_t total_checks, uint32_t dispatch_count,
|
||||
uint64_t exc_entries,
|
||||
uint16_t sio_stat_v, uint16_t sio_ctrl_v,
|
||||
int sio_card_active, int mc_max, int tx_writes)
|
||||
{
|
||||
char path[128];
|
||||
snprintf(path, sizeof(path),
|
||||
"psx_freeze_dump_%s_%lld.json", s_backend, wall);
|
||||
|
||||
FILE *f = fopen(path, "wb");
|
||||
if (!f) return;
|
||||
|
||||
/* Large stdio buffer so multi-MB JSON arrays write efficiently. */
|
||||
static char io_buf[1 << 16];
|
||||
setvbuf(f, io_buf, _IOFBF, sizeof(io_buf));
|
||||
|
||||
fprintf(f,
|
||||
"{\n"
|
||||
" \"backend\":\"%s\",\n"
|
||||
" \"wall_clock_epoch\":%lld,\n"
|
||||
" \"frame_count\":%llu,\n"
|
||||
" \"psx_cycle_count\":%llu,\n"
|
||||
" \"current_func\":\"0x%08X\",\n"
|
||||
" \"last_store_pc\":\"0x%08X\",\n"
|
||||
" \"total_checks\":%llu,\n"
|
||||
" \"dispatch_count\":%u,\n"
|
||||
" \"in_exception\":%d,\n"
|
||||
" \"exception_entries\":%llu,\n"
|
||||
" \"exception_reentry_blocks\":%llu,\n"
|
||||
" \"sio_stat\":\"0x%04X\",\n"
|
||||
" \"sio_ctrl\":\"0x%04X\",\n"
|
||||
" \"sio_card_active\":%d,\n"
|
||||
" \"i_stat\":\"0x%08X\",\n"
|
||||
" \"i_mask\":\"0x%08X\",\n"
|
||||
" \"mc_max_state\":%d,\n"
|
||||
" \"tx_writes\":%d,\n"
|
||||
" \"dirty_ram_blocks\":%llu,\n"
|
||||
" \"dirty_ram_insns\":%llu,\n"
|
||||
" \"wedge_kind\":%u,\n"
|
||||
" \"wedge_kind_name\":\"%s\",\n"
|
||||
" \"caps\":{\"wtrace_all\":%u,\"wtrace\":%u,\"frames\":%u,"
|
||||
"\"sio_pc\":%u,\"thread\":%u,\"restore\":%u,\"fn_entry\":%u,"
|
||||
"\"dirty_block\":%u},\n",
|
||||
s_backend, wall,
|
||||
(unsigned long long)frame, (unsigned long long)cyc,
|
||||
cur_fn, last_store,
|
||||
(unsigned long long)total_checks, dispatch_count,
|
||||
in_exc,
|
||||
(unsigned long long)exc_entries,
|
||||
(unsigned long long)exc_reentry,
|
||||
(unsigned)sio_stat_v, (unsigned)sio_ctrl_v,
|
||||
sio_card_active,
|
||||
i_stat_v, i_mask_v,
|
||||
mc_max, tx_writes,
|
||||
(unsigned long long)g_dirty_ram_blocks_run,
|
||||
(unsigned long long)g_dirty_ram_insns_run,
|
||||
s_last_wedge_kind,
|
||||
(s_last_wedge_kind == 1) ? "hard_freeze" :
|
||||
(s_last_wedge_kind == 2) ? "reentry_storm" : "unknown",
|
||||
(unsigned)DUMP_CAP_WTRACE_ALL,
|
||||
(unsigned)DUMP_CAP_WTRACE,
|
||||
(unsigned)DUMP_CAP_FRAME_HISTORY,
|
||||
(unsigned)DUMP_CAP_SIO_PC,
|
||||
(unsigned)DUMP_CAP_THREAD_TRACE,
|
||||
(unsigned)DUMP_CAP_RESTORE_TRACE,
|
||||
(unsigned)DUMP_CAP_FN_ENTRY,
|
||||
(unsigned)DUMP_CAP_DIRTY_BLOCK);
|
||||
|
||||
fputs(" \"heartbeat_ring\":[\n", f);
|
||||
uint32_t avail = s_ring_count;
|
||||
uint32_t start = (s_ring_count < RING_CAP) ? 0 : s_ring_head;
|
||||
for (uint32_t i = 0; i < avail; i++) {
|
||||
HbRingEntry *e = &s_ring[(start + i) % RING_CAP];
|
||||
fprintf(f,
|
||||
" {\"wall\":%lld,\"frame\":%llu,\"cyc\":%llu,"
|
||||
"\"exc_re\":%llu,\"dirty_insns\":%llu,"
|
||||
"\"cur_fn\":\"0x%08X\",\"store_pc\":\"0x%08X\","
|
||||
"\"i_stat\":\"0x%08X\",\"sio_stat\":\"0x%04X\","
|
||||
"\"sio_ctrl\":\"0x%04X\",\"in_exc\":%u,\"mc_max\":%u}%s\n",
|
||||
e->wall_clock,
|
||||
(unsigned long long)e->frame_count,
|
||||
(unsigned long long)e->psx_cycle_count,
|
||||
(unsigned long long)e->exc_reentry,
|
||||
(unsigned long long)e->dirty_ram_insns,
|
||||
e->current_func, e->last_store_pc,
|
||||
e->i_stat, (unsigned)e->sio_stat, (unsigned)e->sio_ctrl,
|
||||
(unsigned)e->in_exception, (unsigned)e->mc_max_state,
|
||||
(i + 1 < avail) ? "," : "");
|
||||
}
|
||||
fputs(" ],\n", f);
|
||||
|
||||
fputs(" \"wtrace_all\":", f);
|
||||
debug_server_freeze_dump_wtrace_all_json(f, DUMP_CAP_WTRACE_ALL);
|
||||
fputs(",\n", f);
|
||||
|
||||
fputs(" \"wtrace\":", f);
|
||||
debug_server_freeze_dump_wtrace_json(f, DUMP_CAP_WTRACE);
|
||||
fputs(",\n", f);
|
||||
|
||||
fputs(" \"frame_history\":", f);
|
||||
debug_server_freeze_dump_frame_history_json(f, DUMP_CAP_FRAME_HISTORY);
|
||||
fputs(",\n", f);
|
||||
|
||||
fputs(" \"sio_pc_trace\":", f);
|
||||
debug_server_freeze_dump_sio_pc_json(f, DUMP_CAP_SIO_PC);
|
||||
fputs(",\n", f);
|
||||
|
||||
fputs(" \"thread_trace\":", f);
|
||||
debug_server_freeze_dump_thread_trace_json(f, DUMP_CAP_THREAD_TRACE);
|
||||
fputs(",\n", f);
|
||||
|
||||
fputs(" \"restore_trace\":", f);
|
||||
debug_server_freeze_dump_restore_trace_json(f, DUMP_CAP_RESTORE_TRACE);
|
||||
fputs(",\n", f);
|
||||
|
||||
fputs(" \"fn_entry\":", f);
|
||||
debug_server_freeze_dump_fn_entry_json(f, DUMP_CAP_FN_ENTRY);
|
||||
fputs(",\n", f);
|
||||
|
||||
fputs(" \"dirty_block\":", f);
|
||||
debug_server_freeze_dump_dirty_block_json(f, DUMP_CAP_DIRTY_BLOCK);
|
||||
fputs(",\n", f);
|
||||
|
||||
#ifdef _WIN32
|
||||
/* Main-thread call stack — only for hard freezes (wedge_kind==1).
|
||||
* For reentry storms the main thread is still running; capturing its
|
||||
* stack would just show a random recompiled-block frame. */
|
||||
fputs(" \"main_stack\":", f);
|
||||
if (s_last_wedge_kind == 1) {
|
||||
freeze_dump_main_stack_json(f);
|
||||
} else {
|
||||
fputs("[]", f);
|
||||
}
|
||||
fputs("\n", f);
|
||||
#else
|
||||
fputs(" \"main_stack\":[]\n", f);
|
||||
#endif
|
||||
|
||||
fputs("}\n", f);
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
static void heartbeat_write(void) {
|
||||
uint64_t cyc = psx_get_cycle_count();
|
||||
uint64_t frame = s_frame_count;
|
||||
@@ -120,6 +413,50 @@ static void heartbeat_write(void) {
|
||||
s_ring_head = (s_ring_head + 1) % RING_CAP;
|
||||
if (s_ring_count < RING_CAP) s_ring_count++;
|
||||
|
||||
/* ---- Wedge detection: arm-once auto-dump ----
|
||||
* Walk back WEDGE_WINDOW_TICKS in the heartbeat ring (just pushed
|
||||
* above) and compute deltas. Trigger if either:
|
||||
* A. frame_count delta == 0 (hard freeze)
|
||||
* B. exc_reentry delta > threshold (reentry storm; soft freeze)
|
||||
*
|
||||
* Only fires once the ring has enough history. When the wedge clears
|
||||
* (a healthy tick: frames advancing AND exc_re quiet), re-arm. */
|
||||
uint32_t wedge_kind = 0; /* 0=healthy, 1=hard freeze, 2=reentry storm */
|
||||
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;
|
||||
|
||||
if (frame_delta == 0)
|
||||
wedge_kind = 1;
|
||||
else if (excre_delta > WEDGE_EXC_REENTRY_DELTA_THRESHOLD)
|
||||
wedge_kind = 2;
|
||||
}
|
||||
|
||||
if (wedge_kind != 0) {
|
||||
if (s_dump_armed) {
|
||||
s_dump_armed = 0;
|
||||
s_last_wedge_kind = wedge_kind;
|
||||
freeze_dump_write(wall, frame, cyc, exc_reentry, cur_fn, last_store,
|
||||
i_stat, i_mask, in_exc, total_checks,
|
||||
dispatch_count, exc_entries,
|
||||
sio_stat, sio_ctrl, card_active,
|
||||
mc_max, tx_writes);
|
||||
}
|
||||
} else {
|
||||
/* Healthy tick: re-arm for the next wedge. */
|
||||
s_dump_armed = 1;
|
||||
}
|
||||
|
||||
/* Buffer sized for current-state JSON + ring (~256B per ring entry). */
|
||||
static char buf[64 * 1024];
|
||||
int n = snprintf(buf, sizeof(buf),
|
||||
@@ -254,6 +591,16 @@ void freeze_heartbeat_start(const char *backend_label) {
|
||||
s_backend[n] = 0;
|
||||
}
|
||||
#ifdef _WIN32
|
||||
/* Duplicate the main thread's pseudo-handle into a real handle so the
|
||||
* heartbeat thread can SuspendThread/GetThreadContext for stack capture
|
||||
* on hard freezes. GetCurrentThread() returns a pseudo-handle that's
|
||||
* only valid in the calling thread, so the duplication is required. */
|
||||
s_main_thread_id = GetCurrentThreadId();
|
||||
if (!DuplicateHandle(GetCurrentProcess(), GetCurrentThread(),
|
||||
GetCurrentProcess(), &s_main_thread,
|
||||
THREAD_ALL_ACCESS, FALSE, 0)) {
|
||||
s_main_thread = NULL;
|
||||
}
|
||||
s_thread = CreateThread(NULL, 0, heartbeat_thread, NULL, 0, NULL);
|
||||
if (s_thread) s_started = 1;
|
||||
#endif
|
||||
|
||||
+19
-4
@@ -103,6 +103,12 @@ static uint32_t dma_direction; /* bits 29-30 in GPUSTAT */
|
||||
/* LCF (even/odd line in interlace, toggles per vblank) */
|
||||
static uint32_t lcf; /* bit 31 */
|
||||
|
||||
/* I_STAT — defined in memory.c; declared early so gpu_read_gpustat can
|
||||
* raise the VBLANK bit when a synthetic vblank fires from a tight
|
||||
* BIOS-shell GPUSTAT poll loop, without going through the
|
||||
* vblank-callback path (which would call SDL_Delay/Sleep). */
|
||||
extern uint32_t i_stat;
|
||||
|
||||
/* Display area start (GP1(05h)) */
|
||||
static uint32_t display_area_x;
|
||||
static uint32_t display_area_y;
|
||||
@@ -213,11 +219,21 @@ void gpu_init(void) {
|
||||
|
||||
uint32_t gpu_read_gpustat(void) {
|
||||
/* Advance vblank when polled enough times from within a single function.
|
||||
* This handles BIOS VSYNC wait loops that poll LCF in tight loops. */
|
||||
* This handles BIOS VSYNC wait loops that poll LCF in tight loops.
|
||||
*
|
||||
* IMPORTANT: only update emulation state here (LCF + I_STAT). Do NOT
|
||||
* fire the vblank callback — that calls sdl_vblank_present, which
|
||||
* runs SDL_RenderPresent and SDL_Delay (Sleep). Entering Sleep from
|
||||
* an MMIO-read code path means recompiled MIPS code spends real wall
|
||||
* time inside Sleep — wrong context, hard to reason about, and
|
||||
* accumulates host stack frames inside the recompiled call tree.
|
||||
* The callback fires from the proper VBLANK trigger in
|
||||
* psx_check_interrupts (cycle-paced). */
|
||||
gpustat_poll_count++;
|
||||
if (gpustat_poll_count >= GPUSTAT_POLL_VBLANK_THRESHOLD) {
|
||||
gpustat_poll_count = 0;
|
||||
gpu_vblank_tick();
|
||||
lcf ^= 1;
|
||||
i_stat |= (1u << 0); /* IRQ_VBLANK */
|
||||
}
|
||||
|
||||
uint32_t stat = 0;
|
||||
@@ -320,8 +336,7 @@ static uint16_t rgb888_to_rgb555(uint32_t color24) {
|
||||
return (uint16_t)((r >> 3) | ((g >> 3) << 5) | ((b >> 3) << 10));
|
||||
}
|
||||
|
||||
/* I_STAT owned by memory.c — needed to set IRQ_VBLANK from poll-triggered vblank */
|
||||
extern uint32_t i_stat;
|
||||
/* i_stat extern declared earlier in this file (above gpu_read_gpustat). */
|
||||
|
||||
void gpu_vblank_tick(void) {
|
||||
lcf ^= 1;
|
||||
|
||||
@@ -79,8 +79,24 @@ static uint64_t exception_reentry_blocks;
|
||||
* very next psx_check_interrupts re-enters immediately. */
|
||||
static int post_exception_cooldown;
|
||||
|
||||
/* setjmp target for ReturnFromException during handler dispatch. */
|
||||
static jmp_buf exception_jmpbuf;
|
||||
/* setjmp target for ReturnFromException during handler dispatch.
|
||||
*
|
||||
* IMPORTANT: longjmp(exception_jmpbuf, ...) MUST execute on the same
|
||||
* Windows fiber that called setjmp; otherwise RSP is restored to that
|
||||
* fiber's stack while the OS still tracks a different fiber as current,
|
||||
* corrupting fiber state and eventually deadlocking SwitchToFiber.
|
||||
*
|
||||
* s_exception_owner_fiber records which fiber called setjmp. If a
|
||||
* longjmp request originates on a different fiber, the caller must:
|
||||
* (1) set s_pending_exception_longjmp = code
|
||||
* (2) SwitchToFiber back to s_exception_owner_fiber
|
||||
* That fiber's wrapped SwitchToFiber call will observe the flag on
|
||||
* return and execute the longjmp on the correct stack. */
|
||||
jmp_buf exception_jmpbuf; /* non-static so traps.c can deferred-longjmp */
|
||||
#ifdef _WIN32
|
||||
void* g_exception_owner_fiber = NULL;
|
||||
int g_pending_exception_longjmp = 0;
|
||||
#endif
|
||||
extern int g_psx_dispatch_depth;
|
||||
|
||||
int psx_get_in_exception(void) { return in_exception; }
|
||||
@@ -123,15 +139,39 @@ void interrupts_init(void) {
|
||||
* in_exception and return, effectively letting the interrupted
|
||||
* code resume at the saved EPC through normal dispatch.
|
||||
*/
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
/* Defer a longjmp to the fiber that owns the current exception setjmp.
|
||||
* If we're on the owning fiber already, longjmp immediately. Otherwise
|
||||
* record the requested code, SwitchToFiber back to the owner, and the
|
||||
* post-SwitchToFiber check in traps.c psx_change_thread_fiber (or the
|
||||
* fiber entry helper) will execute the longjmp on the correct stack. */
|
||||
static void deferred_exception_longjmp(int code) {
|
||||
if (!g_exception_owner_fiber || GetCurrentFiber() == g_exception_owner_fiber) {
|
||||
longjmp(exception_jmpbuf, code);
|
||||
}
|
||||
g_pending_exception_longjmp = code;
|
||||
SwitchToFiber(g_exception_owner_fiber);
|
||||
/* If we end up back here, the owner didn't honor the flag (bug).
|
||||
* Fall through to direct longjmp as a last resort — even though
|
||||
* the stack is wrong, the alternative is hanging silently. */
|
||||
longjmp(exception_jmpbuf, code);
|
||||
}
|
||||
#else
|
||||
static void deferred_exception_longjmp(int code) {
|
||||
longjmp(exception_jmpbuf, code);
|
||||
}
|
||||
#endif
|
||||
|
||||
void psx_exception_longjmp(void) {
|
||||
debug_server_log_restore_event(2, debug_cpu_ptr ? debug_cpu_ptr->pc : 0, 1);
|
||||
longjmp(exception_jmpbuf, 1);
|
||||
deferred_exception_longjmp(1);
|
||||
}
|
||||
|
||||
void psx_restore_state_escape(void) {
|
||||
if (in_exception) {
|
||||
debug_server_log_restore_event(1, debug_cpu_ptr ? debug_cpu_ptr->pc : 0, 2);
|
||||
longjmp(exception_jmpbuf, 2);
|
||||
deferred_exception_longjmp(2);
|
||||
}
|
||||
/* Not in exception context — return normally, let caller's `return;` handle it. */
|
||||
}
|
||||
@@ -305,6 +345,15 @@ void psx_check_interrupts(CPUState* cpu) {
|
||||
target_pc = hi_val + (uint32_t)(int32_t)lo_val;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
/* Record which fiber owns this setjmp. Any subsequent longjmp must
|
||||
* happen on this same fiber; if a non-owner fiber needs to longjmp
|
||||
* it must SwitchToFiber back here first (see deferred_exception_longjmp). */
|
||||
void *prev_owner_fiber = g_exception_owner_fiber;
|
||||
int prev_pending = g_pending_exception_longjmp;
|
||||
g_exception_owner_fiber = GetCurrentFiber();
|
||||
g_pending_exception_longjmp = 0;
|
||||
#endif
|
||||
for (;;) {
|
||||
int jmp_val = setjmp(exception_jmpbuf);
|
||||
if (jmp_val == 2) {
|
||||
@@ -327,6 +376,12 @@ void psx_check_interrupts(CPUState* cpu) {
|
||||
/* jmp_val 0 (normal return) or 1 (ReturnFromException): done. */
|
||||
break;
|
||||
}
|
||||
#ifdef _WIN32
|
||||
/* Restore previous exception-owner state. Supports nested exceptions
|
||||
* if they ever arise (uncommon but harmless). */
|
||||
g_exception_owner_fiber = prev_owner_fiber;
|
||||
g_pending_exception_longjmp = prev_pending;
|
||||
#endif
|
||||
|
||||
/* Restore the interrupted code's registers.
|
||||
*
|
||||
|
||||
@@ -262,6 +262,18 @@ static int psx_change_thread_fiber(CPUState* cpu, uint32_t target_tcb)
|
||||
debug_server_log_thread_event(8, cpu, current_tcb, target_tcb, 0);
|
||||
SwitchToFiber(target->fiber);
|
||||
|
||||
/* If a non-owner fiber requested an exception longjmp while we were
|
||||
* suspended, it deferred it by SwitchToFiber'ing back to us (the
|
||||
* owner). Honor it now from the correct stack. */
|
||||
extern void* g_exception_owner_fiber;
|
||||
extern int g_pending_exception_longjmp;
|
||||
extern jmp_buf exception_jmpbuf;
|
||||
if (g_pending_exception_longjmp && GetCurrentFiber() == g_exception_owner_fiber) {
|
||||
int code = g_pending_exception_longjmp;
|
||||
g_pending_exception_longjmp = 0;
|
||||
longjmp(exception_jmpbuf, code);
|
||||
}
|
||||
|
||||
debug_server_log_thread_event(9, cpu, target_tcb, current_tcb, 0);
|
||||
cpu->pc = 0;
|
||||
return 1;
|
||||
|
||||
Reference in New Issue
Block a user