Two root causes: preserve mod state across rebuilds; never save a fabricated PC

STAGING (runtime.cmake): the framework catalog staging ran rm -rf on the whole
build-dir mods/ tree before recopying. But both catalogs own only a packages/
subtree -- mods/state.toml is USER STATE written at runtime, recording which
mods are enabled with which options. Every rebuild therefore silently disabled
every mod: 8 MB, widescreen, framerate reverting off again and again, which
presented as "the mods regressed" (aspect ratios stopped working, the game
fail-fasted on 0xB0 without its 8 MB). Scope the clear to mods/packages; the
deleted-mods-don't-survive guarantee is intact because that is all the catalogs
ship.

SAVESTATE (savestate.c): a save polled with resume_pc hint 0 substituted a
candidate from a chain ending in sticky-BB latches and $ra -- values that pass
the sanity check while being the wrong place to resume. The first save of a
session (hint not yet published) wrote such a state; it loaded, ran minutes off
the rails, and died at PC=0. Poison that looks valid at save time. hint==0 now
routes into the existing defer machinery: the save completes at the next poll
with a published block-leader PC, or fails loudly after the timeout. Verified:
first save of a fresh session now records pc=0x8015FAEC, no fallback line.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
tetrisgm
2026-08-17 23:26:39 -07:00
parent 686ccd3c00
commit 1469550ce5
2 changed files with 18 additions and 2 deletions
+8 -1
View File
@@ -1282,8 +1282,15 @@ function(psxrecomp_add_runtime_target target)
# appearing on the Mods page (and inflate the release packagers'
# catalog assertions). This runs before the game's own staging, so
# both catalogs land on a clean slate.
#
# Scoped to mods/packages, NOT mods/: both catalogs own only a
# packages/ subtree, while mods/state.toml is USER STATE written at
# runtime (which mods are enabled, with which options). Wiping the
# whole tree silently disabled every enabled mod on every rebuild --
# 8 MB, widescreen, framerate all reverting off, repeatedly misread
# as the mods themselves regressing.
COMMAND ${CMAKE_COMMAND} -E rm -rf
"$<TARGET_FILE_DIR:${target}>/mods"
"$<TARGET_FILE_DIR:${target}>/mods/packages"
COMMAND ${CMAKE_COMMAND} -E copy_directory
"${PSXRECOMP_ROOT}/mods/builtin"
"$<TARGET_FILE_DIR:${target}>/mods"
+10 -1
View File
@@ -687,7 +687,16 @@ void savestate_poll(CPUState* cpu, uint32_t resume_pc) {
int slot = s_save_pending;
char path[600];
uint32_t pc = savestate_resolve_resume_pc(cpu, resume_pc);
if (!savestate_resume_pc_ok(pc)) {
/* hint==0 ALSO defers, even when the resolver found a plausible-looking
* substitute. The substitute chain ends in sticky-BB latches and $ra,
* which pass the sanity check while being the wrong place to resume:
* the first F7 of a session saved such a state, it loaded, ran for
* ~150M instructions off the rails, and died at PC=0 -- poison that
* looks valid at save time and only fails minutes later. Deferring
* reuses the existing retry: the save completes at the next poll where
* a real block-leader PC is published, or fails LOUDLY after the
* timeout instead of writing a corrupt state silently. */
if (resume_pc == 0u || !savestate_resume_pc_ok(pc)) {
/* FMV/present edges often poll with hint=0; wait briefly for a
* sticky BB / IRQ latch rather than writing pc=0 poison. */
const double now = savestate_mono_ms();