From 1838b4a983c1166c5e954bd7ff2dff4e4301d331 Mon Sep 17 00:00:00 2001 From: Alexandros Mandravillis Date: Tue, 1 Sep 2026 00:08:01 +0300 Subject: [PATCH 1/9] Harden function override boundaries --- TCP_COMMANDS.md | 2 +- docs/MOD_PACKAGES.md | 39 +++-- recompiler/src/full_function_emitter.cpp | 13 +- .../tests/full_function_emitter_test.cpp | 19 +++ runtime/CMakeLists.txt | 14 +- runtime/include/func_override.h | 52 ++++++- runtime/include/mod_packages.h | 1 + runtime/include/mod_plugins.h | 9 ++ runtime/include/overlay_loader.h | 13 ++ runtime/src/debug_server.c | 23 ++- runtime/src/dirty_ram_interp.c | 23 +++ runtime/src/func_override.c | 123 +++++++++++++-- runtime/src/mod_packages.cpp | 6 +- runtime/src/mod_runtime.cpp | 147 +++++++++++++++--- runtime/src/overlay_loader.c | 10 +- runtime/tests/test_func_override.c | 139 +++++++++++++++++ runtime/tests/test_func_override_routes.py | 39 +++++ runtime/tests/test_mod_runtime.cpp | 43 +++++ 18 files changed, 640 insertions(+), 75 deletions(-) create mode 100644 runtime/tests/test_func_override_routes.py diff --git a/TCP_COMMANDS.md b/TCP_COMMANDS.md index 081ff36ef..da149da8f 100644 --- a/TCP_COMMANDS.md +++ b/TCP_COMMANDS.md @@ -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) | diff --git a/docs/MOD_PACKAGES.md b/docs/MOD_PACKAGES.md index 110e86e44..5d50db81a 100644 --- a/docs/MOD_PACKAGES.md +++ b/docs/MOD_PACKAGES.md @@ -322,20 +322,31 @@ 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. Invalid +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 diff --git a/recompiler/src/full_function_emitter.cpp b/recompiler/src/full_function_emitter.cpp index 601984910..ae7caf1c6 100644 --- a/recompiler/src/full_function_emitter.cpp +++ b/recompiler/src/full_function_emitter.cpp @@ -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"; @@ -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"; diff --git a/recompiler/tests/full_function_emitter_test.cpp b/recompiler/tests/full_function_emitter_test.cpp index 747e977d4..f9245c937 100644 --- a/recompiler/tests/full_function_emitter_test.cpp +++ b/recompiler/tests/full_function_emitter_test.cpp @@ -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() { @@ -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); diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index e12d475f3..73a1f9862 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -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) @@ -297,6 +298,14 @@ if(BUILD_TESTING) include ../recompiler/lib/toml11) 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) @@ -548,6 +557,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, diff --git a/runtime/include/func_override.h b/runtime/include/func_override.h index 3e7362597..aac89a8d8 100644 --- a/runtime/include/func_override.h +++ b/runtime/include/func_override.h @@ -32,11 +32,12 @@ * function reached from interpreted code, while registration and the * func_override inventory both still look healthy. * - * SCOPE: overrides are CALL-site keyed. A guest tail transfer (j / jr) into - * an overridden address from the dirty-RAM block loop is a TRANSFER, not a - * call, and does not consult the hook — see dirty_ram_interp.c's compiled - * handoff sites. Registering on a function only ever entered by tail - * transfer will show calls == 0. + * Guest calls and unlinked function tail transfers (`j`, or `jr` through a + * register other than $ra) consult the same override. A handled override + * normally resumes at $ra. If the body or guest + * code that it invokes leaves a nonzero cpu->pc, dispatch preserves that + * continuation instead. This is required for longjmp-style guest control + * flow and for wrappers whose original function does not return locally. * * THE CONTRACT * @@ -119,6 +120,7 @@ extern "C" { #define FO_MAX_OVERRIDES 128 #define FO_MAX_ID 64 #define FO_MAX_GUARD_WORDS 4 +#define FO_MAX_CODE_RANGES 16 /* `credit` value: the override body states its own timing by calling * psx_advance_cycles() itself (mandatory for wraps — see CYCLE ACCOUNTING @@ -131,6 +133,10 @@ extern "C" { #define FO_ERR_ARGS 2 #define FO_ERR_DUPLICATE 3 +#define FO_GUARD_NONE 0 +#define FO_GUARD_WORDS 1 +#define FO_GUARD_CODE_CRC32 2 + /* An override implementation. Reads arguments from cpu->gpr[4..7], writes * the return value to cpu->gpr[2]. Returning nonzero means "handled"; * returning 0 declines this call and the original code runs instead. */ @@ -156,6 +162,24 @@ int func_override_add_guarded(const char *id, uint32_t addr, FuncOverrideFn fn, const uint32_t *expected_words, int n_words, int32_t credit); +/* Exact code-identity guard for overlays and reused dirty-RAM addresses. + * `lo_len_pairs` contains n_ranges immutable {guest_start, byte_length} + * pairs. The concatenated bytes must match expected_crc (IEEE CRC32) before + * the override can run. The ranges are copied at registration. They must be + * word-aligned, non-overlapping RAM ranges and must cover addr. This uses the + * overlay loader's page-generation cache, so unchanged code does not re-hash + * on every call. A short prefix guard is only a quick sanity check; use this + * API when the resident overlay generation must be identified exactly. */ +int func_override_add_exact(const char *id, uint32_t addr, FuncOverrideFn fn, + const uint32_t *lo_len_pairs, int n_ranges, + uint32_t expected_crc, int32_t credit); + +/* Validate the range shape used by func_override_add_exact. Exposed so the + * package registry can reject bad constructor input before plan selection. */ +int func_override_code_ranges_valid(uint32_t addr, + const uint32_t *lo_len_pairs, + int n_ranges); + /* Call a guest function from inside an override (or any native code on the * dispatch thread): args already placed in cpu->gpr[4..7], returns after the * callee completes; result in cpu->gpr[2]. `site_ra` is the return address @@ -182,6 +206,13 @@ void func_override_guest_call(struct CPUState *cpu, uint32_t target, * guest-level wrap would observe). */ void func_override_call_original(struct CPUState *cpu); +/* Shared dispatcher boundary. It clears stale cpu->pc before the consult, + * restores it when the override declines, and on a handled call uses + * default_pc only when the override did not choose a non-local continuation. + * Generated dispatch and dirty-RAM j/jr tail entry both use this helper. */ +int func_override_try_dispatch(struct CPUState *cpu, uint32_t target, + uint32_t default_pc); + /* Register on behalf of a mod package plan. Identical to the two calls above * (pass n_words 0 for unguarded) except the entry is tagged package-armed, so * func_override_reset_package_armed can drop it when the plan is cleared. @@ -190,6 +221,11 @@ void func_override_call_original(struct CPUState *cpu); int func_override_add_package(const char *id, uint32_t addr, FuncOverrideFn fn, const uint32_t *expected_words, int n_words, int32_t credit); +int func_override_add_package_exact(const char *id, uint32_t addr, + FuncOverrideFn fn, + const uint32_t *lo_len_pairs, + int n_ranges, uint32_t expected_crc, + int32_t credit); /* Install the dispatcher hook. Call once at startup AFTER registering (the * mod runtime calls it again after arming package-gated overrides — safe). @@ -209,6 +245,10 @@ void func_override_install(void); * peers' builds and not player-selectable. */ int func_override_reset_package_armed(void); +/* Package-plan preflight uses this to ignore entries from an older selected + * plan while still detecting conflicts with direct registrations. */ +int func_override_is_package(int index); + /* Introspection, surfaced by the `func_override` TCP command. An override * whose `calls` stays 0 was never reached: wrong address, or that code path * never ran. `guard_misses` counts consults declined by the residency @@ -222,6 +262,8 @@ int func_override_get_ex(int index, char *id_out, size_t id_cap, uint32_t *addr_out, uint64_t *calls_out, uint64_t *guard_misses_out, int *guarded_out, int32_t *credit_out); +int func_override_get_guard_info(int index, int *kind_out, int *count_out, + uint32_t *expected_crc_out); #ifdef __cplusplus } diff --git a/runtime/include/mod_packages.h b/runtime/include/mod_packages.h index 19352f6b9..9a92980e2 100644 --- a/runtime/include/mod_packages.h +++ b/runtime/include/mod_packages.h @@ -379,6 +379,7 @@ bool mod_register_activation_plugin(const std::string& id, void (*callback)(void bool mod_register_vblank_plugin(const std::string& id, void (*callback)(void)); bool mod_plugin_registered(const std::string& id); bool mod_register_function_override_marker(const std::string& id); +bool mod_plugin_id_valid(const std::string& id); void mod_invoke_activation_plugin(const std::string& id); void mod_invoke_vblank_plugin(const std::string& id); void mod_clear_plugins_for_tests(); diff --git a/runtime/include/mod_plugins.h b/runtime/include/mod_plugins.h index d38dd136c..72477dbf5 100644 --- a/runtime/include/mod_plugins.h +++ b/runtime/include/mod_plugins.h @@ -56,6 +56,15 @@ int psx_mod_register_function_override(const char* id, uint32_t address, const uint32_t* expected_words, int n_words, int32_t credit); +/* Exact overlay identity form. `lo_len_pairs` contains n_ranges guest code + * {start, byte_length} pairs. The override runs only while their concatenated + * CRC32 equals expected_crc. Use this for overlay addresses; the short word + * prefix above does not identify a complete overlay generation. */ +int psx_mod_register_function_override_exact( + const char* id, uint32_t address, PSXModFunctionOverrideFn fn, + const uint32_t* lo_len_pairs, int n_ranges, uint32_t expected_crc, + int32_t credit); + /* Narrow guest services available to trusted plugin callbacks. */ int psx_mod_game_started(void); uint8_t psx_mod_read_byte(uint32_t address); diff --git a/runtime/include/overlay_loader.h b/runtime/include/overlay_loader.h index 122f3dc7a..402252822 100644 --- a/runtime/include/overlay_loader.h +++ b/runtime/include/overlay_loader.h @@ -59,6 +59,19 @@ void overlay_loader_clear_lazy_miss(void); * cache so native vs interp is re-decided against restored bytes. */ void overlay_loader_resync_validation_after_restore(void); +/* Exact code-range identity shared by generated static overlays and guarded + * function overrides. The pair array must remain at a stable address so the + * page-generation cache can key it without copying. */ +int psx_overlay_static_code_matches(const uint32_t *lo_len_pairs, + uint32_t count, + uint32_t expected_crc); +void overlay_loader_static_match_stats(uint64_t *rehashes, + uint64_t *crc_misses, + uint64_t *gen_fastpath); +/* Range-array addresses are cache identities. Call this before mutable owners + * reuse an address for different ranges. Generated arrays are immutable. */ +void overlay_loader_static_match_cache_clear(void); + /* Step 2.8: re-scan the cache dir for DLLs compiled after init and clear the * checked-regions memo so the next dispatch reconsiders the cache. Idempotent * (loaded DLLs stay loaded); emu thread only. */ diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index 0b171b99c..e5a8d9251 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -9004,9 +9004,9 @@ static void handle_wide_full(int id, const char *json) } /* Function-override tier introspection (func_override.h): count + per-entry - * {id, addr, calls, guard_misses}. calls == 0 = never reached (wrong address - * or path never ran); guard_misses = consults declined by the residency - * guard. */ + * {id, addr, calls, guard identity, guard_misses}. calls == 0 = never reached + * (wrong address or path never ran); guard_misses = consults declined by the + * residency guard. */ static void handle_func_override(int id, const char *json) { extern int func_override_count(void); @@ -9014,8 +9014,11 @@ static void handle_func_override(int id, const char *json) uint32_t *addr_out, uint64_t *calls_out, uint64_t *guard_misses_out, int *guarded_out, int32_t *credit_out); + extern int func_override_get_guard_info(int index, int *kind_out, + int *count_out, + uint32_t *expected_crc_out); (void)json; - char buf[16 * 1024]; + char buf[32 * 1024]; int n = snprintf(buf, sizeof(buf), "{\"id\":%d,\"ok\":true,\"count\":%d,\"overrides\":[", id, func_override_count()); @@ -9024,11 +9027,17 @@ static void handle_func_override(int id, const char *json) uint32_t addr = 0; uint64_t calls = 0, misses = 0; int guarded = 0; + int guard_kind = 0, guard_count = 0; + uint32_t guard_crc = 0; int32_t credit = 0; char creditstr[16]; if (!func_override_get_ex(i, oid, sizeof(oid), &addr, &calls, &misses, &guarded, &credit)) break; + (void)func_override_get_guard_info(i, &guard_kind, &guard_count, + &guard_crc); + const char *guard_name = guard_kind == 2 ? "code_crc32" + : guard_kind == 1 ? "words" : "none"; /* credit: the declared cycle policy — a number (fixed per-call * charge) or the string "self" (the body charges its own). */ if (credit < 0) @@ -9043,10 +9052,12 @@ static void handle_func_override(int id, const char *json) * into an overflow. */ const int w = snprintf(buf + n, sizeof(buf) - (size_t)n, "%s{\"id\":\"%s\",\"addr\":\"0x%08X\",\"calls\":%llu," - "\"guard_misses\":%llu,\"guarded\":%d,\"credit\":%s}", + "\"guard_misses\":%llu,\"guarded\":%d," + "\"guard_kind\":\"%s\",\"guard_count\":%d," + "\"guard_crc\":\"0x%08X\",\"credit\":%s}", i ? "," : "", oid, addr, (unsigned long long)calls, (unsigned long long)misses, - guarded, creditstr); + guarded, guard_name, guard_count, guard_crc, creditstr); if (w < 0) break; n += w; if ((size_t)n >= sizeof(buf) - 256) { diff --git a/runtime/src/dirty_ram_interp.c b/runtime/src/dirty_ram_interp.c index 84013a016..cdbdcd6d8 100644 --- a/runtime/src/dirty_ram_interp.c +++ b/runtime/src/dirty_ram_interp.c @@ -35,6 +35,7 @@ #include "lockstep.h" #include "starvation_ring.h" #include "fntrace.h" /* fntrace_is_game_started / fntrace_mark_game_started */ +#include "func_override.h" #include #include @@ -3210,6 +3211,28 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_ OV_FPLOG_RET1(); } target = cpu->pc; + /* J/JR is an unlinked function tail entry. It has the same guest + * return obligation as a call but reuses the caller's $ra. Check + * the override after the interrupt safe point and before local + * dirty flow, then surface the continuation so dispatch stays + * flat. Conditional branches are intra-function flow and do not + * use this function-entry hook. Precise/replay mode keeps the + * same plain-transfer policy as JAL/JALR above. */ + { + const uint32_t opc = insn >> 26; + const int unlinked_tail = + opc == 0x02u || + (opc == 0x00u && (insn & 0x3Fu) == 0x08u && + ((insn >> 21) & 0x1Fu) != 31u); + if (unlinked_tail && !g_precise_mode && !g_ls_replay_active && + func_override_try_dispatch(cpu, target, cpu->gpr[31])) { + g_dirty_ram_blocks_run++; + if (pc_entry) pc_entry->insns += (uint64_t)insns_executed; + g_dirty_interp_chain_target = cpu->pc; + OV_FPLOG_RET1(); + } + } + target = cpu->pc; #ifdef PSX_HAS_GAME_DISPATCH if (target != 0) { /* §20 FIX — the long-run idle freeze. A guest TAIL-transfer (j/jr/ diff --git a/runtime/src/func_override.c b/runtime/src/func_override.c index 9575d04d2..614405d2b 100644 --- a/runtime/src/func_override.c +++ b/runtime/src/func_override.c @@ -12,6 +12,7 @@ #include #include "cpu_state.h" +#include "overlay_loader.h" #include "psx_cycles.h" /* Residency-guard reads go through the UNTRACED peek, never psx_read_word. @@ -39,6 +40,9 @@ typedef struct { uint64_t guard_misses; uint32_t guard[FO_MAX_GUARD_WORDS]; int n_guard; /* 0 = unguarded */ + uint32_t ranges[FO_MAX_CODE_RANGES * 2]; + int n_ranges; /* 0 = no exact CRC identity guard */ + uint32_t expected_crc; int package; /* 1 = armed by a mod package plan */ int32_t credit; /* >= 0 charged per handled call; FO_CREDIT_SELF = body self-charges */ @@ -65,13 +69,44 @@ static Entry *find(uint32_t phys) return NULL; } +int func_override_code_ranges_valid(uint32_t addr, + const uint32_t *lo_len_pairs, + int n_ranges) +{ + if (!lo_len_pairs || n_ranges < 1 || n_ranges > FO_MAX_CODE_RANGES) + return 0; + const uint32_t entry = normalise(addr); + int entry_covered = 0; + for (int i = 0; i < n_ranges; i++) { + const uint32_t lo = normalise(lo_len_pairs[i * 2]); + const uint32_t len = lo_len_pairs[i * 2 + 1]; + if ((lo & 3u) || (len & 3u) || len < 4u || + lo >= 2u * 1024u * 1024u || + len > 2u * 1024u * 1024u - lo) + return 0; + if (entry >= lo && entry - lo <= len - 4u) entry_covered = 1; + for (int p = 0; p < i; p++) { + const uint32_t prev_lo = normalise(lo_len_pairs[p * 2]); + const uint32_t prev_hi = prev_lo + lo_len_pairs[p * 2 + 1]; + const uint32_t hi = lo + len; + if (lo < prev_hi && prev_lo < hi) return 0; + } + } + return entry_covered; +} + static int add_common(const char *id, uint32_t addr, FuncOverrideFn fn, - const uint32_t *guard, int n_guard, int package, - int32_t credit) + const uint32_t *guard, int n_guard, + const uint32_t *ranges, int n_ranges, + uint32_t expected_crc, int package, int32_t credit) { if (!fn) return FO_ERR_ARGS; if (n_guard < 0 || n_guard > FO_MAX_GUARD_WORDS) return FO_ERR_ARGS; if (n_guard > 0 && !guard) return FO_ERR_ARGS; + if (n_guard > 0 && n_ranges > 0) return FO_ERR_ARGS; + if (n_ranges < 0 || n_ranges > FO_MAX_CODE_RANGES) return FO_ERR_ARGS; + if (n_ranges > 0 && !func_override_code_ranges_valid( + addr, ranges, n_ranges)) return FO_ERR_ARGS; /* The credit is a required statement of timing intent (header, CYCLE * ACCOUNTING): a fixed per-call charge, or FO_CREDIT_SELF. Any other * negative value is a typo, not a policy. */ @@ -98,6 +133,9 @@ static int add_common(const char *id, uint32_t addr, FuncOverrideFn fn, e->fn = fn; for (int i = 0; i < n_guard; i++) e->guard[i] = guard[i]; e->n_guard = n_guard; + for (int i = 0; i < n_ranges * 2; i++) e->ranges[i] = ranges[i]; + e->n_ranges = n_ranges; + e->expected_crc = expected_crc; e->package = package ? 1 : 0; e->credit = credit; s_count++; @@ -107,7 +145,7 @@ static int add_common(const char *id, uint32_t addr, FuncOverrideFn fn, int func_override_add(const char *id, uint32_t addr, FuncOverrideFn fn, int32_t credit) { - return add_common(id, addr, fn, NULL, 0, 0, credit); + return add_common(id, addr, fn, NULL, 0, NULL, 0, 0, 0, credit); } int func_override_add_guarded(const char *id, uint32_t addr, FuncOverrideFn fn, @@ -115,7 +153,16 @@ int func_override_add_guarded(const char *id, uint32_t addr, FuncOverrideFn fn, int32_t credit) { if (n_words < 1) return FO_ERR_ARGS; - return add_common(id, addr, fn, expected_words, n_words, 0, credit); + return add_common(id, addr, fn, expected_words, n_words, NULL, 0, 0, + 0, credit); +} + +int func_override_add_exact(const char *id, uint32_t addr, FuncOverrideFn fn, + const uint32_t *lo_len_pairs, int n_ranges, + uint32_t expected_crc, int32_t credit) +{ + return add_common(id, addr, fn, NULL, 0, lo_len_pairs, n_ranges, + expected_crc, 0, credit); } int func_override_add_package(const char *id, uint32_t addr, FuncOverrideFn fn, @@ -124,7 +171,17 @@ int func_override_add_package(const char *id, uint32_t addr, FuncOverrideFn fn, { if (n_words < 0 || n_words > FO_MAX_GUARD_WORDS) return FO_ERR_ARGS; return add_common(id, addr, fn, n_words ? expected_words : NULL, n_words, - 1, credit); + NULL, 0, 0, 1, credit); +} + +int func_override_add_package_exact(const char *id, uint32_t addr, + FuncOverrideFn fn, + const uint32_t *lo_len_pairs, + int n_ranges, uint32_t expected_crc, + int32_t credit) +{ + return add_common(id, addr, fn, NULL, 0, lo_len_pairs, n_ranges, + expected_crc, 1, credit); } /* The hook. Runs on EVERY dispatch, so the common path — nothing registered @@ -136,6 +193,10 @@ static int hook(CPUState *cpu, uint32_t phys) for (int i = 0; i < s_count; i++) { Entry *e = &s_entries[i]; if (e->phys != phys) continue; + /* Every matched-address consult counts, including an exact/prefix + * identity miss and the one-shot original bypass. This makes calls + * a reachability counter rather than an execution counter. */ + e->calls++; /* One-shot bypass: func_override_call_original re-dispatched this * address; let the original backend take it exactly once. */ if (s_bypass_phys == phys) { @@ -159,12 +220,11 @@ static int hook(CPUState *cpu, uint32_t phys) return 0; } } - /* Count the CONSULT, not just the handled case: an override that - * declines still proves the address was reached through a hooked - * path, which is exactly what a diagnostic probe needs (calls == 0 - * means "this call never crosses a hook", the bypass class of bug - * that hid the interp local-chain gap). */ - e->calls++; + if (e->n_ranges && !psx_overlay_static_code_matches( + e->ranges, (uint32_t)e->n_ranges, e->expected_crc)) { + e->guard_misses++; + return 0; + } { const uint32_t saved_active = s_active_phys; s_active_phys = phys; @@ -184,6 +244,20 @@ static int hook(CPUState *cpu, uint32_t phys) return 0; } +int func_override_try_dispatch(CPUState *cpu, uint32_t target, + uint32_t default_pc) +{ + if (!cpu || !g_psx_func_override_hook) return 0; + const uint32_t saved_pc = cpu->pc; + cpu->pc = 0; + if (!g_psx_func_override_hook(cpu, normalise(target))) { + cpu->pc = saved_pc; + return 0; + } + if (cpu->pc == 0) cpu->pc = default_pc; + return 1; +} + void func_override_guest_call(CPUState *cpu, uint32_t target, uint32_t site_ra) { /* Spoof $ra to the original call site so callees, fntrace and crash @@ -240,6 +314,10 @@ int func_override_reset_package_armed(void) } s_count = w; if (removed) { + /* Entry compaction and later package re-arming can reuse the address + * of an exact range array. The overlay matcher keys immutable arrays + * by address, so discard those cache identities before reuse. */ + overlay_loader_static_match_cache_clear(); s_active_phys = 0; s_bypass_phys = 0; func_override_install(); /* follows s_count back to NULL if empty */ @@ -249,6 +327,11 @@ int func_override_reset_package_armed(void) int func_override_count(void) { return s_count; } +int func_override_is_package(int index) +{ + return (index >= 0 && index < s_count) ? s_entries[index].package : 0; +} + int func_override_get(int index, char *id_out, size_t id_cap, uint32_t *addr_out, uint64_t *calls_out) { @@ -272,7 +355,23 @@ int func_override_get_ex(int index, char *id_out, size_t id_cap, if (addr_out) *addr_out = e->phys; if (calls_out) *calls_out = e->calls; if (guard_misses_out) *guard_misses_out = e->guard_misses; - if (guarded_out) *guarded_out = e->n_guard; + if (guarded_out) *guarded_out = e->n_guard ? e->n_guard + : e->n_ranges; if (credit_out) *credit_out = e->credit; return 1; } + +int func_override_get_guard_info(int index, int *kind_out, int *count_out, + uint32_t *expected_crc_out) +{ + if (index < 0 || index >= s_count) return 0; + const Entry *e = &s_entries[index]; + const int kind = e->n_ranges ? FO_GUARD_CODE_CRC32 + : (e->n_guard ? FO_GUARD_WORDS + : FO_GUARD_NONE); + if (kind_out) *kind_out = kind; + if (count_out) *count_out = e->n_ranges ? e->n_ranges : e->n_guard; + if (expected_crc_out) + *expected_crc_out = e->n_ranges ? e->expected_crc : 0; + return 1; +} diff --git a/runtime/src/mod_packages.cpp b/runtime/src/mod_packages.cpp index 9796244fd..a960ce861 100644 --- a/runtime/src/mod_packages.cpp +++ b/runtime/src/mod_packages.cpp @@ -1456,11 +1456,15 @@ bool mod_register_vblank_plugin(const std::string& id, void (*callback)(void)) { bool mod_register_function_override_marker(const std::string& id) { if (!valid_id(id)) return false; RegisteredPlugin& plugin = registered_plugins()[id]; - if (plugin.function_override) return false; + if (plugin.function_override) return true; plugin.function_override = true; return true; } +bool mod_plugin_id_valid(const std::string& id) { + return valid_id(id); +} + bool mod_plugin_registered(const std::string& id) { const auto found = registered_plugins().find(id); return found != registered_plugins().end() && diff --git a/runtime/src/mod_runtime.cpp b/runtime/src/mod_runtime.cpp index 052033726..661a9235e 100644 --- a/runtime/src/mod_runtime.cpp +++ b/runtime/src/mod_runtime.cpp @@ -101,6 +101,9 @@ struct FunctionOverridePlugin { PSXModFunctionOverrideFn fn = nullptr; uint32_t guard[FO_MAX_GUARD_WORDS] = {0, 0, 0, 0}; int n_guard = 0; + uint32_t ranges[FO_MAX_CODE_RANGES * 2] = {}; + int n_ranges = 0; + uint32_t expected_crc = 0; int32_t credit = 0; bool armed = false; }; @@ -110,6 +113,48 @@ std::vector& function_override_plugins() { return value; } +bool plan_selects_plugin(const ModResolution& plan, const std::string& id) { + return std::any_of( + plan.plugins.begin(), plan.plugins.end(), + [&](const ModResolution::Plugin& plugin) { return plugin.id == id; }); +} + +bool validate_function_override_plan(const ModResolution& plan, + std::string& error) { + std::map claims; + int total = 0; + for (int i = 0; i < func_override_count(); ++i) { + if (func_override_is_package(i)) continue; + char id[FO_MAX_ID]; + uint32_t address = 0; + if (!func_override_get_ex(i, id, sizeof(id), &address, nullptr, + nullptr, nullptr, nullptr)) + continue; + claims[address] = id[0] ? id : "direct registration"; + ++total; + } + for (const FunctionOverridePlugin& pending : function_override_plugins()) { + if (!plan_selects_plugin(plan, pending.plugin)) continue; + const uint32_t address = pending.address & 0x1FFFFFFFu; + const auto inserted = claims.emplace(address, pending.id); + if (!inserted.second) { + std::ostringstream out; + out << "function override collision at 0x" << std::hex + << std::uppercase << std::setw(8) << std::setfill('0') + << address << ": " << inserted.first->second << " and " + << pending.id; + error = out.str(); + return false; + } + ++total; + } + if (total > FO_MAX_OVERRIDES) { + error = "function override plan exceeds FO_MAX_OVERRIDES"; + return false; + } + return true; +} + const ModPackage* selected_package(const std::string& id) { return state().manager.selected_package(id); } @@ -1104,6 +1149,9 @@ bool mod_runtime_initialize(const std::filesystem::path& root, const std::filesystem::path& exe_path, std::string* error) { RuntimeMods& s = state(); + func_override_reset_package_armed(); + for (FunctionOverridePlugin& pending : function_override_plugins()) + pending.armed = false; s.manager.set_root({}); s.plan = {}; s.validation = {}; @@ -1197,6 +1245,10 @@ bool mod_runtime_commit(const std::filesystem::path& disc_path, std::string* err if (error) *error = s.error; return false; } + if (!validate_function_override_plan(plan, s.error)) { + if (error) *error = s.error; + return false; + } for (const ModResolution::Overlay& overlay : plan.overlays) { if (overlay.expected_sha256.empty()) continue; std::string actual; @@ -1220,6 +1272,13 @@ bool mod_runtime_commit(const std::filesystem::path& disc_path, std::string* err if (error) *error = s.error; return false; } + /* A successful commit replaces the old package plan. Disarm its override + * table now, before publishing the new plan, so no dispatch window can + * run callbacks from one plan and overrides from another. Activation + * below will arm the complete new set atomically. */ + func_override_reset_package_armed(); + for (FunctionOverridePlugin& pending : function_override_plugins()) + pending.armed = false; s.plan = std::move(plan); build_disc_index(s); s.effective_disc_path = std::move(effective_disc); @@ -1312,31 +1371,45 @@ extern "C" void mod_runtime_activate_plugins(void) { using namespace PSXRecompV4; RuntimeMods& s = state(); if (!s.initialized || !s.plan.ok) return; + /* Arm the complete selected override set before any activation callback + * can change host or guest state. Commit already preflighted collisions + * and capacity. If the table changed after commit, roll back the complete + * package set and fail closed instead of silently arming a subset. */ + func_override_reset_package_armed(); + for (FunctionOverridePlugin& pending : function_override_plugins()) + pending.armed = false; + for (FunctionOverridePlugin& pending : function_override_plugins()) { + if (!plan_selects_plugin(s.plan, pending.plugin)) continue; + const int rc = pending.n_ranges + ? func_override_add_package_exact( + pending.id.c_str(), pending.address, pending.fn, + pending.ranges, pending.n_ranges, pending.expected_crc, + pending.credit) + : func_override_add_package( + pending.id.c_str(), pending.address, pending.fn, + pending.n_guard ? pending.guard : nullptr, pending.n_guard, + pending.credit); + pending.armed = (rc == FO_OK); + if (rc != FO_OK) { + func_override_reset_package_armed(); + for (FunctionOverridePlugin& item : function_override_plugins()) + item.armed = false; + std::ostringstream out; + out << "function override activation failed for " << pending.id + << " (error " << rc << ")"; + s.error = out.str(); + s.plan.ok = false; + func_override_install(); + std::fprintf(stderr, "psxrecomp: %s\n", s.error.c_str()); + return; + } + } + func_override_install(); for (const ModResolution::Plugin& plugin : s.plan.plugins) { s.current_plugin = &plugin; mod_invoke_activation_plugin(plugin.id); s.current_plugin = nullptr; } - /* Arm the package-gated function overrides for plan-selected plugin - * ids, then (re)install the dispatcher hook. func_override_add refuses - * duplicate addresses; a refusal here means two active plugins claim - * one function, which the resolver should have prevented — the armed - * flag stays false and the `func_override` TCP command shows the gap. */ - for (FunctionOverridePlugin& pending : function_override_plugins()) { - if (pending.armed) continue; - const bool selected = std::any_of( - s.plan.plugins.begin(), s.plan.plugins.end(), - [&](const ModResolution::Plugin& plugin) { - return plugin.id == pending.plugin; - }); - if (!selected) continue; - const int rc = func_override_add_package( - pending.id.c_str(), pending.address, pending.fn, - pending.n_guard ? pending.guard : nullptr, pending.n_guard, - pending.credit); - pending.armed = (rc == FO_OK); - } - func_override_install(); } extern "C" void mod_runtime_on_vblank(void) { @@ -1475,13 +1548,19 @@ extern "C" int psx_mod_register_function_entry_plugin( return 1; } -extern "C" int psx_mod_register_function_override( +static int register_function_override( const char* id, uint32_t address, PSXModFunctionOverrideFn fn, - const uint32_t* expected_words, int n_words, int32_t credit) { + const uint32_t* expected_words, int n_words, + const uint32_t* ranges, int n_ranges, uint32_t expected_crc, + int32_t credit) { using namespace PSXRecompV4; if (!id || !*id || !address || !fn) return 0; if (n_words < 0 || n_words > FO_MAX_GUARD_WORDS) return 0; if (n_words > 0 && !expected_words) return 0; + if (n_words > 0 && n_ranges > 0) return 0; + if (n_ranges < 0 || n_ranges > FO_MAX_CODE_RANGES) return 0; + if (n_ranges > 0 && + !func_override_code_ranges_valid(address, ranges, n_ranges)) return 0; if (credit < FO_CREDIT_SELF) return 0; /* An optional ":label" suffix names this override in diagnostics (the * `func_override` TCP command) without multiplying manifest plugin ids — @@ -1492,6 +1571,7 @@ extern "C" int psx_mod_register_function_override( const std::string plugin_id = colon ? std::string(id, colon - id) : std::string(id); if (plugin_id.empty() || (colon && !colon[1])) return 0; + if (!mod_plugin_id_valid(plugin_id)) return 0; auto& plugins = function_override_plugins(); const auto duplicate = std::find_if( plugins.begin(), plugins.end(), @@ -1506,12 +1586,31 @@ extern "C" int psx_mod_register_function_override( plugin.fn = fn; for (int i = 0; i < n_words; ++i) plugin.guard[i] = expected_words[i]; plugin.n_guard = n_words; + for (int i = 0; i < n_ranges * 2; ++i) plugin.ranges[i] = ranges[i]; + plugin.n_ranges = n_ranges; + plugin.expected_crc = expected_crc; plugin.credit = credit; plugins.push_back(plugin); /* Mark the plugin id available to the package resolver so a manifest can * gate an override-only plugin (multiple overrides may share one). */ - mod_register_function_override_marker(plugin_id); - return 1; + return mod_register_function_override_marker(plugin_id) ? 1 : 0; +} + +extern "C" int psx_mod_register_function_override( + const char* id, uint32_t address, PSXModFunctionOverrideFn fn, + const uint32_t* expected_words, int n_words, int32_t credit) { + return register_function_override(id, address, fn, expected_words, n_words, + nullptr, 0, 0, credit); +} + +extern "C" int psx_mod_register_function_override_exact( + const char* id, uint32_t address, PSXModFunctionOverrideFn fn, + const uint32_t* lo_len_pairs, int n_ranges, uint32_t expected_crc, + int32_t credit) { + if (n_ranges < 1) return 0; + return register_function_override(id, address, fn, nullptr, 0, + lo_len_pairs, n_ranges, expected_crc, + credit); } extern "C" void psx_mod_function_entry(CPUState* cpu, uint32_t address) { diff --git a/runtime/src/overlay_loader.c b/runtime/src/overlay_loader.c index 6a0ca6864..9ab9a869c 100644 --- a/runtime/src/overlay_loader.c +++ b/runtime/src/overlay_loader.c @@ -553,7 +553,6 @@ static uint32_t cand_gensum(const Candidate *c) { return s; } -#ifdef PSX_HAS_OVERLAY_DISPATCH /* Static-overlay validation uses the same exact-code-range contract as the * dynamic DLL loader. Generated code passes immutable {phys_lo, len} pairs and * the CRC of the bytes it was compiled from. A page-generation cache keeps the @@ -643,7 +642,10 @@ void overlay_loader_static_match_stats(uint64_t *rehashes, if (crc_misses) *crc_misses = s_static_match_crc_misses; if (gen_fastpath) *gen_fastpath = s_static_match_gen_fastpath; } -#endif + +void overlay_loader_static_match_cache_clear(void) { + memset(s_static_match_cache, 0, sizeof(s_static_match_cache)); +} /* ---- Per-DLL code-range manifest --------------------------------------- */ /* Strict v2 line format emitted by tools/compile_overlays.py beside each DLL: @@ -2855,9 +2857,7 @@ void overlay_loader_resync_validation_after_restore(void) int i; for (i = 0; i < s_cand_n; i++) s_cand[i].val_gen ^= 0x80000000u; -#ifdef PSX_HAS_OVERLAY_DISPATCH - memset(s_static_match_cache, 0, sizeof(s_static_match_cache)); -#endif + overlay_loader_static_match_cache_clear(); } /* Re-scan the cache dir for DLLs compiled after init (step 2.8 autocompile) diff --git a/runtime/tests/test_func_override.c b/runtime/tests/test_func_override.c index c48fbc388..44cfa6bc7 100644 --- a/runtime/tests/test_func_override.c +++ b/runtime/tests/test_func_override.c @@ -14,6 +14,7 @@ #include #include "cpu_state.h" +#include "crc32.h" #include "func_override.h" extern int (*g_psx_func_override_hook)(CPUState *cpu, uint32_t phys); @@ -38,6 +39,26 @@ static int g_fail = 0; * guard_declines_on_mismatch, and the reason this double is trivial. */ #define FAKE_RAM_WORDS 64 static uint32_t s_ram[FAKE_RAM_WORDS]; +static int s_static_match_cache_clears; + +int psx_overlay_static_code_matches(const uint32_t *lo_len_pairs, + uint32_t count, + uint32_t expected_crc) +{ + uint32_t crc = 0xFFFFFFFFu; + for (uint32_t i = 0; i < count; i++) { + const uint32_t lo = lo_len_pairs[i * 2u] & 0x1FFFFFFFu; + const uint32_t len = lo_len_pairs[i * 2u + 1u]; + if (lo >= sizeof(s_ram) || len > sizeof(s_ram) - lo) return 0; + crc = crc32_update(crc, (const uint8_t *)s_ram + lo, len); + } + return (crc ^ 0xFFFFFFFFu) == expected_crc; +} + +void overlay_loader_static_match_cache_clear(void) +{ + s_static_match_cache_clears++; +} uint32_t psx_peek_word_untraced(uint32_t addr) { @@ -71,6 +92,8 @@ static int s_dispatch_depth = 0; * (what a guest-level wrap would observe). */ static uint32_t s_recursive_original_at = 0; static int s_recursion_budget = 0; +static uint32_t s_original_redirect_at = 0; +static uint32_t s_original_redirect_to = 0; void psx_dispatch_call(CPUState *cpu, uint32_t addr, uint32_t return_addr) { @@ -82,6 +105,9 @@ void psx_dispatch_call(CPUState *cpu, uint32_t addr, uint32_t return_addr) /* Nothing handled it: this is the original body running. */ s_original_runs++; cpu->gpr[2] = 0xAAAAu; + if ((addr & 0x1FFFFFFFu) == + (s_original_redirect_at & 0x1FFFFFFFu)) + cpu->pc = s_original_redirect_to; if (s_recursive_original_at && (addr & 0x1FFFFFFFu) == (s_recursive_original_at & 0x1FFFFFFFu) && s_recursion_budget > 0) { @@ -118,6 +144,13 @@ static int impl_wraps(CPUState *cpu) return 1; } +static int impl_redirects(CPUState *cpu) +{ + s_impl_calls++; + cpu->pc = 0x8000D00Du; + return 1; +} + /* ---- helpers ------------------------------------------------------------ */ static void reset_all(void) @@ -129,6 +162,8 @@ static void reset_all(void) s_original_runs = 0; s_recursive_original_at = 0; s_recursion_budget = 0; + s_original_redirect_at = 0; + s_original_redirect_to = 0; memset(s_ram, 0, sizeof(s_ram)); } @@ -278,7 +313,104 @@ static void test_guard_declines_on_mismatch(void) CHECK(guarded == 2, "guard word count must be reported, got %d", guarded); CHECK(misses == 2, "two guard misses expected, got %llu", (unsigned long long)misses); + CHECK(calls == 3, "all three matched-address consults must count, got %llu", + (unsigned long long)calls); + } +} + +static void test_exact_code_identity_covers_the_complete_range(void) +{ + CPUState cpu; + memset(&cpu, 0, sizeof(cpu)); + reset_all(); + + const uint32_t addr = 0x80000080u; + static const uint32_t ranges[2] = {0x00000080u, 20u}; + for (int i = 0; i < 5; i++) s_ram[32 + i] = 0x10000000u + (uint32_t)i; + const uint32_t crc = crc32_compute((const uint8_t *)&s_ram[32], 20u); + CHECK(func_override_add_exact("t.exact", addr, impl_handles, + ranges, 1, crc, 0) == FO_OK, + "exact-range add must succeed"); + func_override_install(); + + CHECK(consult(&cpu, addr) == 1, "complete matching code identity must handle"); + for (int i = 0; i < func_override_count(); i++) { + uint32_t a = 0, reported_crc = 0; + int kind = -1, count = 0; + if (!func_override_get_ex(i, NULL, 0, &a, NULL, NULL, NULL, NULL) || + a != (addr & 0x1FFFFFFFu)) continue; + CHECK(func_override_get_guard_info(i, &kind, &count, &reported_crc), + "exact guard information must be inspectable"); + CHECK(kind == FO_GUARD_CODE_CRC32 && count == 1 && reported_crc == crc, + "inventory must identify the exact CRC guard"); } + s_ram[36] ^= 1u; /* word five: outside the legacy four-word prefix */ + CHECK(consult(&cpu, addr) == 0, + "a changed fifth word must reject the exact code identity"); + CHECK(s_impl_calls == 1, "identity mismatch must not run the override"); +} + +static void test_dispatch_preserves_nonlocal_continuations(void) +{ + CPUState cpu; + memset(&cpu, 0, sizeof(cpu)); + reset_all(); + + const uint32_t redirect_at = 0x80007000u; + CHECK(func_override_add("t.redirect", redirect_at, impl_redirects, 0) == FO_OK, + "redirect add"); + func_override_install(); + cpu.gpr[31] = 0x80001234u; + CHECK(func_override_try_dispatch(&cpu, redirect_at, cpu.gpr[31]) == 1, + "redirect override must handle"); + CHECK(cpu.pc == 0x8000D00Du, + "handled non-local continuation must not be overwritten with $ra"); + + const uint32_t plain_at = 0x80007100u; + CHECK(func_override_add("t.plain_return", plain_at, impl_handles, 0) == FO_OK, + "plain return add"); + cpu.pc = 0xFFFFFFFFu; + CHECK(func_override_try_dispatch(&cpu, plain_at, cpu.gpr[31]) == 1, + "plain override must handle"); + CHECK(cpu.pc == cpu.gpr[31], "zero continuation must default to $ra"); +} + +static void test_wrap_preserves_original_nonlocal_continuation(void) +{ + CPUState cpu; + memset(&cpu, 0, sizeof(cpu)); + reset_all(); + + const uint32_t addr = 0x80007200u; + CHECK(func_override_add("t.wrap_redirect", addr, impl_wraps, + FO_CREDIT_SELF) == FO_OK, + "redirecting wrap add"); + func_override_install(); + cpu.gpr[31] = 0x80002222u; + s_original_redirect_at = addr; + s_original_redirect_to = 0x8000BEEFu; + CHECK(func_override_try_dispatch(&cpu, addr, cpu.gpr[31]) == 1, + "redirecting wrap must handle"); + CHECK(cpu.pc == s_original_redirect_to, + "wrap must preserve the original's non-local continuation"); + CHECK(cpu.gpr[31] == 0x80002222u, "wrap must restore the caller's $ra"); +} + +static void test_guest_call_preserves_nonlocal_continuation(void) +{ + CPUState cpu; + memset(&cpu, 0, sizeof(cpu)); + reset_all(); + + const uint32_t target = 0x80007300u; + cpu.gpr[31] = 0x80003333u; + s_original_redirect_at = target; + s_original_redirect_to = 0x8000CAFEu; + func_override_guest_call(&cpu, target, 0x80004444u); + CHECK(cpu.pc == s_original_redirect_to, + "guest_call must preserve the callee's non-local continuation"); + CHECK(cpu.gpr[31] == 0x80003333u, + "guest_call must restore the native caller's $ra"); } static void test_call_original_is_one_shot(void) @@ -368,10 +500,13 @@ static void test_package_reset_drops_only_package_entries(void) CHECK(consult(&cpu, pkg_at) == 1, "package override armed"); const int before = func_override_count(); + const int clears_before = s_static_match_cache_clears; const int dropped = func_override_reset_package_armed(); CHECK(dropped >= 1, "reset must report dropping the package entry"); CHECK(func_override_count() == before - dropped, "count must shrink by exactly the dropped entries"); + CHECK(s_static_match_cache_clears == clears_before + 1, + "package slot reuse must invalidate exact-identity cache keys"); /* This is the netplay-divergence regression: after clear-mods the package * override must be gone, while the game's own always-on reimplementation @@ -498,6 +633,10 @@ int main(void) test_duplicate_address_refused(); test_handled_and_declined_both_count_as_consults(); test_guard_declines_on_mismatch(); + test_exact_code_identity_covers_the_complete_range(); + test_dispatch_preserves_nonlocal_continuations(); + test_wrap_preserves_original_nonlocal_continuation(); + test_guest_call_preserves_nonlocal_continuation(); test_call_original_is_one_shot(); test_bypass_is_consumed_so_recursion_reconsults(); test_call_original_outside_override_is_a_noop(); diff --git a/runtime/tests/test_func_override_routes.py b/runtime/tests/test_func_override_routes.py new file mode 100644 index 000000000..9e4ed8ab4 --- /dev/null +++ b/runtime/tests/test_func_override_routes.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +"""Pin the generated and dirty-RAM function-override entry routes.""" + +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] +DIRTY = (ROOT / "runtime/src/dirty_ram_interp.c").read_text(encoding="utf-8") +EMITTER = (ROOT / "recompiler/src/full_function_emitter.cpp").read_text( + encoding="utf-8" +) + + +def require(condition: bool, message: str) -> None: + if not condition: + raise AssertionError(message) + + +def main() -> None: + helper = "func_override_try_dispatch(cpu, target, cpu->gpr[31])" + pump = DIRTY.index("dirty_ram_pump_boundary(cpu, target, 1)") + tail = DIRTY.index(helper, pump) + local = DIRTY.index("if (allow_local_dirty_flow", tail) + require(pump < tail < local, + "J/JR override entry must follow the IRQ safe point and precede local flow") + require("!g_precise_mode && !g_ls_replay_active" in DIRTY[pump:local], + "tail override entry must keep precise/replay plain-transfer policy") + require("((insn >> 21) & 0x1Fu) != 31u" in DIRTY[pump:local], + "JR $ra returns must not be treated as function tail entries") + require("func_override_try_dispatch(cpu, addr, cpu->gpr[31])" in EMITTER, + "generated dispatch must use the continuation-preserving helper") + require("g_psx_func_override_hook(cpu, addr & 0x1FFFFFFFu)) {\n" + " cpu->pc = cpu->gpr[31];" not in EMITTER, + "generated dispatch must not overwrite non-local continuations") + print("function override entry routes passed") + + +if __name__ == "__main__": + main() diff --git a/runtime/tests/test_mod_runtime.cpp b/runtime/tests/test_mod_runtime.cpp index 00aa2a248..f2bd48e75 100644 --- a/runtime/tests/test_mod_runtime.cpp +++ b/runtime/tests/test_mod_runtime.cpp @@ -77,6 +77,9 @@ extern "C" int fntrace_is_game_started(void) { return 1; } * paths run for real. The tier's own behavior is pinned by * test_func_override.c; here the doubles only need to satisfy the link. */ extern "C" uint32_t psx_peek_word_untraced(uint32_t) { return 0; } +extern "C" int psx_overlay_static_code_matches(const uint32_t*, uint32_t, + uint32_t) { return 1; } +extern "C" void overlay_loader_static_match_cache_clear(void) {} extern "C" void psx_dispatch_call(struct CPUState*, uint32_t, uint32_t) {} extern "C" { uint64_t psx_cycle_count = 0; @@ -98,6 +101,8 @@ static void test_activation_plugin(void) { activation_calls++; } +static int test_function_override(CPUState*) { return 1; } + static void check(bool value, const char* message) { if (!value) { std::cerr << "FAIL: " << message << "\n"; @@ -508,6 +513,44 @@ int main() { "unestablished display geometry must report zero so callers skip " "drawing instead of guessing"); + check(psx_mod_register_function_override( + "Invalid ID:bad", 0x80018000u, test_function_override, + nullptr, 0, 0) == 0, + "invalid package plugin ids must be rejected at registration"); + const uint32_t exact_ranges[2] = {0x00018000u, 4u}; + check(psx_mod_register_function_override_exact( + "runtime.test-vblank:exact", 0x80018000u, + test_function_override, exact_ranges, 1, 0x12345678u, 0) == 1, + "exact package override must queue"); + check(PSXRecompV4::mod_runtime_commit(stock_path, &error), error.c_str()); + mod_runtime_activate_plugins(); + check(func_override_count() == 1, + "selected exact package override must arm"); + int guard_kind = -1, guard_count = 0; + uint32_t guard_crc = 0; + check(func_override_get_guard_info(0, &guard_kind, &guard_count, + &guard_crc) == 1 && + guard_kind == FO_GUARD_CODE_CRC32 && guard_count == 1 && + guard_crc == 0x12345678u, + "armed package inventory must retain exact code identity"); + check(PSXRecompV4::mod_runtime_clear_for_netplay(&error), error.c_str()); + check(func_override_count() == 0, + "clearing the package plan must disarm the exact override"); + check(psx_mod_register_function_override( + "runtime.test-vblank:first", 0x80018100u, + test_function_override, nullptr, 0, 0) == 1, + "first package override must queue"); + check(psx_mod_register_function_override( + "runtime.test-vblank:second", 0x80018100u, + test_function_override, nullptr, 0, 0) == 1, + "a second package claim may queue until selection is known"); + check(!PSXRecompV4::mod_runtime_commit(stock_path, &error), + "commit must reject two selected package overrides at one address"); + check(error.find("function override collision") != std::string::npos, + "collision rejection must name the cause"); + check(func_override_count() == 0, + "a rejected package override set must arm no partial entries"); + fs::remove_all(root, ec); if (failures) return 1; std::cout << "mod runtime tests passed\n"; From 9e4468462f55bc1636dce8a334e71bb17143d5a8 Mon Sep 17 00:00:00 2001 From: Alexandros Mandravillis Date: Tue, 1 Sep 2026 01:21:30 +0300 Subject: [PATCH 2/9] Add function override lifecycle coverage --- docs/TESTING.md | 2 +- recompiler/CMakeLists.txt | 16 ++- runtime/include/func_override.h | 9 +- runtime/src/func_override.c | 2 + runtime/tests/overlay_pair_dedup_harness.c | 122 +++++++++++++++++- runtime/tests/test_func_override.c | 56 ++++++++ .../tests/test_overlay_pair_dedup_runtime.py | 13 +- tools/aot_overlay_spike/README.md | 4 +- 8 files changed, 210 insertions(+), 14 deletions(-) diff --git a/docs/TESTING.md b/docs/TESTING.md index 2fe84b139..43b63ab3a 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -70,7 +70,7 @@ then wire them in. |---|---| | `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. | +| `runtime/tests/test_overlay_pair_dedup_runtime.py` | Registered as `overlay_pair_dedup_runtime`. The Python driver builds its companion harness and redistributable shared-library fixtures. It covers exact static-code identity, cache replacement and restore behavior, pair publication, deduplication, and rejection boundaries. | ## Tests that are not in `ctest` and should not be diff --git a/recompiler/CMakeLists.txt b/recompiler/CMakeLists.txt index 4e74eddcd..79da2551b 100644 --- a/recompiler/CMakeLists.txt +++ b/recompiler/CMakeLists.txt @@ -628,8 +628,17 @@ if(BUILD_TESTING) WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/..) endforeach() + # This test builds redistributable fixture DLLs and links a portable + # harness against the real overlay loader. It covers publication-pair + # deduplication plus static code-identity replacement/restore caching. + add_test( + NAME overlay_pair_dedup_runtime + COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/../runtime/tests/test_overlay_pair_dedup_runtime.py + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/..) + # ---- 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, @@ -646,12 +655,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} diff --git a/runtime/include/func_override.h b/runtime/include/func_override.h index aac89a8d8..b6cea3025 100644 --- a/runtime/include/func_override.h +++ b/runtime/include/func_override.h @@ -169,13 +169,15 @@ int func_override_add_guarded(const char *id, uint32_t addr, FuncOverrideFn fn, * word-aligned, non-overlapping RAM ranges and must cover addr. This uses the * overlay loader's page-generation cache, so unchanged code does not re-hash * on every call. A short prefix guard is only a quick sanity check; use this - * API when the resident overlay generation must be identified exactly. */ + * API when the resident overlay generation must be identified exactly. + * n_ranges must be 1..FO_MAX_CODE_RANGES. */ int func_override_add_exact(const char *id, uint32_t addr, FuncOverrideFn fn, const uint32_t *lo_len_pairs, int n_ranges, uint32_t expected_crc, int32_t credit); -/* Validate the range shape used by func_override_add_exact. Exposed so the - * package registry can reject bad constructor input before plan selection. */ +/* Validate the range shape used by func_override_add_exact. n_ranges must be + * 1..FO_MAX_CODE_RANGES. Exposed so the package registry can reject bad + * constructor input before plan selection. */ int func_override_code_ranges_valid(uint32_t addr, const uint32_t *lo_len_pairs, int n_ranges); @@ -221,6 +223,7 @@ int func_override_try_dispatch(struct CPUState *cpu, uint32_t target, int func_override_add_package(const char *id, uint32_t addr, FuncOverrideFn fn, const uint32_t *expected_words, int n_words, int32_t credit); +/* Exact package form. n_ranges must be 1..FO_MAX_CODE_RANGES. */ int func_override_add_package_exact(const char *id, uint32_t addr, FuncOverrideFn fn, const uint32_t *lo_len_pairs, diff --git a/runtime/src/func_override.c b/runtime/src/func_override.c index 614405d2b..0cb471976 100644 --- a/runtime/src/func_override.c +++ b/runtime/src/func_override.c @@ -161,6 +161,7 @@ int func_override_add_exact(const char *id, uint32_t addr, FuncOverrideFn fn, const uint32_t *lo_len_pairs, int n_ranges, uint32_t expected_crc, int32_t credit) { + if (n_ranges < 1) return FO_ERR_ARGS; return add_common(id, addr, fn, NULL, 0, lo_len_pairs, n_ranges, expected_crc, 0, credit); } @@ -180,6 +181,7 @@ int func_override_add_package_exact(const char *id, uint32_t addr, int n_ranges, uint32_t expected_crc, int32_t credit) { + if (n_ranges < 1) return FO_ERR_ARGS; return add_common(id, addr, fn, NULL, 0, lo_len_pairs, n_ranges, expected_crc, 1, credit); } diff --git a/runtime/tests/overlay_pair_dedup_harness.c b/runtime/tests/overlay_pair_dedup_harness.c index 617aba1cd..19ebfe08d 100644 --- a/runtime/tests/overlay_pair_dedup_harness.c +++ b/runtime/tests/overlay_pair_dedup_harness.c @@ -1,6 +1,7 @@ #define PSX_OVERLAY_DLL_BUILD 1 #include "overlay_loader.h" #undef PSX_OVERLAY_DLL_BUILD +#include "crc32.h" #include #include @@ -22,6 +23,8 @@ _Static_assert(PSX_OVERLAY_TEST_CANDIDATE_CAP == 4, static uint8_t s_ram[RAM_SIZE]; static uint8_t s_scratch[1024]; +static uint32_t s_page_generation; +static int s_ram_available = 1; uint32_t g_debug_current_func_addr; uint32_t g_debug_last_store_pc; @@ -36,10 +39,10 @@ uint64_t s_frame_count; int g_shadow_mmio_watch; uint64_t g_shadow_mmio_hits; -uint8_t *memory_get_ram_ptr(void) { return s_ram; } +uint8_t *memory_get_ram_ptr(void) { return s_ram_available ? s_ram : NULL; } uint8_t *memory_get_scratchpad_ptr(void) { return s_scratch; } uint32_t overlay_watch_pagegen_sum(uint32_t phys, uint32_t len) { - (void)phys; (void)len; return 0; + (void)phys; (void)len; return s_page_generation; } void overlay_watch_set_range(uint32_t phys, uint32_t len) { (void)phys; (void)len; @@ -57,6 +60,36 @@ void dirty_ram_xprobe_call_note(CPUState *cpu, uint32_t target, (void)cpu; (void)target; (void)ra; (void)phase; } +int psx_netplay_is_resimulating(void) { return 0; } +void psx_pgxp_load(CPUState *cpu, uint32_t instr, uint32_t addr, + uint32_t value) { + (void)cpu; (void)instr; (void)addr; (void)value; +} +void psx_pgxp_store(CPUState *cpu, uint32_t instr, uint32_t addr, + uint32_t value) { + (void)cpu; (void)instr; (void)addr; (void)value; +} +void psx_pgxp_alu(CPUState *cpu, uint32_t instr, uint32_t result, + uint32_t s1, uint32_t s2) { + (void)cpu; (void)instr; (void)result; (void)s1; (void)s2; +} +void psx_pgxp_muldiv(CPUState *cpu, uint32_t instr, uint32_t hi, + uint32_t lo, uint32_t s1, uint32_t s2) { + (void)cpu; (void)instr; (void)hi; (void)lo; (void)s1; (void)s2; +} +void psx_pgxp_cop2(CPUState *cpu, uint32_t instr, uint32_t value, + uint32_t addr) { + (void)cpu; (void)instr; (void)value; (void)addr; +} +uint32_t psx_ws_cull_keep_result(uint32_t vanilla, uint32_t forced) { + (void)forced; return vanilla; +} +uint32_t psx_ws_aspect_cone_result(uint32_t site, uint32_t vanilla, + uint32_t forced) { + (void)site; (void)forced; return vanilla; +} +uint32_t psx_ws_angle_widen(uint32_t vanilla) { return vanilla; } + void psx_dispatch_call(CPUState *cpu, uint32_t addr, uint32_t ra) { (void)cpu; (void)addr; (void)ra; } @@ -223,6 +256,89 @@ static int expect_int(const char *what, long long actual, long long expected) { return 0; } +static int expect_static_match_stats(uint64_t want_rehashes, + uint64_t want_misses, + uint64_t want_fastpath) { + uint64_t rehashes = 0, misses = 0, fastpath = 0; + overlay_loader_static_match_stats(&rehashes, &misses, &fastpath); + return expect_int("static rehashes", (long long)rehashes, + (long long)want_rehashes) & + expect_int("static CRC misses", (long long)misses, + (long long)want_misses) & + expect_int("static generation fast-path", (long long)fastpath, + (long long)want_fastpath); +} + +static int static_match_lifecycle(void) { + static const uint32_t ranges[] = { + 0x80001000u, 7u, + 0xA0003000u, 9u, + }; + static const uint32_t zero_length[] = {0x80001000u, 0u}; + static const uint32_t out_of_bounds[] = {0x801FFFFCu, 8u}; + uint32_t crc = 0xFFFFFFFFu; + int ok = 1; + + s_ram_available = 1; + s_page_generation = 0; + for (uint32_t i = 0; i < RAM_SIZE; i++) + s_ram[i] = (uint8_t)((i * 37u + 11u) & 0xFFu); + crc = crc32_update(crc, s_ram + 0x1000u, 7u); + crc = crc32_update(crc, s_ram + 0x3000u, 9u) ^ 0xFFFFFFFFu; + overlay_loader_static_match_cache_clear(); + + ok &= expect_int("cold exact match", + psx_overlay_static_code_matches(ranges, 2u, crc), 1); + ok &= expect_static_match_stats(1u, 0u, 0u); + ok &= expect_int("warm exact match", + psx_overlay_static_code_matches(ranges, 2u, crc), 1); + ok &= expect_static_match_stats(1u, 0u, 1u); + + const uint8_t original = s_ram[0x3004u]; + s_ram[0x3004u] ^= 0x5Au; + s_page_generation++; + ok &= expect_int("replacement rejected", + psx_overlay_static_code_matches(ranges, 2u, crc), 0); + ok &= expect_static_match_stats(2u, 1u, 1u); + ok &= expect_int("warm replacement rejected", + psx_overlay_static_code_matches(ranges, 2u, crc), 0); + ok &= expect_static_match_stats(2u, 1u, 2u); + + s_ram[0x3004u] = original; + s_page_generation++; + ok &= expect_int("restored bytes accepted", + psx_overlay_static_code_matches(ranges, 2u, crc), 1); + ok &= expect_static_match_stats(3u, 1u, 2u); + ok &= expect_int("warm restored bytes accepted", + psx_overlay_static_code_matches(ranges, 2u, crc), 1); + ok &= expect_static_match_stats(3u, 1u, 3u); + + overlay_loader_static_match_cache_clear(); + ok &= expect_int("explicit reset forces rehash", + psx_overlay_static_code_matches(ranges, 2u, crc), 1); + ok &= expect_static_match_stats(4u, 1u, 3u); + + ok &= expect_int("null ranges rejected", + psx_overlay_static_code_matches(NULL, 1u, crc), 0); + ok &= expect_int("zero range count rejected", + psx_overlay_static_code_matches(ranges, 0u, crc), 0); + ok &= expect_int("range-count cap enforced", + psx_overlay_static_code_matches(ranges, 4097u, crc), 0); + ok &= expect_int("zero length rejected", + psx_overlay_static_code_matches(zero_length, 1u, crc), 0); + ok &= expect_int("out-of-bounds range rejected", + psx_overlay_static_code_matches(out_of_bounds, 1u, crc), 0); + s_ram_available = 0; + ok &= expect_int("missing RAM rejected", + psx_overlay_static_code_matches(ranges, 2u, crc), 0); + s_ram_available = 1; + ok &= expect_static_match_stats(4u, 7u, 3u); + + if (ok) + puts("PASS static-match replacement/restore/reset lifecycle"); + return ok ? 0 : 1; +} + static uint32_t loader_owner_count(void) { uint32_t loads = 0; overlay_loader_get_counters(&loads, NULL, NULL, NULL, NULL, NULL, @@ -273,6 +389,8 @@ static int reveal_second_pair(const char *second) { } int main(int argc, char **argv) { + if (argc == 2 && strcmp(argv[1], "--static-match-lifecycle") == 0) + return static_match_lifecycle(); if (argc != 5) { fprintf(stderr, "usage: %s \n", argv[0]); diff --git a/runtime/tests/test_func_override.c b/runtime/tests/test_func_override.c index 44cfa6bc7..820b38c93 100644 --- a/runtime/tests/test_func_override.c +++ b/runtime/tests/test_func_override.c @@ -204,6 +204,61 @@ static void test_add_rejects_bad_input(void) CHECK(func_override_add_guarded("t.gnull", 0x80001000u, impl_handles, NULL, 2, 0) == FO_ERR_ARGS, "guarded with NULL words must be refused"); + static const uint32_t range[2] = {0x80001000u, 4u}; + CHECK(func_override_add_exact("t.x0", 0x80001000u, impl_handles, + range, 0, 0, 0) == FO_ERR_ARGS, + "exact override with zero ranges must be refused"); + CHECK(func_override_add_package_exact("t.px0", 0x80001000u, impl_handles, + range, 0, 0, 0) == FO_ERR_ARGS, + "package exact override with zero ranges must be refused"); +} + +static void test_code_range_validator_edges(void) +{ + static const uint32_t valid_aliases[] = { + 0xA0000100u, 4u, + 0x80000200u, 8u, + }; + static const uint32_t adjacent[] = { + 0x80000100u, 4u, + 0x80000104u, 4u, + }; + static const uint32_t unaligned_start[] = {0x80000102u, 4u}; + static const uint32_t unaligned_length[] = {0x80000100u, 6u}; + static const uint32_t short_length[] = {0x80000100u, 0u}; + static const uint32_t start_outside_ram[] = {0x80200000u, 4u}; + static const uint32_t crosses_ram_end[] = {0x801FFFFCu, 8u}; + static const uint32_t misses_entry[] = {0x80000100u, 4u}; + static const uint32_t alias_overlap[] = { + 0x80000100u, 8u, + 0xA0000104u, 4u, + }; + + CHECK(!func_override_code_ranges_valid(0x80000100u, NULL, 1), + "NULL exact range array must be refused"); + CHECK(!func_override_code_ranges_valid(0x80000100u, valid_aliases, 0), + "zero exact ranges must be refused"); + CHECK(!func_override_code_ranges_valid(0x80000100u, valid_aliases, + FO_MAX_CODE_RANGES + 1), + "exact range count above the cap must be refused"); + CHECK(func_override_code_ranges_valid(0xA0000204u, valid_aliases, 2), + "KSEG aliases and discontiguous ranges must validate"); + CHECK(func_override_code_ranges_valid(0x80000104u, adjacent, 2), + "adjacent ranges must not count as overlap"); + CHECK(!func_override_code_ranges_valid(0x80000100u, unaligned_start, 1), + "unaligned exact range start must be refused"); + CHECK(!func_override_code_ranges_valid(0x80000100u, unaligned_length, 1), + "unaligned exact range length must be refused"); + CHECK(!func_override_code_ranges_valid(0x80000100u, short_length, 1), + "exact range shorter than one instruction must be refused"); + CHECK(!func_override_code_ranges_valid(0x80200000u, start_outside_ram, 1), + "exact range starting outside RAM must be refused"); + CHECK(!func_override_code_ranges_valid(0x801FFFFCu, crosses_ram_end, 1), + "exact range crossing the RAM end must be refused"); + CHECK(!func_override_code_ranges_valid(0x80000104u, misses_entry, 1), + "exact ranges must cover the override entry"); + CHECK(!func_override_code_ranges_valid(0x80000100u, alias_overlap, 2), + "overlap through KSEG aliases must be refused"); } static void test_duplicate_address_refused(void) @@ -630,6 +685,7 @@ int main(void) { test_install_is_null_until_registered(); test_add_rejects_bad_input(); + test_code_range_validator_edges(); test_duplicate_address_refused(); test_handled_and_declined_both_count_as_consults(); test_guard_declines_on_mismatch(); diff --git a/runtime/tests/test_overlay_pair_dedup_runtime.py b/runtime/tests/test_overlay_pair_dedup_runtime.py index 4f3cc1dac..389dc57cc 100644 --- a/runtime/tests/test_overlay_pair_dedup_runtime.py +++ b/runtime/tests/test_overlay_pair_dedup_runtime.py @@ -27,17 +27,25 @@ def codegen_leaf() -> str: hash_header = (RUNTIME / "include" / "overlay_codegen_hash.h") version = "0" code_hash = "00000000" + flavor = "0" if api.is_file(): match = re.search(r"PSX_OVERLAY_CODEGEN_VER\s+(\d+)", api.read_text(encoding="utf-8")) if match: version = match.group(1) + match = re.search(r"PSX_OVERLAY_FLAVOR\s+(\d+)", + api.read_text(encoding="utf-8")) + if match: + flavor = match.group(1) if hash_header.is_file(): match = re.search(r"PSX_OVERLAY_CODEGEN_HASH\s+0x([0-9A-Fa-f]{8})", hash_header.read_text(encoding="utf-8")) if match: code_hash = match.group(1).lower() - return f"cg{version}_{code_hash}" + # overlay_loader_init below uses config hash zero. Keep the fixture on the + # runtime's complete cache identity so it reaches the loader behavior under + # test instead of stopping at the stale-cache guard (FAIL-061). + return f"cg{version}_{code_hash}_gc00000000_f{flavor}" def arch_abi() -> str: @@ -181,10 +189,11 @@ def main() -> int: compile_fixture(args.gcc, full_second, instance=2) compile_fixture(args.gcc, partial, instance=1, partial=True) compile_harness(args.gcc, harness) + run([str(harness), "--static-match-lifecycle"]) for name in ("alias-at-cap", "manifest-mismatch", "provenance-mismatch", "cross-tier", "partial-first"): scenario(tmp, harness, full_first, full_second, partial, name) - print("PASS: executable overlay whole-pair dedup behavior") + print("PASS: executable overlay lifecycle and whole-pair dedup behavior") return 0 diff --git a/tools/aot_overlay_spike/README.md b/tools/aot_overlay_spike/README.md index d524b27ca..2a60fedcd 100644 --- a/tools/aot_overlay_spike/README.md +++ b/tools/aot_overlay_spike/README.md @@ -144,7 +144,9 @@ optional coverage to the next command. `runtime/tests/test_overlay_pair_dedup_runtime.py` compiles the real loader at a four-slot test cap plus real shared-library fixtures, and behaviorally covers an exact alias at capacity, handle/init/flush ownership, staged rescan idempotence, -manifest/provenance/tier negatives, and partial-export non-authority. +manifest/provenance/tier negatives, and partial-export non-authority. The same +test also covers static code-identity cache replacement, restoration, reset, +and invalid-range rejection against the real loader. Dynamic compilation serializes canonically sorted recipes under a whole-command namespace lock so a full cache has the same accepted subset on every clean run. GCC-first basename shadowing matches the loader, and non-growing replacements From 7c5f6294a4dece7527f187f1557d5977c7c64f32 Mon Sep 17 00:00:00 2001 From: Alexandros Mandravillis Date: Tue, 1 Sep 2026 02:47:40 +0300 Subject: [PATCH 3/9] Fix fork review findings --- docs/TESTING.md | 16 +++++++------ recompiler/CMakeLists.txt | 21 ++++++++++------- runtime/src/dirty_ram_interp.c | 26 ++++++++++++++-------- runtime/src/mod_runtime.cpp | 3 ++- runtime/tests/test_func_override_routes.py | 18 +++++++-------- 5 files changed, 50 insertions(+), 34 deletions(-) diff --git a/docs/TESTING.md b/docs/TESTING.md index 43b63ab3a..36947f7c2 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -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 registers it only when CMake +finds a GCC-compatible C compiler (`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` | Registered as `overlay_pair_dedup_runtime`. The Python driver builds its companion harness and redistributable shared-library fixtures. It covers exact static-code identity, cache replacement and restore behavior, pair publication, deduplication, and rejection boundaries. | ## Tests that are not in `ctest` and should not be diff --git a/recompiler/CMakeLists.txt b/recompiler/CMakeLists.txt index 79da2551b..f8429a95a 100644 --- a/recompiler/CMakeLists.txt +++ b/recompiler/CMakeLists.txt @@ -628,14 +628,19 @@ if(BUILD_TESTING) WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/..) endforeach() - # This test builds redistributable fixture DLLs and links a portable - # harness against the real overlay loader. It covers publication-pair - # deduplication plus static code-identity replacement/restore caching. - add_test( - NAME overlay_pair_dedup_runtime - COMMAND ${Python3_EXECUTABLE} - ${CMAKE_CURRENT_SOURCE_DIR}/../runtime/tests/test_overlay_pair_dedup_runtime.py - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/..) + # 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. + find_program(PSX_OVERLAY_FIXTURE_CC NAMES gcc cc clang) + if(PSX_OVERLAY_FIXTURE_CC) + 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}/..) + endif() # ---- known-failing guards: DISABLED, not de-registered --------------- # These two were previously left out of the list above with a comment diff --git a/runtime/src/dirty_ram_interp.c b/runtime/src/dirty_ram_interp.c index cdbdcd6d8..df41fa6b6 100644 --- a/runtime/src/dirty_ram_interp.c +++ b/runtime/src/dirty_ram_interp.c @@ -3211,20 +3211,29 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_ OV_FPLOG_RET1(); } target = cpu->pc; - /* J/JR is an unlinked function tail entry. It has the same guest - * return obligation as a call but reuses the caller's $ra. Check - * the override after the interrupt safe point and before local - * dirty flow, then surface the continuation so dispatch stays - * flat. Conditional branches are intra-function flow and do not - * use this function-entry hook. Precise/replay mode keeps the - * same plain-transfer policy as JAL/JALR above. */ + uint32_t target_phys = target & 0x1FFFFFFFu; + /* A J/JR can be an unlinked function tail entry or an ordinary + * intra-function jump. Only a known static or overlay function + * entry proves the former. Check the override after the interrupt + * safe point and before local dirty flow, then surface the + * continuation so dispatch stays flat. Precise/replay mode keeps + * the same plain-transfer policy as JAL/JALR above. */ { const uint32_t opc = insn >> 26; const int unlinked_tail = opc == 0x02u || (opc == 0x00u && (insn & 0x3Fu) == 0x08u && ((insn >> 21) & 0x1Fu) != 31u); - if (unlinked_tail && !g_precise_mode && !g_ls_replay_active && + int proven_tail_entry = + target != 0u && target_phys != phys && + overlay_loader_is_candidate(target_phys); +#ifdef PSX_HAS_GAME_DISPATCH + if (!proven_tail_entry && target != 0u && + target_phys != phys) + proven_tail_entry = psx_game_is_function_entry(target); +#endif + if (unlinked_tail && proven_tail_entry && + !g_precise_mode && !g_ls_replay_active && func_override_try_dispatch(cpu, target, cpu->gpr[31])) { g_dirty_ram_blocks_run++; if (pc_entry) pc_entry->insns += (uint64_t)insns_executed; @@ -3261,7 +3270,6 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_ cpu->pc = target; /* surfaced; trampoline re-dispatches flat */ } #endif - uint32_t target_phys = target & 0x1FFFFFFFu; if (allow_local_dirty_flow && target != 0 && target != stop_addr && phys_is_overlay_flow_region(target_phys) && diff --git a/runtime/src/mod_runtime.cpp b/runtime/src/mod_runtime.cpp index 661a9235e..4deded850 100644 --- a/runtime/src/mod_runtime.cpp +++ b/runtime/src/mod_runtime.cpp @@ -1400,7 +1400,8 @@ extern "C" void mod_runtime_activate_plugins(void) { s.error = out.str(); s.plan.ok = false; func_override_install(); - std::fprintf(stderr, "psxrecomp: %s\n", s.error.c_str()); + /* The launcher reads s.error through provider_error. Keep this + * failure on that existing diagnostics boundary. */ return; } } diff --git a/runtime/tests/test_func_override_routes.py b/runtime/tests/test_func_override_routes.py index 9e4ed8ab4..14dc4d7fe 100644 --- a/runtime/tests/test_func_override_routes.py +++ b/runtime/tests/test_func_override_routes.py @@ -1,14 +1,11 @@ #!/usr/bin/env python3 -"""Pin the generated and dirty-RAM function-override entry routes.""" +"""Pin the dirty-RAM function-override entry route.""" from pathlib import Path ROOT = Path(__file__).resolve().parents[2] DIRTY = (ROOT / "runtime/src/dirty_ram_interp.c").read_text(encoding="utf-8") -EMITTER = (ROOT / "recompiler/src/full_function_emitter.cpp").read_text( - encoding="utf-8" -) def require(condition: bool, message: str) -> None: @@ -27,11 +24,14 @@ def main() -> None: "tail override entry must keep precise/replay plain-transfer policy") require("((insn >> 21) & 0x1Fu) != 31u" in DIRTY[pump:local], "JR $ra returns must not be treated as function tail entries") - require("func_override_try_dispatch(cpu, addr, cpu->gpr[31])" in EMITTER, - "generated dispatch must use the continuation-preserving helper") - require("g_psx_func_override_hook(cpu, addr & 0x1FFFFFFFu)) {\n" - " cpu->pc = cpu->gpr[31];" not in EMITTER, - "generated dispatch must not overwrite non-local continuations") + require("proven_tail_entry" in DIRTY[pump:tail], + "J/JR override entry must require a proven function entry") + require("overlay_loader_is_candidate(target_phys)" in DIRTY[pump:tail], + "dynamic tail entry must require an exact overlay entry") + require("psx_game_is_function_entry(target)" in DIRTY[pump:tail], + "static tail entry must require a generated function entry") + require("target_phys != phys" in DIRTY[pump:tail], + "an intra-function jump to the current entry must not re-consult") print("function override entry routes passed") From 15f336252dbb4c4e8410e6f25a4c2cbee16af1ce Mon Sep 17 00:00:00 2001 From: Alexandros Mandravillis Date: Tue, 1 Sep 2026 02:57:21 +0300 Subject: [PATCH 4/9] Require dirty tail provenance --- docs/TESTING.md | 4 ++-- recompiler/CMakeLists.txt | 19 +++++++++++-------- runtime/src/dirty_ram_interp.c | 17 ++++++++++++----- runtime/tests/test_func_override_routes.py | 11 ++++++++--- 4 files changed, 33 insertions(+), 18 deletions(-) diff --git a/docs/TESTING.md b/docs/TESTING.md index 36947f7c2..e4e6bead5 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -59,8 +59,8 @@ runtime tree cannot configure until a BIOS has been generated, and these need neither. The `overlay_pair_dedup_runtime` test is an executable loader test, not a -source-invariant guard. A plain recompiler build registers it only when CMake -finds a GCC-compatible C compiler (`gcc`, `cc`, or `clang`). The test builds +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. ## Known-failing tests (registered as disabled) diff --git a/recompiler/CMakeLists.txt b/recompiler/CMakeLists.txt index f8429a95a..1f0c62f4d 100644 --- a/recompiler/CMakeLists.txt +++ b/recompiler/CMakeLists.txt @@ -631,15 +631,18 @@ if(BUILD_TESTING) # 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. + # 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) - if(PSX_OVERLAY_FIXTURE_CC) - 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}/..) + 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 --------------- diff --git a/runtime/src/dirty_ram_interp.c b/runtime/src/dirty_ram_interp.c index df41fa6b6..81bd093f4 100644 --- a/runtime/src/dirty_ram_interp.c +++ b/runtime/src/dirty_ram_interp.c @@ -3224,14 +3224,21 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_ opc == 0x02u || (opc == 0x00u && (insn & 0x3Fu) == 0x08u && ((insn >> 21) & 0x1Fu) != 31u); - int proven_tail_entry = - target != 0u && target_phys != phys && + int current_is_function_entry = + overlay_loader_is_candidate(phys); + int target_is_function_entry = overlay_loader_is_candidate(target_phys); #ifdef PSX_HAS_GAME_DISPATCH - if (!proven_tail_entry && target != 0u && - target_phys != phys) - proven_tail_entry = psx_game_is_function_entry(target); + if (!current_is_function_entry) + current_is_function_entry = + psx_game_is_function_entry(addr); + if (!target_is_function_entry) + target_is_function_entry = + psx_game_is_function_entry(target); #endif + const int proven_tail_entry = + target != 0u && target_phys != phys && + current_is_function_entry && target_is_function_entry; if (unlinked_tail && proven_tail_entry && !g_precise_mode && !g_ls_replay_active && func_override_try_dispatch(cpu, target, cpu->gpr[31])) { diff --git a/runtime/tests/test_func_override_routes.py b/runtime/tests/test_func_override_routes.py index 14dc4d7fe..a5896372e 100644 --- a/runtime/tests/test_func_override_routes.py +++ b/runtime/tests/test_func_override_routes.py @@ -24,14 +24,19 @@ def main() -> None: "tail override entry must keep precise/replay plain-transfer policy") require("((insn >> 21) & 0x1Fu) != 31u" in DIRTY[pump:local], "JR $ra returns must not be treated as function tail entries") - require("proven_tail_entry" in DIRTY[pump:tail], - "J/JR override entry must require a proven function entry") + require("current_is_function_entry" in DIRTY[pump:tail], + "J/JR override entry must prove the current function entry") + require("target_is_function_entry" in DIRTY[pump:tail], + "J/JR override entry must prove the target function entry") + require("current_is_function_entry && target_is_function_entry" in + DIRTY[pump:tail], + "J/JR override entry must require both proven endpoints") require("overlay_loader_is_candidate(target_phys)" in DIRTY[pump:tail], "dynamic tail entry must require an exact overlay entry") require("psx_game_is_function_entry(target)" in DIRTY[pump:tail], "static tail entry must require a generated function entry") require("target_phys != phys" in DIRTY[pump:tail], - "an intra-function jump to the current entry must not re-consult") + "a jump back to the current function entry must not re-consult") print("function override entry routes passed") From 2183cc99605b3e1cc254a30ed4fc28d2079100bf Mon Sep 17 00:00:00 2001 From: Alexandros Mandravillis Date: Tue, 1 Sep 2026 03:10:07 +0300 Subject: [PATCH 5/9] Close late override failure paths --- docs/MOD_PACKAGES.md | 5 +- runtime/CMakeLists.txt | 2 + runtime/src/dirty_ram_interp.c | 59 ++++++++++++---------- runtime/src/mod_runtime.cpp | 20 +++++++- runtime/tests/test_func_override_routes.py | 16 +++--- runtime/tests/test_mod_runtime.cpp | 33 ++++++++++++ 6 files changed, 97 insertions(+), 38 deletions(-) diff --git a/docs/MOD_PACKAGES.md b/docs/MOD_PACKAGES.md index 5d50db81a..94da74191 100644 --- a/docs/MOD_PACKAGES.md +++ b/docs/MOD_PACKAGES.md @@ -331,8 +331,9 @@ 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. Invalid -plugin ids fail during registration. +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. 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. diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 73a1f9862..dd08959fc 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -297,6 +297,8 @@ 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 diff --git a/runtime/src/dirty_ram_interp.c b/runtime/src/dirty_ram_interp.c index 81bd093f4..440a49ee0 100644 --- a/runtime/src/dirty_ram_interp.c +++ b/runtime/src/dirty_ram_interp.c @@ -2870,6 +2870,13 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_ int _ovfp = overlay_fp_enabled() && overlay_cache_window_contains(phys) && overlay_loader_is_candidate(phys); + uint32_t current_function_entry_phys = + overlay_loader_is_candidate(phys) ? phys : 0u; +#ifdef PSX_HAS_GAME_DISPATCH + if (current_function_entry_phys == 0u && + psx_game_is_function_entry(addr)) + current_function_entry_phys = phys; +#endif uint32_t _in_regs[34]; if (_ovfp) { overlay_regs_snap(_in_regs, cpu); @@ -3218,35 +3225,31 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_ * safe point and before local dirty flow, then surface the * continuation so dispatch stays flat. Precise/replay mode keeps * the same plain-transfer policy as JAL/JALR above. */ - { - const uint32_t opc = insn >> 26; - const int unlinked_tail = - opc == 0x02u || - (opc == 0x00u && (insn & 0x3Fu) == 0x08u && - ((insn >> 21) & 0x1Fu) != 31u); - int current_is_function_entry = - overlay_loader_is_candidate(phys); - int target_is_function_entry = - overlay_loader_is_candidate(target_phys); + const uint32_t opc = insn >> 26; + const int unlinked_tail = + opc == 0x02u || + (opc == 0x00u && (insn & 0x3Fu) == 0x08u && + ((insn >> 21) & 0x1Fu) != 31u); + int target_is_function_entry = + overlay_loader_is_candidate(target_phys); #ifdef PSX_HAS_GAME_DISPATCH - if (!current_is_function_entry) - current_is_function_entry = - psx_game_is_function_entry(addr); - if (!target_is_function_entry) - target_is_function_entry = - psx_game_is_function_entry(target); + if (!target_is_function_entry) + target_is_function_entry = + psx_game_is_function_entry(target); #endif - const int proven_tail_entry = - target != 0u && target_phys != phys && - current_is_function_entry && target_is_function_entry; - if (unlinked_tail && proven_tail_entry && - !g_precise_mode && !g_ls_replay_active && - func_override_try_dispatch(cpu, target, cpu->gpr[31])) { - g_dirty_ram_blocks_run++; - if (pc_entry) pc_entry->insns += (uint64_t)insns_executed; - g_dirty_interp_chain_target = cpu->pc; - OV_FPLOG_RET1(); - } + const uint32_t source_phys = pc & 0x1FFFFFFFu; + const int proven_tail_entry = + target != 0u && target_phys != source_phys && + current_function_entry_phys != 0u && + target_phys != current_function_entry_phys && + target_is_function_entry; + if (unlinked_tail && proven_tail_entry && + !g_precise_mode && !g_ls_replay_active && + func_override_try_dispatch(cpu, target, cpu->gpr[31])) { + g_dirty_ram_blocks_run++; + if (pc_entry) pc_entry->insns += (uint64_t)insns_executed; + g_dirty_interp_chain_target = cpu->pc; + OV_FPLOG_RET1(); } target = cpu->pc; #ifdef PSX_HAS_GAME_DISPATCH @@ -3281,6 +3284,8 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_ target != stop_addr && phys_is_overlay_flow_region(target_phys) && dirty_ram_is_dirty(target_phys)) { + if (unlinked_tail && target_is_function_entry) + current_function_entry_phys = target_phys; /* A runtime overlay may start executing while its final code * bytes are still being installed. Entry-time native validation * must reject that partial image, but local dirty flow used to diff --git a/runtime/src/mod_runtime.cpp b/runtime/src/mod_runtime.cpp index 4deded850..759a0a0fa 100644 --- a/runtime/src/mod_runtime.cpp +++ b/runtime/src/mod_runtime.cpp @@ -1367,6 +1367,12 @@ extern "C" void mod_runtime_enable_disc_patches(void) { PSXRecompV4::state().disc_enabled = true; } +#ifdef PSX_MOD_RUNTIME_TEST +extern "C" void mod_runtime_clear_function_override_plugins_for_tests(void) { + PSXRecompV4::function_override_plugins().clear(); +} +#endif + extern "C" void mod_runtime_activate_plugins(void) { using namespace PSXRecompV4; RuntimeMods& s = state(); @@ -1398,7 +1404,19 @@ extern "C" void mod_runtime_activate_plugins(void) { out << "function override activation failed for " << pending.id << " (error " << rc << ")"; s.error = out.str(); - s.plan.ok = false; + /* A late table change invalidated the committed selection. Drop + * every part of that plan. Main-RAM writes, disc patches, and + * callbacks must not continue after its override set failed. */ + s.plan = {}; + s.validation = {}; + s.raw_disc_index.clear(); + s.user_disc_index.clear(); + s.raw_overlay_index.clear(); + s.user_overlay_index.clear(); + s.effective_disc_path.clear(); + s.main_applied = true; + s.disc_enabled = false; + s.disc_guard_failed = true; func_override_install(); /* The launcher reads s.error through provider_error. Keep this * failure on that existing diagnostics boundary. */ diff --git a/runtime/tests/test_func_override_routes.py b/runtime/tests/test_func_override_routes.py index a5896372e..cc46203c2 100644 --- a/runtime/tests/test_func_override_routes.py +++ b/runtime/tests/test_func_override_routes.py @@ -18,25 +18,25 @@ def main() -> None: pump = DIRTY.index("dirty_ram_pump_boundary(cpu, target, 1)") tail = DIRTY.index(helper, pump) local = DIRTY.index("if (allow_local_dirty_flow", tail) - require(pump < tail < local, + update = DIRTY.index("current_function_entry_phys = target_phys", local) + require(pump < tail < local < update, "J/JR override entry must follow the IRQ safe point and precede local flow") require("!g_precise_mode && !g_ls_replay_active" in DIRTY[pump:local], "tail override entry must keep precise/replay plain-transfer policy") require("((insn >> 21) & 0x1Fu) != 31u" in DIRTY[pump:local], "JR $ra returns must not be treated as function tail entries") - require("current_is_function_entry" in DIRTY[pump:tail], - "J/JR override entry must prove the current function entry") + require("current_function_entry_phys" in DIRTY[pump:tail], + "J/JR override entry must track current function provenance") require("target_is_function_entry" in DIRTY[pump:tail], "J/JR override entry must prove the target function entry") - require("current_is_function_entry && target_is_function_entry" in - DIRTY[pump:tail], - "J/JR override entry must require both proven endpoints") + require("source_phys = pc & 0x1FFFFFFFu" in DIRTY[pump:tail], + "J/JR override entry must use the current transfer source") + require("target_phys != current_function_entry_phys" in DIRTY[pump:tail], + "a back-edge to the current function entry must not re-consult") require("overlay_loader_is_candidate(target_phys)" in DIRTY[pump:tail], "dynamic tail entry must require an exact overlay entry") require("psx_game_is_function_entry(target)" in DIRTY[pump:tail], "static tail entry must require a generated function entry") - require("target_phys != phys" in DIRTY[pump:tail], - "a jump back to the current function entry must not re-consult") print("function override entry routes passed") diff --git a/runtime/tests/test_mod_runtime.cpp b/runtime/tests/test_mod_runtime.cpp index f2bd48e75..6ae864c17 100644 --- a/runtime/tests/test_mod_runtime.cpp +++ b/runtime/tests/test_mod_runtime.cpp @@ -102,6 +102,7 @@ static void test_activation_plugin(void) { } static int test_function_override(CPUState*) { return 1; } +extern "C" void mod_runtime_clear_function_override_plugins_for_tests(void); static void check(bool value, const char* message) { if (!value) { @@ -551,6 +552,38 @@ int main() { check(func_override_count() == 0, "a rejected package override set must arm no partial entries"); + /* A direct registration can race with the interval between plan commit + * and package activation. The late duplicate must disable the complete + * committed plan, not apply its main or disc patches without its + * function override. */ + PSXRecompV4::mod_clear_plugins_for_tests(); + mod_runtime_clear_function_override_plugins_for_tests(); + check(psx_mod_register_function_override_exact( + "runtime.test-vblank:race", 0x80018000u, + test_function_override, exact_ranges, 1, 0x12345678u, 0) == 1, + "race package override must queue"); + check(PSXRecompV4::mod_runtime_initialize( + root, "SLUS-RUNTIME", 0x80002000, {}, &error), + error.c_str()); + check(PSXRecompV4::mod_runtime_commit(stock_path, &error), error.c_str()); + check(func_override_add("runtime.direct-race", 0x80018000u, + test_function_override, 0) == FO_OK, + "direct override must occupy the committed package address"); + mod_runtime_activate_plugins(); + ram[0x1000] = 1; ram[0x1001] = 2; ram[0x1002] = 3; ram[0x1003] = 4; + mod_runtime_on_dispatch(0x80002000); + check(ram[0x1000] == 1 && ram[0x1003] == 4, + "late override failure must disable main-memory plan writes"); + mod_runtime_enable_disc_patches(); + std::array disabled_disc{}; + disabled_disc[15] = 2; + disabled_disc[18] = 0; + disabled_disc[24 + 10] = 0xcc; + mod_runtime_patch_disc_sector( + 3, 1, disabled_disc.data(), (uint32_t)disabled_disc.size()); + check(disabled_disc[24 + 10] == 0xcc, + "late override failure must disable disc plan writes"); + fs::remove_all(root, ec); if (failures) return 1; std::cout << "mod runtime tests passed\n"; From 345f1a1a88d29551a46e1fea6a4c828170e0d583 Mon Sep 17 00:00:00 2001 From: Alexandros Mandravillis Date: Tue, 1 Sep 2026 03:21:21 +0300 Subject: [PATCH 6/9] Restore disc patches after retry --- docs/MOD_PACKAGES.md | 3 ++- runtime/src/mod_runtime.cpp | 1 + runtime/tests/test_mod_runtime.cpp | 22 ++++++++++++++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/docs/MOD_PACKAGES.md b/docs/MOD_PACKAGES.md index 94da74191..329e71b91 100644 --- a/docs/MOD_PACKAGES.md +++ b/docs/MOD_PACKAGES.md @@ -333,7 +333,8 @@ 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. Invalid plugin ids fail during registration. +selected plan. A later successful commit clears this failure state. Invalid +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. diff --git a/runtime/src/mod_runtime.cpp b/runtime/src/mod_runtime.cpp index 759a0a0fa..d38dd5f95 100644 --- a/runtime/src/mod_runtime.cpp +++ b/runtime/src/mod_runtime.cpp @@ -1283,6 +1283,7 @@ bool mod_runtime_commit(const std::filesystem::path& disc_path, std::string* err build_disc_index(s); s.effective_disc_path = std::move(effective_disc); s.main_applied = false; + s.disc_guard_failed = false; s.error.clear(); return true; } diff --git a/runtime/tests/test_mod_runtime.cpp b/runtime/tests/test_mod_runtime.cpp index 6ae864c17..a334fc123 100644 --- a/runtime/tests/test_mod_runtime.cpp +++ b/runtime/tests/test_mod_runtime.cpp @@ -584,6 +584,28 @@ int main() { check(disabled_disc[24 + 10] == 0xcc, "late override failure must disable disc plan writes"); + /* Remove the package registration that conflicted with the direct entry. + * A new successful commit must clear the failure latch and make its disc + * plan usable again. */ + mod_runtime_clear_function_override_plugins_for_tests(); + check(PSXRecompV4::mod_runtime_commit(stock_path, &error), error.c_str()); + mod_runtime_activate_plugins(); + check(func_override_count() == 1, + "a corrected plan must preserve the direct override"); + mod_runtime_enable_disc_patches(); + std::array recovered_disc{}; + recovered_disc[15] = 2; + recovered_disc[18] = 0; + recovered_disc[24 + 10] = 0xcc; + recovered_disc[24 + 20] = 0x11; + recovered_disc[24 + 21] = 0x22; + recovered_disc[24 + 22] = 0x33; + recovered_disc[24 + 23] = 0x44; + mod_runtime_patch_disc_sector( + 3, 1, recovered_disc.data(), (uint32_t)recovered_disc.size()); + check(recovered_disc[24 + 10] == 0xdd, + "a corrected plan must recover disc patching after late failure"); + fs::remove_all(root, ec); if (failures) return 1; std::cout << "mod runtime tests passed\n"; From ec7fccb13e9d0611faa3161dcaf489662404ac7e Mon Sep 17 00:00:00 2001 From: Alexandros Mandravillis Date: Tue, 1 Sep 2026 03:28:49 +0300 Subject: [PATCH 7/9] Preserve disc patch enable state --- runtime/src/mod_runtime.cpp | 1 - runtime/tests/test_mod_runtime.cpp | 1 - 2 files changed, 2 deletions(-) diff --git a/runtime/src/mod_runtime.cpp b/runtime/src/mod_runtime.cpp index d38dd5f95..fafa27186 100644 --- a/runtime/src/mod_runtime.cpp +++ b/runtime/src/mod_runtime.cpp @@ -1416,7 +1416,6 @@ extern "C" void mod_runtime_activate_plugins(void) { s.user_overlay_index.clear(); s.effective_disc_path.clear(); s.main_applied = true; - s.disc_enabled = false; s.disc_guard_failed = true; func_override_install(); /* The launcher reads s.error through provider_error. Keep this diff --git a/runtime/tests/test_mod_runtime.cpp b/runtime/tests/test_mod_runtime.cpp index a334fc123..b4a851024 100644 --- a/runtime/tests/test_mod_runtime.cpp +++ b/runtime/tests/test_mod_runtime.cpp @@ -592,7 +592,6 @@ int main() { mod_runtime_activate_plugins(); check(func_override_count() == 1, "a corrected plan must preserve the direct override"); - mod_runtime_enable_disc_patches(); std::array recovered_disc{}; recovered_disc[15] = 2; recovered_disc[18] = 0; From 7863541ef2ee8f2d0c6fc78833299b45e4908332 Mon Sep 17 00:00:00 2001 From: Alexandros Mandravillis Date: Wed, 16 Sep 2026 21:25:10 +0300 Subject: [PATCH 8/9] runtime: skip override entry lookups when no hook is armed The J/JR tail-entry route ran overlay_loader_is_candidate and psx_game_is_function_entry on every interpreted transfer, and both again at each dirty_ram_dispatch_inner entry, before anything checked g_psx_func_override_hook. With no override armed those lookups can only feed a route that declines, so test the hook first. Provenance stays zero while the hook is NULL, which can only make the route decline. The route test now pins the hook test ahead of the lookups. --- runtime/src/dirty_ram_interp.c | 35 +++++++++++++++------- runtime/tests/test_func_override_routes.py | 8 +++++ 2 files changed, 33 insertions(+), 10 deletions(-) diff --git a/runtime/src/dirty_ram_interp.c b/runtime/src/dirty_ram_interp.c index 440a49ee0..35306fed3 100644 --- a/runtime/src/dirty_ram_interp.c +++ b/runtime/src/dirty_ram_interp.c @@ -484,6 +484,9 @@ uint32_t g_overlay_region_floor = OVERLAY_REGION_FLOOR_DEFAULT; * high-loading game pins it; main.cpp sets it at game load. See the header. */ uint32_t g_text_image_lo = DIRTY_RAM_KERNEL_WINDOW_END; +/* func_override.c: NULL unless an override is armed. */ +extern int (*g_psx_func_override_hook)(CPUState *cpu, uint32_t phys); + #ifdef PSX_HAS_GAME_DISPATCH extern int psx_dispatch_game_compiled(CPUState* cpu, uint32_t addr); extern int psx_game_address_in_text(uint32_t addr); @@ -2870,13 +2873,19 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_ int _ovfp = overlay_fp_enabled() && overlay_cache_window_contains(phys) && overlay_loader_is_candidate(phys); - uint32_t current_function_entry_phys = - overlay_loader_is_candidate(phys) ? phys : 0u; + /* Function-entry provenance only feeds the J/JR override route below. + * With no override armed, skip both entry lookups so a build without + * overrides pays nothing here. Zero is the conservative value: it can + * only make the route decline, never consult on an unproven entry. */ + uint32_t current_function_entry_phys = 0u; + if (g_psx_func_override_hook) { + if (overlay_loader_is_candidate(phys)) + current_function_entry_phys = phys; #ifdef PSX_HAS_GAME_DISPATCH - if (current_function_entry_phys == 0u && - psx_game_is_function_entry(addr)) - current_function_entry_phys = phys; + else if (psx_game_is_function_entry(addr)) + current_function_entry_phys = phys; #endif + } uint32_t _in_regs[34]; if (_ovfp) { overlay_regs_snap(_in_regs, cpu); @@ -3230,13 +3239,19 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_ opc == 0x02u || (opc == 0x00u && (insn & 0x3Fu) == 0x08u && ((insn >> 21) & 0x1Fu) != 31u); - int target_is_function_entry = - overlay_loader_is_candidate(target_phys); -#ifdef PSX_HAS_GAME_DISPATCH - if (!target_is_function_entry) + /* Test the hook before the entry lookups: every interpreted + * J/JR reaches this point, and with no override armed the + * lookups can only feed a route that declines anyway. */ + int target_is_function_entry = 0; + if (unlinked_tail && g_psx_func_override_hook) { target_is_function_entry = - psx_game_is_function_entry(target); + overlay_loader_is_candidate(target_phys); +#ifdef PSX_HAS_GAME_DISPATCH + if (!target_is_function_entry) + target_is_function_entry = + psx_game_is_function_entry(target); #endif + } const uint32_t source_phys = pc & 0x1FFFFFFFu; const int proven_tail_entry = target != 0u && target_phys != source_phys && diff --git a/runtime/tests/test_func_override_routes.py b/runtime/tests/test_func_override_routes.py index cc46203c2..4e9adfe6b 100644 --- a/runtime/tests/test_func_override_routes.py +++ b/runtime/tests/test_func_override_routes.py @@ -37,6 +37,14 @@ def main() -> None: "dynamic tail entry must require an exact overlay entry") require("psx_game_is_function_entry(target)" in DIRTY[pump:tail], "static tail entry must require a generated function entry") + gate = DIRTY.find("if (unlinked_tail && g_psx_func_override_hook)", pump, tail) + lookup = DIRTY.find("overlay_loader_is_candidate(target_phys)", pump, tail) + require(0 <= gate < lookup, + "J/JR entry lookups must wait for an armed override hook") + entry = DIRTY.index("uint32_t current_function_entry_phys = 0u;") + require(DIRTY.find("if (g_psx_func_override_hook)", entry, entry + 400) + > entry, + "dispatch entry provenance lookups must wait for an armed hook") print("function override entry routes passed") From ae97ae7ae9f0b033efd3aaf766e478953be24584 Mon Sep 17 00:00:00 2001 From: Alexandros Mandravillis Date: Wed, 16 Sep 2026 21:25:10 +0300 Subject: [PATCH 9/9] mods: re-arm package overrides on an offline rematch mod_runtime_commit now disarms the previous package override set, but the offline lobby rematch commits and then jumps to session_reboot, past mod_runtime_activate_plugins. The rematch ran with the selected overrides silently off. Split the arming half of activation into mod_runtime_arm_function_overrides and call it after the rematch commit. Activation callbacks still do not re-run on rematch, as before. A late collision fails closed like activation and now prints why instead of staying silent. --- runtime/include/mod_runtime.h | 7 +++++++ runtime/src/main.cpp | 10 ++++++++++ runtime/src/mod_runtime.cpp | 29 +++++++++++++++++++++-------- runtime/tests/test_mod_runtime.cpp | 13 +++++++++++++ 4 files changed, 51 insertions(+), 8 deletions(-) diff --git a/runtime/include/mod_runtime.h b/runtime/include/mod_runtime.h index 20a3b8479..43a7a34dc 100644 --- a/runtime/include/mod_runtime.h +++ b/runtime/include/mod_runtime.h @@ -22,6 +22,13 @@ bool mod_runtime_commit(const std::filesystem::path& disc_path = {}, * user's persisted offline selection on disk. Netplay is always vanilla for * now (no synced mod plans). */ bool mod_runtime_clear_for_netplay(std::string* error = nullptr); +/* Arm the committed plan's package function overrides without running + * activation callbacks. mod_runtime_commit disarms the previous set, so a + * path that commits without activating (the offline lobby rematch, which + * jumps past mod_runtime_activate_plugins) must re-arm here or the selected + * overrides stay silently off. Fails closed like activation: on a late + * table collision the whole plan is dropped and *error names the entry. */ +bool mod_runtime_arm_function_overrides(std::string* error = nullptr); const std::string& mod_runtime_fingerprint(); const std::filesystem::path& mod_runtime_effective_disc_path(); diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index 4d6c00816..681d15fcc 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -14742,6 +14742,16 @@ int main(int argc, char** argv) { mod_error.c_str()); SDL_Quit(); return 1; + } else if (!PSXRecompV4::mod_runtime_arm_function_overrides( + &mod_error)) { + /* Commit disarmed the previous override set and + * session_reboot is past mod_runtime_activate_plugins, + * so re-arm here. On failure the plan is dropped and the + * rematch runs vanilla; say so rather than stay silent. */ + std::fprintf(stderr, + "psxrecomp: selected mods disabled for " + "rematch: %s\n", + mod_error.c_str()); } } apply_netplay_local_viewport_aspect(net_cfg.enabled); diff --git a/runtime/src/mod_runtime.cpp b/runtime/src/mod_runtime.cpp index fafa27186..f54cd713a 100644 --- a/runtime/src/mod_runtime.cpp +++ b/runtime/src/mod_runtime.cpp @@ -1374,14 +1374,14 @@ extern "C" void mod_runtime_clear_function_override_plugins_for_tests(void) { } #endif -extern "C" void mod_runtime_activate_plugins(void) { - using namespace PSXRecompV4; +namespace PSXRecompV4 { + +bool mod_runtime_arm_function_overrides(std::string* error) { RuntimeMods& s = state(); - if (!s.initialized || !s.plan.ok) return; - /* Arm the complete selected override set before any activation callback - * can change host or guest state. Commit already preflighted collisions - * and capacity. If the table changed after commit, roll back the complete - * package set and fail closed instead of silently arming a subset. */ + if (!s.initialized || !s.plan.ok) return true; + /* Commit already preflighted collisions and capacity. If the table + * changed after commit, roll back the complete package set and fail + * closed instead of silently arming a subset. */ func_override_reset_package_armed(); for (FunctionOverridePlugin& pending : function_override_plugins()) pending.armed = false; @@ -1420,10 +1420,23 @@ extern "C" void mod_runtime_activate_plugins(void) { func_override_install(); /* The launcher reads s.error through provider_error. Keep this * failure on that existing diagnostics boundary. */ - return; + if (error) *error = s.error; + return false; } } func_override_install(); + return true; +} + +} // namespace PSXRecompV4 + +extern "C" void mod_runtime_activate_plugins(void) { + using namespace PSXRecompV4; + RuntimeMods& s = state(); + if (!s.initialized || !s.plan.ok) return; + /* Arm the complete selected override set before any activation callback + * can change host or guest state. */ + if (!mod_runtime_arm_function_overrides()) return; for (const ModResolution::Plugin& plugin : s.plan.plugins) { s.current_plugin = &plugin; mod_invoke_activation_plugin(plugin.id); diff --git a/runtime/tests/test_mod_runtime.cpp b/runtime/tests/test_mod_runtime.cpp index b4a851024..24846115b 100644 --- a/runtime/tests/test_mod_runtime.cpp +++ b/runtime/tests/test_mod_runtime.cpp @@ -527,6 +527,19 @@ int main() { mod_runtime_activate_plugins(); check(func_override_count() == 1, "selected exact package override must arm"); + /* Offline lobby rematch: commit again, then jump past activation. The + * commit disarms the previous set; the rematch path must re-arm it + * without re-running activation callbacks. */ + const int activations_before_rematch = activation_calls; + check(PSXRecompV4::mod_runtime_commit(stock_path, &error), error.c_str()); + check(func_override_count() == 0, + "commit must disarm the previous package override set"); + check(PSXRecompV4::mod_runtime_arm_function_overrides(&error), + error.c_str()); + check(func_override_count() == 1, + "rematch re-arm must restore the committed package override"); + check(activation_calls == activations_before_rematch, + "rematch re-arm must not run activation callbacks"); int guard_kind = -1, guard_count = 0; uint32_t guard_crc = 0; check(func_override_get_guard_info(0, &guard_kind, &guard_count,