mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 10:19:47 -04:00
fdbd2b515f
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>
96 lines
3.5 KiB
Python
96 lines
3.5 KiB
Python
"""Extract the card-data byte stream the BIOS received during OPTIONS-black.
|
|
|
|
The kernel SIO byte ISR at 0xBFC16024 emits `sb v0,0x0(k0)` — every byte
|
|
the card delivers gets stored at k0 (which increments per byte). Filter
|
|
wtrace_all by that PC, sort by seq, and reconstruct the byte stream.
|
|
Compare to tomba.0.mcr / tomba.1.mcr.
|
|
"""
|
|
import json, os, sys
|
|
from collections import defaultdict
|
|
|
|
path = sys.argv[1] if len(sys.argv) > 1 else \
|
|
'F:/Projects/TombaRecomp/psx_freeze_dump_psx-runtime_1778800465.json'
|
|
mcr_root = 'F:/Projects/TombaRecomp/saves'
|
|
|
|
with open(path) as f:
|
|
d = json.load(f)
|
|
|
|
# Filter by the kernel-SIO-handler byte-store PC. The handler lives at
|
|
# BIOS 0xBFC16024, but the recompiler runs the RAM mirror at 0x00006524
|
|
# (= 0xBFC16024 - 0xBFC10000 + 0x500). Either could show up depending
|
|
# on whether code came from ROM (BIOS path) or RAM-mirror (kernel).
|
|
STORE_PCS = ('0xBFC16024', '0x00006524', '0x06524')
|
|
|
|
hits = [e for e in d['wtrace_all']
|
|
if e['pc'].upper() in (p.upper() for p in STORE_PCS)
|
|
and e.get('w', 1) == 1]
|
|
print(f'BIOS card-byte stores: {len(hits)} hits')
|
|
if not hits:
|
|
sys.exit(1)
|
|
|
|
hits.sort(key=lambda e: int(e['seq']))
|
|
|
|
# Group by contiguous bursts. Each card command reads N bytes consecutively
|
|
# into a buffer; bursts are separated by gaps in seq (handler not running).
|
|
bursts = []
|
|
cur = []
|
|
for i, e in enumerate(hits):
|
|
if cur and int(e['seq']) > int(cur[-1]['seq']) + 5000:
|
|
bursts.append(cur)
|
|
cur = []
|
|
cur.append(e)
|
|
if cur:
|
|
bursts.append(cur)
|
|
print(f'Bursts: {len(bursts)}')
|
|
for i, b in enumerate(bursts):
|
|
addrs = [int(e['addr'], 16) for e in b]
|
|
print(f' Burst {i}: {len(b)} bytes, addr range '
|
|
f'{hex(min(addrs))}..{hex(max(addrs))}, seq '
|
|
f'{b[0]["seq"]}..{b[-1]["seq"]}')
|
|
|
|
# Examine the most-recent (likely complete) burst.
|
|
b = bursts[-1] if bursts else []
|
|
print(f'\n=== Last burst ({len(b)} bytes) ===')
|
|
print('First 32 entries:')
|
|
for e in b[:32]:
|
|
print(f' seq={e["seq"]} addr={e["addr"]} val=0x{int(e["new"], 16) & 0xFF:02X} frame={e["frame"]}')
|
|
print('...')
|
|
print('Last 16 entries:')
|
|
for e in b[-16:]:
|
|
print(f' seq={e["seq"]} addr={e["addr"]} val=0x{int(e["new"], 16) & 0xFF:02X} frame={e["frame"]}')
|
|
|
|
# Bytes in the order they were stored.
|
|
recv = bytes(int(e['new'], 16) & 0xFF for e in b)
|
|
print(f'\nReceived stream hex (first 256 bytes):')
|
|
for i in range(0, min(256, len(recv)), 32):
|
|
print(f' {i:04x}: ' + ' '.join(f'{b:02x}' for b in recv[i:i+32]))
|
|
|
|
# Compute the BIOS-style XOR checksum over the first 128 bytes.
|
|
if len(recv) >= 129:
|
|
xor = 0
|
|
for byte in recv[:128]:
|
|
xor ^= byte
|
|
print(f'\nXOR of first 128 bytes: 0x{xor:02X}')
|
|
print(f'Byte at offset 128 (claimed checksum): 0x{recv[128]:02X}')
|
|
print(f'Match: {xor == recv[128]}')
|
|
|
|
# Compare to the actual save files.
|
|
for fp in ('F:/Projects/TombaRecomp/saves/card1.mcd',
|
|
'F:/Projects/TombaRecomp/saves/card2.mcd'):
|
|
fname = os.path.basename(fp)
|
|
if not os.path.exists(fp):
|
|
print(f'\n{fname}: not found')
|
|
continue
|
|
with open(fp, 'rb') as fh:
|
|
mcr = fh.read()
|
|
print(f'\n{fname}: {len(mcr)} bytes')
|
|
print('First 256 bytes:')
|
|
for i in range(0, 256, 32):
|
|
print(f' {i:04x}: ' + ' '.join(f'{b:02x}' for b in mcr[i:i+32]))
|
|
# See if our recv stream matches any window of the mcr.
|
|
if len(recv) > 32:
|
|
needle = recv[:32]
|
|
idx = mcr.find(needle)
|
|
print(f' Match of first 32 received bytes anywhere in {fname}: '
|
|
f'{hex(idx) if idx >= 0 else "NO"}')
|