From ab400a3953c5ac4190e30f5a1c941db0e84e4d2d Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Sat, 24 Oct 2026 10:08:15 -0700 Subject: [PATCH 1/2] Regenerate OpenBIOS dispatch with the function override tier --- generated/OpenBIOS.emitter.sha | 2 +- generated/OpenBIOS_dispatch.c | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/generated/OpenBIOS.emitter.sha b/generated/OpenBIOS.emitter.sha index 8979439c..ddefaefb 100644 --- a/generated/OpenBIOS.emitter.sha +++ b/generated/OpenBIOS.emitter.sha @@ -1 +1 @@ -86f7329c2692e76133c40fa11cc8cfac214302d8238e05369dc8b263f4dc76fd +6b3ba0804aeca3d52f6e44dd26ec7a8e82f40edce68ecf01ff14705373aef847 diff --git a/generated/OpenBIOS_dispatch.c b/generated/OpenBIOS_dispatch.c index 53da932f..67f5443a 100644 --- a/generated/OpenBIOS_dispatch.c +++ b/generated/OpenBIOS_dispatch.c @@ -36,6 +36,13 @@ extern int (*g_psx_bios_hle_hook)(CPUState* cpu, uint32_t phys); extern uint32_t g_psx_mod_guest_functions; extern int psx_mod_dispatch_guest_function(CPUState*, uint32_t); +/* Game function-override tier (runtime/include/func_override.h): + * hand-written C registered against a guest address. NULL (the + * default) = no overrides, dispatch identical to a build without + * the tier. Returns 1 when the override handled the call; the + * guest resumes at $ra exactly as if the original ran jr $ra. */ +extern int (*g_psx_func_override_hook)(CPUState* cpu, uint32_t phys); + #ifdef PSX_HAS_GAME_DISPATCH extern int psx_game_address_in_text(uint32_t addr); #endif @@ -9111,6 +9118,18 @@ static void psx_dispatch_impl(CPUState* cpu, uint32_t addr, uint32_t stop_addr) /* Byte-guarded A0/B0/C0 call-vector tail stubs. */ if (!found && psx_bios_try_native_call_stub(cpu, addr)) found = 1; + /* Function-override tier: AFTER the BIOS tiers (an override + * can never shadow a kernel service vector), BEFORE every + * game code backend, so one address-keyed hook covers the + * static EXE, runtime-loaded overlays and dirty RAM alike. + * Handled (rc 1) => the override completed against guest + * state; resume at $ra via the trampoline's normal + * return/tail contract. rc 0 => fall through untouched. */ + if (!found && g_psx_func_override_hook && + g_psx_func_override_hook(cpu, addr & 0x1FFFFFFFu)) { + cpu->pc = cpu->gpr[31]; + found = 1; + } #ifdef PSX_HAS_GAME_DISPATCH /* Game EXEs can overlap the BIOS shell copy window at * physical 0x30000-0x5AFFF. If the target belongs to the From d486a78acf0fcccbedbae9479551abd28a9802cf Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Sat, 24 Oct 2026 11:16:39 -0700 Subject: [PATCH 2/2] Scope package overrides and defer snapshots across their native stacks --- runtime/include/func_override.h | 11 +++++++++++ runtime/include/mod_plugins.h | 4 +++- runtime/src/func_override.c | 20 +++++++++++++++++++- runtime/src/mod_runtime.cpp | 17 ++++++++++++----- runtime/tests/test_func_override.c | 6 +++++- runtime/tests/test_mod_runtime.cpp | 30 +++++++++++++++++++++++++++++- 6 files changed, 79 insertions(+), 9 deletions(-) diff --git a/runtime/include/func_override.h b/runtime/include/func_override.h index 3e736259..2e114219 100644 --- a/runtime/include/func_override.h +++ b/runtime/include/func_override.h @@ -190,6 +190,17 @@ 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); +/* Scoped package variant. The owner supplies a stable plan context and + * invokes fn inside that plugin's resource/completion context. The plan must + * disarm package entries before replacing that context. */ +typedef int (*FuncOverrideInvoker)(struct CPUState *cpu, FuncOverrideFn fn, + const void *context); +int func_override_add_package_scoped(const char *id, uint32_t addr, FuncOverrideFn fn, + const uint32_t *expected_words, int n_words, int32_t credit, + FuncOverrideInvoker invoke, const void *context); +/* Nonzero while a native override stack is live, including nested guest + * calls. Save/load and rewind must defer until this returns to zero. */ +int func_override_active(void); /* Install the dispatcher hook. Call once at startup AFTER registering (the * mod runtime calls it again after arming package-gated overrides — safe). diff --git a/runtime/include/mod_plugins.h b/runtime/include/mod_plugins.h index bd2cb9e9..671b41fe 100644 --- a/runtime/include/mod_plugins.h +++ b/runtime/include/mod_plugins.h @@ -145,7 +145,9 @@ uint32_t psx_mod_guest_cycle_scale(void); * selects that plugin — the same gating as vblank/activation callbacks. The * callback obeys the guest ABI (args cpu->gpr[4..7], result cpu->gpr[2]); * return 1 = handled (guest resumes at $ra), 0 = decline (original runs). - * Wrap semantics via func_override_call_original(); optional residency + * The owning plugin resource context is active throughout the callback and + * restored across nested callbacks. Snapshots wait until its native stack + * unwinds. Wrap semantics via func_override_call_original(); optional residency * guard via expected_words (NULL/0 = unguarded). * * `id` may carry an optional ":label" suffix ("pkg.feature:aim") that names diff --git a/runtime/src/func_override.c b/runtime/src/func_override.c index 9575d04d..97985f6b 100644 --- a/runtime/src/func_override.c +++ b/runtime/src/func_override.c @@ -35,6 +35,8 @@ typedef struct { char id[FO_MAX_ID]; uint32_t phys; /* normalised: KSEG bits masked off */ FuncOverrideFn fn; + FuncOverrideInvoker invoke; + const void *context; uint64_t calls; uint64_t guard_misses; uint32_t guard[FO_MAX_GUARD_WORDS]; @@ -127,6 +129,22 @@ int func_override_add_package(const char *id, uint32_t addr, FuncOverrideFn fn, 1, credit); } +int func_override_add_package_scoped(const char *id, uint32_t addr, FuncOverrideFn fn, + const uint32_t *expected_words, int n_words, int32_t credit, + FuncOverrideInvoker invoke, const void *context) +{ + if (!invoke || !context) return FO_ERR_ARGS; + const int rc = func_override_add_package(id,addr,fn,expected_words,n_words,credit); + if (rc == FO_OK) { + Entry *entry = &s_entries[s_count-1]; + entry->invoke = invoke; + entry->context = context; + } + return rc; +} + +int func_override_active(void) { return s_active_phys != 0; } + /* The hook. Runs on EVERY dispatch, so the common path — nothing registered * for this address — must stay cheap. A linear scan over a handful of * entries beats a hash for realistic counts and keeps the code honest; @@ -168,7 +186,7 @@ static int hook(CPUState *cpu, uint32_t phys) { const uint32_t saved_active = s_active_phys; s_active_phys = phys; - const int handled = e->fn(cpu) ? 1 : 0; + const int handled = (e->invoke ? e->invoke(cpu,e->fn,e->context) : e->fn(cpu)) ? 1 : 0; s_active_phys = saved_active; /* Charge the declared credit only when HANDLED: a decline runs * the original, which accrues its exact cycles by executing. diff --git a/runtime/src/mod_runtime.cpp b/runtime/src/mod_runtime.cpp index b6b7bf8d..e2e09ba2 100644 --- a/runtime/src/mod_runtime.cpp +++ b/runtime/src/mod_runtime.cpp @@ -3003,6 +3003,12 @@ extern "C" const char* mod_runtime_activation_error(void) { return s_override_activation_error.empty() ? nullptr : s_override_activation_error.c_str(); } +static int invoke_package_override(CPUState* cpu, FuncOverrideFn fn, const void* context) { + using namespace PSXRecompV4; + PluginCallbackScope scope(state(),static_cast(context),cpu); + return fn(cpu); +} + extern "C" void mod_runtime_activate_plugins(void) { using namespace PSXRecompV4; HostLaunchTimingScope timing(HOST_LAUNCH_PLUGIN_ACTIVATION); @@ -3025,12 +3031,13 @@ extern "C" void mod_runtime_activate_plugins(void) { s.extent_start = 0; /* Arm the entire tier before activation callbacks change host state. */ for (auto& pending : function_override_plugins()) { - const bool selected = std::any_of(s.plan.plugins.begin(),s.plan.plugins.end(), + const auto owner = std::find_if(s.plan.plugins.begin(),s.plan.plugins.end(), [&](const ModResolution::Plugin& p) { return p.id == pending.plugin; }); - if (!selected) continue; - const int rc = s.netplay_view_plan ? FO_ERR_ARGS : func_override_add_package( + if (owner == s.plan.plugins.end()) continue; + const int rc = s.netplay_view_plan ? FO_ERR_ARGS : func_override_add_package_scoped( pending.id.c_str(),pending.address,pending.fn, - pending.n_guard ? pending.guard : nullptr,pending.n_guard,pending.credit); + pending.n_guard ? pending.guard : nullptr,pending.n_guard,pending.credit, + invoke_package_override,&*owner); if (rc != FO_OK) { s_override_activation_error = "Cannot arm function override " + pending.id + " (registration error " + std::to_string(rc) + ")"; @@ -3777,7 +3784,7 @@ static int game_netplay_function_entry(CPUState* cpu, uint32_t address) { } extern "C" int psx_mod_function_entry_active(void) { - return PSXRecompV4::function_entry_depth != 0; + return PSXRecompV4::function_entry_depth != 0 || func_override_active(); } extern "C" void mod_runtime_function_entry_context_save(ModFunctionEntryContext *out) { diff --git a/runtime/tests/test_func_override.c b/runtime/tests/test_func_override.c index c48fbc38..3ad9a6a3 100644 --- a/runtime/tests/test_func_override.c +++ b/runtime/tests/test_func_override.c @@ -98,6 +98,7 @@ static int s_impl_calls, s_decline_calls, s_wrap_calls; static int impl_handles(CPUState *cpu) { + CHECK(func_override_active(),"handled native override stack is active"); s_impl_calls++; cpu->gpr[2] = 0x1234u; return 1; @@ -106,6 +107,7 @@ static int impl_handles(CPUState *cpu) static int impl_declines(CPUState *cpu) { (void)cpu; + CHECK(func_override_active(),"declining native override stack is active"); s_decline_calls++; return 0; /* the decline-only probe idiom the TCP inventory relies on */ } @@ -135,7 +137,9 @@ static void reset_all(void) static int consult(CPUState *cpu, uint32_t addr) { if (!g_psx_func_override_hook) return 0; - return g_psx_func_override_hook(cpu, addr & 0x1FFFFFFFu); + const int handled = g_psx_func_override_hook(cpu, addr & 0x1FFFFFFFu); + CHECK(!func_override_active(),"top-level native override stack has unwound"); + return handled; } /* ---- tests -------------------------------------------------------------- */ diff --git a/runtime/tests/test_mod_runtime.cpp b/runtime/tests/test_mod_runtime.cpp index 33a55313..5e5e7b08 100644 --- a/runtime/tests/test_mod_runtime.cpp +++ b/runtime/tests/test_mod_runtime.cpp @@ -31,11 +31,21 @@ namespace fs = std::filesystem; static std::array ram; +extern "C" int (*g_psx_func_override_hook)(CPUState*, uint32_t); static int failures; static int activation_calls; static int override_activation_calls; static void override_activate() { ++override_activation_calls; } -static int override_body(CPUState*) { return 1; } +static int override_scope_checks; +static int override_body(CPUState*) { + char option[16]{}; + if (psx_mod_current_option_value("scope",option,sizeof option) && + !std::strcmp(option,"owned") && psx_mod_function_entry_active()) ++override_scope_checks; + mod_runtime_on_vblank(); + if (psx_mod_current_option_value("scope",option,sizeof option) && + !std::strcmp(option,"owned") && psx_mod_function_entry_active()) ++override_scope_checks; + return 1; +} static int plugin_calls; static int restore_calls; static int option_renderer_activations; @@ -2459,6 +2469,18 @@ game_id = "SLUS-OVERRIDE" id = "replace" name = "Replace" default_enabled = true +[[option]] +feature = "replace" +id = "scope" +label = "Scope" +type = "choice" +default = "owned" +[[option.choice]] +value = "owned" +label = "Owned" +[[option.choice]] +value = "other" +label = "Other" [[plugin]] feature = "replace" id = "override.test" @@ -2471,6 +2493,12 @@ id = "override.test" check(PSXRecompV4::mod_runtime_commit({},&error),error.c_str()); mod_runtime_activate_plugins(); check(!mod_runtime_activation_error() && func_override_count()==2 && override_activation_calls==1,"selected overrides arm together"); + CPUState override_cpu{}; + check(g_psx_func_override_hook && g_psx_func_override_hook(&override_cpu,0x6004),"invoke package override"); + check(override_scope_checks==2 && !psx_mod_function_entry_active(), + "override owns its resource context across VBlank and blocks mid-stack snapshots"); + char no_scope[16]{}; + check(!psx_mod_current_option_value("scope",no_scope,sizeof no_scope),"override restores its caller's context"); mod_runtime_activate_plugins(); check(!mod_runtime_activation_error() && func_override_count()==2 && override_activation_calls==2,"session reactivation replaces entries without duplicates"); check(PSXRecompV4::mod_runtime_clear_for_netplay(&error),error.c_str());