Files
psxrecomp/tools/_options_session/_smoke_normalized.py
T
Matthew Stanley fdbd2b515f 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>
2026-05-15 12:49:13 -07:00

62 lines
2.1 KiB
Python

"""Smoke-test the normalized wtrace verb set on BOTH backends.
Both backends must accept identical JSON for the same verbs. This
script issues the same six probes against runtime (4470) and beetle
(4380) and compares response shapes. Any divergence is a parity bug
in the debug server; fix it before relying on cross-backend tooling.
"""
from __future__ import annotations
import sys, os, json
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from _dbg import call, RUNTIME_PORT, BEETLE_PORT
def probe(name, **kw):
out = {}
for label, port in [("runtime", RUNTIME_PORT), ("beetle", BEETLE_PORT)]:
try:
out[label] = call(port, name, **kw)
except Exception as e:
out[label] = {"error": str(e)}
return out
def shape(r):
if not isinstance(r, dict): return type(r).__name__
return sorted(k for k in r.keys() if k != "id")
print("== wtrace_disarm_all (idempotent reset of armed ranges) ==")
r = probe("wtrace_disarm_all")
print(" runtime:", r["runtime"])
print(" beetle :", r["beetle"])
print("\n== wtrace_arm lo=0x80090C80 hi=0x80090DE0 ==")
r = probe("wtrace_arm", lo="0x80090C80", hi="0x80090DE0")
print(" runtime:", r["runtime"])
print(" beetle :", r["beetle"])
print("\n== wtrace_ranges ==")
r = probe("wtrace_ranges")
print(" runtime shape:", shape(r["runtime"]))
print(" beetle shape:", shape(r["beetle"]))
print(" runtime count:", r["runtime"].get("count"))
print(" beetle count:", r["beetle"].get("count"))
print("\n== wtrace_stats ==")
r = probe("wtrace_stats")
print(" runtime:", r["runtime"])
print(" beetle :", r["beetle"])
print("\n== wtrace_dump count=3 ==")
r = probe("wtrace_dump", count=3)
for label in ("runtime", "beetle"):
resp = r[label]
print(f" {label} top-level shape:", shape(resp))
entries = resp.get("entries", [])
print(f" {label} entries: {len(entries)}")
if entries:
e0 = entries[0]
print(f" {label} entry[0] keys:", sorted(e0.keys()))
print(f" {label} entry[0] sample: addr={e0.get('addr')} new={e0.get('new')} pc={e0.get('pc')} w={e0.get('w')} frame={e0.get('frame')}")