# TCP Debug Server Commands Protocol: **JSON over newline**, one object per line, responses on same connection. - Request shape: `{"id": N, "cmd": "", ...params}` - Success: `{"id": N, "ok": true, ...data}` - Failure: `{"id": N, "ok": false, "error": ""}` There are **two live** servers, both implementing this protocol with overlapping command sets: | Server | Port | Source | |---|---|---| | **Native** (our recompiled runtime) | `4370` | `runtime/src/debug_server.c` | | **Beetle PSX** (oracle) | `4380` | `runtime/src/beetle_debug_server.c` | > **DuckStation (port 4371) was retired as the oracle on 2026-05-05** and is no > longer built from this repository — there is no `duckstation` entry in > `.gitmodules`. The **D** column in the curated inventory below is historical > and reflects DuckStation, not Beetle. For authoritative per-command > native/Beetle coverage, use the generated > [Complete command index](#complete-command-index-generated) at the bottom of > this file, which is derived from the two servers' command tables. The `debug_client.py` CLI can target either, or `compare` two at once to diff state live — that's how divergence hunts work. ```bash python tools/debug_client.py [args] # native (port 4370) python tools/debug_client.py --port 4380 # psx-beetle python tools/debug_client.py --ds [args] # duckstation (port 4371) python tools/debug_client.py compare # run on both, diff results ``` Commands without a bespoke CLI mapping pass through generically: extra args of the form `key=value` become JSON fields (ints when numeric, else strings), so every server command is reachable, e.g. `debug_client.py --port 4370 gpu_frame_dump frame=14528 count=65536`. `host_osd_shot path=` captures the current host text bitmap (status/toast and diagnostic input rows), including in hidden OpenGL runs. It does not need a visible window or alter guest VRAM. An empty overlay returns an error. The image is the text layer, rather than the full game presentation. --- ## 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 — up to the full 2 MB in ONE response line. `dump_ram` is an alias (the old chunked multi-line variant is gone: it broke the one-request/one-response protocol and wedged the server) | | `write_ram` | ✓ | ✓ | `addr`, `val` | Write **one byte** to PS1 address space. Note the parameter is `val` (not `hex`), and the write is a single byte per call — this row previously documented both incorrectly | | `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, display depth, `screen_offset_y`, draw offset, GPUSTAT, clip rect, xfer state. A positive screen offset moves 24-bit scanout down from the PAL or NTSC broadcast centre | | `screenshot_hires` | ✓ | ✓ | `path` | PNG of the **supersampled** surface (the present path the window uses), at `display × gr_scale()`. ⚠ `screenshot`/`screenshot_file` capture native 15-bit VRAM and are **blind to anything that only exists in the hi-res mirror** — geometry correction, SSAA edges, perspective UVs — so they show a clean frame while the player sees a broken one. Use this one to verify those. Under OpenGL it reads the hr FBO directly (the CPU-side mirror is native there), or the high-resolution window when that mode is on, so it is the capture that proves an internal-resolution preset: 4K is 2880×2160 and 8K 5760×4320 for a 320×240 display. Falls back to the native resolve (and reports `scale: 1`) when no hi-res surface exists | | `video_info` | ✓ | | — | Internal resolution end to end: `preset` (0 unset, 1 native, -1 match display, else target lines), `reference_lines`, `requested_scale` (what the preset asked for), `effective_scale` (what the backend allocated), `internal_lines`, the GL limit (`gl_max_dim`, `gl_max_scale`, `gl_clamp_reason`: 1 texture, 2 budget, 4 ceiling; `gl_alloc_retries`, `gl_budget_mib`), the hr surface (`fbo_w`/`fbo_h`, `hr_scale`), the window in points (`window_w`/`window_h`) and pixels (`drawable_w`/`drawable_h`, `hidpi_window`), the display area (`display_x`/`display_y` VRAM start, `display_w`/`display_h`), and the windowed high-resolution surface when a scale is past the full-VRAM limit (`windowed`; `hires_window_x`/`hires_window_w` in native columns and `hires_fbo_w`/`hires_fbo_h` for the tile holding the last presented display; `hires_window_tiles`, more than one when side-by-side buffers are too wide for one surface; `hires_window_mib` for all tiles; `hires_window_grows`, tile allocations so far) | | `screenshot_wide_hires` | ✓ | | `path`, `base_x` | The displayed band of the native-wide surface at internal resolution (`wide_w×S` by `height×S`). `present_shot` is capped at the window; this checks a widescreen + internal-resolution combination at full size | | `present_shot` | ✓ | | `path` | PNG of the **composed present surface** — the frame after the backend fits the display buffer to the window, so it carries the presented aspect. ⚠ every other capture resolves the display buffer *before* that fit: on a 508×256 display in a 4:3 window they answer 508×256 while the player sees 640×480. Use this one for anything aspect-shaped (widescreen, letterbox), where a pre-fit buffer would hide the very stage the change touches. Staged and fulfilled on the next present, so the ack means *queued* — poll `present_shot_seq`. Unavailable headless and on the Vulkan backend (its swapchain has no readback hook) | | `present_shot_seq` | ✓ | | — | Completion counter for `present_shot`, plus `wrote` (1 = that completion produced a PNG). Sample before staging, poll until `seq` moves. Advances on success *and* failure, so the poll always terminates | | `fmv_chroma` | ✓ | | `on` (optional: 0/1) | Read or set `[video] fmv_chroma_smoothing` live. Replies `on` and `frames` (24-bit presents smoothed so far) | | `texture_lod` | ✓ | | `mode` (optional: 0 off, 1 mipmap), `aniso` (optional: 1..16) | Read or set `[video] texture_lod` / `anisotropic_filtering` live. Replies `mode`, `aniso` | | `bloom` | ✓ | | `strength` (0..2), `threshold`, `time` (0/1), all optional | Read or set bloom live (`[video] bloom`). Replies the settings, `passes` and, while `time` is on, `us_mean`: the mean `glFinish`-fenced pass time over `timed` passes since the last query | | `dithering` | ✓ | | `mode` (optional: 0 off, 1 on (default, native grid), 2 fine (internal pixels)) | Read or set `[video] dithering` live. Replies `mode` | | `post_aa` | ✓ | | `mode` (optional: 0 off, 1 fxaa, 2 fxaa_hq) | Read or set post-process anti-aliasing live (`[video] antialiasing_mode`). Replies `mode`, `passes` (filtered presents so far) and `gpu_us`: the mean pass time since the last query when the run has `PSX_POST_AA_TIME=N` (N passes per present between `glFinish` fences, a measurement mode), else 0 | | `hd_textures` | ✓ | | optional `replacements`, `dump` (0 or 1), `reload` (1) | HD pack root, switches, backend support, replacement count, matched/ready/applied draw counts, queued/pending dumps, composition hits/builds/pixels, GL uploads/evictions, decoded images/evictions/bytes. Optional controls update the configured host pack on the emulation thread; no configured root is an error. Reload retains unchanged upload identities. OpenGL displays replacements; software/Vulkan retain original artwork. Pair with `present_shot` for visual evidence and native VRAM probes for architectural data | | `gl_interp` | ✓ | | — | OpenGL frame-rate presenter ([FRAME_RATE.md](FRAME_RATE.md)): enabled/suspended, host and target Hz, swaps, `source` (`vblank`/`flip`), `flip_period`, `captures` (new source frames) and `duplicates` (VBlanks that re-presented the same frame) | | `render_pass_stats` | ✓ | | — | Render passes ([RENDER_PASSES.md](RENDER_PASSES.md)): plans, phases wanted/planned (shedding), passes, rollbacks (`nesting_repairs`: watchdog aborts whose skipped frame exits the restore undid), dropped device stores by class, `verify_mismatch` under `PSX_RENDER_PASS_VERIFY=1`, host-time split per pass, smoothed pass cost (`cost_us`; `cost_rewarms`: estimates no pass had run on for a while, measured again), presents made from pass images (`late_presents`: held past the frame's planned end because the next flip was late; `expired`: frames whose images stopped showing after several frame lengths without a flip), pass image textures allocated (`image_textures`, `image_bytes`), `status` (`psx_mod_render_pass_status`: 0 ready, 1 no presenter, 2 backend, 3 disabled, 4 session, 5 fast-forward, 6 busy), `backups_reused` (passes that reused the previous pass's VRAM backup), `spans` / `span_failures` and the last failure `span_fail` (`psx_mod_run_guest_span`: reason, exit PC, `$ra`, PC after a call that did not return) | | `window_size` | ✓ | | `w`, `h` | Resize the game window as a player would (Fit / adaptive widescreen follow it): window-shaped checks in `--hidden-window` runs. Pair with `present_shot` | | `render_pass_dump` | ✓ | | `path`, `count` | Write the images (the game's own frame, then each pass in phase order) of the next `count` frames that get passes as `/g__a.png` | | `render_pass_refuse` | ✓ | | `on` | Make the OpenGL backend decline render passes (`status` 2, BACKEND), as a renderer mode without them would; tests a plugin's fallback. `PSX_RENDER_PASS_REFUSE=1` does the same from start | | `geom_correction` | | ✓ | — | `[video] geometry_correction` / `perspective_texturing` engagement: enable flag plus free-running `geometry_vertex_hits` and `perspective_triangles` totals. Both enhancements silently fall back to the faithful path on anything they cannot prove is projected geometry, so a zero counter with the flag on means the title never qualifies — sample twice and diff for a rate. Also `texture_correction`. Its `pgxp` object adds the engine's per-lookup counters, the `position_fallback` / `preserve_projection` / `culling` switches, `ppp_produced` and `ppp_window_fallback` (exact projections outside the agreement window, shadowed with the IR path instead), geometry-corrected triangles split into `tri_precise` / `tri_mixed` / `tri_native` (the mixed share is the mesh-cracking exposure), `rect_bypass` / `rect_partial` (fully / partly precise textured quads that matched the 2D rectangle shortcut; only the former leave it) and `nclip_precise` / `nclip_disagree` / `nclip_corrected` (NCLIPs with an exact determinant, whose exact sign differs from the integer one, and that precise culling corrected); `ENHANCEMENTS.md` G1.11/G1.12 | | `pgxp` | ✓ | | `geometry`, `texture`, `cpu_mode`, `tolerance`, `position_fallback`, `preserve_projection`, `culling` (all optional) | Live-tune PGXP for same-scene A/B: arm or disarm geometry / texture correction and set the engine knobs, without a restart. `culling` changes guest NCLIP results, so a toggle mid-run makes the run diverge from one without it. Replies with the resulting state (`enabled`, `cpu_mode`, `tolerance`, `position_fallback`, `preserve_projection`, `culling`, `suppress`, `active`). `ENHANCEMENTS.md` G1.10-G1.12 Also reports the renderer side: `depth_buffer`, cumulative `depth_tris`, `depth_clears`, `seam_tris`, and `depth_clear_reasons` (config, area, fill, threshold, present) | | `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) | | `irq_exc_stats` | ✓ | | — | Cumulative per-source exception accounting since boot: deliveries and guest cycles from entry to exit, charged to every `I_STAT & I_MASK` bit pending at entry (`sw` = software interrupt only). Diff two snapshots for a window. `irqctx_ring` entries carry the matching `exit_cycle` | | `dma_hold_stats` | ✓ | | — | Cumulative CPU holds for kicked GPU linked-list walks (the kicking store returns only when the walk ends): count, guest `held_cycles`, host `host_us`, words, advance steps, and the `exc_*` share for kicks made inside an exception handler (that host time is what `phase_profile` reports as `exc_share`) | | `dma_hold_ring` | ✓ | | `count`, `frame_lo`, `frame_hi` | Newest held kicks (ring of 4096): cycle, frame, held cycles, words, steps, host µs, kick PC, MADR, `in_exc`, `still_active` | | `event_state` | | ✓ | — | EvCB table summary (stub on DS — events are BIOS-level) | | `overlay_state` | | ✓ | — | Current overlay info | | `cdrom_sector_dump` | ✓ | | `offset`, `len` | Dump bytes from the last CD-ROM sector observed by the controller, including LBA/mode metadata | | `cdrom_sector_history` | ✓ | | `count`, optional `lba` | Dump newest CD-ROM sector history entries, including raw XA subheader fields, CPU/audio delivery flags, and the first 128 bytes | | `cdrom_sector_history_clear` | ✓ | | — | Reset the CD-ROM sector history ring | | `watch` | ✓ | ✓ | `addr` | Set byte-level memory watchpoint (fires per-frame on change) | | `unwatch` | ✓ | ✓ | `addr` | Remove memory watchpoint | | `set_input` | ✓ | ✓ | `buttons`, optional `frames`, optional `lx`, `ly`, `rx`, `ry`, `pad_type`, optional `port` | Override pad1 buttons and optional analog axes (PS1 inverted bitmask, 0 = pressed; axes 0-255). Debug builds also accept `pad_type` (0 digital, 1 DualShock, 2 JogCon) to test an emulated device identity. Holds until `clear_input` on both backends; pass `frames=N` (beetle) to auto-release after N frames. Runtime: `layer="host"` instead arms a virtual P1 gamepad (`buttons`, `lx`..`ry`, `lt`/`rt` 0-255) that feeds the normal offline input path (controller source, title pad transform, trigger values) and host shortcut polling, also headless; a plain override still wins while armed; `clear_input` disarms it. `port=2` (psx-runtime) drives pad 2 instead (digital buttons; plugs a digital pad into port 2 while driven) for headless two-player modes; `press` takes `port` too | | `clear_input` | ✓ | ✓ | optional `port` | Remove input and analog axis overrides (`port` 1 or 2 clears one port; default both) | | `rewind_status` | ✓ | | — | Local Rewind: `enabled`, `open`, `title_blocked` (`psx_mod_set_rewind_blocked`) and `snaps` held in the ring | | `snapshot_status` | ✓ | | — | Shared save/rewind capture guards: `callback_active`, `host_call_depth`, resume `site`/`pc`, and `safe`. A busy boundary can legitimately report unsafe; use `savestate_status` to check whether a requested operation completed | | `turbo` | ✓ | | `enabled` | Enable/disable TCP-controlled frontend turbo for fast-forward validation | | `turbo_state` | ✓ | | — | Query TCP-controlled turbo state | | `pause` | ✓ | | — | **REMOVED** — still registered, but always returns an error. Query a ring buffer (`fn_entry_tail`, `wtrace_dump`, `gpu_frame_dump`) instead of synthesizing a snapshot | | `continue` (`c`) | ✓ | | — | **REMOVED** — nothing to resume, since `pause` is gone | | `step` | ✓ | | — | **REMOVED** — query a ring buffer over the window of interest instead of advancing N frames synchronously | | `run_to_frame` | ✓ | | — | **REMOVED** — use `frame_range` / `read_frame_ram` against the live frame ring instead | | `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` (optional) | Write a **PNG** of the current display to `path` (default `psx_screenshot.png` in the runtime cwd); single metadata response `{path,width,height}`. `screenshot_file` is an alias; the old inline-hex-row `screenshot` is gone (it streamed h+1 response lines per request and poisoned the connection) | | `scanline` | ✓ | | optional `on` (0/1), optional `pct` (0..100) | Live A/B toggle for the present-time scanline post-process. Omit `on` to keep the current toggle, omit `pct` to keep the current strength; reports `{scanlines, strength_pct}`. Same state the F6 hotkey and `[video] scanlines` drive | | `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 262 144 writes with RA — `WRITE_TRACE_CAP`, `1 << 18`) | | `wtrace_dump` | ✓ | beetle | optional `addr_lo`, `addr_hi`, `count`, `newest` | Dump RAM-write trace entries as JSON. The address filter is applied server-side over the FULL ring before the emit cap — always pass it when hunting a specific buffer, otherwise you only see the oldest `count` entries of the whole ring | | `wtrace_clear` | ✓ | | — | Reset the trace ring | | `mmio_dump` | ✓ | | optional `addr`, `count`, `newest` | Dump the always-on MMIO write ring (256K entries, ALL 0x1F801xxx writes — SPU/DMA traffic rolls it in well under a minute of gameplay; for display history use `gp1_dump`) | | `mmio_clear` | ✓ | | — | Reset the MMIO write ring | | `gp1_dump` | ✓ | | optional `frame_lo`, `frame_hi`, `count`, `newest` | Dump the dedicated ALWAYS-ON GP1 (0x1F801814 display control) ring — 512K entries ≈ 15 min of gameplay (Tomba writes ~10 GP1/frame), survives the general MMIO ring's eviction. Frame filter is server-side over the full ring. Each entry: val + func/pc/cpu_pc/ra/sp/a0/a1/sr/epc/frame | | `gcp_ring` | ✓ | | `frame_lo`, `frame_hi`, then `path` or `key` + `top` | Always-on guest-cycle profile ring (512K samples, one every `period` = 4096 guest cycles by default, `PSX_GCP_PERIOD`; about 63 guest seconds). Each sample: cycle, frame, `weight` (sample periods crossed, so idle-skipped waits stay credited to the waiting code), backend `phase`, exception flag, last block leader (`block`, debug-tools builds), last function entered (`fn`), guest `$ra`, native shard in progress, static-dispatch stamp. No args: ring stats. `path`: write the window as CSV. `key` = `fn`/`block`/`ra`/`native`/`disp`: top `top` keys by guest-cycle share in the window | | `input_route_start` | ✓ | | optional `at_frame` | Start the route queued with `input_route_append` (`buttons`, `frames` per step, counted per pad poll). With `at_frame` the route waits for that vblank count, so it replays on the same guest frames in every run (frame-exact A/B with input) | | `batch_sites` | ✓ | | — | Always-on OpenGL batch-flush attribution: cumulative flushes of non-empty batches by caller source line in `gpu_gl_renderer.c`, as `tex` and `flat` arrays of `[line, count]`. Diff two reads to attribute a window (every flush call site is covered by the `flush_*_batch()` macros) | | `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). ### Boot-time write ranges Set `PSX_WTRACE_BOOT_RANGES=lo-hi[,lo-hi...]` before launching a debug-tools build to retain the first writes to up to 8 half-open RAM ranges from guest instruction zero, in both the boot and the transition (value-change) rings. Addresses are hexadecimal (`0x` optional); KSEG addresses are normalized to physical addresses. The ranges get their own slots after the built-in defaults, so a default set never crowds them out. For example, the Crash Bash investigation that motivated this option can be reproduced without title-specific code: ```powershell $env:PSX_WTRACE_BOOT_RANGES='0x000B3A80-0x000B3B00' .\CrashBashRecomp.exe ``` Connect at any later point and query `wtrace_boot_stats`, `wtrace_boot_summary`, or `wtrace_boot_dump`. Each retained entry includes the write address/value/width, guest PC and return address, register context, frame, and DMA channel. A malformed spec, a range whose `hi` does not exceed `lo`, or more than 8 ranges refuses the whole spec: nothing is armed, a `PSX_WTRACE_BOOT_RANGES refused` line is printed at launch, and `wtrace_boot_stats` / `wtrace_trans_stats` report `env_ranges` (ranges applied) and `env_error` (the reason, empty when accepted). The option is ignored in builds made with debug tools disabled. --- ## 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; see [`internal/DEBUG.md`](internal/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 ` 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). --- ## Server send budget + serve-stall telemetry The server is pumped on the **main thread**: every millisecond it spends sending a response is a millisecond the emulator does not run. Inline responses are bounded — 2 s per zero-progress chunk and **15 s total per response**; a client that exceeds the budget is disconnected (the runtime never stalls indefinitely). Responses bigger than the budget allows must use the `*_dump_file` variants, which write to disk instead of the socket. Cumulative serve-stall is exported as `tcp_send_stall_ms` / `tcp_clients_dropped` in `psx_freeze_heartbeat.json` (plus per-tick `tcp_ms` in its ring) and in every wedge/fatal dump header. **Check these first when diagnosing slow or stalled frames** — on 2026-06-10 two "attract-idle degradations" turned out to be a TCP client trickle-draining mega-dumps, throttling the main loop to 6 fps (all 8 watchdog stack samples inside `WS2_32!send`). A slow-frames wedge with a large `tcp_send_stall_ms` delta over the same window is observer interference, not a guest bug. ## Call-contract (bail) telemetry The dispatch call contract (Bug D family fix, 2026-06-10) guards every generated continuation: it may only run if the guest actually returned to the call site ($ra == site return address, $sp == caller's sp at the call). Violations begin a "bail" unwind that abandons stale C frames and re-dispatches the guest's true target. Counters in `psx_freeze_heartbeat.json` and dump headers: - `bail_first` — contract violations detected (wild returns). Nonzero during gameplay means the game executed a wild control transfer (e.g. Tomba's dead jumptable case `jal 0x80120B3C`, the chest freeze). A small count with the game continuing normally is the fix working. - `bail_resolved` — unwinds that resolved at an enclosing call site whose contract matched (multi-level return). - `bail_flattened` — unwinds that reached the outermost dispatch loop and re-dispatched the wild target on a clean host stack. - `bail_anomaly` — bail flag observed at exception entry (must stay 0; anything else is a runtime bug). --- ## `render_thread` — OpenGL render thread state (native only) `{"cmd":"render_thread"}` reports the `[video] render_thread` pipeline (docs/RENDER_THREAD.md): `active`, `held` (the emulation thread holds the GL context for the rest of this frame), `max_frames`, `records`, `bytes`, `frames_produced` / `frames_consumed`, `presents` recorded and `presents_stale` skipped, `acquires` (sync points) with `recent_acquires` (`frame`, `wait_us`, `reason` — the GL entry point that needed the context), `backpressure_waits` / `backpressure_ms` (the emulation thread waited on the in-flight bound), `ring_full_waits`, `acquire_ms`, `render_busy_ms` / `render_idle_ms` and `ring_high_water`. With the thread off every counter is 0 and `active` is 0. Reading it is not a sync point; a steady game frame should add no `acquires`. ## `frame_fingerprint` — per-frame guest-write fingerprint (native only) Cumulative write hashes, snapshotted at every VBlank into a 32768-frame ring (`runtime/include/frame_fingerprint.h`). Diff two runs of the same seeded input, such as native overlay shards against the interpreter or two builds, to find the first frame where guest behaviour forks. Then arm `record_frame` on that frame in both runs and compare the two ordered logs. - `{"cmd":"frame_fingerprint","count":1024,"frame_lo":N,"frame_hi":M}`: all parameters are optional. Entries come back oldest first. | Column | Role | Covers | |---|---|---| | `cyc` | judge | guest cycle counter at the snapshot | | `wc`, `ws` | judge | main-RAM write count, and an order-independent sum over `(addr, value)` | | `mmio`, `mc` | judge | device-register writes: ordered hash over `(addr, value, store PC)`, and a count | | `sp`, `sc` | judge | scratchpad writes: ordered hash over `(addr, value, store PC)`, and a count | | `qc` | judge | writes that FMV-quiet kept out of every other column | | `wr`, `pc` | locator | main-RAM writes: ordered hash over `(addr, value)`, and an ordered hash over store PCs | **Judge on the judge columns.** Two runs that behaved the same agree on all of them at every frame. `wr` and `pc` can differ even when guest state is identical. DMA and device writes to RAM (MDEC-out, CD, SPU, GPU→RAM) are recorded in the order the host services the device. A native shard flushes cycles at every store barrier, but batched interpreted and static code services the device after the block. As a result, the same writes can interleave differently with CPU stores. A device write also takes whatever CPU store PC came last. Use `wr` and `pc` only to narrow down a fork once a judge column has found it. **Store PCs are exact on every backend.** Static code and the interpreter set `g_debug_last_store_pc` themselves. Native overlay shards write the runtime's copy through the ABI v24 `last_store_pc` pointer. Before v24, shards kept a private copy, so `pc`, `mmio` and `sp` named an older store for every overlay store. **One-frame straddles are not divergences.** Batched code services devices up to a basic block late. So a device write due at, for example, VBlank + 1 cycle can land on the other side of the snapshot in one run. `ws` and `wc` then differ for that single frame, with `wc` off by the number of straddled writes, and agree again at the next one. In a 12000-frame R4 A/B of native shards against the interpreter, 497 frames straddled by exactly one write, and every one re-converged on the next frame. **Turn FMV-quiet off for identity runs.** `PSX_DEBUG_FMV_QUIET` is on unless it is set to `0`. While the MDEC has decoded recently, it stops write recording, and those frames' writes add only to `qc`. If a straddled write falls into a quiet frame in one run and not the other, `ws`, `wc` and `qc` never agree again. `wc + qc` still agrees, which tells this case apart from a real fork. For A/B identity, set `PSX_DEBUG_FMV_QUIET=0` in both runs. `ws` is a multiset sum. It cannot see two writes to one address arriving in the opposite order, even though the final RAM differs. Such a fork shows up in later writes, `cyc`, or `read_ram`. ### A/B identity with `tools/fp_identity.py` `tools/fp_identity.py` applies these rules to two seeded runs of any title. `run` starts the runtime from a command template or from `--runtime`, `--game` and `--disc`. It turbos with no input to frame N and saves this ring and the dispatch-miss count. It sets `PSX_DEBUG_FMV_QUIET=0` and `PSX_OVERLAY_AUTOCOMPILE_OFF=1` unless told otherwise. `--seed` installs a saved overlay state first, so a run can start from a warm or a cold cache. `compare` judges the two runs on the judge columns and reports `wr` and `pc` only as the frame where the runs part. It tolerates the one-write straddle above, and the `wc + qc` shift when FMV-quiet was on. Every tolerated frame is counted and listed, and `--strict` disables both tolerances. It exits `0` IDENTICAL, `1` MISMATCH, `3` INCOMPLETE (frame window, wrapped ring, missing columns from an older runtime, an unconfirmable last-frame straddle, or different FMV-quiet settings once a quiet frame occurred, which leaves the runs not comparable), `4` MISSES (dispatch misses), or `2` on a usage error. `run` exits `3` when it writes no dump. On Windows, a `--launch` backslash is a path separator, not an escape. ```sh L='tools/run_game.sh {build} --debug-port {port} {headless}' python3 psxrecomp/tools/fp_identity.py snapshot build /tmp/seed-warm mkdir -p /tmp/seed-cold python3 psxrecomp/tools/fp_identity.py run warm.json --launch "$L" --port 4781 --frames 12000 --seed /tmp/seed-warm python3 psxrecomp/tools/fp_identity.py run cold.json --launch "$L" --port 4781 --frames 12000 --seed /tmp/seed-cold python3 psxrecomp/tools/fp_identity.py compare warm.json cold.json ``` Run an A/A pair first to show that the run is deterministic. The tool's docstring (`--help`) lists every option. The self-test is `tools/tests/test_fp_identity.py` (ctest `fp_identity`). ## `bios_info` — linked recompiled-BIOS identity (native only) Reports which BIOS image this build's recompiled C was generated from (`psx_bios_image`, emitted into the generated dispatch from the BIOS profile) and whether the loaded ROM matches it: `image_id`, `sha256`, `crc32`, `size`, `bundled` (redistributable image shipped with the game), the kernel-bless window, the HLE anchors (`shell_entry_phys` / `deliver_event_ret`; 0 = structurally unavailable on this BIOS), and `image_wordsum` vs `loaded_wordsum` with `match`. With the launch identity gate a running process always reports `match:1`. - `{"cmd":"bios_info"}` ## `s3_smear_watch` — callee-saved-register smear tripwire (native only) Latches the first interpreted instruction in a PC window whose execution changes `$s3` (`runtime/src/dirty_ram_interp.c`). A `jalr`'s exec_one spans the entire nested native callee, so the latch names the callee that returned with a clobbered callee-saved register; the insn ring is frozen at the latch. - `{"cmd":"s3_smear_watch","lo":"","hi":""}` — arm (each arming fully re-specifies the watch). Optional `"excl":""`: exact encoding to ignore, so a watched loop's own `$s3` advance (e.g. an `addi s3,s3,8` list walk) doesn't trip the latch. - `{"cmd":"s3_smear_watch"}` — report the latch: `valid`, `pc`, `insn`, `s3_old`/`s3_new`, `call_target` (rs at the call site for jr/jalr), `frame`. - `{"cmd":"s3_smear_watch","lo":"0"}` — disarm. ## `callret_watch` — interp JALR call-resolution ring (native only) 64-entry ring (`runtime/src/dirty_ram_interp.c`) recording, for every interpreted JALR whose call PC lies in a window, which resolution tier ran the callee and the full post-call outcome — the complement of `s3_smear_watch`: the tripwire names the callee that came back smeared, this ring names the return path that let it come back. - `{"cmd":"callret_watch","lo":"","hi":""}` — arm (resets the ring). - `{"cmd":"callret_watch"}` — dump (newest last): per entry `cyc`, `f` (frame), `pc`, `tgt`, `path` (`CRES_*` tier code, see the enum in dirty_ram_interp.c; `|0x100` = finish() escaped), pre-call `sp_b`/`ra_b`/`s0_b`/`s3_b`, post-call `pc_a`/`ra_a`/`sp_a`/`s0_a`/ `s3_a`/`v0_a`, `bail`/`rfe`/`esc`/`in_exc` flags, `dstatic`/`dblocks`/ `dexc` engine-attribution deltas across the call, `last_func`. - `{"cmd":"callret_watch","lo":"0"}` — disarm. ## `hle_dump` — BIOS-HLE tier call ring (native only) Always-on ring (`runtime/src/bios_hle.c`, 16K entries) recording every A0/B0/C0 kernel-vector dispatch the HLE tier's hook observes, plus the boot shell-skip event. The hook is installed when EITHER axis is on (`bios_hle_plan.h`), so a run with the boot-skip on but kernel calls left to LLE — the default on the bundled OpenBIOS, which exports no `deliver_event_ret` — reports `backend: "LLE (recompiled BIOS)"` and still fills the ring with `route: 0` vector observations plus the one `route: 2` boot entry. Only a run with BOTH axes off installs no hook and leaves the ring empty; use `bioscall_dump` for LLE-side vector observation there. - `{"cmd":"hle_dump"}` — status: `backend` (`HLE (LLE fallback)` / `LLE (recompiled BIOS)`), `boot_skip`, `boot_turbo_active`, `total`. - `{"cmd":"hle_dump","tail":N}` — last N entries: `seq`, `cycle` (guest cycle), `vec` (0xA0/0xB0/0xC0, or 0x30000 for the boot skip), `fn` ($t1 function number), `a0..a3`, `ra`, `v0` (result when HLE-serviced), `route` (0 = fell through to LLE, 1 = serviced in HLE, 2 = boot shell-skip). - Filters: `"fn":N`, `"route":0|1|2`. --- ## `overlay_static_entries` — which static overlay images ran (native only) `overlay_loader_status` reports one `static_hits` total for the build-time static overlay dispatcher (`GAME_OVERLAY_STATIC_C`); this command splits it per compiled entry address, so a run can prove WHICH image's variants executed (a mod engine linked at `0x80780000` versus a menu overlay at `0x800BF800`). Counters are always on and count real dispatches only, never `psx_overlay_static_can_dispatch` probes. `{"cmd":"overlay_static_entries","addr_lo":"0x80780000","addr_hi":"0x80800000","limit":16}` - `entries`, `total_hits` — whole dispatcher. - `range_lo`/`range_hi` (physical; segment bits ignored), `range_entries`, `range_variants`, `range_hits`, `range_hit_entries` — the requested range. - `top` — up to `limit` (default 64, max 1024) entries of the range with hits, busiest first: `{addr, variants, hits}`. A build without a static dispatcher answers `ok:false`. --- ## `dirty_ram_stats` `per_pc` — interpreted-PC table Snapshot of the open-addressed per-entry-PC table (`g_dirty_ram_pc_table`, `dirty_ram_interp.c`): one object per interpreted entry PC. Read-only; safe to poll while the game runs. - `pc` — physical entry PC. - `hits` — dispatches here (interp block chaining conflated in). - `insns` — instructions executed across those hits. - `entries` — dispatches that arrived from NATIVE code (a call or a fresh dispatch), not interp block-to-block chaining. `entries > 0` marks a real interior entry point — the seed stream for overlay-capture aliasing. - `occ_crc` / `occ_ok` — enrichment (2026-09-05): at the last external entry, the manifest CRC of the static overlay variant whose code ranges span this PC (`psx_overlay_resident_crc_at`; DLL-path titles get the last residency hash instead), and whether it validated then. The CRC is the one the variant was compiled from, so it maps straight back to a capture / `.EMI` section offline. Read the pair as: `occ_ok = 1` — an interior gap inside live native code, an alias seed will make it native; `occ_ok = 0` with a CRC — the band's compiled occupant is resident but CRC-missing (data inside the code range is rewritten at run time, or the section differs from the compiled one), so the whole band runs interpreted and no seed helps; `0x00000000` — nothing compiled spans this PC (BIOS / kernel / boot EXE). - `ext_ra` — enrichment: `$ra` at the most recent external entry, i.e. the caller that reached this interior. A PC reached only through a function-pointer table shows the dispatcher's `ra` here, so the table's call site is recoverable from the snapshot alone (the LOGO effect-handler case) without a live `overlay_fp_log` window. `ext_ra == pc` means the entry was a `jr ra` *return* from native code into an interpreted continuation, not a call. The array is emitted inline and stops before the trailing bitmap diagnostics when the response buffer fills; a partial list is still valid JSON (the cut lands between rows). `per_pc_matching` counts every matching row and `per_pc_emitted` the rows in this response, so a page is complete exactly when `per_pc_skipped + per_pc_emitted == per_pc_matching`. Narrow the table with `{"lo":"0x800BF800","hi":"0x800E0C24"}` (half-open, compared with the KSEG bits masked off) and page with `{"skip":N}` to read all of it. --- ## 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 `runtime/src/beetle_debug_server.c` (Beetle oracle) when the question needs a cross-check against hardware behaviour 3. Keep field names parallel between the two 4. Run `python tools/gen_tcp_commands.py` to refresh the generated index, and add a row to the curated inventory above if the command needs explaining > Step 4 used to read "Update this file", and that did not survive contact with > reality: this document described 47 commands while the servers registered 292. > The index is now generated from the command tables, and > `python tools/gen_tcp_commands.py --check` fails when it drifts. Prose in the > curated inventory is still hand-written and still worth adding. 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**. ## `host_launch_timings` — retained host startup and mod-commit costs `{"cmd":"host_launch_timings","count":256}` reads the newest completed host scopes, oldest completion first. The fixed 256-entry ring is always on, including before TCP starts, and reading it never drains or resets it. `total` is the lifetime completion count; `overwritten` counts completions displaced by ring wrap. `count` is clamped to 1..256. Only completed work is represented. Each entry has `seq`, `scope_id`, `parent_id`, `stage`, `start_us`, `duration_us`, `ok`, `package`, `feature`, and `labels_truncated`. Times use a monotonic host clock, independent of guest frames/cycles. A parent includes its children: **nested durations overlap and must not be summed**. Sequence orders completion; scope IDs identify starts. Separate threads have independent parent chains. Package/feature labels retain at most 63 bytes each; no owner-media paths are recorded. `ok=0` indicates an unsuccessful or incomplete scope exit, including exceptions; it is not a replacement for the existing launch error message. `provider_commit` encloses the launcher's offline mod callback; `provider_netplay_commit` identifies its netplay counterpart. `runtime_commit` encloses the later main-thread commit/clear. Within `commit`, stages distinguish `disc_hash` (absent when the existing digest is reused), `prepare_resources` (with nested per-package/feature `media_provider`), `resolve`, `overlay_verify`, `derived_disc`, `save_state`, and `build_disc_index`. `overlay_worker` records the concurrent startup work; `overlay_join` measures only the later wait. `plugin_activation` contains per-package/feature `plugin_callback` measurements, including Resident Loading when enabled. No work is moved off-thread and no verification or cache policy changes. These scopes do not cover every action between Play and TCP availability. ## Complete command index (generated) **387 commands registered** — 374 on the native server (`runtime/src/debug_server.c`), 61 on the Beetle server (`runtime/src/beetle_debug_server.c`). 85 of 387 have prose above; **302 are index-only**. An index-only command still works — it just has no description here yet. Send it `{"cmd":""}` and read the reply, or find its `handle_*` function in the server source. Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this block has drifted from the code. | Command | Native | Beetle | Described above | |---|:--:|:--:|:--:| | `a0_history` | ✓ | | | | `accurate_blending` | ✓ | | | | `audio_events` | ✓ | ✓ | | | `audio_stats` | ✓ | ✓ | | | `audio_wav` | ✓ | ✓ | | | `autocompile_run` | ✓ | | | | `autocompile_status` | ✓ | | | | `batch_sites` | ✓ | | ✓ | | `bios_info` | ✓ | | ✓ | | `bioscall_dump` | ✓ | | | | `bloom` | ✓ | | ✓ | | `c0_history` | ✓ | | | | `call_focus_dump` | ✓ | | | | `call_focus_reset` | ✓ | | | | `call_focus_stats` | ✓ | | | | `callret_watch` | ✓ | | ✓ | | `capture_freeze` | ✓ | | | | `capture_mark` | ✓ | | | | `capture_quads` | ✓ | | | | `card_buffer_dump` | ✓ | | | | `card_data_writes` | ✓ | | | | `card_data_writes_reset` | ✓ | | | | `card_handoff` | ✓ | | | | `card_mgr_clear` | ✓ | | | | `card_mgr_trace` | ✓ | | | | `card_read_summary` | ✓ | | | | `card_read_summary_reset` | ✓ | | | | `card_trace_dump` | ✓ | | | | `card_txn_dump` | ✓ | | | | `cd_overwrite` | ✓ | | | | `cd_read_log` | ✓ | | | | `cd_reinsert` | ✓ | | | | `cdc_volume` | | ✓ | | | `cdrom_bursts` | ✓ | | | | `cdrom_cmd_dump` | | ✓ | | | `cdrom_cmd_reset` | | ✓ | | | `cdrom_command_history` | ✓ | | | | `cdrom_command_history_clear` | ✓ | | | | `cdrom_instant_rate` | ✓ | | | | `cdrom_sector_dump` | ✓ | | ✓ | | `cdrom_sector_history` | ✓ | | ✓ | | `cdrom_sector_history_clear` | ✓ | | ✓ | | `cdrom_state` | ✓ | | | | `cdrom_timing` | ✓ | | | | `cdrom_timing_dump` | ✓ | | | | `cdrom_trace_clear` | ✓ | | | | `cdrom_trace_dump` | ✓ | | | | `ce_profile` | ✓ | | | | `chain_trace` | ✓ | | | | `clear_input` | ✓ | ✓ | ✓ | | `continue` | ✓ | | ✓ | | `cyc_watch` | ✓ | ✓ | | | `cyc_watch_clear` | ✓ | ✓ | | | `cyc_watch_dump` | ✓ | ✓ | | | `cycles_to_next_event` | ✓ | | | | `d44_ring` | ✓ | | | | `data_shards` | ✓ | | | | `debug_key` | ✓ | | | | `devtrace_ctl` | ✓ | ✓ | | | `devtrace_dump` | ✓ | ✓ | | | `dirty_block_dump_file` | ✓ | | | | `dirty_block_log` | ✓ | | | | `dirty_break_clear` | ✓ | | | | `dirty_break_range` | ✓ | | | | `dirty_break_state` | ✓ | | | | `dirty_flow_log` | ✓ | | | | `dirty_insn_dump_file` | ✓ | | | | `dirty_insn_gate` | ✓ | | | | `dirty_insn_log` | ✓ | | | | `dirty_ram_stats` | ✓ | | ✓ | | `dirty_ram_unsupported` | ✓ | | | | `disasm` | ✓ | | | | `disc_select` | ✓ | | | | `disc_tray_log` | ✓ | | | | `disp_ring` | ✓ | | | | `dispatch_check` | ✓ | | | | `dispatch_stats` | ✓ | | | | `dispatch_tail` | ✓ | | | | `display_aspect` | ✓ | | | | `display_ring_aux` | ✓ | | | | `display_ring_get` | ✓ | | | | `display_ring_stats` | ✓ | | | | `dithering` | ✓ | | ✓ | | `dma_cdrom_history` | ✓ | | | | `dma_hold_ring` | ✓ | | ✓ | | `dma_hold_stats` | ✓ | | ✓ | | `dma_state` | ✓ | | ✓ | | `dma_trace_clear` | ✓ | | | | `dma_trace_dump` | ✓ | | | | `dump_buffer` | ✓ | | | | `dump_ram` | ✓ | ✓ | ✓ | | `dynres` | ✓ | | | | `dynres_check` | ✓ | | | | `dynres_force` | ✓ | | | | `evcb_snapshot` | ✓ | | | | `evcb_walk_dump` | ✓ | | | | `evcb_walk_stats` | ✓ | | | | `event_ring_clear` | ✓ | | | | `event_ring_dump` | ✓ | | | | `event_ring_tail` | ✓ | | | | `exc_ring` | | ✓ | | | `first_failure` | ✓ | ✓ | ✓ | | `fmv_chroma` | ✓ | | ✓ | | `fmv_state` | ✓ | | | | `fn_clear` | ✓ | | | | `fn_disable` | ✓ | | | | `fn_entry_dump` | ✓ | | | | `fn_entry_tail` | ✓ | | ✓ | | `fn_exit_dump` | ✓ | | | | `fn_filter` | ✓ | | | | `fn_stats` | ✓ | | | | `fntrace_arm` | ✓ | ✓ | | | `fntrace_arm_clear` | ✓ | | | | `fntrace_armed` | ✓ | | | | `fntrace_arms` | | ✓ | | | `fntrace_clear` | ✓ | | | | `fntrace_disarm` | | ✓ | | | `fntrace_dump` | ✓ | ✓ | | | `fntrace_reset` | | ✓ | | | `fntrace_unfiltered` | | ✓ | | | `frame` | ✓ | | ✓ | | `frame_fingerprint` | ✓ | | ✓ | | `frame_gen` | ✓ | | | | `frame_perf` | ✓ | | | | `frame_range` | ✓ | ✓ | ✓ | | `frame_timeseries` | ✓ | ✓ | ✓ | | `frame_trace` | ✓ | | | | `frame_wall_ring` | ✓ | | | | `freeze_check` | ✓ | | | | `func_override` | ✓ | | | | `game_options` | ✓ | | | | `gcp_ring` | ✓ | | ✓ | | `geom_correction` | ✓ | | ✓ | | `get_frame` | ✓ | ✓ | ✓ | | `get_quads` | ✓ | | | | `get_registers` | ✓ | ✓ | ✓ | | `get_snapshots` | ✓ | ✓ | ✓ | | `gl_coh_ring` | ✓ | | | | `gl_fbo_peek` | ✓ | | | | `gl_interp` | ✓ | | ✓ | | `gl_present_ring` | ✓ | | | | `gl_texwin_batch` | ✓ | | | | `gl_vram_diff` | ✓ | | | | `gl_wide_fast` | ✓ | | | | `gl_ws_ablate` | ✓ | | | | `gp1_dump` | ✓ | | ✓ | | `gpu_frame_dump` | ✓ | | ✓ | | `gpu_opcodes` | ✓ | | | | `gpu_ring_stats` | ✓ | | | | `gpu_state` | ✓ | | ✓ | | `gpu_timeline` | ✓ | | | | `gte_frame_stats` | ✓ | | | | `gte_intpl_dump` | ✓ | | | | `gte_latch_dump` | ✓ | | | | `gte_ring_dump` | ✓ | | | | `gte_state` | ✓ | | | | `guest_tty_dump` | ✓ | | | | `hd_textures` | ✓ | | ✓ | | `history` | ✓ | ✓ | ✓ | | `hle_dump` | ✓ | | ✓ | | `host_button` | ✓ | | | | `host_key` | ✓ | | | | `host_launch_timings` | ✓ | | ✓ | | `host_mouse` | ✓ | | | | `host_osd_shot` | ✓ | | | | `host_pause` | ✓ | | | | `host_profile` | ✓ | | | | `host_span_ring` | ✓ | | | | `idle_skip` | ✓ | | | | `imask_trace` | ✓ | | | | `input_route_append` | ✓ | | ✓ | | `input_route_clear` | ✓ | | | | `input_route_start` | ✓ | | ✓ | | `input_route_status` | ✓ | | | | `input_route_stop` | ✓ | | | | `insn_freeze` | ✓ | | | | `insn_freeze_snapshot` | ✓ | | | | `insn_freeze_status` | ✓ | | | | `insn_freeze_target` | ✓ | | | | `irq_exc_stats` | ✓ | | ✓ | | `irq_state` | ✓ | | ✓ | | `irqctx_ring` | ✓ | | ✓ | | `kernel_bless` | ✓ | | | | `latency` | ✓ | | | | `load_transitions` | ✓ | | | | `lockstep` | ✓ | | | | `lockstep_func` | ✓ | | | | `mc_status` | ✓ | | | | `mdec_state` | ✓ | | | | `mdec_trace` | ✓ | | | | `mdec_trace_clear` | ✓ | | | | `mem_words` | ✓ | | | | `menu_host` | ✓ | | | | `mmio_clear` | ✓ | | ✓ | | `mmio_dump` | ✓ | | ✓ | | `mmx6_freshfix` | ✓ | | | | `mod_cmd` | ✓ | | ✓ | | `mod_counters` | ✓ | | | | `nclip_stats` | ✓ | | | | `netplay_status` | ✓ | | | | `openxr_control` | ✓ | | | | `openxr_hands` | ✓ | | | | `openxr_hands_override` | ✓ | | | | `openxr_input` | ✓ | | | | `openxr_input_override` | ✓ | | | | `openxr_pose_override` | ✓ | | | | `openxr_stats` | ✓ | | | | `openxr_views` | ✓ | | | | `overlay_candidates` | ✓ | | | | `overlay_capture_dump` | ✓ | | | | `overlay_cps_probe` | ✓ | | | | `overlay_diff_off` | ✓ | | | | `overlay_diff_on` | ✓ | | | | `overlay_dump` | ✓ | | | | `overlay_fp_dump` | ✓ | | | | `overlay_irq_ratelimit` | ✓ | | | | `overlay_irq_suppress_off` | ✓ | | | | `overlay_irq_suppress_on` | ✓ | | | | `overlay_loader_status` | ✓ | | | | `overlay_native_block` | ✓ | | | | `overlay_native_event_granularity` | ✓ | | | | `overlay_native_off` | ✓ | | | | `overlay_native_on` | ✓ | | | | `overlay_native_ring` | ✓ | | | | `overlay_rescan` | ✓ | | | | `overlay_shadow_detail` | ✓ | | | | `overlay_shadow_dump` | ✓ | | | | `overlay_static_entries` | ✓ | | ✓ | | `pace_state` | ✓ | | | | `pad_status` | ✓ | ✓ | | | `parity_ctl` | ✓ | ✓ | | | `parity_dump` | ✓ | ✓ | | | `pause` | ✓ | | ✓ | | `pc_probe_arm` | ✓ | | | | `pc_probe_clear` | ✓ | | | | `pc_probe_dump` | ✓ | | | | `perf` | ✓ | | | | `pgxp` | ✓ | | ✓ | | `pgxp_miss_ring` | ✓ | | | | `pgxp_shadow` | ✓ | | | | `pgxp_store_ring` | ✓ | | | | `pgxp_tri_ring` | ✓ | | | | `phase_hot` | ✓ | | | | `phase_profile` | ✓ | | ✓ | | `ping` | ✓ | ✓ | ✓ | | `post_aa` | ✓ | | ✓ | | `present_image_ring_get` | ✓ | | | | `present_image_ring_stats` | ✓ | | | | `present_ring` | ✓ | | | | `present_shot` | ✓ | | ✓ | | `present_shot_seq` | ✓ | | ✓ | | `press` | ✓ | ✓ | ✓ | | `probe_clear` | ✓ | | | | `probe_trace` | ✓ | | | | `quit` | ✓ | | ✓ | | `ra_load_watch` | ✓ | | | | `read_frame_ram` | ✓ | ✓ | ✓ | | `read_ram` | ✓ | ✓ | ✓ | | `record_frame` | ✓ | | | | `record_frame_dump` | ✓ | | | | `record_reads_dump` | ✓ | | | | `render_pass_dump` | ✓ | | ✓ | | `render_pass_refuse` | ✓ | | ✓ | | `render_pass_stats` | ✓ | | ✓ | | `render_thread` | ✓ | | ✓ | | `resident_events` | ✓ | | | | `resident_status` | ✓ | | | | `restore_trace` | ✓ | | | | `restore_trace_clear` | ✓ | | | | `restore_trace_window` | ✓ | | | | `rewind_status` | ✓ | | ✓ | | `rtrace_arm` | ✓ | ✓ | | | `rtrace_clear` | ✓ | | | | `rtrace_disarm` | | ✓ | | | `rtrace_disarm_all` | | ✓ | | | `rtrace_dump` | ✓ | ✓ | | | `rtrace_ranges` | ✓ | ✓ | | | `rtrace_reset` | | ✓ | | | `rtrace_stats` | ✓ | ✓ | | | `run_to_frame` | ✓ | | ✓ | | `runtime_menu` | ✓ | | | | `s3_smear_watch` | ✓ | | ✓ | | `savestate` | ✓ | | | | `savestate_status` | ✓ | | ✓ | | `scanline` | ✓ | | ✓ | | `screenshot` | ✓ | ✓ | ✓ | | `screenshot_file` | ✓ | ✓ | ✓ | | `screenshot_hires` | ✓ | | ✓ | | `screenshot_wide_hires` | ✓ | | ✓ | | `segment_misses` | ✓ | | | | `session_restart` | ✓ | | | | `set_input` | ✓ | ✓ | ✓ | | `set_snapshot` | ✓ | ✓ | ✓ | | `sio_arm_audit` | ✓ | | | | `sio_burst_stats` | ✓ | | | | `sio_ctrl_reg_clear` | ✓ | | | | `sio_ctrl_reg_trace` | ✓ | | | | `sio_ctrl_reg_window` | ✓ | | | | `sio_irq_dump` | ✓ | | | | `sio_irq_window` | ✓ | | | | `sio_pc_trace` | ✓ | | | | `sio_pc_window` | ✓ | | | | `sio_state` | ✓ | | ✓ | | `sio_trace` | ✓ | ✓ | | | `sio_trace_reset` | | ✓ | | | `sio_trace_window` | ✓ | | | | `sio_write_window` | | ✓ | | | `snapshot_status` | ✓ | | ✓ | | `sp_ring` | ✓ | | | | `spu_events` | ✓ | ✓ | | | `spu_events_reset` | ✓ | | | | `spu_ram` | ✓ | | | | `spu_status` | ✓ | | | | `spu_voices` | ✓ | ✓ | | | `sreg_trace_clear` | ✓ | | | | `sreg_trace_dump` | ✓ | | | | `sreg_trace_find` | ✓ | | | | `sreg_trace_stats` | ✓ | | | | `stack_profile` | ✓ | | | | `starv_ring` | ✓ | | | | `step` | ✓ | | ✓ | | `stereo_dump` | ✓ | | | | `stereo_stats` | ✓ | | | | `synth_recurse` | ✓ | | | | `texture_filter` | ✓ | | | | `texture_lod` | ✓ | | ✓ | | `thread_ctx_ring` | ✓ | | | | `thread_trace` | ✓ | | | | `thread_trace_clear` | ✓ | | | | `timers_state` | ✓ | | | | `turbo` | ✓ | | ✓ | | `turbo_audio_sink` | ✓ | | | | `turbo_loads` | ✓ | | | | `turbo_state` | ✓ | | ✓ | | `unknown_dispatch_log` | ✓ | | | | `unwatch` | ✓ | | ✓ | | `vblank_rate` | ✓ | | | | `video_info` | ✓ | | ✓ | | `video_set` | ✓ | | | | `vk_perf` | ✓ | | | | `vram_peek` | ✓ | ✓ | ✓ | | `vsync_query_hle` | ✓ | | | | `warm_cd_route` | ✓ | | | | `watch` | ✓ | | ✓ | | `wide_full` | ✓ | | | | `wide_shot` | ✓ | | | | `window_size` | ✓ | | ✓ | | `write_ram` | ✓ | | ✓ | | `ws_aspect` | ✓ | | | | `ws_aspect_cone_site` | ✓ | | | | `ws_backdrop_margin` | ✓ | | | | `ws_backdrop_ring` | ✓ | | | | `ws_backdrop_stretch` | ✓ | | | | `ws_census` | ✓ | | | | `ws_dbg_stretch` | ✓ | | | | `ws_dome` | ✓ | | | | `ws_dome_probe` | ✓ | | | | `ws_far_threshold` | ✓ | | | | `ws_hud_mode` | ✓ | | | | `ws_margin` | ✓ | | | | `ws_nw` | ✓ | | | | `ws_tag_stats` | ✓ | | | | `ws_ui_groups` | ✓ | | | | `wtrace_add` | ✓ | | | | `wtrace_all_dump` | ✓ | ✓ | | | `wtrace_all_reset` | ✓ | ✓ | | | `wtrace_all_stats` | ✓ | ✓ | | | `wtrace_arm` | ✓ | ✓ | | | `wtrace_boot_dump` | ✓ | | | | `wtrace_boot_reset` | ✓ | | | | `wtrace_boot_stats` | ✓ | | | | `wtrace_boot_summary` | ✓ | | | | `wtrace_clear` | ✓ | | ✓ | | `wtrace_del` | ✓ | | | | `wtrace_disarm` | ✓ | ✓ | | | `wtrace_disarm_all` | ✓ | ✓ | | | `wtrace_dump` | ✓ | ✓ | ✓ | | `wtrace_range` | ✓ | | ✓ | | `wtrace_ranges` | ✓ | ✓ | | | `wtrace_reset` | ✓ | ✓ | | | `wtrace_stats` | ✓ | ✓ | | | `wtrace_trans_dump` | ✓ | | | | `wtrace_trans_reset` | ✓ | | | | `wtrace_trans_stats` | ✓ | | | | `xlate` | ✓ | | | | `xprobe` | ✓ | | | | `xprobe_arm` | ✓ | | | | `xprobe_watch` | ✓ | | | `wtrace_dump pc_lo= pc_hi=` filters the recorded producer PC, masked to the physical address (KSEG0/KSEG1/KUSEG alias, like `addr_lo`/`addr_hi`), with inclusive lower and exclusive upper bounds, before limiting results. Omitted bounds match every entry; DMA entries use their recorded initiator, and a DMA whose initiator is unknown (0) never matches an explicit PC bound. Address/frame/order filters still apply. `disasm addr= count=<1..256>` inspects aligned guest instructions; default count is 16. Resolved targets use the guest PC. Unknown words are shown verbatim. ## Trusted mod tuning commands | Command | Parameters | Behavior | |---|---|---| | `mod_cmd` | `name`, optional `args` | Calls a statically linked trusted debug callback and returns its integer `result` and escaped text `reply`. | The process owns up to 16 names, each 1-63 bytes. Registering the same name replaces its callback. Registration is independent of package activation; this is a debug-control registry, not a way to activate a disabled mod. Callbacks run on the debug-server thread and may only touch state guarded against its other owners. Guest/shared-state changes must be requested through the plugin's existing emulation callback and obey its session/netplay policy.