From b491a3597f9bfa62d1ce26946d2999ccc73ffe32 Mon Sep 17 00:00:00 2001 From: Yves Date: Sun, 4 Oct 2026 05:54:03 +0800 Subject: [PATCH] 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 --- docs/RENDER_PASSES.md | 19 +++++++++ .../render-pass-mod-callback-rollback.md | 42 +++++++++++++++++++ runtime/include/mod_runtime.h | 8 ++++ runtime/include/render_pass.h | 1 + runtime/src/debug_server.c | 4 +- runtime/src/mod_runtime.cpp | 11 +++++ runtime/src/render_pass.c | 13 +++++- runtime/tests/test_render_pass_abort.c | 33 +++++++++++++++ runtime/tests/test_render_pass_sandbox.c | 8 ++++ 9 files changed, 136 insertions(+), 3 deletions(-) create mode 100644 docs/internal/upstream/render-pass-mod-callback-rollback.md diff --git a/docs/RENDER_PASSES.md b/docs/RENDER_PASSES.md index 879b628e..467a93d8 100644 --- a/docs/RENDER_PASSES.md +++ b/docs/RENDER_PASSES.md @@ -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. diff --git a/docs/internal/upstream/render-pass-mod-callback-rollback.md b/docs/internal/upstream/render-pass-mod-callback-rollback.md new file mode 100644 index 00000000..053123b2 --- /dev/null +++ b/docs/internal/upstream/render-pass-mod-callback-rollback.md @@ -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 . 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 ` 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. diff --git a/runtime/include/mod_runtime.h b/runtime/include/mod_runtime.h index 1a448876..d8d48306 100644 --- a/runtime/include/mod_runtime.h +++ b/runtime/include/mod_runtime.h @@ -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); diff --git a/runtime/include/render_pass.h b/runtime/include/render_pass.h index fb130b5e..3b283a78 100644 --- a/runtime/include/render_pass.h +++ b/runtime/include/render_pass.h @@ -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]; diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index 09357108..d665fa81 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -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 diff --git a/runtime/src/mod_runtime.cpp b/runtime/src/mod_runtime.cpp index 2c6e4a6a..d442eb6d 100644 --- a/runtime/src/mod_runtime.cpp +++ b/runtime/src/mod_runtime.cpp @@ -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(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; diff --git a/runtime/src/render_pass.c b/runtime/src/render_pass.c index 8882dd3c..e207560a 100644 --- a/runtime/src/render_pass.c +++ b/runtime/src/render_pass.c @@ -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; diff --git a/runtime/tests/test_render_pass_abort.c b/runtime/tests/test_render_pass_abort.c index 3f2415c5..1874df85 100644 --- a/runtime/tests/test_render_pass_abort.c +++ b/runtime/tests/test_render_pass_abort.c @@ -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; } diff --git a/runtime/tests/test_render_pass_sandbox.c b/runtime/tests/test_render_pass_sandbox.c index 604fe697..73134184 100644 --- a/runtime/tests/test_render_pass_sandbox.c +++ b/runtime/tests/test_render_pass_sandbox.c @@ -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)