From 8663ce2a33b2096e830ef44870d69be878ac4dd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 28 Sep 2026 11:21:22 -0600 Subject: [PATCH] Savestate: Save state that was missing from several modules sceMpeg's AVC resource flag, whether the VSH is running, sceReg's handle counter, sceNet's pending apctl events and product code block, and the save dialog's copy of the original request (without which the first Update after a load reloaded the request and lost its results). Co-Authored-By: Claude Opus 5.5 (1M context) --- Core/Dialog/PSPSaveDialog.cpp | 17 +++++++++++++++-- Core/HLE/sceKernelModule.cpp | 22 +++++++++++++++++----- Core/HLE/sceMpeg.cpp | 19 ++++++++++++++++++- Core/HLE/sceNet.cpp | 17 ++++++++++++++--- Core/HLE/sceReg.cpp | 17 +++++++++++++++-- 5 files changed, 79 insertions(+), 13 deletions(-) diff --git a/Core/Dialog/PSPSaveDialog.cpp b/Core/Dialog/PSPSaveDialog.cpp index 08a9b88078..d3bde4a164 100755 --- a/Core/Dialog/PSPSaveDialog.cpp +++ b/Core/Dialog/PSPSaveDialog.cpp @@ -1373,8 +1373,9 @@ void PSPSaveDialog::DoState(PointerWrap &p) { // value is never SAVEIO_PENDING. Without this, loading a state taken // while a savedata operation was in flight would restart the operation // instead of resuming from its recorded status. Version 4 keeps the - // results of a finished operation that haven't been taken yet. - auto s = p.Section("PSPSaveDialog", 1, 5); + // results of a finished operation that haven't been taken yet. Version 6 keeps originalRequest, + // or the first Update after a load would reload the request and lose results kept in request. + auto s = p.Section("PSPSaveDialog", 1, 6); if (!s) { return; } @@ -1407,6 +1408,18 @@ void PSPSaveDialog::DoState(PointerWrap &p) { if (s == 4) { DoStateOldPendingWrites(p); } + if (s >= 6) { + Do(p, originalRequest); + } else if (p.mode == p.MODE_READ) { + // Best guess: what the game's request holds now, the way Update reads it. + memset(&originalRequest, 0, sizeof(originalRequest)); + if (Memory::IsValidRange(requestAddr, 4)) { + const u32 size = std::min((u32)sizeof(originalRequest), Memory::ReadUnchecked_U32(requestAddr)); + if (Memory::IsValidRange(requestAddr, size)) { + Memory::Memcpy(&originalRequest, requestAddr, size); + } + } + } } pspUtilityDialogCommon *PSPSaveDialog::GetCommonParam() { diff --git a/Core/HLE/sceKernelModule.cpp b/Core/HLE/sceKernelModule.cpp index 15eeec6a21..bb525627ae 100644 --- a/Core/HLE/sceKernelModule.cpp +++ b/Core/HLE/sceKernelModule.cpp @@ -471,6 +471,9 @@ struct SceKernelSMOption { static int actionAfterModule; static std::set loadedModules; +// Set once we've seen a PSP_MODULE_VSH_MODE module load (i.e. we're booting the VSH rather +// than a game), and reset on the next __KernelLoadExec. See ShouldHLEModuleForLoad below. +static bool g_runningVSH = false; // STATE END ////////////////////////////////////////////////////////////////////////// @@ -479,7 +482,7 @@ static void __KernelModuleInit() { } void __KernelModuleDoState(PointerWrap &p) { - auto s = p.Section("sceKernelModule", 1, 2); + auto s = p.Section("sceKernelModule", 1, 3); if (!s) return; @@ -491,6 +494,19 @@ void __KernelModuleDoState(PointerWrap &p) { if (s >= 2) { Do(p, loadedModules); } + if (s >= 3) { + Do(p, g_runningVSH); + } else if (p.mode == p.MODE_READ) { + // Derive it the way the loader sets it. + g_runningVSH = false; + for (SceUID moduleId : loadedModules) { + u32 error; + PSPModule *module = kernelObjects.Get(moduleId, error); + if (module && ((module->nm.attribute & PSP_MODULE_VSH_MODE) != 0 || equals(module->nm.name, "vsh_module"))) { + g_runningVSH = true; + } + } + } if (p.mode == p.MODE_READ) { u32 error; @@ -1120,10 +1136,6 @@ enum : u32 { ELF_MAGIC = 0x464c457f, }; -// Set once we've seen a PSP_MODULE_VSH_MODE module load (i.e. we're booting the VSH rather -// than a game), and reset on the next __KernelLoadExec. See ShouldHLEModuleForLoad below. -static bool g_runningVSH = false; - // A few flash0 modules (VSH's own bridge/UI/utility libraries) should only ever be genuinely // loaded - rather than faked via any HLE implementation we may have for them - once we know // we're actually running the VSH. A regular game never legitimately loads these, so this only diff --git a/Core/HLE/sceMpeg.cpp b/Core/HLE/sceMpeg.cpp index 87f7e660d2..c2f0f6371a 100644 --- a/Core/HLE/sceMpeg.cpp +++ b/Core/HLE/sceMpeg.cpp @@ -364,6 +364,10 @@ void __MpegInit() { isMpegInit = false; mpegLibVersion = 0x010A; streamIdGen = 1; + useRingbufferPutCallbackMulti = true; + sceMpegAvcResourceAddr = 0; + sceMpegAvcResourceDataAddr = 0; + sceMpegAvcResourceFlags = 0; actionPostPut = __KernelRegisterActionType(PostPutAction::Create); #ifdef USE_FFMPEG @@ -377,7 +381,7 @@ void __MpegInit() { } void __MpegDoState(PointerWrap &p) { - auto s = p.Section("sceMpeg", 1, 4); + auto s = p.Section("sceMpeg", 1, 5); if (!s) return; @@ -394,6 +398,7 @@ void __MpegDoState(PointerWrap &p) { useRingbufferPutCallbackMulti = false; ringbufferPutPacketsAdded = 0; } else { + useRingbufferPutCallbackMulti = true; Do(p, ringbufferPutPacketsAdded); } if (s < 4) { @@ -411,6 +416,18 @@ void __MpegDoState(PointerWrap &p) { __KernelRestoreActionType(actionPostPut, PostPutAction::Create); Do(p, g_mpegCtxs); + + if (s >= 5) { + Do(p, sceMpegAvcResourceFlags); + } else { + sceMpegAvcResourceFlags = 0; + } + if (p.mode == p.MODE_READ) { + // Constant for now, see sceMpegAvcResourceInit. + const bool inited = (sceMpegAvcResourceFlags & MPEG_AVC_RESOURCE_FLAG) != 0; + sceMpegAvcResourceAddr = inited ? 0x10000000 : 0; + sceMpegAvcResourceDataAddr = inited ? sceMpegAvcResourceAddr + 8 : 0; + } } void __MpegShutdown() { diff --git a/Core/HLE/sceNet.cpp b/Core/HLE/sceNet.cpp index 589a6bc1a3..2e6a22fee1 100644 --- a/Core/HLE/sceNet.cpp +++ b/Core/HLE/sceNet.cpp @@ -29,6 +29,7 @@ #include "Common/System/OSD.h" #include "Common/Serialize/Serializer.h" #include "Common/Serialize/SerializeFuncs.h" +#include "Common/Serialize/SerializeDeque.h" #include "Common/Serialize/SerializeMap.h" #include "Common/Data/Format/JSONReader.h" #include "Common/System/System.h" @@ -707,7 +708,7 @@ void netValidateLoopMemory() { // This feels like a dubious proposition, mostly... void __NetDoState(PointerWrap &p) { - auto s = p.Section("sceNet", 1, 6); + auto s = p.Section("sceNet", 1, 7); if (!s) return; @@ -763,6 +764,18 @@ void __NetDoState(PointerWrap &p) { netApctlInfoId = 0; NetApctl_InitDefaultInfo(); } + if (s >= 7) { + // The state only moves on when an event is processed, and each queues the next, so a + // connect in progress would never finish without them. + std::lock_guard apctlGuard(apctlEvtMtx); + Do(p, apctlEvents); + // Allocated from user memory, which the load just replaced. + Do(p, apctlProdCodeAddr); + } else if (p.mode == p.MODE_READ) { + std::lock_guard apctlGuard(apctlEvtMtx); + apctlEvents.clear(); + apctlProdCodeAddr = 0; + } if (p.mode == p.MODE_READ) { // Let's not change "Inited" value when Loading SaveState in the middle of multiplayer to prevent memory & port leaks @@ -770,8 +783,6 @@ void __NetDoState(PointerWrap &p) { netInetInited = cur_netInetInited; g_netInited = cur_netInited; - // Discard leftover events - apctlEvents.clear(); // Discard created resolvers for now (since i'm not sure whether the information in the struct is sufficient or not, and we don't support multi-threading yet anyway) __NetResolverShutdown(); } diff --git a/Core/HLE/sceReg.cpp b/Core/HLE/sceReg.cpp index c43fc4321b..54b0357fa1 100644 --- a/Core/HLE/sceReg.cpp +++ b/Core/HLE/sceReg.cpp @@ -1010,9 +1010,13 @@ static const KeyValue *LookupCategory(std::string_view path, int *count) { } void __RegDoState(PointerWrap &p) { - auto s = p.Section("sceReg", 0, 2); - if (!s) + auto s = p.Section("sceReg", 0, 3); + if (!s) { + if (p.mode == PointerWrap::MODE_READ) { + __RegInit(); + } return; + } Do(p, g_openRegistryMode); Do(p, g_openCategories); if (s >= 2) { @@ -1021,6 +1025,15 @@ void __RegDoState(PointerWrap &p) { // Old states didn't track this. Anything with a category open had the registry open too. g_openRegistryCount = g_openCategories.empty() ? 0 : 1; } + if (s >= 3) { + Do(p, g_handleGen); + } else if (p.mode == PointerWrap::MODE_READ) { + // Don't hand out a handle that's still open in the state. + g_handleGen = 1337; + if (!g_openCategories.empty()) { + g_handleGen = std::max(g_handleGen, g_openCategories.rbegin()->first + 1); + } + } } // Registry level (it seems only /system can exist, so kinda pointless)