Files
psxrecomp/tools/duck_query2.py
T
Matthew Stanley 685687f36f Restructure duckstation as submodule + patch; salvage TCP docs + debug_client
Recovery + restructure after rm -rf destroyed duckstation/build/ earlier this
session. Adopts the nesrecomp pattern (see F:/Projects/nesrecomp): pristine
upstream as submodule, our changes as a patch file that gets auto-applied by a
setup script.

Submodule:
- duckstation/ now pins stenzek/duckstation@ffb33c281 (release-20260328 era)
- .gitmodules records the pin; .gitignore stops ignoring the submodule path but
  still ignores build/ and dep/prebuilt/ (the submodule's own .gitignore also
  handles these)

Patch + setup:
- tools/duckstation/psxrecomp_oracle.patch (1187 lines, 7 files) captures our
  TCP debug-server on port 4371 plus the unfinished pc_break family of commands
  (with the VirtualMemoryAddress compile fix)
- tools/duckstation/setup.sh clones submodule, fetches + verifies + extracts
  prebuilt Windows deps, rewrites stale absolute paths baked into the prebuilt
  CMake metadata, applies the oracle patch. Idempotent.
- tools/duckstation/build.sh configures CMake (VS 17 2022) + runs MSBuild, then
  provisions bin/ with portable marker, BIOS copy, and settings.ini post-fixes
- tools/duckstation/README.md documents the layout, the regeneration flow, and
  the known headless-boot configuration issue

TCP documentation (salvaged from v3):
- TCP_COMMANDS.md: full command table with native (4370) / duckstation (4371)
  / both columns, reflecting actual v4 implementation including wtrace_* and
  pc_break/pc_hit_last families
- tools/debug_client.py: unified CLI from v3 (ports 4370/4371 match v4)
- DEBUG.md: merged v3's END-TO-END COMPLETION RULE, Stale Hypothesis Rule,
  TOOLING RULE, and TOOL LIFECYCLE RULE sections into v4's existing loop

Phase 4 diagnostic artefacts from this session:
- generated/relocation_proofs/vblank_registrar/ — corrected VBlank-stall
  diagnosis. Prior handoff claimed the shell VBlank handler was never installed;
  live TCP inspection of both runtime and DuckStation oracle proved handler IS
  installed at 0x800DFEE0. Real gap is one level up in the kernel IRQ chain at
  *(0xA000E014).
- tools/duck_query*.py, duck_search.py, disasm_ram.py, scan_*.py — focused
  probes used to establish the above; reusable against either server

CLAUDE.md §2 and new §15 document the submodule-based oracle setup and the
"never edit submodule, regenerate patch instead" rule.

Known issues (tracked in tools/duckstation/README.md):
- Fresh settings.ini doesn't complete BIOS boot under -nogui; one-time launch
  with GUI is required to warm up the settings before headless mode works

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

97 lines
3.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Follow-up probe: read IRQ chain blocks, explore 0x800DFEE0 context, disassemble exception entry."""
import socket, json, time
HOST='127.0.0.1'; PORT=4371
def send(cmd, timeout=3.0):
s=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.settimeout(timeout)
s.connect((HOST,PORT))
s.sendall((json.dumps(cmd)+'\n').encode())
buf=b''
deadline = time.time()+timeout
while time.time()<deadline:
try: chunk=s.recv(65536)
except socket.timeout: break
if not chunk: break
buf+=chunk
if b'\n' in buf: break
s.close()
line = buf.split(b'\n',1)[0].decode()
if not line: return None
try: return json.loads(line)
except: return line
def read_ram(addr, length):
r = send({"cmd":"read_ram","addr":f"0x{addr:08X}","len":length})
if not r or not r.get('ok'): return None
return bytes.fromhex(r['hex'])
def dump_words(data, base, annotate=None):
for i in range(0, len(data), 16):
words = [int.from_bytes(data[i+j:i+j+4],'little') for j in range(0, min(16,len(data)-i), 4)]
hexs = ' '.join(f'{w:08X}' for w in words)
annot = ''
if annotate:
for j,w in enumerate(words):
tag = annotate(base+i+j*4, w)
if tag: annot += f' [{base+i+j*4:08X}:{tag}]'
print(f' 0x{base+i:08X}: {hexs}{annot}', flush=True)
# Targets we care about
TARGETS = {
0x8005A5BC: 'handler',
0x8005A540: 'registrar',
0x8005A160: 'init',
0x8005A424: 'cleanup',
0x8005A380: 'dispatch-entry',
0x8005A5FC: 'SetVSync',
0x8005A600: '0xBFC42600',
0xBFC42160: 'init-rom',
0xBFC425BC: 'handler-rom',
}
def annot(addr, val):
if val in TARGETS: return TARGETS[val]
for t,name in TARGETS.items():
if val == t: return name
# Match uncached alias
if (val | 0x20000000) in TARGETS: return TARGETS[val|0x20000000]+'(uncached)'
if (val & ~0x20000000) in TARGETS: return TARGETS[val&~0x20000000]+'(cached)'
return None
print('PING:', send({"cmd":"ping"}))
print('PAUSE:', send({"cmd":"pause"}))
time.sleep(0.3)
# 1. Dump exception entry at 0x80000C80 (from jr $k0 → 0x00000C80)
print('\n=== Exception handler at 0x80000C80 (32 words) ===', flush=True)
d = read_ram(0x00000C80, 128)
if d: dump_words(d, 0x80000C80, annot)
# 2. Dump IRQ handler chain block at 0xA000E1F4, size 0x300 bytes
print('\n=== Handler chain block 0xA000E1F4..+0x300 ===', flush=True)
d = read_ram(0x0000E1F4, 0x300)
if d: dump_words(d, 0xA000E1F4, annot)
# 3. Dump 0x800DFEE0 context (64 bytes before, 64 after)
print('\n=== Around 0x800DFEE0 (where handler ptr lives) ===', flush=True)
d = read_ram(0x000DFEC0, 256)
if d: dump_words(d, 0x800DFEC0, annot)
# 4. Also check the TCB block at 0xA000E1EC, size 4*TCB (each TCB 0xC0 bytes)
print('\n=== 0xA000E1EC (TCB table, 4×0xC0=0x300 bytes) — overlapping with chain block? ===', flush=True)
d = read_ram(0x0000E1EC, 16)
if d: dump_words(d, 0xA000E1EC, annot)
# 5. Dump 0xA000E004 block, size 0x20
print('\n=== 0xA000E004..+0x20 (DCB?) ===', flush=True)
d = read_ram(0x0000E004, 0x30)
if d: dump_words(d, 0xA000E004, annot)
# 6. Dump 0xA000E028 EvCB
print('\n=== EvCB 0xA000E028..+0x1C0 (first 4 slots only, 28 bytes each) ===', flush=True)
d = read_ram(0x0000E028, 4*28)
if d: dump_words(d, 0xA000E028, annot)
print('\nCONTINUE:', send({"cmd":"continue"}))