mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-27 18:52:11 -04:00
Add opt-in native guest function replacement service
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
# Trusted opt-in native function replacements
|
||||
|
||||
Enhanced ports can replace a narrowly selected guest function with statically
|
||||
linked native code, without assigning its additional PC work a simulated PS1
|
||||
instruction budget. Nothing is registered by default. This is an enhancement
|
||||
service, not a change to faithful CPU/GTE/DMA timing.
|
||||
|
||||
1. List the guest entry in `[recompiler] mod_function_entry_funcs` and regenerate.
|
||||
2. During activation of an **opt-in** game plugin, call
|
||||
`psx_mod_set_function_replacement(address, callback)`.
|
||||
3. Validate the guest version, inputs, resources and output capacity before
|
||||
changing anything. Return zero to execute the original implementation, or
|
||||
nonzero only after completing its caller-visible effects.
|
||||
|
||||
Existing function-entry preparation callbacks run first. The replacement
|
||||
dispatcher captures the incoming `$ra` and publishes that address in `cpu->pc`
|
||||
when the callback succeeds, using the normal CPS return convention. Interior
|
||||
compiled continuations bypass the fresh-entry hook. Interpreted entry/local
|
||||
transfer paths also consult the registry, including when a patched prologue
|
||||
prevents static dispatch.
|
||||
|
||||
The callback owns ABI preservation, memory/precision bookkeeping and any
|
||||
intentional timing effects. The service does not add cycles, accelerate clocks,
|
||||
or undo a declined callback's side effects: a callback returning zero **must
|
||||
leave guest/runtime state untouched**. Never use this to skip unrelated game
|
||||
logic. Calls and registration are emulation-thread-only.
|
||||
|
||||
The registry is bounded at 64 exact, nonzero, word-aligned addresses. Replacing
|
||||
an existing registration does not consume another slot. A null callback removes
|
||||
it. The default-empty path returns without scanning the table. Registrations
|
||||
are process configuration, not savestate payload; do not store pointers or
|
||||
function registrations in guest RAM.
|
||||
|
||||
Regeneration changes the generated-code signature. Existing savestates may be
|
||||
rejected by the normal compatibility guard; retain the older build for them and
|
||||
use fresh private checkpoints for same-build comparisons. Do not bypass the
|
||||
guard to make a performance test load.
|
||||
|
||||
Tests cover default-off/declined calls, exact addresses, capacity/reuse/removal,
|
||||
the return ABI (including a callback that clobbers `$ra`), generated opt-in scope
|
||||
and continuation ordering, and both interpreted callback sites.
|
||||
|
||||
Tracking: beads-eio.3.161. The first consumer is Crash's default-off N9 native
|
||||
normal-terrain transform experiment, tracked in beads-t8j.8.
|
||||
@@ -449,6 +449,28 @@ if(BUILD_TESTING)
|
||||
add_test(NAME signed_x_bound_codegen_test
|
||||
COMMAND signed_x_bound_codegen_test)
|
||||
|
||||
add_executable(mod_function_replacement_codegen_test
|
||||
tests/mod_function_replacement_codegen_test.cpp
|
||||
src/ps1_exe_parser.cpp
|
||||
src/mips_decoder.cpp
|
||||
src/control_flow.cpp
|
||||
src/function_analysis.cpp
|
||||
src/code_generator.cpp
|
||||
src/pgxp_hook_emitter.cpp
|
||||
src/annotations.cpp
|
||||
src/bios_address_model.cpp
|
||||
src/config_loader.cpp
|
||||
src/recompiler_patch.cpp
|
||||
)
|
||||
target_include_directories(mod_function_replacement_codegen_test PRIVATE
|
||||
include src lib/rabbitizer/include lib/rabbitizer/cplusplus/include
|
||||
lib/fmt/include lib/toml11 lib/ELFIO)
|
||||
target_link_libraries(mod_function_replacement_codegen_test PRIVATE fmt rabbitizer)
|
||||
add_test(NAME mod_function_replacement_codegen_test
|
||||
COMMAND mod_function_replacement_codegen_test)
|
||||
set_tests_properties(mod_function_replacement_codegen_test PROPERTIES
|
||||
ENVIRONMENT "PSX_CPS=1")
|
||||
|
||||
add_executable(recompiler_patch_cxx17_header_test
|
||||
tests/recompiler_patch_cxx17_header_test.cpp
|
||||
)
|
||||
|
||||
@@ -2784,8 +2784,9 @@ GeneratedFunction CodeGenerator::generate_function(
|
||||
body_ss << config_.indent
|
||||
<< fmt::format(
|
||||
"psx_mod_function_entry(cpu, 0x{:08X}u);"
|
||||
" /* trusted opt-in game-mod hook */\n",
|
||||
func.start_addr);
|
||||
" /* trusted opt-in game-mod hook */\n"
|
||||
" if (psx_mod_try_function_replacement(cpu, 0x{:08X}u)) return;\n",
|
||||
func.start_addr, func.start_addr);
|
||||
}
|
||||
if (config_.ws_sprite_tag_funcs.count(func.start_addr)) {
|
||||
body_ss << config_.indent
|
||||
@@ -3286,6 +3287,7 @@ void CodeGenerator::emit_runtime_externs(std::ostream& ss) const {
|
||||
ss << "extern int psx_game_text_native_ok(uint32_t addr); /* stale-static guard (dispatch shard) */\n";
|
||||
ss << "extern int psx_datashard_enter(CPUState* cpu, uint32_t key); /* data-shard replay/capture (data_shards.c) */\n";
|
||||
ss << "extern void psx_mod_function_entry(CPUState* cpu, uint32_t address); /* trusted opt-in game-mod hook */\n";
|
||||
ss << "extern int psx_mod_try_function_replacement(CPUState* cpu, uint32_t address);\n";
|
||||
ss << "extern void psx_datashard_ret(CPUState* cpu); /* data-shard capture finalize */\n";
|
||||
ss << "extern int psx_vsync_query_hle_enter(CPUState* cpu, uint32_t func, uint32_t counter_addr, uint32_t gpustat_ptr_addr, uint32_t timer1_ptr_addr, uint32_t timer1_cache_addr); /* load_accel.c */\n";
|
||||
ss << "extern void psx_ws_sprite_tag(CPUState* cpu); /* widescreen prim tag (gpu.c) */\n";
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
#include "code_generator.h"
|
||||
#include "control_flow.h"
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
|
||||
static void check(bool condition, const char* message) {
|
||||
if (!condition) {
|
||||
std::fprintf(stderr, "FAIL: %s\n", message);
|
||||
std::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
constexpr uint32_t base = 0x80010000u;
|
||||
PSXRecomp::PS1Executable exe{};
|
||||
exe.header.load_address = exe.header.initial_pc = base;
|
||||
const uint32_t words[] = {0x24020001u, 0x10400002u, 0, 0x24030002u, 0x03E00008u, 0};
|
||||
for (uint32_t word : words)
|
||||
for (unsigned byte = 0; byte < 4; ++byte)
|
||||
exe.code_data.push_back(static_cast<uint8_t>(word >> (byte * 8)));
|
||||
exe.header.file_size = static_cast<uint32_t>(exe.code_data.size());
|
||||
PSXRecomp::Function function{};
|
||||
function.start_addr = base;
|
||||
function.size = exe.header.file_size;
|
||||
function.end_addr = base + function.size;
|
||||
function.name = "replacement_test";
|
||||
PSXRecomp::ControlFlowAnalyzer analyzer(exe);
|
||||
const auto flow = analyzer.analyze_function(function);
|
||||
PSXRecomp::CodeGenConfig config{};
|
||||
config.indent = " ";
|
||||
const auto generate = [&]() {
|
||||
PSXRecomp::CodeGenerator generator(exe, config);
|
||||
return generator.generate_function(function, flow).full_code;
|
||||
};
|
||||
const std::string hook = "if (psx_mod_try_function_replacement(cpu, 0x80010000u)) return;";
|
||||
check(generate().find("psx_mod_try_function_replacement") == std::string::npos,
|
||||
"default configuration emits no replacement call");
|
||||
config.mod_function_entry_funcs.insert(base + 4);
|
||||
check(generate().find("psx_mod_try_function_replacement") == std::string::npos,
|
||||
"listing a different address does not hook this function");
|
||||
config.mod_function_entry_funcs.insert(base);
|
||||
const auto code = generate();
|
||||
const auto at = code.find(hook);
|
||||
check(at != std::string::npos && code.find(hook, at + 1) == std::string::npos,
|
||||
"explicit function entry emits exactly one replacement call");
|
||||
check(code.find("psx_mod_function_entry(cpu, 0x80010000u);") < at,
|
||||
"existing preparation callback runs before replacement");
|
||||
check(at < code.find("block_80010000:"), "fallback body remains after replacement");
|
||||
const auto continuation = code.find("switch (_cont)");
|
||||
check(continuation != std::string::npos && continuation < at,
|
||||
"interior CPS resumes bypass fresh-entry replacement");
|
||||
PSXRecomp::CodeGenerator generator(exe, config);
|
||||
check(generator.build_shared_decls_header({}).find(
|
||||
"extern int psx_mod_try_function_replacement(CPUState* cpu, uint32_t address);") !=
|
||||
std::string::npos,
|
||||
"generated shards declare runtime replacement ABI");
|
||||
std::puts("PASS default-off, exact-entry, fallback and continuation-safe replacement codegen");
|
||||
}
|
||||
@@ -23,6 +23,9 @@ psxrecomp_add_runtime_target(psx-runtime
|
||||
# (docs/ENHANCEMENTS.md G1.10). Titles get their <exe>_pgxp the same way.
|
||||
|
||||
if(BUILD_TESTING)
|
||||
add_executable(mod_function_replacements_test tests/test_mod_function_replacements.c src/mod_function_replacements.c)
|
||||
target_include_directories(mod_function_replacements_test PRIVATE include)
|
||||
add_test(NAME mod_function_replacements_test COMMAND mod_function_replacements_test)
|
||||
add_executable(ws_scene_hold_test tests/test_ws_scene_hold.c)
|
||||
target_include_directories(ws_scene_hold_test PRIVATE include)
|
||||
add_test(NAME ws_scene_hold_test COMMAND ws_scene_hold_test)
|
||||
|
||||
@@ -25,6 +25,15 @@ int psx_mod_register_function_entry_plugin(
|
||||
const char* id, uint32_t address, PSXModFunctionEntryCallback callback);
|
||||
/* Called only from generated functions explicitly listed by the game config. */
|
||||
void psx_mod_function_entry(struct CPUState* cpu, uint32_t address);
|
||||
/* Opt-in native implementation of an explicitly hooked function. Register
|
||||
* during activation. A callback returns nonzero only after completing the
|
||||
* function's caller-visible effects; the dispatcher publishes pc=$ra. A zero
|
||||
* result must leave state untouched and falls through to the original body.
|
||||
* No emulated cycles are credited automatically. Unregister with NULL.
|
||||
* Registration is process configuration, not savestate payload. */
|
||||
typedef int (*PSXModFunctionReplacement)(struct CPUState* cpu, uint32_t address);
|
||||
int psx_mod_set_function_replacement(uint32_t address, PSXModFunctionReplacement callback);
|
||||
int psx_mod_try_function_replacement(struct CPUState* cpu, uint32_t address);
|
||||
|
||||
/* Narrow guest services available to trusted plugin callbacks. */
|
||||
int psx_mod_game_started(void);
|
||||
|
||||
@@ -417,6 +417,7 @@ set(PSXRECOMP_RUNTIME_SOURCES
|
||||
${PSXRECOMP_ROOT}/runtime/src/mod_packages.cpp
|
||||
${PSXRECOMP_ROOT}/runtime/src/mod_runtime.cpp
|
||||
${PSXRECOMP_ROOT}/runtime/src/mod_texture_banks.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/mod_function_replacements.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/psx_keybinds.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/psx_bios_backend.c
|
||||
${PSXRECOMP_ROOT}/runtime/src/psx_netplay.c
|
||||
|
||||
@@ -2883,7 +2883,9 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_
|
||||
* executes the page. */
|
||||
{
|
||||
extern void psx_mod_function_entry(CPUState *cpu, uint32_t address);
|
||||
extern int psx_mod_try_function_replacement(CPUState *cpu, uint32_t address);
|
||||
psx_mod_function_entry(cpu, addr);
|
||||
if (psx_mod_try_function_replacement(cpu, addr)) OV_FPLOG_RET1();
|
||||
}
|
||||
|
||||
/* Per-PC entry counter (visible via dirty_ram_stats). */
|
||||
@@ -3273,7 +3275,15 @@ static int dirty_ram_dispatch_inner(CPUState* cpu, uint32_t addr, uint32_t stop_
|
||||
* function-entry hooks as a surfaced interpreter entry. */
|
||||
{
|
||||
extern void psx_mod_function_entry(CPUState *, uint32_t);
|
||||
extern int psx_mod_try_function_replacement(CPUState *, uint32_t);
|
||||
psx_mod_function_entry(cpu, target);
|
||||
if (psx_mod_try_function_replacement(cpu, target)) {
|
||||
g_dirty_ram_native_handoffs++;
|
||||
g_dirty_ram_blocks_run++;
|
||||
if (pc_entry) pc_entry->insns += (uint64_t)insns_executed;
|
||||
g_dirty_interp_chain_target = cpu->pc;
|
||||
OV_FPLOG_RET1();
|
||||
}
|
||||
}
|
||||
pc = target;
|
||||
current_page = target_phys >> 12;
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
#include "cpu_state.h"
|
||||
#include "mod_plugins.h"
|
||||
|
||||
/* Bounded, allocation-free and emulation-thread-only. Nothing is registered
|
||||
* by default; only trusted linked game code may opt in. */
|
||||
static struct {
|
||||
uint32_t address;
|
||||
PSXModFunctionReplacement callback;
|
||||
} replacements[64];
|
||||
static unsigned replacement_count;
|
||||
int psx_mod_set_function_replacement(uint32_t address, PSXModFunctionReplacement callback) {
|
||||
unsigned i;
|
||||
if (!address || (address & 3))
|
||||
return 0;
|
||||
for (i = 0; i < replacement_count; ++i) {
|
||||
if (replacements[i].address == address) {
|
||||
if (callback)
|
||||
replacements[i].callback = callback;
|
||||
else {
|
||||
replacements[i] = replacements[--replacement_count];
|
||||
replacements[replacement_count].address = 0;
|
||||
replacements[replacement_count].callback = 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
if (!callback)
|
||||
return 1;
|
||||
if (replacement_count == 64)
|
||||
return 0;
|
||||
replacements[replacement_count].address = address;
|
||||
replacements[replacement_count++].callback = callback;
|
||||
return 1;
|
||||
}
|
||||
int psx_mod_try_function_replacement(CPUState *cpu, uint32_t address) {
|
||||
unsigned i;
|
||||
/* No table scan in faithful/default-off games or builds. */
|
||||
if (!replacement_count || !cpu)
|
||||
return 0;
|
||||
for (i = 0; i < replacement_count; ++i)
|
||||
if (replacements[i].address == address) {
|
||||
const uint32_t return_pc = cpu->gpr[31];
|
||||
if (!replacements[i].callback(cpu, address))
|
||||
return 0;
|
||||
cpu->pc = return_pc;
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -14,6 +14,7 @@ def main():
|
||||
'psx_overlay_dispatch(cpu, addr);') < static_dispatch.index(
|
||||
'g_exec_phase = previous_phase;') < static_dispatch.index('if (handled) return 1;')
|
||||
assert source.count('psx_mod_function_entry(cpu, addr);') == 1
|
||||
assert source.count('psx_mod_try_function_replacement(cpu, addr)') == 1
|
||||
local_start = source.index('if (allow_local_dirty_flow && target != 0 &&')
|
||||
local_end = source.index('current_page = target_phys >> 12;', local_start)
|
||||
local = source[local_start:local_end]
|
||||
@@ -24,6 +25,7 @@ def main():
|
||||
assert 'OV_FPLOG_RET1();' in probe
|
||||
assert 'psx_overlay_dispatch(cpu' not in probe
|
||||
assert local.count('psx_mod_function_entry(cpu, target);') == 1
|
||||
assert local.count('psx_mod_try_function_replacement(cpu, target)') == 1
|
||||
assert local.index('overlay_loader_dispatch(cpu, target)') < local.index(
|
||||
'psx_mod_function_entry(cpu, target);') < local.index('pc = target;')
|
||||
print('interpreter mod entry guards: PASS')
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
#include "cpu_state.h"
|
||||
#include "mod_plugins.h"
|
||||
#undef NDEBUG
|
||||
#include <assert.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
static int enabled, calls;
|
||||
static int replace(CPUState *cpu, uint32_t address) {
|
||||
++calls;
|
||||
if (!enabled)
|
||||
return 0;
|
||||
cpu->gpr[2] = address;
|
||||
return 1;
|
||||
}
|
||||
static int replace_with_ra_clobber(CPUState *cpu, uint32_t address) {
|
||||
cpu->gpr[31] = address;
|
||||
return 1;
|
||||
}
|
||||
int main(void) {
|
||||
CPUState cpu = {0}, before;
|
||||
cpu.gpr[31] = 0x80001234;
|
||||
cpu.pc = 0;
|
||||
before = cpu;
|
||||
assert(!psx_mod_try_function_replacement(&cpu, 0x80001000));
|
||||
assert(!memcmp(&before, &cpu, sizeof cpu));
|
||||
assert(!psx_mod_set_function_replacement(0, replace));
|
||||
assert(!psx_mod_set_function_replacement(0x80001001, replace));
|
||||
assert(psx_mod_set_function_replacement(0x80001000, replace));
|
||||
assert(!psx_mod_try_function_replacement(&cpu, 0x80001000));
|
||||
assert(calls == 1 && !memcmp(&before, &cpu, sizeof cpu));
|
||||
enabled = 1;
|
||||
assert(!psx_mod_try_function_replacement(&cpu, 0x80001004));
|
||||
assert(psx_mod_try_function_replacement(&cpu, 0x80001000));
|
||||
assert(cpu.pc == 0x80001234 && cpu.gpr[2] == 0x80001000 && cpu.gpr[31] == before.gpr[31]);
|
||||
assert(psx_mod_set_function_replacement(0x80001000, 0));
|
||||
assert(!psx_mod_try_function_replacement(&cpu, 0x80001000));
|
||||
assert(psx_mod_set_function_replacement(0x80001000, 0));
|
||||
assert(psx_mod_set_function_replacement(0x80001000, replace_with_ra_clobber));
|
||||
assert(!psx_mod_try_function_replacement(0, 0x80001000));
|
||||
cpu.gpr[31] = 0x80001234;
|
||||
assert(psx_mod_try_function_replacement(&cpu, 0x80001000));
|
||||
assert(cpu.pc == 0x80001234 && cpu.gpr[31] == 0x80001000);
|
||||
assert(psx_mod_set_function_replacement(0x80001000, 0));
|
||||
for (unsigned i = 0; i < 64; ++i)
|
||||
assert(psx_mod_set_function_replacement(0x80002000 + 4 * i, replace));
|
||||
assert(!psx_mod_set_function_replacement(0x80004000, replace));
|
||||
assert(psx_mod_set_function_replacement(0x80002000, 0));
|
||||
assert(psx_mod_set_function_replacement(0x80004000, replace));
|
||||
assert(psx_mod_try_function_replacement(&cpu, 0x800020FC));
|
||||
assert(psx_mod_try_function_replacement(&cpu, 0x80004000));
|
||||
for (unsigned i = 1; i < 64; ++i)
|
||||
assert(psx_mod_set_function_replacement(0x80002000 + 4 * i, 0));
|
||||
assert(psx_mod_set_function_replacement(0x80004000, 0));
|
||||
before = cpu;
|
||||
assert(!psx_mod_try_function_replacement(&cpu, 0x80004000));
|
||||
assert(!memcmp(&before, &cpu, sizeof cpu));
|
||||
puts("PASS native replacement opt-in, fallback, return ABI, unregister and capacity");
|
||||
}
|
||||
Reference in New Issue
Block a user