mods: resolve function-entry plugins and run them only for the active plan

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matthew Stanley
2026-09-26 00:27:12 -07:00
parent d62f4b4420
commit 41a76a72d8
5 changed files with 88 additions and 25 deletions
+5 -1
View File
@@ -444,7 +444,11 @@ the same plugin id. Active plugin identities and owners participate in the
canonical plan fingerprint.
An implementation may register an activation callback, a deterministic
guest-VBlank callback, or both under the same id. Activation runs after the
guest-VBlank callback, function-entry hooks, or any combination under the same
id. A function-entry hook (`psx_mod_register_function_entry_plugin`) runs at the
top of a generated function the game config lists in
`[recompiler] mod_function_entry_funcs`; like the other kinds it satisfies a
manifest `[[plugin]]` and runs only while the resolved plan activates its id. Activation runs after the
launcher's final mod-plan commit and before renderer/window initialization; it
is appropriate for a game-owned mod that selects a fixed display aspect or
another pre-boot host feature. VBlank callbacks run from the emulated GPU
+8
View File
@@ -1,5 +1,7 @@
#pragma once
#include "mod_plugins.h"
#include <cstdint>
#include <filesystem>
#include <functional>
@@ -454,9 +456,15 @@ bool mod_register_builtin_resolver(const std::string& id, ModBuiltinResolver res
void mod_clear_builtin_resolvers_for_tests();
bool mod_register_activation_plugin(const std::string& id, void (*callback)(void));
bool mod_register_vblank_plugin(const std::string& id, void (*callback)(void));
/* A function-entry hook is a trusted implementation like the others: manifests
* select it by id, and it runs only while a resolved plan activates that id. */
bool mod_register_function_entry_plugin(const std::string& id, uint32_t address,
PSXModFunctionEntryCallback callback);
bool mod_plugin_registered(const std::string& id);
void mod_invoke_activation_plugin(const std::string& id);
void mod_invoke_vblank_plugin(const std::string& id);
void mod_invoke_function_entry_plugin(const std::string& id, ::CPUState* cpu,
uint32_t address);
void mod_clear_plugins_for_tests();
} // namespace PSXRecompV4
+23 -1
View File
@@ -34,6 +34,9 @@ std::map<std::string, ModBuiltinResolver>& builtin_resolvers() {
struct RegisteredPlugin {
PSXModActivationCallback activation = nullptr;
PSXModVBlankCallback vblank = nullptr;
/* Generated-function entry hooks, keyed by guest address. One id may
* observe several functions. */
std::vector<std::pair<uint32_t, PSXModFunctionEntryCallback>> function_entries;
};
std::map<std::string, RegisteredPlugin>& registered_plugins() {
@@ -1449,10 +1452,21 @@ bool mod_register_vblank_plugin(const std::string& id, void (*callback)(void)) {
return true;
}
bool mod_register_function_entry_plugin(const std::string& id, uint32_t address,
PSXModFunctionEntryCallback callback) {
if (!valid_id(id) || !address || !callback) return false;
RegisteredPlugin& plugin = registered_plugins()[id];
for (const auto& hook : plugin.function_entries)
if (hook.first == address) return false;
plugin.function_entries.emplace_back(address, callback);
return true;
}
bool mod_plugin_registered(const std::string& id) {
const auto found = registered_plugins().find(id);
return found != registered_plugins().end() &&
(found->second.activation || found->second.vblank);
(found->second.activation || found->second.vblank ||
!found->second.function_entries.empty());
}
void mod_invoke_activation_plugin(const std::string& id) {
@@ -1467,6 +1481,14 @@ void mod_invoke_vblank_plugin(const std::string& id) {
found->second.vblank();
}
void mod_invoke_function_entry_plugin(const std::string& id, ::CPUState* cpu,
uint32_t address) {
const auto found = registered_plugins().find(id);
if (found == registered_plugins().end()) return;
for (const auto& hook : found->second.function_entries)
if (hook.first == address) hook.second(cpu, address);
}
void mod_clear_plugins_for_tests() {
registered_plugins().clear();
}
+8 -23
View File
@@ -80,17 +80,6 @@ RuntimeMods& state() {
return value;
}
struct FunctionEntryPlugin {
std::string id;
uint32_t address = 0;
PSXModFunctionEntryCallback callback = nullptr;
};
std::vector<FunctionEntryPlugin>& function_entry_plugins() {
static std::vector<FunctionEntryPlugin> value;
return value;
}
const ModPackage* selected_package(const std::string& id) {
return state().manager.selected_package(id);
}
@@ -1482,22 +1471,18 @@ extern "C" uint32_t psx_mod_display_height(void) {
extern "C" int psx_mod_register_function_entry_plugin(
const char* id, uint32_t address, PSXModFunctionEntryCallback callback) {
using namespace PSXRecompV4;
if (!id || !*id || !address || !callback) return 0;
auto& plugins = function_entry_plugins();
const auto duplicate = std::find_if(
plugins.begin(), plugins.end(), [&](const FunctionEntryPlugin& item) {
return item.id == id && item.address == address;
});
if (duplicate != plugins.end()) return 0;
plugins.push_back(FunctionEntryPlugin{id, address, callback});
return 1;
if (!id || !address || !callback) return 0;
return mod_register_function_entry_plugin(id, address, callback) ? 1 : 0;
}
extern "C" void psx_mod_function_entry(CPUState* cpu, uint32_t address) {
using namespace PSXRecompV4;
if (!cpu) return;
for (const FunctionEntryPlugin& plugin : function_entry_plugins()) {
if (plugin.address == address) plugin.callback(cpu, address);
RuntimeMods& s = state();
if (!cpu || !s.initialized || !s.plan.ok) return;
for (const ModResolution::Plugin& plugin : s.plan.plugins) {
s.current_plugin = &plugin;
mod_invoke_function_entry_plugin(plugin.id, cpu, address);
s.current_plugin = nullptr;
}
}
+44
View File
@@ -368,6 +368,50 @@ int main() {
"is unavailable");
check(reload.set_feature_enabled(
"plugin.mod", "vblank", false, &error), error.c_str());
/* A function-entry hook is selected by id exactly like activation and
* VBlank plugins; a manifest naming only such an implementation must
* resolve, and must still fail closed once it is gone. */
static int entry_hits = 0;
check(mod_register_function_entry_plugin(
"test.entry", 0x80010000u,
+[](CPUState*, uint32_t address) {
if (address == 0x80010000u) entry_hits++;
}),
"function-entry plugin must register");
check(!mod_register_function_entry_plugin(
"test.entry", 0x80010000u, +[](CPUState*, uint32_t) {}),
"duplicate function-entry hook must be rejected");
write_text(root / "packages/entry.mod/1.0.0/manifest.toml",
"format_version = 5\n"
"id = \"entry.mod\"\n"
"version = \"1.0.0\"\n"
"name = \"Entry Mod\"\n"
"resolver = \"declarative\"\n"
"[[target]]\n"
"game_id = \"SLUS-TEST\"\n"
"[[feature]]\n"
"id = \"entry\"\n"
"name = \"Entry Plugin\"\n"
"[[plugin]]\n"
"feature = \"entry\"\n"
"id = \"test.entry\"\n");
check(reload.scan(&error), error.c_str());
check(reload.set_feature_enabled("entry.mod", "entry", true, &error),
error.c_str());
ModResolution entry_resolution = reload.resolve("SLUS-TEST");
check(entry_resolution.ok && entry_resolution.plugins.size() == 1 &&
entry_resolution.plugins[0].id == "test.entry",
"enabled function-entry plugin must resolve");
mod_invoke_function_entry_plugin("test.entry", nullptr, 0x80010004u);
mod_invoke_function_entry_plugin("test.entry", nullptr, 0x80010000u);
check(entry_hits == 1, "function-entry hook must fire only for its address");
mod_clear_plugins_for_tests();
ModResolution entry_unavailable = reload.resolve("SLUS-TEST");
check(!entry_unavailable.ok,
"enabled function-entry plugin must fail closed when unregistered");
check(reload.set_feature_enabled("entry.mod", "entry", false, &error),
error.c_str());
write_text(root / "packages/features.mod/1.0.0/manifest.toml",
manifest("features.mod", "1.0.0",
"\n[[feature]]\n"