mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
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:
@@ -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.
|
||||
@@ -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);
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user