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
2 changes: 1 addition & 1 deletion TCP_COMMANDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ 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 |
| `func_override` | ✓ | | — | Inventory of armed function overrides (`func_override.h`): per entry `id`, guest `addr`, `calls`, `guard_misses`, `guarded`, `guard_kind`, `guard_count`, `guard_crc`, and `credit`. `guard_kind` is `none`, `words`, or `code_crc32`. `guard_crc` is meaningful for `code_crc32`. `credit` is a fixed per-handled-call charge or `"self"` when the body charges its own. `calls` counts every matched-address consult, including guard misses and declines. A decline-only probe therefore proves that an address crosses a hooked path. `calls: 0` means the override was never reached. Package-gated overrides appear only after the mod plan arms them. An id can use `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) |
Expand Down
41 changes: 27 additions & 14 deletions docs/MOD_PACKAGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -322,20 +322,33 @@ 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.
a guest function at a given address. A short prologue-word guard is available
for fixed code. It is not an overlay identity. Use
`psx_mod_register_function_override_exact` for an overlay or a reused dirty-RAM
address. That API checks the CRC32 of all declared code ranges. It uses the
same page-generation cache as native overlay dispatch.

Registration only queues the override. The commit checks the complete selected
set for address collisions and capacity. A collision rejects the plan before
activation. Activation arms the complete set before it runs any activation
callback. An unexpected arming error removes the complete package set.
It also disables all main-memory writes, disc patches, and callbacks from the
selected plan. A later successful commit clears this failure state. Invalid

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The claim that a later successful commit clears the whole failure state is not accurate for disc patches. On a late arming error mod_runtime.cpp sets disc_enabled=false; a subsequent mod_runtime_commit clears main_applied, disc_guard_failed, and the plan but never restores disc_enabled, and mod_runtime_patch_disc_sector returns early while it is false. disc_enabled is only re-enabled by mod_runtime_enable_disc_patches, which is called once at initial boot (main.cpp:13326), so disc overlays remain disabled after a later successful commit/rematch.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docs/MOD_PACKAGES.md, line 336:

<comment>The claim that a later successful commit clears the whole failure state is not accurate for disc patches. On a late arming error mod_runtime.cpp sets disc_enabled=false; a subsequent mod_runtime_commit clears main_applied, disc_guard_failed, and the plan but never restores disc_enabled, and mod_runtime_patch_disc_sector returns early while it is false. disc_enabled is only re-enabled by mod_runtime_enable_disc_patches, which is called once at initial boot (main.cpp:13326), so disc overlays remain disabled after a later successful commit/rematch.</comment>

<file context>
@@ -333,7 +333,8 @@ set for address collisions and capacity. A collision rejects the plan before
 callback. An unexpected arming error removes the complete package set.
 It also disables all main-memory writes, disc patches, and callbacks from the
-selected plan. Invalid plugin ids fail during registration.
+selected plan. A later successful commit clears this failure state. Invalid
+plugin ids fail during registration.
 
</file context>

plugin ids fail during registration.

Calls and unlinked function tail entries use the same override. These entries
are `j`, or `jr` through a register other than `$ra`. A `jr $ra` is a return.
A handled override normally continues at `$ra`. Dispatch preserves a nonzero
`cpu->pc` that the override, a guest call, or a wrapped original selects. Every
matched-address consult increments `calls`. An identity mismatch also
increments `guard_misses`.

Every registration states a required guest-cycle `credit`. Use a fixed charge
for a measured constant cost. Use `0` for a mod with no hardware analog. Use
`FO_CREDIT_SELF` when the body or a wrapped original accounts for its own time.
The full execution contract is in `runtime/include/func_override.h`. Direct
registrations through `func_override_add` bypass package gating and are always
active. Use packages for player-selectable behavior.

`psx_mod_set_load_acceleration(multiplier, release_frames)` is the narrow
pre-boot service for a game-owned fast-loading feature. It changes host
Expand Down
16 changes: 9 additions & 7 deletions docs/TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,19 +58,21 @@ and catch whole regression classes without running a game. Registered from
runtime tree cannot configure until a BIOS has been generated, and these need
neither.

## Known-failing tests (not registered)
The `overlay_pair_dedup_runtime` test is an executable loader test, not a
source-invariant guard. A plain recompiler build keeps it registered but
disables it when CMake cannot find `gcc`, `cc`, or `clang`. The test builds
redistributable fixture libraries and links a harness against the real loader.

Three tests exist and are **deliberately left out of `ctest`** because they fail
today. They are not registered because a suite with a known-red test is a suite
people stop believing — the exact failure mode that took CI off pull requests in
the first place (see `.github/workflows/cli-release.yml`). Fix or retire them,
then wire them in.
## Known-failing tests (registered as disabled)

Two tests are **deliberately disabled in `ctest`** because they fail today. They
stay registered so `ctest -N` counts them without making the suite red. Fix or
retire each test, then remove its `DISABLED` property.

| Test | Status |
|---|---|
| `runtime/tests/test_interpreter_perf_guards.py` | Asserts `psx_devices_mmio_sync` invalidates the inline cycle limit. It does not: the function delegates to `psx_devices_service_to_now()` (which clears `g_psx_cycle_fast_limit`, `psx_cycles.c:161`) **or** to `psx_devices_recompute_deadline()` (`:153-157`), and that second branch never clears it. Needs a timing owner to decide whether the guard found a real hole or the invariant moved. The guard is also partly stale — it still names `s_next_service_cycle`, since renamed to `psx_next_service_cycle`. |
| `runtime/tests/test_runtime_perf_diag_guards.py` | Asserts a substring that is no longer present in the runtime source. Either the diagnostic was removed or it was renamed; the guard has not been updated either way. |
| `runtime/tests/test_overlay_pair_dedup_runtime.py` | Needs its companion harness (`overlay_pair_dedup_harness.c`) built. Unlike the other Python tests it is not source-only, so it needs a build target before it can be registered. |

## Tests that are not in `ctest` and should not be

Expand Down
24 changes: 19 additions & 5 deletions recompiler/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -628,8 +628,25 @@ if(BUILD_TESTING)
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/..)
endforeach()

# This executable loader test is not a source-invariant guard. It
# builds redistributable fixture DLLs and a real-loader harness with a
# GCC-compatible command line. Keep plain MSVC-only recompiler builds
# green when that separate compiler is unavailable, but keep the test
# registered and visible to the registration audit.
find_program(PSX_OVERLAY_FIXTURE_CC NAMES gcc cc clang)
add_test(
NAME overlay_pair_dedup_runtime
COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/../runtime/tests/test_overlay_pair_dedup_runtime.py
--gcc ${PSX_OVERLAY_FIXTURE_CC}
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/..)
if(NOT PSX_OVERLAY_FIXTURE_CC)
set_tests_properties(overlay_pair_dedup_runtime
PROPERTIES DISABLED TRUE)
endif()

# ---- known-failing guards: DISABLED, not de-registered ---------------
# These three were previously left out of the list above with a comment
# These two were previously left out of the list above with a comment
# explaining that they FAIL and that wiring a red check would recreate
# the problem that took CI off PRs. That reasoning is right; the remedy
# was not. De-registering is precisely how a test stops being counted,
Expand All @@ -646,12 +663,9 @@ if(BUILD_TESTING)
# s_next_service_cycle
# runtime_perf_diag_guards asserts a substring no longer present in
# the runtime source
# overlay_pair_dedup_runtime needs its companion harness built; not a
# source-only guard
foreach(_t IN ITEMS
interpreter_perf_guards
runtime_perf_diag_guards
overlay_pair_dedup_runtime)
runtime_perf_diag_guards)
add_test(
NAME ${_t}
COMMAND ${Python3_EXECUTABLE}
Expand Down
13 changes: 7 additions & 6 deletions recompiler/src/full_function_emitter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1717,8 +1717,10 @@ void FullFunctionEmitter::emit_dispatch(
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 += " * guest resumes at $ra unless the body selected a non-local pc. */\n";
out += "extern int (*g_psx_func_override_hook)(CPUState* cpu, uint32_t phys);\n\n";
out += "extern int func_override_try_dispatch(CPUState* cpu, uint32_t target,\n";
out += " uint32_t default_pc);\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 @@ -2056,12 +2058,11 @@ void FullFunctionEmitter::emit_dispatch(
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 += " * Handled (rc 1) => use the body's non-local cpu->pc when\n";
out += " * set, otherwise resume at $ra. rc 0 => fall through\n";
out += " * without changing the incoming cpu->pc. */\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 += " func_override_try_dispatch(cpu, addr, cpu->gpr[31])) {\n";
out += " found = 1;\n";
out += " }\n";
out += "#ifdef PSX_HAS_GAME_DISPATCH\n";
Expand Down
19 changes: 19 additions & 0 deletions recompiler/tests/full_function_emitter_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,24 @@ void complementary_lwl_lwr_stays_native() {
"complementary LWL/LWR remains native");
}

void function_override_preserves_nonlocal_pc() {
const auto result = run_case(
"override-continuation",
{
0x03E00008u, // jr ra
0x00000000u, // nop
},
{function_at(kBase, kBase + 4u, {kBase})});
expect(result.dispatch.find(
"func_override_try_dispatch(cpu, addr, cpu->gpr[31])") !=
std::string::npos,
"generated dispatch uses the continuation-preserving override helper");
expect(result.dispatch.find(
"cpu->pc = cpu->gpr[31];\n found = 1;\n }\n#ifdef PSX_HAS_GAME_DISPATCH") ==
std::string::npos,
"generated dispatch must not overwrite a non-local override continuation");
}

} // namespace

int main() {
Expand All @@ -221,6 +239,7 @@ int main() {
fragment_split_load_falls_back();
noncomplementary_lwl_falls_back();
complementary_lwl_lwr_stays_native();
function_override_preserves_nonlocal_pc();

if (failures != 0) {
std::fprintf(stderr, "%d full-function emitter test(s) failed\n", failures);
Expand Down
16 changes: 15 additions & 1 deletion runtime/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,8 @@ if(BUILD_TESTING)
# declining. Drives the real g_psx_func_override_hook pointer.
add_executable(func_override_test
tests/test_func_override.c
src/func_override.c)
src/func_override.c
src/crc32.c)
target_include_directories(func_override_test PRIVATE include)
add_test(NAME func_override_test COMMAND func_override_test)

Expand Down Expand Up @@ -296,7 +297,17 @@ if(BUILD_TESTING)
target_include_directories(mod_runtime_test PRIVATE
include
../recompiler/lib/toml11)
target_compile_definitions(mod_runtime_test PRIVATE
PSX_MOD_RUNTIME_TEST=1)
target_link_libraries(mod_runtime_test PRIVATE chdr-static)
# sha256_file carries ISOReader's large CHD decode state on this focused
# test's stack. Match the product runtime's Windows stack contract so the
# test reaches its assertions under both MinGW/Clang and MSVC.
if(MINGW)
target_link_options(mod_runtime_test PRIVATE -Wl,--stack,67108864)
elseif(MSVC)
target_link_options(mod_runtime_test PRIVATE /STACK:67108864,67108864)
endif()
add_test(NAME mod_runtime_test COMMAND mod_runtime_test)

if(WIN32)
Expand Down Expand Up @@ -548,6 +559,9 @@ if(BUILD_TESTING)
add_test(NAME cdrom_lid_integration_test
COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tests/test_cdrom_lid_integration.py)
add_test(NAME func_override_route_test
COMMAND ${Python3_EXECUTABLE}
${CMAKE_CURRENT_SOURCE_DIR}/tests/test_func_override_routes.py)
endif()

# Full card protocol regression: read, write, address echo, per-slot state,
Expand Down
Loading