Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 39 additions & 33 deletions TCP_COMMANDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,11 @@ Columns: **N** = native, **D** = DuckStation oracle.
| `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) |
| `func_override` | ✓ | | — | Inventory of armed function overrides (`func_override.h`): per entry `id`, guest `addr`, `calls`, `guard_misses`, `guarded`, `credit` (the declared cycle policy — a per-handled-call charge, or `"self"` when the body charges its own). `calls` counts **consults**, declines included — so a decline-only probe proves an address crosses a hooked path, and `calls: 0` means the override was never reached (wrong address, or that path never ran). Package-gated overrides appear only after the mod plan arms them; an id may read `plugin:label` when one plugin registers several overrides |
| `gpu_state` | ✓ | ✓ | — | Display area, display depth, draw offset, GPUSTAT, clip rect, xfer state |
| `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. Falls back to the native resolve (and reports `scale: 1`) when no hi-res surface exists |
| `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 |
| `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. Falls back to the native resolve (and reports `scale: 1`) when no hi-res surface exists |
| `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 |
| `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 |
| `sio_state` | ✓ | ✓ | — | SIO registers + (native only) pad/memcard protocol + TX/RX history |
| `irq_state` | ✓ | ✓ | — | `I_STAT`, `I_MASK` (both), plus chain state on native |
Expand Down Expand Up @@ -94,30 +95,30 @@ Columns: **N** = native, **D** = DuckStation oracle.
| `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=lo,hi[;lo,hi...]` before launching a debug-tools build to
retain the first writes to one or more half-open RAM ranges from guest
instruction zero. Addresses may be hexadecimal or decimal; KSEG addresses are
normalized to physical addresses. For example, the Crash Bash investigation
that motivated this option can be reproduced without title-specific code:

```powershell
$env:PSX_WTRACE_BOOT='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. The option is ignored in builds made with debug tools disabled.

---

## Divergence-hunt workflow
¹ 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=lo,hi[;lo,hi...]` before launching a debug-tools build to
retain the first writes to one or more half-open RAM ranges from guest
instruction zero. Addresses may be hexadecimal or decimal; KSEG addresses are
normalized to physical addresses. For example, the Crash Bash investigation
that motivated this option can be reproduced without title-specific code:
```powershell
$env:PSX_WTRACE_BOOT='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. 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's `DEBUG.md`):

Expand Down Expand Up @@ -216,7 +217,7 @@ ring names the return path that let it come back.
`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.
- `{"cmd":"callret_watch","lo":"0"}` — disarm.

## `hle_dump` — BIOS-HLE tier call ring (native only)

Expand Down Expand Up @@ -265,9 +266,9 @@ The TCP server is the canonical instrumentation surface. Rule 3 in `CLAUDE.md` i

## Complete command index (generated)

**306 commands registered** — 293 on the native server (`runtime/src/debug_server.c`), 61 on the Beetle server (`runtime/src/beetle_debug_server.c`).

51 of 306 have prose above; **255 are index-only**. An index-only command still works — it just has no description here yet. Send it `{"cmd":"<name>"}` and read the reply, or find its `handle_*` function in the server source.
**311 commands registered** — 298 on the native server (`runtime/src/debug_server.c`), 61 on the Beetle server (`runtime/src/beetle_debug_server.c`).
52 of 311 have prose above; **259 are index-only**. An index-only command still works — it just has no description here yet. Send it `{"cmd":"<name>"}` 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.

Expand Down Expand Up @@ -312,6 +313,7 @@ Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this bloc
| `cdrom_sector_history_clear` | ✓ | | ✓ |
| `cdrom_state` | ✓ | | |
| `cdrom_timing` | ✓ | | |
| `cdrom_timing_dump` | ✓ | | |
| `cdrom_trace_clear` | ✓ | | |
| `cdrom_trace_dump` | ✓ | | |
| `ce_profile` | ✓ | | |
Expand Down Expand Up @@ -381,6 +383,7 @@ Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this bloc
| `frame_range` | ✓ | ✓ | ✓ |
| `frame_timeseries` | ✓ | ✓ | ✓ |
| `freeze_check` | ✓ | | |
| `func_override` | ✓ | | ✓ |
| `game_options` | ✓ | | |
| `geom_correction` | ✓ | | ✓ |
| `get_frame` | ✓ | ✓ | ✓ |
Expand Down Expand Up @@ -464,8 +467,8 @@ Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this bloc
| `phase_profile` | ✓ | | |
| `ping` | ✓ | ✓ | ✓ |
| `present_ring` | ✓ | | |
| `present_shot` | ✓ | | ✓ |
| `present_shot_seq` | ✓ | | ✓ |
| `present_shot` | ✓ | | ✓ |
| `present_shot_seq` | ✓ | | ✓ |
| `press` | ✓ | ✓ | |
| `probe_clear` | ✓ | | |
| `probe_trace` | ✓ | | |
Expand All @@ -490,6 +493,7 @@ Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this bloc
| `run_to_frame` | ✓ | | ✓ |
| `s3_smear_watch` | ✓ | | ✓ |
| `savestate` | ✓ | | |
| `savestate_status` | ✓ | | |
| `screenshot` | ✓ | ✓ | ✓ |
| `screenshot_file` | ✓ | ✓ | ✓ |
| `screenshot_hires` | ✓ | | ✓ |
Expand Down Expand Up @@ -555,6 +559,7 @@ Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this bloc
| `ws_hud_mode` | ✓ | | |
| `ws_margin` | ✓ | | |
| `ws_nw` | ✓ | | |
| `ws_ui_groups` | ✓ | | |
| `wtrace_add` | ✓ | | |
| `wtrace_all_dump` | ✓ | ✓ | |
| `wtrace_all_reset` | ✓ | ✓ | |
Expand All @@ -579,5 +584,6 @@ Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this bloc
| `xlate` | ✓ | | |
| `xprobe` | ✓ | | |
| `xprobe_arm` | ✓ | | |
| `xprobe_watch` | ✓ | | |

<!-- END AUTOGENERATED COMMAND INDEX -->
17 changes: 17 additions & 0 deletions docs/MOD_PACKAGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,23 @@ frames. Trusted callbacks receive only the narrow C services exposed by
`runtime/include/mod_plugins.h`. Games should continue to use declarative
patches and overlays when those operations are sufficient.

An implementation may also register **function overrides** under a plugin id
(`psx_mod_register_function_override`): hand-written C that replaces or wraps
a guest function at a given address, with an optional prologue-word residency
guard. Registration only queues the override; it is ARMED into the dispatcher
tier when the resolved plan selects that plugin — the same gating as the other
callback kinds, so an override-only plugin id counts as available to the
resolver. Every registration states a required guest-cycle `credit` (a fixed
per-handled-call charge, `0` for a mod with no hardware analog, or
`FO_CREDIT_SELF` when the body — or a wrapped original — accounts for its own
time). The full execution contract (guest ABI, decline semantics,
`func_override_call_original` wrap primitive, `func_override_guest_call`,
determinism and cycle-accounting policy) is documented in
`runtime/include/func_override.h`. Overrides registered directly through
`func_override_add` (game `EXTRAS_SOURCES` constructors, the progressive-
decompilation idiom) bypass package gating and are always active; packages
are the right home for anything a player should be able to toggle.

`psx_mod_set_load_acceleration(multiplier, release_frames)` is the narrow
pre-boot service for a game-owned fast-loading feature. It changes host
wall-clock pacing only: guest VBlanks, CD deadlines, interrupts, callbacks, and
Expand Down
18 changes: 18 additions & 0 deletions recompiler/src/full_function_emitter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1713,6 +1713,12 @@ void FullFunctionEmitter::emit_dispatch(
out += " * NULL (the default) = pure LLE, dispatch identical to a build without\n";
out += " * the tier. */\n";
out += "extern int (*g_psx_bios_hle_hook)(CPUState* cpu, uint32_t phys);\n\n";
out += "/* Game function-override tier (runtime/include/func_override.h):\n";
out += " * hand-written C registered against a guest address. NULL (the\n";
out += " * default) = no overrides, dispatch identical to a build without\n";
out += " * the tier. Returns 1 when the override handled the call; the\n";
out += " * guest resumes at $ra exactly as if the original ran jr $ra. */\n";
out += "extern int (*g_psx_func_override_hook)(CPUState* cpu, uint32_t phys);\n\n";
out += "#ifdef PSX_HAS_GAME_DISPATCH\n";
out += "extern int psx_game_address_in_text(uint32_t addr);\n";
out += "#endif\n\n";
Expand Down Expand Up @@ -2046,6 +2052,18 @@ void FullFunctionEmitter::emit_dispatch(
out += " /* Byte-guarded A0/B0/C0 call-vector tail stubs. */\n";
out += " if (!found && psx_bios_try_native_call_stub(cpu, addr))\n";
out += " found = 1;\n";
out += " /* Function-override tier: AFTER the BIOS tiers (an override\n";
out += " * can never shadow a kernel service vector), BEFORE every\n";
out += " * game code backend, so one address-keyed hook covers the\n";
out += " * static EXE, runtime-loaded overlays and dirty RAM alike.\n";
out += " * Handled (rc 1) => the override completed against guest\n";
out += " * state; resume at $ra via the trampoline's normal\n";
out += " * return/tail contract. rc 0 => fall through untouched. */\n";
out += " if (!found && g_psx_func_override_hook &&\n";
out += " g_psx_func_override_hook(cpu, addr & 0x1FFFFFFFu)) {\n";
out += " cpu->pc = cpu->gpr[31];\n";
out += " found = 1;\n";
out += " }\n";
out += "#ifdef PSX_HAS_GAME_DISPATCH\n";
out += " /* Game EXEs can overlap the BIOS shell copy window at\n";
out += " * physical 0x30000-0x5AFFF. If the target belongs to the\n";
Expand Down
11 changes: 11 additions & 0 deletions runtime/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,16 @@ if(BUILD_TESTING)
target_include_directories(bios_hle_plan_test PRIVATE include)
add_test(NAME bios_hle_plan_test COMMAND bios_hle_plan_test)

# Every func_override failure mode is silent at runtime: a registration
# that is never consulted, a package override that survives clear-mods
# (a netplay divergence), a residency guard that corrupts instead of
# declining. Drives the real g_psx_func_override_hook pointer.
add_executable(func_override_test
tests/test_func_override.c
src/func_override.c)
target_include_directories(func_override_test PRIVATE include)
add_test(NAME func_override_test COMMAND func_override_test)

# A cue, conventional raw image, or Steam .car image is a valid pick; each
# must mount the same disc and identify off the same data track.
add_executable(disc_path_resolve_test
Expand Down Expand Up @@ -276,6 +286,7 @@ if(BUILD_TESTING)
add_executable(mod_runtime_test
tests/test_mod_runtime.cpp
src/mod_runtime.cpp
src/func_override.c
src/mod_packages.cpp
src/disc_path.cpp
src/cue_sheet.cpp
Expand Down
Loading