From 9e4cf574db1e217b5221d93fc958fbfde2d511ad Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Sat, 24 Oct 2026 14:31:31 -0700 Subject: [PATCH] Poll staged user snapshots before interrupt maintenance fast returns --- runtime/include/savestate.h | 4 ++++ runtime/src/interrupts.c | 16 +++++++++++++++ runtime/src/savestate.c | 22 +++++++++++++++++--- runtime/tests/test_host_snapshot_guard.c | 26 ++++++++++++++++++++++++ 4 files changed, 65 insertions(+), 3 deletions(-) diff --git a/runtime/include/savestate.h b/runtime/include/savestate.h index 0a4d9505..1805fb14 100644 --- a/runtime/include/savestate.h +++ b/runtime/include/savestate.h @@ -114,6 +114,10 @@ int savestate_request_load_blob_protocol(const void* data, size_t size); /* 1 while a staged save/load has not yet been consumed by savestate_poll. */ int savestate_pending(void); +/* Read-only hot-path hint for interrupt maintenance. Updated with staged + * requests; actual execution still passes every savestate_poll safety gate. */ +extern int g_psx_savestate_pending; + /* Machine-readable lifecycle receipt for deterministic debug harnesses. */ void savestate_status_json(char* buf, size_t cap); diff --git a/runtime/src/interrupts.c b/runtime/src/interrupts.c index 073ef35b..6278c77a 100644 --- a/runtime/src/interrupts.c +++ b/runtime/src/interrupts.c @@ -26,6 +26,7 @@ */ #include "interrupts.h" +#include "savestate.h" #include "psx_memory.h" #include "sio.h" #include "timers.h" @@ -1116,6 +1117,15 @@ int psx_interrupt_delivery_needed(const CPUState* cpu) { return 1; } +static void poll_staged_user_snapshot(CPUState* cpu) { + extern uint32_t g_dirty_safe_resume_pc; + if (!in_exception && g_psx_savestate_pending) { + uint32_t pc = g_dirty_safe_resume_pc ? g_dirty_safe_resume_pc + : s_compiled_interrupt_resume_pc; + savestate_poll(cpu, pc); + } +} + void psx_check_interrupts(CPUState* cpu) { /* Frozen guest time (psx_cycle_freeze.h): no delivery, and no * check-boundary bookkeeping that the stock timeline would not do. */ @@ -1195,6 +1205,12 @@ void psx_check_interrupts(CPUState* cpu) { } } + /* Quiet native/HLE gameplay may execute far fewer than 0x4000 BB checks + * in a frame. A staged user snapshot must not wait for that amortized + * maintenance counter: short actions/voices can finish before capture. + * Keep exception, host-adapter and resume-PC gates in savestate_poll, and + * retain the canonical netplay edge gate above. Idle work stays amortized. */ + poll_staged_user_snapshot(cpu); s_irq_path_entry++; /* MotK VLC / FMV hot edge: sticky unmasked I_STAT (CD/VBlank) while diff --git a/runtime/src/savestate.c b/runtime/src/savestate.c index 72d6fdac..b891666e 100644 --- a/runtime/src/savestate.c +++ b/runtime/src/savestate.c @@ -53,10 +53,13 @@ static uint32_t s_entry_pc; static int s_configured = 0; static int s_save_pending = -1; /* slot, or -1 */ static int s_load_pending = -1; +int g_psx_savestate_pending = 0; static int s_load_completed = 0; static int s_load_failed = 0; static uint32_t s_status_generation = 0; static int s_status_pending = 0; +static uint32_t s_status_polls; +static const char* s_status_wait = ""; static int s_status_last_ok = 0; static int s_status_last_load = 0; static int s_status_last_slot = -1; @@ -659,6 +662,9 @@ static int request_save_inner(int slot) { s_last_save_pc = 0; /* block netplay transfer until this write stamps a PC */ s_save_defer_slot = -1; s_save_pending = slot; + s_status_polls = 0; + s_status_wait = "boundary"; + g_psx_savestate_pending = 1; s_status_pending = 1; s_status_last_load = 0; s_status_last_slot = slot; @@ -684,6 +690,9 @@ static int request_load_inner(int slot) { s_load_failed = 0; s_load_completed = 0; s_load_pending = slot; + s_status_polls = 0; + s_status_wait = "boundary"; + g_psx_savestate_pending = 1; s_status_pending = 1; s_status_last_load = 1; s_status_last_slot = slot; @@ -735,6 +744,7 @@ int savestate_request_load_blob_protocol(const void* data, size_t size) { s_load_failed = 0; s_load_completed = 0; s_load_pending = 0; /* non-negative: poll will prefer the blob */ + g_psx_savestate_pending = 1; return 1; } @@ -746,10 +756,11 @@ void savestate_status_json(char* buf, size_t cap) { if (!buf || cap == 0) return; snprintf(buf, cap, "\"generation\":%u,\"pending\":%d,\"last_ok\":%d," - "\"last_op\":\"%s\",\"last_slot\":%d", + "\"last_op\":\"%s\",\"last_slot\":%d," + "\"polls\":%u,\"wait\":\"%s\"", (unsigned)s_status_generation, s_status_pending, s_status_last_ok, s_status_last_load ? "load" : "save", - s_status_last_slot); + s_status_last_slot, s_status_polls, s_status_pending ? s_status_wait : ""); } int savestate_take_load_completed(void) { @@ -777,7 +788,9 @@ uint32_t savestate_last_save_pc(void) { void savestate_poll(CPUState* cpu, uint32_t resume_pc) { if (s_save_pending < 0 && s_load_pending < 0) return; /* hot path: nothing staged */ extern int psx_mod_function_entry_active(void); - if (psx_mod_function_entry_active()) return; + ++s_status_polls; + if (psx_mod_function_entry_active()) { s_status_wait = "mod_callback"; return; } + s_status_wait = "resume_pc"; if (s_save_pending >= 0) { int slot = s_save_pending; @@ -810,6 +823,7 @@ void savestate_poll(CPUState* cpu, uint32_t resume_pc) { if (now - s_save_defer_t0 < 2000.0) return; s_save_pending = -1; + g_psx_savestate_pending = s_load_pending >= 0; s_save_defer_slot = -1; s_last_save_pc = 0; s_save_failed = 1; @@ -830,6 +844,7 @@ void savestate_poll(CPUState* cpu, uint32_t resume_pc) { psx_frontend_on_savestate_notify(0, slot, 0); } else { s_save_pending = -1; + g_psx_savestate_pending = s_load_pending >= 0; s_save_defer_slot = -1; if (pc != resume_pc && resume_pc == 0u) { fprintf(stderr, @@ -872,6 +887,7 @@ void savestate_poll(CPUState* cpu, uint32_t resume_pc) { int slot = s_load_pending; int loaded = 0; s_load_pending = -1; + g_psx_savestate_pending = s_save_pending >= 0; char path[600]; const double t_load0 = savestate_mono_ms(); double t_after_boot = t_load0; diff --git a/runtime/tests/test_host_snapshot_guard.c b/runtime/tests/test_host_snapshot_guard.c index 50efd514..a4186450 100644 --- a/runtime/tests/test_host_snapshot_guard.c +++ b/runtime/tests/test_host_snapshot_guard.c @@ -5,6 +5,12 @@ int g_cosim_dirty_pump_site; int g_call_unit_depth; int g_psx_dispatch_depth; uint32_t g_dirty_safe_resume_pc; +int g_psx_savestate_pending; +static unsigned snapshot_polls; +static uint32_t snapshot_pc; +void savestate_poll(CPUState* cpu, uint32_t pc) { + (void)cpu; ++snapshot_polls; snapshot_pc = pc; +} static void check(int ok, const char* message) { if (!ok) { fprintf(stderr, "FAIL: %s\n", message); exit(1); } @@ -12,6 +18,26 @@ static void check(int ok, const char* message) { int main(void) { uint32_t pc = 0x80025070u; + CPUState cpu = {0}; + s_compiled_interrupt_resume_pc = pc; + poll_staged_user_snapshot(&cpu); + check(snapshot_polls == 0, "idle snapshot work stays amortized"); + g_psx_savestate_pending = 1; + poll_staged_user_snapshot(&cpu); + check(snapshot_polls == 1 && snapshot_pc == pc, + "staged request reaches the next native boundary"); + in_exception = 1; + poll_staged_user_snapshot(&cpu); + check(snapshot_polls == 1, "exception cannot service a user snapshot"); + in_exception = 0; + g_dirty_safe_resume_pc = pc + 4; + poll_staged_user_snapshot(&cpu); + check(snapshot_polls == 2 && snapshot_pc == pc + 4, + "staged dirty-code request uses its own resume boundary"); + g_psx_savestate_pending = 0; + poll_staged_user_snapshot(&cpu); + check(snapshot_polls == 2, "completed request stops urgent polling"); + g_dirty_safe_resume_pc = 0; check(psx_irq_resume_context_snapshot_safe_at(pc), "native boundary admitted"); psx_snapshot_host_call_begin(); psx_snapshot_host_call_begin();