Files
psxrecomp/tools/oracle_diff.py
T
Matthew Stanley 82aeca98a7 feat: embed Beetle PSX libretro core as golden oracle for RAM comparison
Adds a new psx-beetleoracle build target that embeds beetle-psx-libretro
(mednafen PSX core) alongside the recompiled BIOS. Both execute the same
SCPH1001.BIN and advance in lock-step, enabling byte-level RAM comparison
via the TCP debug server.

New infrastructure:
- psx_oracle_backend.h: abstract oracle backend interface (init, run_frame,
  get_ram, get_vram, get_scratchpad, read_byte/word, get_cpu_regs, sync_to_state)
- beetle_psx_bridge.cpp: libretro bridge with CPU register access via
  PSX_CPU->GetRegister(), VRAM via GPU_get_vram(), scratchpad via ScratchRAM
- psx_oracle_cmds.c: TCP commands — find_first_divergence, emu_read_ram,
  emu_read_vram, emu_read_scratchpad, emu_cpu_regs, emu_step, emu_sync,
  emu_sync_press
- psx_oracle_stub.c: no-op stubs for non-oracle builds
- tools/oracle_diff.py: standalone Python RAM comparison tool
- tools/build_oracle.sh: build script for beetle-psx + oracle target

Also includes: SIO card probe diagnostic counters (mc_probes, mc_acks,
mc_cmds, tx_writes), enhanced dispatch miss logging with register context,
memory_get_ram_ptr() for oracle RAM comparison.

Build: cd beetle-psx && make platform=mingw_x86_64 STATIC_LINKING=1 HAVE_LIGHTREC=0
       cd runtime/build && cmake --build . --target psx-beetleoracle

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-25 00:15:01 -07:00

155 lines
5.3 KiB
Python

#!/usr/bin/env python3
"""
oracle_diff.py -- Golden-oracle RAM comparison tool.
Connects to both the psxrecomp runtime (port 4370) and DuckStation oracle
(port 4371) and compares RAM byte-by-byte.
Usage:
python tools/oracle_diff.py # Full 2MB diff
python tools/oracle_diff.py 0x0000 0x8000 # Kernel RAM only
python tools/oracle_diff.py 0x7400 0x7600 -v # Card state, verbose
"""
import socket
import json
import sys
import time
import argparse
def send_cmd(port, cmd_json, timeout=3.0):
"""Send a JSON command to the debug server and return the parsed response."""
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(timeout)
try:
sock.connect(("127.0.0.1", port))
sock.sendall((json.dumps(cmd_json) + "\n").encode())
data = b""
while True:
chunk = sock.recv(8192)
if not chunk:
break
data += chunk
if b"\n" in data:
break
return json.loads(data.decode().strip())
finally:
sock.close()
def read_ram(port, addr, length):
"""Read RAM from a debug server, returns bytes."""
resp = send_cmd(port, {"cmd": "read_ram", "addr": f"0x{addr:08X}", "len": length})
if not resp.get("ok"):
raise RuntimeError(f"read_ram failed on port {port}: {resp}")
return bytes.fromhex(resp["hex"])
def find_first_divergence(recomp_port, oracle_port, lo=0, hi=0x1FFFFF, context=16, chunk_size=4096):
"""Compare RAM between recomp and oracle, return first difference."""
first_diff = None
diff_count = 0
total_scanned = 0
addr = lo
while addr <= hi:
length = min(chunk_size, hi - addr + 1)
recomp_data = read_ram(recomp_port, addr, length)
oracle_data = read_ram(oracle_port, addr, length)
for i in range(len(recomp_data)):
total_scanned += 1
if recomp_data[i] != oracle_data[i]:
diff_count += 1
if first_diff is None:
first_diff = addr + i
addr += length
if first_diff is None:
return {
"match": True,
"bytes_scanned": total_scanned,
}
# Read context around first difference
ctx_lo = max(lo, first_diff - context)
ctx_hi = min(hi, first_diff + context)
ctx_len = ctx_hi - ctx_lo + 1
recomp_ctx = read_ram(recomp_port, ctx_lo, ctx_len)
oracle_ctx = read_ram(oracle_port, ctx_lo, ctx_len)
return {
"match": False,
"first_diff": first_diff,
"recomp_val": recomp_ctx[first_diff - ctx_lo],
"oracle_val": oracle_ctx[first_diff - ctx_lo],
"diff_count": diff_count,
"bytes_scanned": total_scanned,
"ctx_lo": ctx_lo,
"ctx_hi": ctx_hi,
"recomp_ctx": recomp_ctx.hex(),
"oracle_ctx": oracle_ctx.hex(),
}
def print_context(result, verbose=False):
"""Pretty-print a divergence result with hex context."""
if result["match"]:
print(f"MATCH: {result['bytes_scanned']:,} bytes scanned, no differences.")
return
addr = result["first_diff"]
print(f"DIVERGENCE at 0x{addr:08X}: recomp=0x{result['recomp_val']:02X} oracle=0x{result['oracle_val']:02X}")
print(f" Total differences: {result['diff_count']:,} / {result['bytes_scanned']:,} bytes")
if verbose:
ctx_lo = result["ctx_lo"]
recomp = bytes.fromhex(result["recomp_ctx"])
oracle = bytes.fromhex(result["oracle_ctx"])
print(f"\n Context [0x{ctx_lo:08X} .. 0x{result['ctx_hi']:08X}]:")
print(f" {'Addr':>10s} {'Recomp':>6s} {'Oracle':>6s} {'Match':>5s}")
for i in range(len(recomp)):
a = ctx_lo + i
r = recomp[i]
o = oracle[i]
marker = " " if r == o else " <---"
print(f" 0x{a:08X} 0x{r:02X} 0x{o:02X} {marker}")
def main():
parser = argparse.ArgumentParser(description="Oracle RAM comparison tool")
parser.add_argument("lo", nargs="?", default="0x0000", help="Start address (hex)")
parser.add_argument("hi", nargs="?", default="0x1FFFFF", help="End address (hex)")
parser.add_argument("-v", "--verbose", action="store_true", help="Show context bytes")
parser.add_argument("--recomp-port", type=int, default=4370, help="Recomp debug port")
parser.add_argument("--oracle-port", type=int, default=4371, help="Oracle debug port")
parser.add_argument("--context", type=int, default=16, help="Context bytes around first diff")
args = parser.parse_args()
lo = int(args.lo, 0)
hi = int(args.hi, 0)
# Verify both servers are reachable
for port, name in [(args.recomp_port, "recomp"), (args.oracle_port, "oracle")]:
try:
resp = send_cmd(port, {"cmd": "ping"})
if resp.get("ok"):
print(f" {name} (port {port}): connected (frame {resp.get('frame', '?')})")
else:
print(f" {name} (port {port}): ping failed")
sys.exit(1)
except Exception as e:
print(f" {name} (port {port}): connection failed: {e}")
sys.exit(1)
print(f"\nComparing RAM 0x{lo:08X}..0x{hi:08X} ({hi - lo + 1:,} bytes)...")
result = find_first_divergence(args.recomp_port, args.oracle_port, lo, hi, args.context)
print()
print_context(result, args.verbose)
if __name__ == "__main__":
main()