Files
psxrecomp/tools/duck_query3.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

77 lines
2.8 KiB
Python

"""Probe A0/B0/C0 kernel vector tables and the RAM trampoline chain."""
import socket, json, time, struct
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(data, base, label=''):
print(f'--- {label} 0x{base:08X}..+0x{len(data):X} ---', flush=True)
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)]
print(f' 0x{base+i:08X}: ' + ' '.join(f'{w:08X}' for w in words), flush=True)
send({"cmd":"pause"})
time.sleep(0.3)
# A0/B0/C0 trampolines at RAM 0xA0, 0xB0, 0xC0
for name, addr in [('A0',0xA0),('B0',0xB0),('C0',0xC0)]:
d = read_ram(addr, 16)
if d: dump(d, 0x80000000|addr, f'{name} vector trampoline')
# Now, each A0/B0/C0 dispatcher loads a function pointer from a table. The table
# base is computed internally. Let me scan a wider window around common locations
# where the A0/B0/C0 pointer tables might live.
# Based on standard PS1 layout, tables are often at 0x80000200..0x80000400 or in
# the kernel RAM 0x8000_0500+. Let's dump that.
d = read_ram(0x00000000, 0x200)
if d: dump(d, 0x80000000, 'RAM 0x80000000..0x200 (vectors)')
# Search kernel RAM 0x80000500..0x80009000 for the handler address 0x8005A5BC
# AND the registrar address 0x8005A540 AND SetVSync 0x8005A5FC
print('\n--- Searching kernel RAM 0x80000500..0x80009000 for shell kernel ptrs ---', flush=True)
d = read_ram(0x00000500, 0x8B00)
if d:
targets = {
bytes.fromhex('bca50580'): '0x8005A5BC handler',
bytes.fromhex('40a50580'): '0x8005A540 registrar',
bytes.fromhex('60a10580'): '0x8005A160 init',
bytes.fromhex('fca50580'): '0x8005A5FC SetVSync',
bytes.fromhex('24a40580'): '0x8005A424 cleanup',
bytes.fromhex('80a30580'): '0x8005A380 chain-install',
}
for m,label in targets.items():
pos=0
while True:
i=d.find(m,pos)
if i<0: break
a = 0x80000000 | (0x500 + i)
print(f' {label}: at 0x{a:08X}', flush=True)
pos = i+1
send({"cmd":"continue"})
print('RESUMED', flush=True)