mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-11-12 17:19:28 +01:00
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:
1 parent
60dc6687f2
commit
09f65a860c
5 files changed
+152
-5
No files matched your search
@@ -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");
|
||||
|
||||
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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[]) {
|
||||
|
||||
Reference in new issue
Block a user