Poll staged user snapshots before interrupt maintenance fast returns

This commit is contained in:
Matthew Stanley
2026-10-24 14:31:31 -07:00
parent 161ef53b20
commit f29503ffcd
4 changed files with 65 additions and 3 deletions
+4
View File
@@ -126,6 +126,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);
typedef struct PSXSavestateStatus {
+16
View File
@@ -27,6 +27,7 @@
#include "interrupts.h"
#include "mod_runtime.h"
#include "savestate.h"
#include "psx_memory.h"
#include "sio.h"
#include "timers.h"
@@ -1118,6 +1119,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. */
@@ -1197,6 +1207,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
+19 -3
View File
@@ -61,10 +61,13 @@ static int s_content_scope_valid = 1;
static char s_content_token[71]; /* "_mods_" + complete SHA-256 + NUL */
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;
@@ -788,6 +791,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;
@@ -814,6 +820,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;
@@ -866,6 +875,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;
}
@@ -886,10 +896,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) {
@@ -917,7 +928,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;
@@ -950,6 +963,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;
@@ -970,6 +984,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,
@@ -1018,6 +1033,7 @@ void savestate_poll(CPUState* cpu, uint32_t resume_pc) {
(s_load_blob && s_load_blob_len > 0) ? "blob" : "file";
#endif
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;
+26
View File
@@ -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();