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:
Matthew Stanley
2026-05-15 12:49:13 -07:00
parent 747410abe9
commit fdbd2b515f
52 changed files with 2943 additions and 18 deletions
+22
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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));
+348 -1
View File
@@ -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
View File
@@ -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;
+59 -4
View File
@@ -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.
*
+12
View File
@@ -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;