Merge pull request #22370 from hrydgard/more-savestate-fixes

Claude code review: More savestate fixes
This commit is contained in:
Henrik Rydgård authored and GitHub committed 2026-09-28 12:12:40 -06:00
commit 2ec92c10a3
27 files changed
+228 -26

No files matched your search

+6 -1
View File
@@ -551,7 +551,8 @@ void DoState(PointerWrap &p) {
event_types[i].callback = AntiCrashCallback;
event_types[i].name = "INVALID EVENT";
}
nextEventTypeRestoreId = n - 1;
// The state's own events are 0..n-1, so one it doesn't have gets the first id after those.
nextEventTypeRestoreId = n;
usedEventTypes.clear();
}
// Needed in every pass, or each restore would look like a duplicate and get a new id.
@@ -578,6 +579,10 @@ void DoState(PointerWrap &p) {
lastGlobalTimeTicks = 0;
lastGlobalTimeUs = 0;
}
if (p.mode == PointerWrap::MODE_READ) {
// A debugger's run-until deadline is in emulated us, and the ticks it maps to just changed.
RecomputeBreakDeadline();
}
__AudioCPUMHzChange();
}
+15 -2
View File
@@ -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() {
+2 -1
View File
@@ -1093,7 +1093,8 @@ void DirectoryFileSystem::DoState(PointerWrap &p) {
Do(p, entry.guestFilename);
Do(p, entry.access);
u32 err;
bool opened = entry.hFile.Open(basePath, entry.guestFilename, entry.access, err);
// The original open created the file, so an exclusive create would fail now.
bool opened = entry.hFile.Open(basePath, entry.guestFilename, (FileAccess)(entry.access & ~FILEACCESS_EXCL), err);
bool brokenFile = !opened;
if (!opened) {
ERROR_LOG(Log::FileSystem, "Failed to reopen file while loading state: %s", entry.guestFilename.c_str());
+6
View File
@@ -857,6 +857,12 @@ void ISOFileSystem::DoState(PointerWrap &p) {
std::string path;
Do(p, path);
of.file = GetFromPath(path);
if (!of.file) {
// Loaded against a different image. Like a file that's gone from a directory,
// drop the handle rather than keep one that points at nothing.
ERROR_LOG(Log::FileSystem, "Failed to reopen file while loading state: %s", path.c_str());
continue;
}
} else {
of.file = NULL;
}
+8 -2
View File
@@ -252,8 +252,8 @@ static bool g_disableHLELatched;
static DisableHLEFlags g_unavailableDisableFlags = (DisableHLEFlags)0;
// Process compat flags.
static DisableHLEFlags ComputeDisableHLEFlags() {
DisableHLEFlags flags = (DisableHLEFlags)g_Config.iDisableHLE | AlwaysDisableHLEFlags();
static DisableHLEFlags ComputeDisableHLEFlags(DisableHLEFlags alwaysDisabled = AlwaysDisableHLEFlags()) {
DisableHLEFlags flags = (DisableHLEFlags)g_Config.iDisableHLE | alwaysDisabled;
if (PSP_CoreParameter().compat.flags().DisableHLESceFont) {
flags |= DisableHLEFlags::sceFont;
}
@@ -472,6 +472,12 @@ void HLEDoState(PointerWrap &p) {
g_effectiveDisableHLE = (DisableHLEFlags)disableHLE;
g_disableHLELatched = true;
}
} else if (p.mode == p.MODE_READ) {
// Older states didn't save the flags. They were all made before any module graduated past
// these, so resolving their imports against today's defaults would leave the ones since
// (sceMpeg, sceFont, the leaf libraries...) as unresolved stubs.
g_effectiveDisableHLE = ComputeDisableHLEFlags(DisableHLEFlags::scePsmf | DisableHLEFlags::scePsmfPlayer | DisableHLEFlags::sceCcc);
g_disableHLELatched = true;
}
// Can't be inside a syscall when saving state, reset this so errors aren't misleading.
+5 -1
View File
@@ -43,8 +43,12 @@ void __AACShutdown() {
void __AACDoState(PointerWrap &p) {
auto s = p.Section("sceAAC", 0, 1);
if (!s)
if (!s) {
if (p.mode == PointerWrap::MODE_READ) {
__AACShutdown();
}
return;
}
Do(p, g_aacMap);
}
+4
View File
@@ -255,6 +255,10 @@ void __AtracDoState(PointerWrap &p) {
u32 count = (u32)g_pendingOutput.size();
Do(p, count);
if (p.mode == PointerWrap::MODE_READ) {
if (!p.CheckRead((size_t)count * sizeof(u32) * 2)) {
g_pendingOutput.clear();
return;
}
g_pendingOutput.resize(count);
}
for (AtracPendingOutput &pending : g_pendingOutput) {
+7 -1
View File
@@ -658,9 +658,15 @@ void Register_sceAudiocodec() {
void __sceAudiocodecDoState(PointerWrap &p){
auto s = p.Section("AudioList", 0, 2);
if (!s) {
oldStateLoaded = true;
if (p.mode == PointerWrap::MODE_READ) {
clearDecoders();
oldStateLoaded = true;
}
return;
}
if (p.mode == PointerWrap::MODE_READ) {
oldStateLoaded = false;
}
int count = (int)g_audioDecoderContexts.size();
Do(p, count);
+4
View File
@@ -299,6 +299,10 @@ void __DisplayDoState(PointerWrap &p) {
Do(p, lastFlipCycles);
Do(p, nextFlipCycles);
}
if (p.mode == p.MODE_READ) {
// Not saved. Start counting again rather than carry over the session before the load.
lastFlipsTooFrequent = 0;
}
gpu->DoState(p);
+3
View File
@@ -1107,6 +1107,9 @@ void __FontDoState(PointerWrap &p) {
Do(p, actionPostAllocCallback);
__KernelRestoreActionType(actionPostAllocCallback, PostAllocCallback::Create);
if (s >= 2) {
useAllocCallbacks = true;
}
Do(p, actionPostOpenCallback);
__KernelRestoreActionType(actionPostOpenCallback, PostOpenCallback::Create);
if (s >= 2) {
+3
View File
@@ -70,6 +70,9 @@ void __ImposeDoState(PointerWrap &p) {
if (s >= 2) {
Do(p, imposeChanges);
Do(p, imposeAvls);
} else if (p.mode == p.MODE_READ) {
imposeChanges = 0;
imposeAvls = 0;
}
}
+3
View File
@@ -726,6 +726,9 @@ void __IoDoState(PointerWrap &p) {
if (s >= 3) {
Do(p, lastMemStickState);
Do(p, lastMemStickFatState);
} else if (p.mode == p.MODE_READ) {
lastMemStickState = MemoryStick_State();
lastMemStickFatState = MemoryStick_FatState();
}
for (int i = 0; i < PSP_COUNT_FDS; ++i) {
+5
View File
@@ -170,6 +170,7 @@ void __KernelInit()
__HttpInit();
__NpInit();
__RegInit();
__NpDrmInit();
SaveState::Init(); // Must be after IO, as it may create a directory
Reporting::Init();
@@ -314,6 +315,10 @@ void __KernelDoState(PointerWrap &p)
__UsbGpsDoState(p);
__UsbMicDoState(p);
__RegDoState(p);
// These two were written long ago but never called, so older states lack them.
__DmacDoState(p);
__UsbCamDoState(p);
__NpDrmDoState(p);
// IMPORTANT! Add new sections last!
}
+17 -5
View File
@@ -471,6 +471,9 @@ struct SceKernelSMOption {
static int actionAfterModule;
static std::set<SceUID> 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<PSPModule>(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
+6 -1
View File
@@ -158,8 +158,13 @@ void __Mp3Shutdown() {
void __Mp3DoState(PointerWrap &p) {
auto s = p.Section("sceMp3", 0, 3);
if (!s)
if (!s) {
if (p.mode == PointerWrap::MODE_READ) {
__Mp3Shutdown();
resourceInited = false;
}
return;
}
if (s >= 2) {
Do(p, g_mp3Map);
+18 -1
View File
@@ -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() {
+14 -3
View File
@@ -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<std::recursive_mutex> 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<std::recursive_mutex> 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();
}
+21
View File
@@ -1,6 +1,8 @@
#include "ext/libkirk/AES.h"
#include "ext/libkirk/amctrl.h"
#include "Common/Serialize/Serializer.h"
#include "Common/Serialize/SerializeFuncs.h"
#include "Core/HLE/scePspNpDrm_user.h"
#include "Core/MemMapHelpers.h"
#include "Core/HLE/HLE.h"
@@ -16,6 +18,25 @@ static const int PSP_NPDRM_LICENSEE_KEY_LENGTH = 0x10;
static u8 licenseeKey[PSP_NPDRM_LICENSEE_KEY_LENGTH];
static bool isLicenseeKeySet = false;
void __NpDrmInit() {
memset(licenseeKey, 0, sizeof(licenseeKey));
isLicenseeKeySet = false;
}
void __NpDrmDoState(PointerWrap &p) {
auto s = p.Section("sceNpDrm", 0, 1);
if (!s) {
// Older states didn't keep the key. The game set it once at startup, so EDATA it opens after
// the load can't be decrypted until it runs again.
if (p.mode == PointerWrap::MODE_READ) {
__NpDrmInit();
}
return;
}
Do(p, isLicenseeKeySet);
DoArray(p, licenseeKey, PSP_NPDRM_LICENSEE_KEY_LENGTH);
}
// Check if the file is an encrypted EDAT file by reading the magic number
static bool isEncrypted(u32 edataFd) {
// Check for "\0PSPEDAT" magic number
+2
View File
@@ -5,6 +5,8 @@
class PointerWrap;
void Register_sceNpDrm();
void __NpDrmInit();
void __NpDrmDoState(PointerWrap &p);
// A module wrapped in an NPDRM "\0PSPEDAT" container has its PRX encrypted against a key built
// from that container's header and, usually, the licensee key the game handed over through
+15 -2
View File
@@ -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)
+2
View File
@@ -109,6 +109,8 @@ void __UmdDoState(PointerWrap &p)
if (g_UMDReplacePermit && p.mode == p.MODE_READ) {
System_Notify(SystemNotification::UI);
}
} else if (p.mode == p.MODE_READ) {
g_UMDReplacePermit = false;
}
if (s > 2) {
Do(p, umdInsertChangeEvent);
+9 -1
View File
@@ -58,12 +58,20 @@ void __UsbCamInit() {
}
void __UsbCamDoState(PointerWrap &p) {
const bool wasCapturing = config->mode == Camera::Mode::Video;
auto s = p.Section("sceUsbCam", 0, 1);
if (!s) {
if (p.mode == p.MODE_READ) {
// Older states didn't save the camera, so leave it off.
if (wasCapturing) {
Camera::stopCapture();
}
config->mode = Camera::Mode::Unused;
config->type = Camera::ConfigType::CfNone;
}
return;
}
const bool wasCapturing = config->mode == Camera::Mode::Video;
Do(p, *config);
if (p.mode == p.MODE_READ) {
if (config->mode == Camera::Mode::Video) { // stillImage? TBD
+12 -4
View File
@@ -45,11 +45,19 @@ void __UsbGpsInit() {
}
void __UsbGpsDoState(PointerWrap &p) {
auto s = p.Section("sceUsbGps", 0, 1);
if (!s)
return;
const bool wasOn = gpsStatus == GPS_STATE_ON;
auto s = p.Section("sceUsbGps", 0, 1);
if (!s) {
// Older states didn't save it, so leave it off.
if (p.mode == p.MODE_READ) {
if (wasOn) {
System_GPSCommand("close");
}
gpsStatus = GPS_STATE_OFF;
}
return;
}
Do(p, gpsStatus);
if (p.mode == p.MODE_READ) {
if (gpsStatus == GPS_STATE_ON) {
+12
View File
@@ -126,6 +126,14 @@ void __UsbMicDoState(PointerWrap &p) {
eventMicBlockingResume = -1;
CoreTiming::RestoreRegisterEvent(eventMicBlockingResume, "MicBlockingResume", &__MicBlockingResume);
waitingThreads.clear();
// Nor was the mic, so leave it off.
if (Microphone::isMicStarted()) {
Microphone::stopMic();
}
numNeedSamples = 0;
curTargetAddr = 0;
readMicDataLength = 0;
micState = 0;
}
return;
}
@@ -146,6 +154,10 @@ void __UsbMicDoState(PointerWrap &p) {
if (s > 2) {
Do(p, curTargetAddr);
Do(p, readMicDataLength);
} else if (p.mode == p.MODE_READ) {
// The host mic thread writes to curTargetAddr, so don't leave the one from before the load.
curTargetAddr = 0;
readMicDataLength = 0;
}
if (!audioBuf && numNeedSamples > 0) {
audioBuf = new QueueBuf(numNeedSamples << 1);
+8
View File
@@ -194,6 +194,14 @@ void __VideocodecDoState(PointerWrap &p) {
auto s = p.Section("sceVideocodec", 0, 1);
if (!s) {
if (p.mode == p.MODE_READ) {
// A state from before this module. Don't keep the contexts and ME memory of the session
// before the load.
ClearContexts(false);
g_meRam.clear();
g_meRam.shrink_to_fit();
g_meAlloc.Shutdown();
}
return;
}
+18
View File
@@ -22,9 +22,27 @@
void BufferQueue::DoState(PointerWrap &p) {
auto s = p.Section("BufferQueue", 0, 2);
const int allocatedSize = bufQueueSize;
Do(p, bufQueueSize);
Do(p, start);
Do(p, end);
if (p.mode == PointerWrap::MODE_READ && bufQueue) {
// Normally the owner allocated the same size, but don't write past the buffer if not.
const int size = bufQueueSize;
if (size <= 0 || start < 0 || end < 0 || start > size || end > size || !p.CheckRead(size)) {
bufQueueSize = allocatedSize;
clear();
p.SetError(PointerWrap::ERROR_FAILURE);
return;
}
if (size != allocatedSize) {
const int savedStart = start;
const int savedEnd = end;
alloc(size);
start = savedStart;
end = savedEnd;
}
}
if (bufQueue) {
DoArray(p, bufQueue, bufQueueSize);
}
+3 -1
View File
@@ -70,7 +70,9 @@ namespace MIPSComp {
if (!s)
return;
bool dummy = false;
// This is startDefaultPrefix. Writing false made a JIT loading the state assume an uneaten
// prefix for the rest of the session.
bool dummy = currentMIPS->HasDefaultPrefix();
Do(p, dummy);
if (s >= 2) {
dummy = true;