Reject partial function override activation before session callbacks

This commit is contained in:
Matthew Stanley
2026-10-24 09:31:11 -07:00
parent 4cbedf5579
commit 56b6839602
6 changed files with 97 additions and 25 deletions
+7
View File
@@ -1182,3 +1182,10 @@ existing post-commit path, not the preparation worker.
The `prepare_resources` method is separate from read-only `resolve`, so editing
launcher settings does not trigger conversions. Runtime activation still sees
only fully verified immutable resources.
Function-override package arming is transactional and runs before activation
callbacks. Address collisions or capacity failures reject the launch through
`mod_runtime_activation_error()` and remove all package entries, retaining
always-on direct implementations. Every session rearms its selected plan.
Replacement overrides cannot be selected as netplay local-view plugins: they
change shared execution rather than only sandboxed presentation.
+3 -2
View File
@@ -493,9 +493,9 @@ between Play and TCP availability.
## Complete command index (generated)
**386 commands registered** — 373 on the native server (`runtime/src/debug_server.c`), 61 on the Beetle server (`runtime/src/beetle_debug_server.c`).
**387 commands registered** — 374 on the native server (`runtime/src/debug_server.c`), 61 on the Beetle server (`runtime/src/beetle_debug_server.c`).
85 of 386 have prose above; **301 are index-only**. An index-only command still works — it just has no description here yet. Send it `{"cmd":"<name>"}` and read the reply, or find its `handle_*` function in the server source.
85 of 387 have prose above; **302 are index-only**. An index-only command still works — it just has no description here yet. Send it `{"cmd":"<name>"}` and read the reply, or find its `handle_*` function in the server source.
Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this block has drifted from the code.
@@ -630,6 +630,7 @@ Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this bloc
| `frame_trace` | ✓ | | |
| `frame_wall_ring` | ✓ | | |
| `freeze_check` | ✓ | | |
| `func_override` | ✓ | | |
| `game_options` | ✓ | | |
| `gcp_ring` | ✓ | | ✓ |
| `geom_correction` | ✓ | | ✓ |
+2
View File
@@ -113,6 +113,8 @@ void mod_runtime_set_progress_hook(void (*hook)(void));
/* Invokes activation callbacks for the committed plan. Call after the final
* launcher commit and before renderer/window initialization. */
void mod_runtime_activate_plugins(void);
/* NULL on success; a non-NULL arming failure requires refusing this launch. */
const char* mod_runtime_activation_error(void);
void mod_runtime_on_vblank(void);
/* Host VBlank query for the active title's optional disc-request provider. */
int mod_runtime_disc_request(int *ready);
+4
View File
@@ -20412,6 +20412,10 @@ int main(int argc, char** argv) {
netplay && PSXRecompV4::mod_runtime_netplay_view_active() ? 1 : 0);
psx_mod_immediate_begin_session(netplay ? 1 : 0);
mod_runtime_activate_plugins();
if (const char* error = mod_runtime_activation_error()) {
std::fprintf(stderr,"psxrecomp: %s\n",error);
return false;
}
func_override_install();
if (!psx_mod_immediate_session_valid()) {
std::fprintf(stderr, "psxrecomp: immediate-selector enhancements are offline-only\n");
+32 -23
View File
@@ -1696,8 +1696,8 @@ bool mod_runtime_clear_for_netplay(std::string* error) {
* diverging from a peer without the mod. Also re-arm-able: dropping the
* armed flag lets a later plan register the same override again. */
const int disarmed = func_override_reset_package_armed();
if (disarmed > 0)
for (FunctionOverridePlugin& pending : function_override_plugins())
(void)disarmed;
for (FunctionOverridePlugin& pending : function_override_plugins())
pending.armed = false;
if (error) error->clear();
std::fprintf(stdout, "psxrecomp: mods cleared for netplay (vanilla session)\n");
@@ -2998,6 +2998,11 @@ bool mod_runtime_read_disc_file_sectors(const std::string& path, uint32_t max_by
}
} // namespace PSXRecompV4
static std::string s_override_activation_error;
extern "C" const char* mod_runtime_activation_error(void) {
return s_override_activation_error.empty() ? nullptr : s_override_activation_error.c_str();
}
extern "C" void mod_runtime_activate_plugins(void) {
using namespace PSXRecompV4;
HostLaunchTimingScope timing(HOST_LAUNCH_PLUGIN_ACTIVATION);
@@ -3005,6 +3010,9 @@ extern "C" void mod_runtime_activate_plugins(void) {
psx_host_set_hud_draw_cb(nullptr, nullptr);
mod_reset_plugin_sessions();
RuntimeMods& s = state();
s_override_activation_error.clear();
func_override_reset_package_armed();
for (auto& pending : function_override_plugins()) pending.armed = false;
s.ticket_ready = false;
psx_mod_set_disc_request_provider(nullptr);
psx_projection_reset_session();
@@ -3015,6 +3023,27 @@ extern "C" void mod_runtime_activate_plugins(void) {
s.disc_extents.clear();
s.audio_tracks.clear();
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 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(
pending.id.c_str(),pending.address,pending.fn,
pending.n_guard ? pending.guard : nullptr,pending.n_guard,pending.credit);
if (rc != FO_OK) {
s_override_activation_error = "Cannot arm function override " + pending.id +
" (registration error " + std::to_string(rc) + ")";
s.error = s_override_activation_error;
s.plan.ok = false;
func_override_reset_package_armed();
for (auto& item : function_override_plugins()) item.armed = false;
clear_function_entry_hooks();
return;
}
pending.armed = true;
}
func_override_install();
s.activating = true;
psx_mod_immediate_begin_activation();
for (const ModResolution::Plugin& plugin : s.plan.plugins) {
@@ -3024,26 +3053,6 @@ extern "C" void mod_runtime_activate_plugins(void) {
mod_invoke_activation_plugin(plugin.id);
plugin_timing.success();
}
/* 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();
psx_mod_immediate_end_activation();
s.activating = false;
build_function_entry_hooks(s);
@@ -3531,7 +3540,7 @@ 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) {
using namespace PSXRecompV4;
if (!id || !*id || !address || !fn) return 0;
if (!id || !*id || !(address & 0x1fffffffu) || (address & 3u) || !fn) return 0;
if (n_words < 0 || n_words > FO_MAX_GUARD_WORDS) return 0;
if (n_words > 0 && !expected_words) return 0;
if (credit < FO_CREDIT_SELF) return 0;
+49
View File
@@ -1,4 +1,5 @@
#include "mod_runtime.h"
#include "func_override.h"
#include "mod_netplay.h"
#include "host_launch_timing.h"
#include "host_overlay.h"
@@ -32,6 +33,9 @@ namespace fs = std::filesystem;
static std::array<uint8_t, 2 * 1024 * 1024> ram;
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 plugin_calls;
static int restore_calls;
static int option_renderer_activations;
@@ -540,6 +544,7 @@ int main(int argc, char** argv) {
const fs::path root = fs::temp_directory_path() / "psxrecomp-mod-runtime-test";
#endif
std::error_code ec;
fs::remove_all(root, ec);
const std::vector<uint8_t> stock(8 * 2352, 0);
std::vector<uint8_t> overlay(3000);
@@ -2439,6 +2444,50 @@ int main(int argc, char** argv) {
"resolver preserves missing selection for existing downstream validation");
}
{
const auto base = root/"function-overrides";
write_text(base/"bundled/override.test/1.0.0/manifest.toml", R"toml(
format_version = 5
id = "override.test"
version = "1.0.0"
name = "Overrides"
resolver = "declarative"
save_compatibility = "shared"
[[target]]
game_id = "SLUS-OVERRIDE"
[[feature]]
id = "replace"
name = "Replace"
default_enabled = true
[[plugin]]
feature = "replace"
id = "override.test"
)toml");
check(PSXRecompV4::mod_register_activation_plugin("override.test",override_activate),"register override activation");
check(psx_mod_register_function_override("override.test:first",0x80006004,override_body,nullptr,0,0),"queue first override");
check(psx_mod_register_function_override("override.test:second",0x80006000,override_body,nullptr,0,0),"queue second override");
check(!psx_mod_register_function_override("override.test:bad",0x80006001,override_body,nullptr,0,0),"reject misaligned override");
check(PSXRecompV4::mod_runtime_initialize(base,"SLUS-OVERRIDE",0x80002000,{},&error),error.c_str());
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");
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());
check(func_override_count()==0,"cleared plan disarms all package entries");
override_activation_calls=0;
check(func_override_add("direct",0x80006000,override_body,0)==FO_OK,"register colliding direct override");
check(PSXRecompV4::mod_runtime_initialize(base,"SLUS-OVERRIDE",0x80002000,{},&error),error.c_str());
check(PSXRecompV4::mod_runtime_commit({},&error),error.c_str());
mod_runtime_activate_plugins();
check(mod_runtime_activation_error()!=nullptr && override_activation_calls==0,
"late collision rejects launch before callbacks");
check(func_override_count()==1,"collision rolls back all package entries and preserves direct entry");
check(PSXRecompV4::mod_runtime_clear_for_netplay(&error),error.c_str());
mod_runtime_activate_plugins();
check(!mod_runtime_activation_error() && func_override_count()==1,"vanilla session clears failure and retains direct tier");
}
fs::remove_all(root, ec);
if (failures) return 1;
std::cout << "mod runtime tests passed\n";