render passes: restore mod callback context after watchdog aborts

Adapted from the original feature commits, retaining their author credit:
[6d44e21d](https://github.com/FractalEngineer/psxrecomp/commit/6d44e21df7ccedd1acbfa31844a53bf8bd8b316e)
[35b209d4](https://github.com/FractalEngineer/psxrecomp/commit/35b209d406c168e14f51bb8bbd549742473c7388)

Source scope and current-master validation: docs/internal/upstream/render-pass-mod-callback-rollback.md

Co-authored-by: Yves <calibratedbeats@gmail.com>
This commit is contained in:
Yves
2026-10-04 05:54:03 +08:00
parent dedbe47c97
commit b491a3597f
9 changed files with 136 additions and 3 deletions
+19
View File
@@ -219,3 +219,22 @@ Tests (runtime ctest unless noted):
The GPU half of the VRAM transaction (hr colour, stencil, raw mirror blits)
needs an OpenGL context and is checked at runtime by
`PSX_RENDER_PASS_VERIFY=1`.
### Nested mod callbacks and watchdog rollback
The host checkpoint includes the mod function-entry depth and current plugin
owner. A pass can interrupt an outer callback; restoring to zero would lose
that caller's context. Watchdog longjmp restores the saved depth/owner instead,
and normal-return verification checks their balance. This context stays local
to the committed session and is never part of a guest savestate.
`render_pass_stats.last_abort_detail` names skipped host exits and remains
latched across successful passes until session reset.
### Nested mod callback rollback
The host checkpoint includes function-entry depth and current plugin owner.
A pass can interrupt an outer callback; watchdog rollback restores its exact
saved context rather than resetting depth to zero. Normal-return verification
also checks balance. This host context is never serialized in guest savestates.
TCP render_pass_stats.last_abort_detail names skipped exits and remains latched
across successes until session reset.
@@ -0,0 +1,42 @@
# Render-pass callback rollback: source and validation
Reconstructed from [FractalEngineer/psxrecomp](https://github.com/FractalEngineer/psxrecomp), developed on
`vr-dev` at `9976567eb8e95510ca7eeeeccd9af3a260885de5`.
Exact source commits:
- [6d44e21d](https://github.com/FractalEngineer/psxrecomp/commit/6d44e21df7ccedd1acbfa31844a53bf8bd8b316e)
- [35b209d4](https://github.com/FractalEngineer/psxrecomp/commit/35b209d406c168e14f51bb8bbd549742473c7388)
Original author: Yves <calibratedbeats@gmail.com>. The feature commit retains
that exact human author identity. The focused port consolidates the selected
source hunks and corrections. Its commit also cites these sources and carries
the original human contributor's `Co-authored-by: Yves <calibratedbeats@gmail.com>` trailer.
Adapted: Callback depth/owner snapshot and restore on the existing render transaction, normal-return imbalance detection and focused recovery documentation.
Deliberately excluded: Paired-eye extension tests (included with stereo), game save files and scene-specific replay setup.
The feature was reconstructed on upstream master
`db62eec15f81c88aef3d6dab51d4e566a7167d23`, retaining current upstream APIs,
renderer/input policy and build integration. Validation after reconstruction:
Nested callback/owner rollback, nonzero outer callback, normal-return imbalance and watchdog recovery regressions.
The motivating consumer is [Medal of Honor, SLUS-00974 (NTSC-U)](https://github.com/FractalEngineer/Medal-Of-Honor-PSX-Recomp/tree/e323885958c2ae0b0df847bed6175f9e76524442).
Game-specific integration remains in that repository. Consuming this change
requires a deliberate framework pin update in the game repo and a rebuild.
This is a host/runtime/tool change; it requires no BIOS or game C regeneration.
Historical alpha acceptance used the older framework checkpoint above. It is
separate from current-master branch verification. Fresh game, renderer and
headset checks, where applicable, are recorded in the PR validation notes;
uncertain hardware/runtime behavior must not be inferred from a clean link.
## Current-master integration validation
Windows, GCC 13.2, Ninja, Release. Runtime test checks remain active with NDEBUG enabled.
Release abort/sandbox tests pass for nested callback depth/owner, nonzero outer context, mod-only normal-return imbalance and watchdog recovery. Fresh integrated flat gameplay is reachable after save/load. Historical slot-load recovery after an in-game watchdog is recorded separately.
Integration: [Medal of Honor, SLUS-00974 NTSC-U](https://github.com/FractalEngineer/Medal-Of-Honor-PSX-Recomp/tree/e323885958c2ae0b0df847bed6175f9e76524442), built against the corrected framework stack with `PSXRECOMP_ROOT`. Bundled OpenBIOS LLE cold boot, menu navigation, mission gameplay, fresh savestate creation/loading and post-load input/frame advancement were checked with screenshots. An older alpha slot-5 savestate was refused; compatibility with historical states is not established. Memory-card round trips, attract-demo completion, other games and fresh headset visuals were not tested.
The registered recompiler suite reports 132 passes, four failures and three disabled tests. The four failures reproduce on unchanged upstream master in this Windows toolchain: `portable_settings_paths_test`, `optional_netplay_auth`, `compile_overlays_static_tail` and `aot_overlay_discovery`. The introduced BIOS-fingerprint and GL-guard failures are fixed.
+8
View File
@@ -50,6 +50,14 @@ void mod_runtime_on_savestate_loaded(void);
* launcher commit and before renderer/window initialization. */
void mod_runtime_activate_plugins(void);
void mod_runtime_on_vblank(void);
/* Host-only context for a nested render transaction. Restore the interrupted
* callback's depth/owner after longjmp; never serialize this into guest saves. */
typedef struct ModFunctionEntryContext {
uint32_t depth;
const void *plugin;
} ModFunctionEntryContext;
void mod_runtime_function_entry_context_save(ModFunctionEntryContext *out);
void mod_runtime_function_entry_context_restore(const ModFunctionEntryContext *in);
void mod_runtime_patch_disc_sector(uint32_t lba, int raw_sector,
uint8_t* bytes, uint32_t size);
void mod_runtime_enable_disc_patches(void);
+1
View File
@@ -48,6 +48,7 @@ typedef struct RenderPassStats {
uint64_t watchdog; /* of which: guest-cycle watchdog overruns */
uint64_t vram_leaks; /* GPU writes outside the declared rect (dropped) */
uint64_t nesting_repairs; /* aborts whose skipped exits the restore undid */
char last_abort_detail[192]; /* skipped host exits; latched until session reset */
uint64_t verify_checks; /* PSX_RENDER_PASS_VERIFY comparisons */
uint64_t verify_mismatch; /* ... that found a difference */
uint64_t dropped[RENDER_PASS_DROP_CLASSES];
+2 -2
View File
@@ -8041,7 +8041,7 @@ static void handle_render_pass_stats(int id, const char *json)
"\"backups_reused\":%llu,\"pass_attempts\":%llu,"
"\"argument_refused\":%llu,\"status_refused\":%llu,"
"\"begin_refused\":%llu,\"checkpoint_refused\":%llu,"
"\"last_failure\":%s}",
"\"last_failure\":%s,\"last_abort_detail\":\"%s\"}",
id, (unsigned long long)st.plans, (unsigned long long)st.planned,
(unsigned long long)st.wanted, (unsigned long long)st.refused,
(unsigned long long)st.passes, (unsigned long long)st.aborted,
@@ -8071,7 +8071,7 @@ static void handle_render_pass_stats(int id, const char *json)
(unsigned long long)st.pass_attempts,
(unsigned long long)st.argument_refused, (unsigned long long)st.status_refused,
(unsigned long long)st.begin_refused, (unsigned long long)st.checkpoint_refused,
failure_json);
failure_json, st.last_abort_detail);
}
/* render_pass_refuse on=<0|1>: make the OpenGL backend decline render passes
+11
View File
@@ -1603,6 +1603,17 @@ extern "C" int psx_mod_function_entry_active(void) {
return PSXRecompV4::function_entry_depth != 0;
}
extern "C" void mod_runtime_function_entry_context_save(ModFunctionEntryContext *out) {
out->depth = PSXRecompV4::function_entry_depth;
out->plugin = PSXRecompV4::state().current_plugin;
}
extern "C" void mod_runtime_function_entry_context_restore(const ModFunctionEntryContext *in) {
PSXRecompV4::function_entry_depth = in->depth;
PSXRecompV4::state().current_plugin =
static_cast<const PSXRecompV4::ModResolution::Plugin *>(in->plugin);
}
extern "C" void mod_runtime_patch_disc_sector(uint32_t lba, int raw_sector,
uint8_t* bytes, uint32_t size) {
using namespace PSXRecompV4;
+12 -1
View File
@@ -28,6 +28,7 @@
#include "gpu.h"
#include "gpu_gl_renderer.h"
#include "mod_plugins.h"
#include "mod_runtime.h"
#include "overlay_loader.h"
#include "psx_icache.h"
#include "timers.h"
@@ -108,6 +109,7 @@ typedef struct RenderPassNesting {
int ov_active_depth;
uint32_t ov_inprogress;
void (*ov_flush)(void);
ModFunctionEntryContext mod_entry;
} RenderPassNesting;
static void nesting_save(RenderPassNesting *n) {
@@ -125,6 +127,7 @@ static void nesting_save(RenderPassNesting *n) {
dirty_ram_ld_delay_save(&n->ld_delay);
overlay_loader_native_nesting(&n->ov_active_depth, &n->ov_inprogress);
n->ov_flush = g_overlay_flush_pending_cycles;
mod_runtime_function_entry_context_save(&n->mod_entry);
}
static void nesting_restore(const RenderPassNesting *n) {
@@ -142,6 +145,7 @@ static void nesting_restore(const RenderPassNesting *n) {
dirty_ram_ld_delay_restore(&n->ld_delay);
overlay_loader_set_native_nesting(n->ov_active_depth, n->ov_inprogress);
g_overlay_flush_pending_cycles = n->ov_flush;
mod_runtime_function_entry_context_restore(&n->mod_entry);
}
/* PSX_RENDER_PASS_VERIFY: a pass that returned normally must leave the
@@ -158,7 +162,9 @@ static int nesting_balanced(const RenderPassNesting *n) {
now.exec_phase == n->exec_phase &&
now.precise_mode == n->precise_mode &&
now.ov_active_depth == n->ov_active_depth &&
now.ov_flush == n->ov_flush;
now.ov_flush == n->ov_flush &&
now.mod_entry.depth == n->mod_entry.depth &&
now.mod_entry.plugin == n->mod_entry.plugin;
}
/* After a watchdog abort, before the restore: which exits the longjmp
@@ -193,6 +199,9 @@ static int nesting_describe(const RenderPassNesting *n, int bb_defer_ck,
RP_NOTE(now.exec_phase != n->exec_phase, "exec phase %d->%d",
now.exec_phase, n->exec_phase);
RP_NOTE(now.ld_delay.armed != n->ld_delay.armed, "pending load");
RP_NOTE(now.mod_entry.depth != n->mod_entry.depth, "mod entries %+d",
(int)now.mod_entry.depth - (int)n->mod_entry.depth);
RP_NOTE(now.mod_entry.plugin != n->mod_entry.plugin, "mod owner");
#undef RP_NOTE
return count;
}
@@ -593,6 +602,8 @@ int psx_mod_render_pass(struct CPUState *cpu, const PSXModRenderPass *pass,
if (nesting_describe(&s_ck.nest, s_freeze.bb_defer, s_abort_detail,
sizeof s_abort_detail))
s_stats.nesting_repairs++;
snprintf(s_stats.last_abort_detail, sizeof s_stats.last_abort_detail,
"%s", s_abort_detail);
ok = 0;
}
s_stats.guest_cycles_last = psx_cycle_count - cycles_before;
+33
View File
@@ -21,8 +21,16 @@
#include "cpu_state.h"
#include "dirty_ram_interp.h"
#include "mod_plugins.h"
#include "mod_runtime.h"
#include "psx_cyc.h"
static ModFunctionEntryContext s_mod_entry;
void mod_runtime_function_entry_context_save(ModFunctionEntryContext *out) {
*out = s_mod_entry;
}
void mod_runtime_function_entry_context_restore(const ModFunctionEntryContext *in) {
s_mod_entry = *in;
}
static int failures;
#define CHECK(c, m) do { if (!(c)) { fprintf(stderr, "FAIL: %s\n", m); failures++; } } while (0)
@@ -197,6 +205,9 @@ static void guest_frame(CPUState *cpu, const Frames *f, int level) {
int prev_ov_depth = s_ov_active_depth;
uint32_t prev_ov_ip = s_ov_inprogress;
void (*prev_flush)(void) = g_overlay_flush_pending_cycles;
ModFunctionEntryContext prev_mod = s_mod_entry;
s_mod_entry.depth++;
s_mod_entry.plugin = f;
(void)guard;
psx_cyc_bb_defer_begin();
g_call_unit_depth = prev_unit + 1; /* overlay_loader_call_native */
@@ -225,6 +236,7 @@ static void guest_frame(CPUState *cpu, const Frames *f, int level) {
}
}
s_mod_entry = prev_mod;
s_ld.armed = 0; /* retired on interpreter exit */
g_precise_mode = prev_precise;
g_dirty_interp_active = prev_active;
@@ -251,7 +263,15 @@ static int leaky_pass_fn(struct CPUState *cpu, void *user, uint32_t alpha_q16) {
return 1;
}
static int mod_leaky_pass_fn(struct CPUState *cpu, void *user, uint32_t alpha_q16) {
(void)cpu; (void)user; (void)alpha_q16;
s_mod_entry.depth++;
s_mod_entry.plugin = user;
return 1;
}
typedef struct Live {
ModFunctionEntryContext mod_entry;
int bb_defer, unit, dma, host, dma_ch, active, phase, precise, dispatch;
int ov_depth;
uint32_t ov_ip, batch, resume;
@@ -262,6 +282,7 @@ typedef struct Live {
static void snap(Live *l) {
memset(l, 0, sizeof *l);
l->mod_entry = s_mod_entry;
l->bb_defer = g_psx_cyc_bb_defer;
l->unit = g_call_unit_depth;
l->dma = g_dma_exec_depth;
@@ -289,6 +310,8 @@ static void check_live(const Live *a, const char *when) {
#define SAME(field, what) do { \
snprintf(m, sizeof m, "%s: %s restored", when, what); \
CHECK(a->field == b.field, m); } while (0)
SAME(mod_entry.depth, "mod callback depth");
SAME(mod_entry.plugin, "mod callback owner");
SAME(bb_defer, "g_psx_cyc_bb_defer");
SAME(unit, "g_call_unit_depth");
SAME(dma, "g_dma_exec_depth");
@@ -332,6 +355,8 @@ int main(void) {
/* The live context the pass interrupts: inside two generated functions
* (the game's VSync entry hook), a native shard's flush hook installed,
* the interpreter's resume latch and a pending load of its own. */
s_mod_entry.depth = 1;
s_mod_entry.plugin = &cpu;
g_psx_cyc_bb_defer = 2;
g_psx_cyc_batch = 9;
g_psx_dispatch_depth = 3;
@@ -356,6 +381,8 @@ int main(void) {
CHECK(st.watchdog == 1 && st.aborted == 1 && st.passes == 0,
"the watchdog cut it off once");
CHECK(st.nesting_repairs == 1, "the landing found and undid skipped exits");
CHECK(strstr(st.last_abort_detail, "mod entries +5") &&
strstr(st.last_abort_detail, "mod owner"), "TCP abort detail includes skipped mod exits");
CHECK(!st.disabled, "one fault does not disable passes");
CHECK(s_open_passes == 0 && s_kept == 0, "the presenter closed the pass, no image");
CHECK(!g_psx_render_pass_active, "time is live again");
@@ -386,6 +413,12 @@ int main(void) {
CHECK(st.verify_mismatch == 1, "an unbalanced pass is reported in verify mode");
check_live(&live, "after an unbalanced pass");
snap(&live);
(void)psx_mod_render_pass(&cpu, &pass, mod_leaky_pass_fn, NULL);
render_pass_get_stats(&st);
CHECK(st.verify_mismatch == 2, "mod-only imbalance is reported");
check_live(&live, "after a mod-only unbalanced pass");
printf(failures ? "FAILED (%d)\n" : "ALL PASS\n", failures);
return failures ? 1 : 0;
}
+8
View File
@@ -34,9 +34,17 @@
#include "cpu_state.h"
#include "dirty_ram_interp.h"
#include "mod_plugins.h"
#include "mod_runtime.h"
#include "psx_cycles.h"
#include "timers.h"
static ModFunctionEntryContext s_mod_entry;
void mod_runtime_function_entry_context_save(ModFunctionEntryContext *out) {
*out = s_mod_entry;
}
void mod_runtime_function_entry_context_restore(const ModFunctionEntryContext *in) {
s_mod_entry = *in;
}
static int failures;
#define CHECK(c, m) do { if (!(c)) { fprintf(stderr, "FAIL: %s\n", m); failures++; } } while (0)