mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
f8841ead30
Adds a RAM-write trace hook to mednafen-PSX (SB/SH/SW handlers fire g_psxrecomp_wtrace_cb after each write), a ring buffer + TCP commands in psx-beetleoracle, and verification scripts that drive CROSS in both Beetle and recomp and diff the gate-byte writers. Conclusion (docs/CASE_A_AOT_GAP.md): the [0x7568+slot] LSB-clear writers are at RAM PCs 0x5EF4 / 0x5FA8 (and 0x4F54 sets BUSY). Recomp never dispatches any of those PCs, never executes the 0x4F00..0x4FFF or 0x5800..0x5FFF RAM windows, and never writes the gate. Selective coverage pattern (interp reaches 0xCF0/0x52xx/0x6xxx but not 0x4Fxx/0x5xxx) is install/dispatch path gap, not interpreter bug. PLAN.md:941 already names this work as a hard gate. Beetle-side modifications live inside the vendored beetle-psx/ git clone; docs/beetle_wtrace_hook.patch captures the diff so they survive a re-clone. Memcard symptom debugging stops here. Next: trace-generated AOT RAM-code manifest + recompiler ingestion (Phases B–F). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
126 lines
3.9 KiB
Python
126 lines
3.9 KiB
Python
"""Cheap verification: does recomp execute the Beetle-identified RAM writer PCs?
|
|
|
|
Queries:
|
|
1. dirty_ram_stats — block/insn/abort counters + per-PC histogram
|
|
2. dispatch_check on every Beetle writer PC
|
|
3. wtrace_dump filtered to 0x7568..0x756C
|
|
4. wtrace_stats — total writes ever recorded
|
|
5. dispatch_tail — last N dispatched targets (sanity that dispatch is alive)
|
|
|
|
Drives a CROSS press if --press is given (default: just queries current state).
|
|
"""
|
|
import socket, json, sys
|
|
|
|
|
|
PCS_OF_INTEREST = [
|
|
0x0000445C,
|
|
0x00004D6C,
|
|
0x00004F0C,
|
|
0x00004F54,
|
|
0x00005DB8,
|
|
0x00005DD0,
|
|
0x00005DD8,
|
|
0x00005EF4,
|
|
0x00005FA8,
|
|
0x00006524,
|
|
]
|
|
|
|
|
|
def call(d, timeout=15.0):
|
|
s = socket.create_connection(('127.0.0.1', 4370))
|
|
s.settimeout(timeout)
|
|
s.sendall((json.dumps(d) + '\n').encode())
|
|
buf = b''
|
|
while True:
|
|
try:
|
|
chunk = s.recv(65536)
|
|
except socket.timeout:
|
|
break
|
|
if not chunk:
|
|
break
|
|
buf += chunk
|
|
depth = 0; instr = False; esc = False
|
|
for b in buf:
|
|
if esc: esc = False; continue
|
|
if b == 0x5C: esc = True; continue
|
|
if b == 0x22: instr = not instr; continue
|
|
if instr: continue
|
|
if b == 0x7B: depth += 1
|
|
elif b == 0x7D: depth -= 1
|
|
if depth == 0 and buf.strip():
|
|
break
|
|
s.close()
|
|
return json.loads(buf.decode())
|
|
|
|
|
|
def maybe_press():
|
|
if '--press' not in sys.argv:
|
|
return
|
|
print('=== driving CROSS press ===')
|
|
# set_input takes BIOS pad word (active-low). 0xBFFF = CROSS only.
|
|
print(call({'id': 1, 'cmd': 'set_input', 'buttons': '0xBFFF'}))
|
|
# Wait ~4s for shell to react and chain to spin.
|
|
import time; time.sleep(4)
|
|
# Release.
|
|
print(call({'id': 2, 'cmd': 'clear_input'}))
|
|
time.sleep(1)
|
|
|
|
|
|
print('=== ping ===')
|
|
print(json.dumps(call({'id': 0, 'cmd': 'ping'})))
|
|
|
|
maybe_press()
|
|
|
|
print()
|
|
print('=== dirty_ram_stats ===')
|
|
r = call({'id': 1, 'cmd': 'dirty_ram_stats'})
|
|
print(f"blocks_run = {r['blocks_run']}")
|
|
print(f"insns_run = {r['insns_run']}")
|
|
print(f"aborts = {r['aborts']}")
|
|
print(f"dirty_bitmap = {r['dirty_bitmap']}")
|
|
print(f"per_pc table size: {len(r.get('per_pc',[]))}")
|
|
hit_pcs = set()
|
|
for p in sorted(r.get('per_pc', []), key=lambda e: int(e['pc'], 16)):
|
|
pc_int = int(p['pc'], 16)
|
|
hit_pcs.add(pc_int)
|
|
print(f" pc={p['pc']:>10} hits={p['hits']:>10} insns={p['insns']:>10}")
|
|
|
|
print()
|
|
print('=== Beetle-writer PC dispatch_check ===')
|
|
for pc in PCS_OF_INTEREST:
|
|
r = call({'id': 100 + pc, 'cmd': 'dispatch_check', 'addr': f'0x{pc:08X}'})
|
|
in_dri = pc in hit_pcs
|
|
print(f" PC=0x{pc:08X} static_dispatch={r.get('found','?')!s:>5} "
|
|
f"dirty_ram_interp_hit={in_dri}")
|
|
|
|
print()
|
|
print('=== wtrace_stats ===')
|
|
print(json.dumps(call({'id': 200, 'cmd': 'wtrace_stats'}), indent=2))
|
|
|
|
print()
|
|
print('=== wtrace ranges ===')
|
|
print(json.dumps(call({'id': 201, 'cmd': 'wtrace_ranges'}), indent=2))
|
|
|
|
print()
|
|
print('=== wtrace_dump (full ring, filtered to 0x7568-756C below) ===')
|
|
r = call({'id': 202, 'cmd': 'wtrace_dump'})
|
|
total = r.get('total_seq', 0)
|
|
entries = r.get('entries', [])
|
|
print(f"# total writes ever={total} returned={len(entries)}")
|
|
|
|
gate_writes = [e for e in entries
|
|
if int(e.get('addr', '0x0'), 16) in (0x7568, 0x7569, 0x756A, 0x756B)]
|
|
print(f"# writes touching 0x7568..756B: {len(gate_writes)}")
|
|
for e in gate_writes[:50]:
|
|
print(f" seq={e.get('seq'):>6} addr={e.get('addr')} "
|
|
f"old={e.get('old_val'):>10} new={e.get('new_val'):>10} "
|
|
f"width={e.get('width')} ra={e.get('ra')} "
|
|
f"frame={e.get('frame')} func={e.get('func_addr')}")
|
|
|
|
print()
|
|
print('=== dispatch_tail (last 20 dispatched targets — sanity) ===')
|
|
r = call({'id': 203, 'cmd': 'dispatch_tail', 'count': '20'})
|
|
print(f"# total dispatches={r.get('total')} returned={r.get('count')}")
|
|
for a in r.get('addrs', [])[-20:]:
|
|
print(f" {a}")
|