From ef8961e17ac0de0353f98f4158f267bb3985b30f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 19 Oct 2026 20:54:29 +0200 Subject: [PATCH] Freeze frame: Keep away from GE stepping, and keep other loads Freeze-frame saves and loads its own snapshot outside SaveState::Process, so it missed the check for a display list stopped in the GE debugger: stepping a draw in a frozen frame reloaded the state under the list. And a slot load or rewind while frozen was undone by the next frame's freeze reload, while saying "Loaded State". Now it becomes the frozen state, and is a full load (the GPU only skips its resets for the freeze's own reloads). Co-Authored-By: Claude Opus 5.5 (1M context) --- Core/SaveState.cpp | 24 +++++++++++++++++++++++- UI/EmuScreen.cpp | 4 ++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/Core/SaveState.cpp b/Core/SaveState.cpp index e73ab34cd3..b19b7ccd99 100644 --- a/Core/SaveState.cpp +++ b/Core/SaveState.cpp @@ -1094,6 +1094,22 @@ int g_screenshotFailures; #endif } + // Freeze-frame reloads its snapshot every frame, which would undo any other load right away. So a + // load while frozen becomes the new frozen state (freezeNext snapshots it), and is a full load: + // the GPU only skips its resets for freeze-frame's own reloads, which it tells by frozen. + struct FrozenLoadScope { + const bool frozen = PSP_CoreParameter().frozen; + FrozenLoadScope() { + PSP_CoreParameter().frozen = false; + } + ~FrozenLoadScope() { + if (frozen) { + PSP_CoreParameter().frozen = true; + PSP_CoreParameter().freezeNext = true; + } + } + }; + // What a save's worker job needs, and its completion after. struct SaveJob { Path path; @@ -1294,7 +1310,11 @@ int g_screenshotFailures; saveStateInitialGitVersion = read.gitVersion; SaveStart state; std::string errorString; - CChunkFileReader::Error result = CChunkFileReader::LoadPtr(read.buffer.get(), read.size, state, &errorString); + CChunkFileReader::Error result; + { + FrozenLoadScope frozenScope; + result = CChunkFileReader::LoadPtr(read.buffer.get(), read.size, state, &errorString); + } read.buffer.reset(); if (result == CChunkFileReader::ERROR_NONE) { @@ -1348,6 +1368,8 @@ int g_screenshotFailures; std::string message; std::string metadata; std::string errorString; + // Covers HandleLoadFailure()'s retries below too. + FrozenLoadScope frozenScope; CChunkFileReader::Error result = rewindStates.FinishRestore(&errorString, &metadata); if (result == CChunkFileReader::ERROR_NONE) { message = sc->T("Loaded State"); diff --git a/UI/EmuScreen.cpp b/UI/EmuScreen.cpp index 42b984b292..7651e3d8ed 100644 --- a/UI/EmuScreen.cpp +++ b/UI/EmuScreen.cpp @@ -1783,6 +1783,10 @@ ScreenRenderFlags EmuScreen::RunEmulation(bool skipBufferEffects) { if (Achievements::HardcoreModeActive()) { PSP_CoreParameter().freezeNext = false; PSP_CoreParameter().frozen = false; + } else if (coreState == CORE_STEPPING_GE || coreState == CORE_RUNNING_GE) { + // A display list stopped in the GE debugger still belongs to the sceGe call that started + // it. Loading over that, or saving it mid-list, leaves them out of step. SaveState::Process() + // waits this out too. } else if (PSP_CoreParameter().freezeNext) { PSP_CoreParameter().frozen = true; PSP_CoreParameter().freezeNext = false;