Review integration: scope override resource access and snapshot boundaries

# Conflicts:
#	generated/OpenBIOS.emitter.sha
This commit is contained in:
Matthew Stanley
2026-10-24 11:16:59 -07:00
6 changed files with 79 additions and 9 deletions
+11
View File
@@ -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).
+3 -1
View File
@@ -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
+19 -1
View File
@@ -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.
+12 -5
View File
@@ -3091,6 +3091,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<const ModResolution::Plugin*>(context),cpu);
return fn(cpu);
}
extern "C" void mod_runtime_activate_plugins(void) {
using namespace PSXRecompV4;
HostLaunchTimingScope timing(HOST_LAUNCH_PLUGIN_ACTIVATION);
@@ -3115,12 +3121,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) + ")";
@@ -3867,7 +3874,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) {
+5 -1
View File
@@ -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 -------------------------------------------------------------- */
+29 -1
View File
@@ -36,11 +36,21 @@
namespace fs = std::filesystem;
static std::array<uint8_t, 2 * 1024 * 1024> 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;
@@ -2573,6 +2583,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"
@@ -2585,6 +2607,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());