SaveState: Store what the game was booted from, and read a state's start without loading it

SaveStart (still version 6) also stores whether the game was booted from an ISO, a PBP, an
ELF or a disc directory. PeekBootInfo decompresses only the start of a state to read them with
the disc ID, so a boot from a state alone can be set up before the state is loaded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Henrik RydgårdandClaude Opus 5.5 committed 2026-11-01 09:54:54 +01:00
1 parent 60dc6687f2
commit 09f65a860c
5 files changed
+152 -5

No files matched your search

+25 -2
View File
@@ -403,7 +403,8 @@ CChunkFileReader::Error CChunkFileReader::GetFileTitle(const Path &filename, std
}
// Streams the file through the decompressor, so the compressed data is never all in memory at once.
static bool DecompressZstdFromFile(File::IOFile &file, size_t compressedSize, u8 *out, size_t outSize) {
// With onlyStart, stops once out is full, rather than requiring the data to be exactly that long.
static bool DecompressZstdFromFile(File::IOFile &file, size_t compressedSize, u8 *out, size_t outSize, bool onlyStart = false) {
ZSTD_DCtx *ctx = ZSTD_createDCtx();
if (!ctx) {
return false;
@@ -414,6 +415,9 @@ static bool DecompressZstdFromFile(File::IOFile &file, size_t compressedSize, u8
size_t ret = 1;
bool success = true;
while (left > 0 && success) {
if (onlyStart && output.pos == outSize) {
break;
}
const size_t chunk = std::min(left, in.size());
if (!file.ReadBytes(in.data(), chunk)) {
ERROR_LOG(Log::SaveState, "ChunkReader: Error reading file");
@@ -422,7 +426,7 @@ static bool DecompressZstdFromFile(File::IOFile &file, size_t compressedSize, u8
}
left -= chunk;
ZSTD_inBuffer input{ in.data(), chunk, 0 };
while (input.pos < input.size) {
while (input.pos < input.size && !(onlyStart && output.pos == outSize)) {
const size_t inPos = input.pos;
const size_t outPos = output.pos;
ret = ZSTD_decompressStream(ctx, &output, &input);
@@ -434,6 +438,9 @@ static bool DecompressZstdFromFile(File::IOFile &file, size_t compressedSize, u8
}
}
ZSTD_freeDCtx(ctx);
if (onlyStart) {
return success && output.pos == outSize;
}
// ret is 0 once a frame is completely decoded.
return success && ret == 0 && output.pos == outSize;
}
@@ -465,6 +472,22 @@ static bool CompressZstd(const u8 *data, size_t size, std::vector<u8> *out) {
return !ZSTD_isError(remaining);
}
CChunkFileReader::Error CChunkFileReader::LoadFileStart(const Path &filename, u8 *out, size_t size) {
File::IOFile pFile(filename, "rb");
SChunkHeader header;
Error err = LoadFileHeader(pFile, header, nullptr);
if (err != ERROR_NONE) {
return err;
}
if (!header.Compress) {
return size <= header.ExpectedSize && pFile.ReadBytes(out, size) ? ERROR_NONE : ERROR_BAD_FILE;
}
if (SerializeCompressType(header.Compress) != SerializeCompressType::ZSTD || size > header.UncompressedSize) {
return ERROR_BAD_FILE;
}
return DecompressZstdFromFile(pFile, header.ExpectedSize, out, size, true) ? ERROR_NONE : ERROR_BAD_FILE;
}
CChunkFileReader::Error CChunkFileReader::LoadFile(const Path &filename, std::string *gitVersion, u8 *&_buffer, size_t &sz) {
if (!File::Exists(filename)) {
ERROR_LOG(Log::SaveState, "ChunkReader: File doesn't exist");
+3
View File
@@ -360,6 +360,9 @@ public:
// The file half of Load() and Save(), for doing it away from the state itself. LoadFile hands back
// a new[]'d buffer; SaveFile takes ownership of a malloc'd one (as from MeasureAndSavePtr).
static Error LoadFile(const Path &filename, std::string *gitVersion, u8 *&buffer, size_t &sz);
// Reads only the first size bytes of the state, decompressing no more than that. Not for snappy
// compressed (old) states.
static Error LoadFileStart(const Path &filename, u8 *out, size_t size);
static Error SaveFile(const Path &filename, const std::string &title, const char *gitVersion, u8 *buffer, size_t sz);
private:
+55 -3
View File
@@ -204,6 +204,26 @@ int g_screenshotFailures;
// we should probably just read back memory then instead (GTA for example). But this is a really unimportant edge case,
// when in-game it's just not an issue.
static constexpr size_t DISC_ID_SIZE = 16;
static constexpr size_t DISC_VERSION_SIZE = 8;
static BootType GetBootType(IdentifiedFileType type) {
switch (type) {
case IdentifiedFileType::PSP_ISO:
case IdentifiedFileType::PSP_ISO_NP:
return BootType::ISO;
case IdentifiedFileType::PSP_PBP:
case IdentifiedFileType::PSP_PBP_DIRECTORY:
return BootType::PBP;
case IdentifiedFileType::PSP_ELF:
return BootType::ELF;
case IdentifiedFileType::PSP_DISC_DIRECTORY:
return BootType::DISC_DIRECTORY;
default:
return BootType::UNKNOWN;
}
}
void SaveStart::DoState(PointerWrap &p) {
// Nothing may still be writing PSP memory while it's saved, or be left to write into what's loaded.
__UtilityWaitForIO();
@@ -217,15 +237,19 @@ int g_screenshotFailures;
return;
if (s >= 6) {
// Fixed width and first, so the game a state belongs to can be read without loading it.
char discID[16]{};
char discVersion[8]{};
// Fixed width and first, so the game a state belongs to can be read without loading it
// (see PeekBootInfo).
char discID[DISC_ID_SIZE]{};
char discVersion[DISC_VERSION_SIZE]{};
u8 bootType = 0;
if (p.mode != p.MODE_READ) {
truncate_cpy(discID, g_paramSFO.GetValueString("DISC_ID").c_str());
truncate_cpy(discVersion, g_paramSFO.GetValueString("DISC_VERSION").c_str());
bootType = (u8)GetBootType(PSP_CoreParameter().fileType);
}
p.DoVoid(discID, sizeof(discID));
p.DoVoid(discVersion, sizeof(discVersion));
Do(p, bootType);
if (p.mode == p.MODE_READ) {
const std::string stateDiscID(discID, strnlen(discID, sizeof(discID)));
const std::string runningDiscID = g_paramSFO.GetValueString("DISC_ID");
@@ -597,6 +621,34 @@ int g_screenshotFailures;
return title + " (" + filename + ")";
}
bool PeekBootInfo(const Path &filename, BootInfo *info, std::string *error) {
// The start of SaveStart, laid out as PointerWrap::Section writes it: a 16 byte name and the
// version, then the fields from version 6.
u8 start[16 + 4 + DISC_ID_SIZE + DISC_VERSION_SIZE + 1];
if (CChunkFileReader::LoadFileStart(filename, start, sizeof(start)) != CChunkFileReader::ERROR_NONE) {
*error = "Couldn't read the state";
return false;
}
char name[16]{};
strncpy(name, "SaveStart", sizeof(name));
if (memcmp(start, name, sizeof(name)) != 0) {
*error = "Not a savestate";
return false;
}
s32 version;
memcpy(&version, start + 16, sizeof(version));
if (version < 6) {
*error = "The state is too old to say which game it's from";
return false;
}
const char *discID = (const char *)start + 20;
const char *discVersion = discID + DISC_ID_SIZE;
info->discID.assign(discID, strnlen(discID, DISC_ID_SIZE));
info->discVersion.assign(discVersion, strnlen(discVersion, DISC_VERSION_SIZE));
info->bootType = (BootType)start[sizeof(start) - 1];
return true;
}
std::string GetTitle(const Path &filename) {
std::string title;
if (CChunkFileReader::GetFileTitle(filename, &title) == CChunkFileReader::ERROR_NONE) {
+19
View File
@@ -94,6 +94,25 @@ namespace SaveState {
std::string GetTitle(const Path &filename);
// What the game was booted from. Stored in states, so the values are fixed.
enum class BootType : u8 {
UNKNOWN = 0,
ISO = 1,
PBP = 2,
ELF = 3,
DISC_DIRECTORY = 4,
};
// What a boot needs to know about a state before it's loaded.
struct BootInfo {
std::string discID;
std::string discVersion;
BootType bootType = BootType::UNKNOWN;
};
// Reads only the start of the state. Fails for states older than the disc ID in them.
bool PeekBootInfo(const Path &filename, BootInfo *info, std::string *error);
// Load the specified file into the current state (async.) The file is read while emulation goes
// on, and the state applied in a later Process().
// Returns true if queued. The callback runs from Process(), once the state has been applied.
+50
View File
@@ -119,6 +119,7 @@
#include "Core/FileSystems/ISOFileSystem.h"
#include "Core/FileSystems/DirectoryFileSystem.h"
#include "Core/FileSystems/MetaFileSystem.h"
#include "Core/SaveState.h"
#include "Core/HLE/sceUmd.h"
#include "Core/Loaders.h"
#include "Core/MemMap.h"
@@ -293,6 +294,54 @@ bool TestSaveStateMounts() {
return true;
}
// The start of a state as SaveStart writes it, and a little more.
struct PeekTestState {
int version;
void DoState(PointerWrap &p) {
auto s = p.Section("SaveStart", 1, version);
if (!s)
return;
if (s >= 6) {
char discID[16] = "ULUS12345";
char discVersion[8] = "1.01";
u8 bootType = (u8)SaveState::BootType::PBP;
p.DoVoid(discID, sizeof(discID));
p.DoVoid(discVersion, sizeof(discVersion));
Do(p, bootType);
}
std::vector<u8> rest(100000, 0x55);
Do(p, rest);
}
};
static bool SavePeekTestState(const Path &path, int version) {
PeekTestState state{ version };
u8 *data = nullptr;
size_t size = 0;
if (CChunkFileReader::MeasureAndSavePtr(state, &data, &size) != CChunkFileReader::ERROR_NONE) {
return false;
}
return CChunkFileReader::SaveFile(path, "Test", "v1.0", data, size) == CChunkFileReader::ERROR_NONE;
}
bool TestSaveStatePeek() {
const Path path = UnitTestTempPath("unittest_peek.ppst");
SaveState::BootInfo info;
std::string error;
EXPECT_TRUE(SavePeekTestState(path, 6));
EXPECT_TRUE(SaveState::PeekBootInfo(path, &info, &error));
EXPECT_EQ_STR(info.discID, std::string("ULUS12345"));
EXPECT_EQ_STR(info.discVersion, std::string("1.01"));
EXPECT_TRUE(info.bootType == SaveState::BootType::PBP);
EXPECT_TRUE(SavePeekTestState(path, 5));
EXPECT_FALSE(SaveState::PeekBootInfo(path, &info, &error));
File::Delete(path);
return true;
}
bool TestInetPton() {
uint8_t ip4[4];
EXPECT_EQ_INT(net::inet_pton(AF_INET, "1.2.3.4", ip4), 1);
@@ -4229,6 +4278,7 @@ TestItem availableTests[] = {
TEST_ITEM(StackWalk),
TEST_ITEM(InetPton),
TEST_ITEM(SaveStateMounts),
TEST_ITEM(SaveStatePeek),
};
int main(int argc, const char *argv[]) {