mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
Preserve restored handoff and snapshot host-call continuations
This commit is contained in:
@@ -30,6 +30,15 @@ if(BUILD_TESTING)
|
||||
target_include_directories(mod_texture_banks_test PRIVATE include)
|
||||
add_test(NAME mod_texture_banks_test COMMAND mod_texture_banks_test)
|
||||
if(NOT MSVC)
|
||||
add_executable(host_snapshot_guard_test tests/test_host_snapshot_guard.c)
|
||||
add_executable(fntrace_restore_handoff_test
|
||||
tests/test_fntrace_restore_handoff.c src/fntrace.c)
|
||||
foreach(test_target host_snapshot_guard_test fntrace_restore_handoff_test)
|
||||
target_include_directories(${test_target} PRIVATE include src)
|
||||
target_compile_options(${test_target} PRIVATE -O2 -flto -ffunction-sections -fdata-sections)
|
||||
target_link_options(${test_target} PRIVATE -flto -Wl,--gc-sections)
|
||||
add_test(NAME ${test_target} COMMAND ${test_target})
|
||||
endforeach()
|
||||
add_executable(ws_world_scene_test tests/test_ws_world_scene.c)
|
||||
target_include_directories(ws_world_scene_test PRIVATE include src)
|
||||
target_compile_options(ws_world_scene_test PRIVATE -O2 -flto -ffunction-sections -fdata-sections)
|
||||
|
||||
@@ -84,6 +84,7 @@ typedef struct {
|
||||
* never allowed) -> normal boot + recapture.
|
||||
*/
|
||||
enum {
|
||||
BS_SEC_HANDOFF = 0x12, /* game-start latch; optional for legacy snapshots */
|
||||
BS_SEC_MODMEM = 0x11, /* allocated opt-in CPU/GPU enhancement arenas */
|
||||
BS_SEC_CPU = 0x01, /* CPUState: gpr/pc/hi/lo/cop0/gte_data/gte_ctrl */
|
||||
BS_SEC_RAM = 0x02, /* 2 MB main RAM */
|
||||
|
||||
@@ -89,6 +89,9 @@ void fntrace_record(CPUState* cpu, uint32_t target);
|
||||
* First dispatch into [lo, hi) calls cdrom_notify_game_started(). */
|
||||
void fntrace_set_game_range(uint32_t lo, uint32_t hi);
|
||||
int fntrace_is_game_started(void);
|
||||
/* Restore the handoff latch without repeating boot-only RAM/CD mutations.
|
||||
* -1 infers legacy snapshots from the registered entry's live bytes. */
|
||||
void fntrace_restore_game_started(int started);
|
||||
/* One-shot game-start handoff. Idempotent — safe to call from any path
|
||||
* (compiled dispatch, dirty-RAM interpreter, generated entry function).
|
||||
* Performs dirty-image baseline clear, low-boot scratch clear, CD speed
|
||||
|
||||
@@ -8,6 +8,12 @@ extern "C" {
|
||||
#endif
|
||||
|
||||
struct CPUState;
|
||||
/* A host adapter that preserves registers outside guest RAM is not a
|
||||
* serializable continuation. Defer snapshots until it returns; scheduler
|
||||
* escapes discard the scope with the abandoned host stack. */
|
||||
void psx_snapshot_host_call_begin(void);
|
||||
void psx_snapshot_host_call_end(void);
|
||||
void psx_snapshot_host_call_reset(void);
|
||||
|
||||
/* IRQ bit positions in I_STAT/I_MASK */
|
||||
#define IRQ_VBLANK 0
|
||||
|
||||
@@ -454,9 +454,11 @@ 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));
|
||||
bool mod_register_savestate_plugin(const std::string& id, void (*callback)(void));
|
||||
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_savestate_plugin(const std::string& id);
|
||||
void mod_clear_plugins_for_tests();
|
||||
|
||||
} // namespace PSXRecompV4
|
||||
|
||||
@@ -21,6 +21,10 @@ int psx_mod_register_activation_plugin(const char* id,
|
||||
PSXModActivationCallback callback);
|
||||
int psx_mod_register_vblank_plugin(const char* id,
|
||||
PSXModVBlankCallback callback);
|
||||
/* Runs after successful guest-state restore, before the restored PC resumes.
|
||||
* Rebind host hooks here; do not advance guest gameplay as a VBlank would. */
|
||||
int psx_mod_register_savestate_plugin(const char* id,
|
||||
PSXModActivationCallback callback);
|
||||
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. */
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "gpu_vram_dirty.h"
|
||||
#include "cpu_state.h" /* gte_canonicalize_cpu_state after CPU wire restore */
|
||||
#include "interrupts.h"
|
||||
#include "fntrace.h"
|
||||
#include "psx_cycles.h"
|
||||
#include "psx_icache.h" /* g_psx_icache_tv — fetch-cost tags in BS_SEC_ICACHE */
|
||||
#include "pst_wire.h"
|
||||
@@ -372,11 +373,18 @@ static int boot_state_save_to(BsOut* o, const CPUState* cpu,
|
||||
h.codegen_hash = (uint32_t)PSX_OVERLAY_CODEGEN_HASH;
|
||||
h.abi_tag = (int32_t)PSX_OVERLAY_ABI_TAG;
|
||||
h.codegen_ver = (uint32_t)PSX_OVERLAY_CODEGEN_VER;
|
||||
h.section_count = 16 + (psx_mod_memory_snapshot_bytes() ? 1u : 0u);
|
||||
h.section_count = 17 + (psx_mod_memory_snapshot_bytes() ? 1u : 0u);
|
||||
|
||||
ok = write_header_le(o, &h);
|
||||
|
||||
if (ok) ok = write_cpu_section(o, cpu);
|
||||
if (ok) {
|
||||
uint8_t phase[4];
|
||||
PstW w;
|
||||
pst_w_init(&w, phase, sizeof phase);
|
||||
ok = pst_w_u32(&w, (uint32_t)fntrace_is_game_started()) &&
|
||||
write_section(o, BS_SEC_HANDOFF, phase, sizeof phase);
|
||||
}
|
||||
if (ok) ok = write_section(o, BS_SEC_RAM, memory_get_ram_ptr(), RAM_SIZE);
|
||||
if (ok) ok = write_section(o, BS_SEC_SPAD, memory_get_scratchpad_ptr(), SPAD_SIZE);
|
||||
if (ok && psx_mod_memory_snapshot_bytes())
|
||||
@@ -816,6 +824,7 @@ int boot_state_load_buffer(const uint8_t* file, size_t file_len,
|
||||
(1u<<BS_SEC_SIO)|(1u<<BS_SEC_MDEC)|(1u<<BS_SEC_DIRTY)|
|
||||
(psx_mod_memory_snapshot_bytes() ? (1u<<BS_SEC_MODMEM) : 0u);
|
||||
uint32_t seen = 0;
|
||||
int game_started = -1;
|
||||
int ok = 1;
|
||||
const double t0 = boot_state_mono_ms();
|
||||
double inflate_ms = 0.0;
|
||||
@@ -894,7 +903,14 @@ int boot_state_load_buffer(const uint8_t* file, size_t file_len,
|
||||
}
|
||||
|
||||
t_sec = boot_state_mono_ms();
|
||||
if (!apply_section(tag, apply_ptr, apply_len, cpu, entry_pc)) ok = 0;
|
||||
if (tag == BS_SEC_HANDOFF) {
|
||||
PstR r;
|
||||
uint32_t phase = 0;
|
||||
pst_r_init(&r, apply_ptr, apply_len);
|
||||
if (apply_len != 4 || !pst_r_u32(&r, &phase) || phase > 1) ok = 0;
|
||||
else game_started = (int)phase;
|
||||
}
|
||||
else if (!apply_section(tag, apply_ptr, apply_len, cpu, entry_pc)) ok = 0;
|
||||
else if (tag < 32) seen |= (1u << tag);
|
||||
{
|
||||
double dt = boot_state_mono_ms() - t_sec;
|
||||
@@ -909,6 +925,11 @@ int boot_state_load_buffer(const uint8_t* file, size_t file_len,
|
||||
if (!ok || (seen & required) != required)
|
||||
return 0;
|
||||
|
||||
/* Re-running the normal handoff after a cold gameplay restore clears the
|
||||
* restored low kernel scratch and dirtiness baseline, then captures a
|
||||
* bogus boot state. CD speed is already in the CD-ROM section. */
|
||||
fntrace_restore_game_started(game_started);
|
||||
|
||||
/* RAM was memcpy'd; force overlay revalidation before resume. */
|
||||
overlay_watch_invalidate_after_ram_restore();
|
||||
|
||||
|
||||
@@ -47,6 +47,15 @@ void fntrace_set_game_range(uint32_t lo, uint32_t hi) {
|
||||
|
||||
int fntrace_is_game_started(void) { return s_game_started; }
|
||||
|
||||
void fntrace_restore_game_started(int started) {
|
||||
if (started < 0) {
|
||||
extern int dirty_ram_text_image_matches(uint32_t phys, uint32_t len);
|
||||
started = s_game_entry_phys != 0 &&
|
||||
dirty_ram_text_image_matches(s_game_entry_phys, 32u);
|
||||
}
|
||||
s_game_started = started != 0;
|
||||
}
|
||||
|
||||
/* Centralised game-start transition. Idempotent — safe to call from both
|
||||
* the dispatcher (fntrace_record) and the generated entry-point function.
|
||||
* Performs the complete handoff side effects: dirty-image baseline clear,
|
||||
|
||||
@@ -632,6 +632,13 @@ int psx_irq_resume_context_snapshot_site(void)
|
||||
return g_cosim_dirty_pump_site;
|
||||
}
|
||||
|
||||
static unsigned s_snapshot_host_call_depth;
|
||||
void psx_snapshot_host_call_begin(void) { ++s_snapshot_host_call_depth; }
|
||||
void psx_snapshot_host_call_end(void) {
|
||||
if (s_snapshot_host_call_depth) --s_snapshot_host_call_depth;
|
||||
}
|
||||
void psx_snapshot_host_call_reset(void) { s_snapshot_host_call_depth = 0; }
|
||||
|
||||
uint32_t psx_irq_resume_context_snapshot_pc(void)
|
||||
{
|
||||
return g_dirty_safe_resume_pc;
|
||||
@@ -639,6 +646,7 @@ uint32_t psx_irq_resume_context_snapshot_pc(void)
|
||||
|
||||
int psx_irq_resume_context_snapshot_safe_at(uint32_t resume_pc)
|
||||
{
|
||||
if (s_snapshot_host_call_depth) return 0;
|
||||
if (g_cosim_dirty_pump_site == 0)
|
||||
return 1;
|
||||
|
||||
|
||||
@@ -522,6 +522,16 @@ void dirty_ram_register_text_image(uint32_t phys_lo, const uint8_t *bytes,
|
||||
|
||||
int dirty_ram_text_image_registered(void) { return text_ref_image != NULL; }
|
||||
|
||||
/* Cold legacy restores have no serialized handoff latch. Check live bytes
|
||||
* directly: the page-clean shortcut is not evidence of a loaded game after
|
||||
* RAM/dirty-bit replacement, and a warm session's latch is not evidence either. */
|
||||
int dirty_ram_text_image_matches(uint32_t phys, uint32_t len) {
|
||||
phys &= 0x1FFFFFFFu;
|
||||
return text_ref_image && len && phys >= text_ref_lo && phys < text_ref_hi &&
|
||||
len <= text_ref_hi - phys &&
|
||||
memcmp(ram + phys, text_ref_image + phys - text_ref_lo, len) == 0;
|
||||
}
|
||||
|
||||
static inline void text_guard_note_write(uint32_t phys, uint32_t val, int size) {
|
||||
if (!text_ref_image) return;
|
||||
if (phys < text_ref_lo || phys + (uint32_t)size > text_ref_hi) return;
|
||||
|
||||
@@ -32,6 +32,7 @@ std::map<std::string, ModBuiltinResolver>& builtin_resolvers() {
|
||||
}
|
||||
|
||||
struct RegisteredPlugin {
|
||||
void (*savestate)(void) = nullptr;
|
||||
PSXModActivationCallback activation = nullptr;
|
||||
PSXModVBlankCallback vblank = nullptr;
|
||||
};
|
||||
@@ -1452,7 +1453,21 @@ bool mod_register_vblank_plugin(const std::string& id, void (*callback)(void)) {
|
||||
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.savestate);
|
||||
}
|
||||
|
||||
bool mod_register_savestate_plugin(const std::string& id, void (*callback)(void)) {
|
||||
if (!valid_id(id) || !callback) return false;
|
||||
RegisteredPlugin& plugin = registered_plugins()[id];
|
||||
if (plugin.savestate) return false;
|
||||
plugin.savestate = callback;
|
||||
return true;
|
||||
}
|
||||
|
||||
void mod_invoke_savestate_plugin(const std::string& id) {
|
||||
const auto found = registered_plugins().find(id);
|
||||
if (found != registered_plugins().end() && found->second.savestate)
|
||||
found->second.savestate();
|
||||
}
|
||||
|
||||
void mod_invoke_activation_plugin(const std::string& id) {
|
||||
@@ -3663,3 +3678,9 @@ extern "C" int psx_mod_register_activation_plugin(
|
||||
return id &&
|
||||
PSXRecompV4::mod_register_activation_plugin(id, callback) ? 1 : 0;
|
||||
}
|
||||
|
||||
extern "C" int psx_mod_register_savestate_plugin(
|
||||
const char* id, PSXModActivationCallback callback) {
|
||||
return id &&
|
||||
PSXRecompV4::mod_register_savestate_plugin(id, callback) ? 1 : 0;
|
||||
}
|
||||
|
||||
@@ -1287,6 +1287,11 @@ extern "C" void mod_runtime_on_savestate_loaded(void) {
|
||||
applied = true;
|
||||
}
|
||||
s.main_applied = true;
|
||||
for (const ModResolution::Plugin& plugin : s.plan.plugins) {
|
||||
s.current_plugin = &plugin;
|
||||
mod_invoke_savestate_plugin(plugin.id);
|
||||
s.current_plugin = nullptr;
|
||||
}
|
||||
if (applied)
|
||||
std::fprintf(stdout,
|
||||
"psxrecomp: reapplied mod plan %s after savestate restore\n",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
#include "cpu_state.h"
|
||||
#include "interrupts.h"
|
||||
#include "psx_bss.h"
|
||||
#include "psx_runtime.h" /* fix B: psx_exc_escape_reason_t + g_exc_escape_reason */
|
||||
#include "debug_server.h"
|
||||
@@ -810,6 +811,7 @@ void psx_scheduler_run(CPUState* cpu)
|
||||
g_sched_escape.reason = PSX_RUN_CONTINUE;
|
||||
for (;;) {
|
||||
if (setjmp(g_scheduler_jmpbuf) != 0) {
|
||||
psx_snapshot_host_call_reset();
|
||||
/* A structured escape unwound the native stack to here. Reset the
|
||||
* invariants the skipped psx_dispatch_impl frames would otherwise
|
||||
* own. No exception_jmpbuf frame is ever skipped (switch fires only
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
#include "fntrace.h"
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
static int effects, image_matches;
|
||||
void dirty_ram_clear_image_baseline(void) { ++effects; }
|
||||
void memory_clear_low_boot_scratch(void) { ++effects; }
|
||||
void cdrom_notify_game_started(void) { ++effects; }
|
||||
void boot_state_trigger_capture(const CPUState* cpu) { (void)cpu; ++effects; }
|
||||
int dirty_ram_text_image_matches(uint32_t phys, uint32_t len) {
|
||||
return image_matches && phys == 0x6B58Cu && len == 32u;
|
||||
}
|
||||
static void check(int ok, const char* message) {
|
||||
if (!ok) { fprintf(stderr, "FAIL: %s\n", message); exit(1); }
|
||||
}
|
||||
int main(void) {
|
||||
CPUState cpu = {0};
|
||||
fntrace_set_game_range(0x8006B58Cu, 0);
|
||||
fntrace_restore_game_started(1);
|
||||
fntrace_mark_game_started(&cpu);
|
||||
check(fntrace_is_game_started() && !effects, "cold game restore skips destructive handoff");
|
||||
fntrace_restore_game_started(0);
|
||||
check(!fntrace_is_game_started() && !effects, "pre-game restore resets warm latch");
|
||||
fntrace_mark_game_started(&cpu);
|
||||
check(effects == 4, "normal boot retains all handoff effects");
|
||||
fntrace_mark_game_started(&cpu);
|
||||
check(effects == 4, "handoff remains one-shot");
|
||||
image_matches = 1;
|
||||
fntrace_set_game_range(0x8006B58Cu, 0);
|
||||
fntrace_restore_game_started(-1);
|
||||
check(fntrace_is_game_started() && effects == 4, "legacy game restore infers loaded entry");
|
||||
image_matches = 0;
|
||||
fntrace_restore_game_started(-1);
|
||||
check(!fntrace_is_game_started(), "legacy BIOS restore does not inherit warm game latch");
|
||||
puts("restored handoff checks passed");
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
#include "interrupts.c"
|
||||
|
||||
int g_cosim_dirty_pump_site;
|
||||
int g_call_unit_depth;
|
||||
int g_psx_dispatch_depth;
|
||||
uint32_t g_dirty_safe_resume_pc;
|
||||
|
||||
static void check(int ok, const char* message) {
|
||||
if (!ok) { fprintf(stderr, "FAIL: %s\n", message); exit(1); }
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
uint32_t pc = 0x80025070u;
|
||||
check(psx_irq_resume_context_snapshot_safe_at(pc), "native boundary admitted");
|
||||
psx_snapshot_host_call_begin();
|
||||
psx_snapshot_host_call_begin();
|
||||
check(!psx_irq_resume_context_snapshot_safe_at(pc), "nested host adapter deferred");
|
||||
psx_snapshot_host_call_end();
|
||||
check(!psx_irq_resume_context_snapshot_safe_at(pc), "outer adapter still active");
|
||||
psx_snapshot_host_call_end();
|
||||
check(psx_irq_resume_context_snapshot_safe_at(pc), "register restore admits snapshot");
|
||||
g_cosim_dirty_pump_site = 1;
|
||||
g_dirty_safe_resume_pc = pc;
|
||||
check(psx_irq_resume_context_snapshot_safe_at(pc), "retired interpreter boundary admitted");
|
||||
psx_snapshot_host_call_begin();
|
||||
check(!psx_irq_resume_context_snapshot_safe_at(pc), "interpreter adapter deferred");
|
||||
psx_snapshot_host_call_reset(); /* scheduler longjmp abandoned the adapter */
|
||||
check(psx_irq_resume_context_snapshot_safe_at(pc), "scheduler escape releases abandoned scope");
|
||||
psx_snapshot_host_call_end(); /* no underflow */
|
||||
check(psx_irq_resume_context_snapshot_safe_at(pc), "unmatched end stays safe");
|
||||
check(!psx_irq_resume_context_snapshot_safe_at(pc + 4), "existing resume-PC guard retained");
|
||||
g_call_unit_depth = 1;
|
||||
check(!psx_irq_resume_context_snapshot_safe_at(pc), "existing call-unit guard retained");
|
||||
puts("host snapshot continuation checks passed");
|
||||
return 0;
|
||||
}
|
||||
@@ -20,6 +20,7 @@ static std::array<uint8_t, 2 * 1024 * 1024> ram;
|
||||
static int failures;
|
||||
static int activation_calls;
|
||||
static int plugin_calls;
|
||||
static int restore_calls;
|
||||
|
||||
extern "C" uint8_t psx_read_byte(uint32_t address) {
|
||||
return ram[address & 0x1fffffu];
|
||||
@@ -61,6 +62,9 @@ extern "C" uint32_t psx_mod_gpu_dma_memory_alloc(uint32_t, uint32_t) {
|
||||
return 0;
|
||||
}
|
||||
extern "C" int psx_ws_x_margin(void) { return 0; }
|
||||
extern "C" void gpu_ws_tag_hud_primitive(uint32_t, int) {}
|
||||
extern "C" void gpu_ws_tag_world_primitive(uint32_t, int) {}
|
||||
extern "C" void gpu_ws_set_adaptive_backdrop_preload(int) {}
|
||||
|
||||
/* Stand-in for the GPU's display geometry. psx_mod_display_width/height must
|
||||
* report exactly what the presenter reports -- a plugin drawing an overlay
|
||||
@@ -82,6 +86,11 @@ static void test_activation_plugin(void) {
|
||||
activation_calls++;
|
||||
}
|
||||
|
||||
static void test_restore_plugin(void) {
|
||||
restore_calls++;
|
||||
if (ram[0x1000] != 0xa1) failures++; /* main plan must be reapplied first */
|
||||
}
|
||||
|
||||
static void check(bool value, const char* message) {
|
||||
if (!value) {
|
||||
std::cerr << "FAIL: " << message << "\n";
|
||||
@@ -308,6 +317,10 @@ int main() {
|
||||
check(PSXRecompV4::mod_register_vblank_plugin(
|
||||
"runtime.test-vblank", test_vblank_plugin),
|
||||
"runtime test plugin must register");
|
||||
check(psx_mod_register_savestate_plugin("runtime.test-vblank", test_restore_plugin),
|
||||
"restore callback must register through the C API");
|
||||
check(!psx_mod_register_savestate_plugin("runtime.test-vblank", test_restore_plugin),
|
||||
"duplicate restore callback must be rejected");
|
||||
check(PSXRecompV4::mod_runtime_initialize(
|
||||
root, "SLUS-RUNTIME", 0x80002000, {}, &error),
|
||||
error.c_str());
|
||||
@@ -347,6 +360,8 @@ int main() {
|
||||
ram[0x1200] = 2; ram[0x1201] = 0;
|
||||
ram[0x1202] = 1; ram[0x1203] = 0x32;
|
||||
mod_runtime_on_savestate_loaded();
|
||||
check(restore_calls == 1 && activation_calls == 1 && plugin_calls == 1,
|
||||
"restore must rebind hooks without activation or a gameplay VBlank");
|
||||
check(ram[0x1000] == 0xa1 && ram[0x1003] == 0xa4 &&
|
||||
ram[0x1100] == 42 && ram[0x1102] == 43 &&
|
||||
ram[0x1200] == 1 && ram[0x1201] == 0x42 &&
|
||||
|
||||
Reference in New Issue
Block a user