diff --git a/docs/MOD_PACKAGES.md b/docs/MOD_PACKAGES.md index 4a6b9631..99b7c93e 100644 --- a/docs/MOD_PACKAGES.md +++ b/docs/MOD_PACKAGES.md @@ -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 diff --git a/runtime/include/mod_packages.h b/runtime/include/mod_packages.h index b4860c92..13d13c19 100644 --- a/runtime/include/mod_packages.h +++ b/runtime/include/mod_packages.h @@ -1,5 +1,7 @@ #pragma once +#include "mod_plugins.h" + #include #include #include @@ -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 diff --git a/runtime/src/mod_packages.cpp b/runtime/src/mod_packages.cpp index 150a5a3d..006aa815 100644 --- a/runtime/src/mod_packages.cpp +++ b/runtime/src/mod_packages.cpp @@ -34,6 +34,9 @@ std::map& 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> function_entries; }; std::map& 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(); } diff --git a/runtime/src/mod_runtime.cpp b/runtime/src/mod_runtime.cpp index 92e4ee97..fe974f02 100644 --- a/runtime/src/mod_runtime.cpp +++ b/runtime/src/mod_runtime.cpp @@ -80,17 +80,6 @@ RuntimeMods& state() { return value; } -struct FunctionEntryPlugin { - std::string id; - uint32_t address = 0; - PSXModFunctionEntryCallback callback = nullptr; -}; - -std::vector& function_entry_plugins() { - static std::vector 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; } } diff --git a/runtime/tests/test_mod_packages.cpp b/runtime/tests/test_mod_packages.cpp index b86b15a3..c7d90002 100644 --- a/runtime/tests/test_mod_packages.cpp +++ b/runtime/tests/test_mod_packages.cpp @@ -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"