Files
psxrecomp/TCP_COMMANDS.md
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

6.4 KiB
Raw Blame History

TCP Debug Server Commands

Protocol: JSON over newline, one object per line, responses on same connection.

  • Request shape: {"id": N, "cmd": "<command>", ...params}
  • Success: {"id": N, "ok": true, ...data}
  • Failure: {"id": N, "ok": false, "error": "<msg>"}

There are two servers, both implementing this protocol with overlapping command sets:

Server Port Source
Native (our recompiled runtime) 4370 runtime/src/debug_server.c
DuckStation (oracle) 4371 duckstation/src/core/psxrecomp_debug_server.cpp (patched, see tools/duckstation/psxrecomp_oracle.patch)

The debug_client.py CLI can target either, or compare two at once to diff state live — that's how divergence hunts work.

python tools/debug_client.py <cmd> [args]           # native (port 4370)
python tools/debug_client.py --ds <cmd> [args]      # duckstation (port 4371)
python tools/debug_client.py compare <cmd>          # run on both, diff results

Command inventory

Columns: N = native, D = DuckStation oracle.

Command N D Params Description
ping / frame ✓ ✓ — Heartbeat + current frame number
get_registers (regs) ✓ ✓ — All 32 GPRs + PC + HI + LO (native also: COP0 SR/Cause/EPC, I_STAT, I_MASK)
read_ram ✓ ✓ addr, len Read bytes from PS1 address space as hex string
write_ram ✓ ✓ addr, hex Write bytes to PS1 address space
dump_ram ✓ ✓ addr, len Chunked RAM read (up to 64 KB)
read_scratch ✓ addr, len Read PS1 scratchpad (0x1F800000 region)
read_vram / vram_peek ✓¹ ✓ x, y, w, h Read 16-bit VRAM pixels (max 128×128)
gpu_state ✓ ✓ — Display area, draw offset, GPUSTAT, clip rect, xfer state
sio_state ✓ ✓ — SIO registers + (native only) pad/memcard protocol + TX/RX history
irq_state ✓ ✓ — I_STAT, I_MASK (both), plus chain state on native
dma_state ✓ — DPCR, DICR, all 7 channel states (madr/bcr/chcr)
event_state ✓ — EvCB table summary (stub on DS — events are BIOS-level)
overlay_state ✓ — Current overlay info
watch ✓ ✓ addr Set byte-level memory watchpoint (fires per-frame on change)
unwatch ✓ ✓ addr Remove memory watchpoint
set_input ✓ ✓ buttons Override pad1 buttons (PS1 inverted bitmask, 0 = pressed)
clear_input ✓ ✓ — Remove input override
pause ✓ ✓ — Pause emulation
continue (c) ✓ ✓ — Resume emulation
step ✓ ✓ [count] Step N frames (default 1)
run_to_frame ✓ ✓ frame Run until frame number, then pause
history ✓ ✓ — Ring buffer stats (frames available)
get_frame ✓ ✓ frame Full frame record from ring buffer
frame_range ✓ ✓ start, end Range query, max 200 frames
frame_timeseries ✓ ✓ start, end Compact timeseries, max 200 frames
set_snapshot ✓ ✓ slot, addr, size Configure per-frame RAM snapshot region (slots 0-3)
get_snapshots ✓ ✓ — Show snapshot config
screenshot ✓ ✓ path Save display to PNG
first_failure ✓ — Find first divergence point between runs (native-side tracking)
read_frame_ram ✓ addr, len, frame Read RAM as of a specific frame (from ring buffer)
wtrace_range ✓ lo, hi Set RAM-write trace range (ring of 1024 writes with RA)
wtrace_dump ✓ — Dump RAM-write trace entries as JSON
wtrace_clear ✓ — Reset the trace ring
pc_break ✓² addr DS execute breakpoint, state captured on hit (via pc_hit_last)
pc_unbreak ✓² addr Remove an execute breakpoint
pc_break_list ✓² — List active execute breakpoints
pc_hit_last ✓² — Captured state (PC, $ra, all GPRs, COP0) from most recent PC break hit
pc_hit_clear ✓² — Clear the last-hit record
quit ✓ — Shutdown native runtime

¹ Native vram_peek is the legacy name; DS calls it read_vram. Same semantics.
² The pc_* family is specific to the DS oracle: DuckStation's CPU core honours CPU::AddBreakpointWithCallback, while our native runtime dispatches whole recompiled functions (no mid-function PC breaks).


Divergence-hunt workflow

When a recompiled-BIOS bug is suspected, the two servers let you find the first divergence instead of chasing symptoms. Standard procedure (inherited from v3's DEBUG.md):

  1. Sync state via PC + registers, not frame number. Frames drift after even a single timing glitch. Pause both servers; compare get_registers until they match.
  2. Dump both sides fully. Compare get_frame, gpu_state, irq_state, dma_state (DS), dump_ram over the same regions.
  3. Byte-level comparison. Tiny mismatches usually point at one subsystem. Use debug_client.py compare <cmd> for automatic diff.
  4. Find the earliest mismatch, not a later symptom. Ring-buffer queries (frame_range, read_frame_ram) help locate which frame went wrong.
  5. Trace the write. Use watch to catch the divergent store, or DS's pc_break on the suspect function entry. Look at $ra in pc_hit_last to identify the caller chain.
  6. Classify. codegen (recompiler generates wrong instruction), runtime (MMIO or kernel simulation wrong), timing (IRQ cadence), or BIOS (real-hardware quirk we didn't model).
  7. Minimal fix in the correct subsystem. Never hand-deliver state to hide the symptom (see CLAUDE.md §0).

Rule when the server can't answer your question

If an inspection need isn't covered by the existing commands, do not fall back to printf or log files. Instead:

  1. Add a handler in runtime/src/debug_server.c (native)
  2. Add the matching handler in duckstation/src/core/psxrecomp_debug_server.cpp (DS), regenerate the patch via the instructions in tools/duckstation/README.md
  3. Keep field names parallel between the two
  4. Update this file

The TCP server is the canonical instrumentation surface. Rule 3 in CLAUDE.md is absolute: no fprintf(stderr, …) in source code, ever, for any reason.