Implement in-memory savestates (#1693)

* Refactor Savestate::Var{8,16,32,64}

- They now delegate to VarArray
- They're declared in the class header so they're likely to be inlined

* First crack at refactoring Savestate to work in-memory

- Well, third, but who's counting?

* Implement Savestate::Finish

* Remove the VersionMajor and VersionMinor fields

- Instead, pull their values directly from the savestate buffer

* Mark a new constructor as explicit

* Rename Reset to Rewind

* Fix a linebreak

* Implement Savestate::Rewind

* Add ROMManager::ClearBackupState

* Refactor ROMManager to use the refactored Savestate

* Capitalize "Least"

- It was driving me nuts

* Add a log call

* Increase default Savestate buffer length to 32MB

* Use C-style file I/O instead of C++-style

- Dumping bytes to a file with C++'s standard library is a MONSTROUS PAIN IN THE ASS

* Quote the savestate's file path for clarity

* Write the savestate's length into the header

* Add some extra logging calls

* Fix section-loading

* Remove the deprecated Savestate constructor

* Convert a char* to a u32 with memcpy, not a cast

* Fix section-handling in loads

* Include <cstring> in Savestate.h

- This was causing a build error on Linux
This commit is contained in:
Jesse Talavera-Greenberg
2023-06-12 17:56:09 -04:00
committed by GitHub
parent ca7fb4f55e
commit 391ad8c95e
6 changed files with 437 additions and 225 deletions

View File

@ -17,12 +17,16 @@
*/
#include <stdio.h>
#include <cassert>
#include <cstring>
#include "Savestate.h"
#include "Platform.h"
using Platform::Log;
using Platform::LogLevel;
static const char* SAVESTATE_MAGIC = "MELN";
/*
Savestate format
@ -46,230 +50,148 @@ using Platform::LogLevel;
* different minor means adjustments may have to be made
*/
// TODO: buffering system! or something of that sort
// repeated fread/fwrite is slow on Switch
Savestate::Savestate(const std::string& filename, bool save)
Savestate::Savestate(void *buffer, u32 size, bool save) :
Error(false),
Saving(save),
CurSection(NO_SECTION),
buffer(static_cast<u8 *>(buffer)),
buffer_offset(0),
buffer_length(size),
buffer_owned(false),
finished(false)
{
const char* magic = "MELN";
Error = false;
if (save)
if (Saving)
{
Saving = true;
file = Platform::OpenLocalFile(filename, "wb");
if (!file)
{
Log(LogLevel::Error, "savestate: file %s doesn't exist\n", filename.c_str());
Error = true;
return;
}
VersionMajor = SAVESTATE_MAJOR;
VersionMinor = SAVESTATE_MINOR;
fwrite(magic, 4, 1, file);
fwrite(&VersionMajor, 2, 1, file);
fwrite(&VersionMinor, 2, 1, file);
fseek(file, 8, SEEK_CUR); // length to be fixed later
WriteSavestateHeader();
}
else
{
Saving = false;
file = Platform::OpenFile(filename, "rb");
if (!file)
// Ensure that the file starts with "MELN"
u32 read_magic = 0;
Var32(&read_magic);
if (read_magic != *((u32*)SAVESTATE_MAGIC))
{
Log(LogLevel::Error, "savestate: file %s doesn't exist\n", filename.c_str());
Log(LogLevel::Error, "savestate: expected magic number %#08x (%s), got %#08x\n",
*((u32*)SAVESTATE_MAGIC),
SAVESTATE_MAGIC,
read_magic
);
Error = true;
return;
}
u32 len;
fseek(file, 0, SEEK_END);
len = (u32)ftell(file);
fseek(file, 0, SEEK_SET);
u32 buf = 0;
fread(&buf, 4, 1, file);
if (buf != ((u32*)magic)[0])
u16 major = 0;
Var16(&major);
if (major != SAVESTATE_MAJOR)
{
Log(LogLevel::Error, "savestate: invalid magic %08X\n", buf);
Log(LogLevel::Error, "savestate: bad version major %d, expecting %d\n", major, SAVESTATE_MAJOR);
Error = true;
return;
}
VersionMajor = 0;
VersionMinor = 0;
fread(&VersionMajor, 2, 1, file);
if (VersionMajor != SAVESTATE_MAJOR)
u16 minor = 0;
Var16(&minor);
if (minor > SAVESTATE_MINOR)
{
Log(LogLevel::Error, "savestate: bad version major %d, expecting %d\n", VersionMajor, SAVESTATE_MAJOR);
Log(LogLevel::Error, "savestate: state from the future, %d > %d\n", minor, SAVESTATE_MINOR);
Error = true;
return;
}
fread(&VersionMinor, 2, 1, file);
if (VersionMinor > SAVESTATE_MINOR)
u32 read_length = 0;
Var32(&read_length);
if (read_length != buffer_length)
{
Log(LogLevel::Error, "savestate: state from the future, %d > %d\n", VersionMinor, SAVESTATE_MINOR);
Log(LogLevel::Error, "savestate: expected a length of %d, got %d\n", buffer_length, read_length);
Error = true;
return;
}
buf = 0;
fread(&buf, 4, 1, file);
if (buf != len)
{
Log(LogLevel::Error, "savestate: bad length %d\n", buf);
Error = true;
return;
}
// The next 4 bytes are reserved
buffer_offset += 4;
}
}
fseek(file, 4, SEEK_CUR);
Savestate::Savestate(u32 initial_size) :
Error(false),
Saving(true), // Can't load from an empty buffer
CurSection(NO_SECTION),
buffer(nullptr),
buffer_offset(0),
buffer_length(initial_size),
buffer_owned(true),
finished(false)
{
buffer = static_cast<u8 *>(malloc(buffer_length));
if (buffer == nullptr)
{
Log(LogLevel::Error, "savestate: failed to allocate %d bytes\n", buffer_length);
Error = true;
return;
}
CurSection = -1;
WriteSavestateHeader();
}
Savestate::~Savestate()
{
if (Error) return;
if (Saving)
{
if (CurSection != 0xFFFFFFFF)
{
u32 pos = (u32)ftell(file);
fseek(file, CurSection+4, SEEK_SET);
u32 len = pos - CurSection;
fwrite(&len, 4, 1, file);
fseek(file, pos, SEEK_SET);
}
fseek(file, 0, SEEK_END);
u32 len = (u32)ftell(file);
fseek(file, 8, SEEK_SET);
fwrite(&len, 4, 1, file);
if (Saving && !finished && !buffer_owned && !Error)
{ // If we haven't finished saving, and there hasn't been an error...
Finish();
// No need to close the active section for an owned buffer,
// it's about to be thrown out.
}
if (file) fclose(file);
if (buffer_owned)
{
free(buffer);
}
}
void Savestate::Section(const char* magic)
{
if (Error) return;
if (Error || finished) return;
if (Saving)
{
if (CurSection != 0xFFFFFFFF)
// Go back to the current section's header and write the length
CloseCurrentSection();
CurSection = buffer_offset;
// Write the new section's magic number
VarArray((void*)magic, 4);
// The next 4 bytes are the length, which we'll come back to later.
u32 zero = 0;
Var32(&zero);
// The 8 bytes afterward are reserved, so we skip them.
Var32(&zero);
Var32(&zero);
}
else
{
u32 section_offset = FindSection(magic);
if (section_offset != NO_SECTION)
{
u32 pos = (u32)ftell(file);
fseek(file, CurSection+4, SEEK_SET);
u32 len = pos - CurSection;
fwrite(&len, 4, 1, file);
fseek(file, pos, SEEK_SET);
buffer_offset = section_offset;
}
CurSection = (u32)ftell(file);
fwrite(magic, 4, 1, file);
fseek(file, 12, SEEK_CUR);
}
else
{
fseek(file, 0x10, SEEK_SET);
for (;;)
else
{
u32 buf = 0;
fread(&buf, 4, 1, file);
if (buf != ((u32*)magic)[0])
{
if (buf == 0)
{
Log(LogLevel::Error, "savestate: section %s not found. blarg\n", magic);
return;
}
buf = 0;
fread(&buf, 4, 1, file);
fseek(file, buf-8, SEEK_CUR);
continue;
}
fseek(file, 12, SEEK_CUR);
break;
Log(LogLevel::Error, "savestate: section %s not found. blarg\n", magic);
Error = true;
}
}
}
void Savestate::Var8(u8* var)
{
if (Error) return;
if (Saving)
{
fwrite(var, 1, 1, file);
}
else
{
fread(var, 1, 1, file);
}
}
void Savestate::Var16(u16* var)
{
if (Error) return;
if (Saving)
{
fwrite(var, 2, 1, file);
}
else
{
fread(var, 2, 1, file);
}
}
void Savestate::Var32(u32* var)
{
if (Error) return;
if (Saving)
{
fwrite(var, 4, 1, file);
}
else
{
fread(var, 4, 1, file);
}
}
void Savestate::Var64(u64* var)
{
if (Error) return;
if (Saving)
{
fwrite(var, 8, 1, file);
}
else
{
fread(var, 8, 1, file);
}
}
void Savestate::Bool32(bool* var)
{
// for compability
// for compatibility
if (Saving)
{
u32 val = *var;
@ -285,14 +207,175 @@ void Savestate::Bool32(bool* var)
void Savestate::VarArray(void* data, u32 len)
{
if (Error) return;
if (Error || finished) return;
assert(buffer_offset <= buffer_length);
if (Saving)
{
fwrite(data, len, 1, file);
if (buffer_offset + len > buffer_length)
{ // If writing the given data would take us past the buffer's end...
Log(LogLevel::Warn, "savestate: %u-byte write would exceed %u-byte savestate buffer\n", len, buffer_length);
if (!(buffer_owned && Resize(buffer_length * 2 + len)))
{ // If we're not allowed to resize this buffer, or if we are but failed...
Log(LogLevel::Error, "savestate: Failed to write %d bytes to savestate\n", len);
Error = true;
return;
}
// The buffer's length is doubled, plus however much memory is needed for this write.
// This way we can write the data and reduce the chance of needing to resize again.
}
memcpy(buffer + buffer_offset, data, len);
}
else
{
fread(data, len, 1, file);
if (buffer_offset + len > buffer_length)
{ // If reading the requested amount of data would take us past the buffer's edge...
Log(LogLevel::Error, "savestate: %u-byte read would exceed %u-byte savestate buffer\n", len, buffer_length);
Error = true;
return;
// Can't realloc here.
// Not only do we not own the buffer pointer (when loading a state),
// but we can't magically make the desired data appear.
}
memcpy(data, buffer + buffer_offset, len);
}
buffer_offset += len;
}
void Savestate::Finish()
{
if (Error || finished) return;
CloseCurrentSection();
WriteStateLength();
finished = true;
}
void Savestate::Rewind(bool save)
{
Error = false;
Saving = save;
CurSection = NO_SECTION;
buffer_offset = 0;
finished = false;
}
void Savestate::CloseCurrentSection()
{
if (CurSection != NO_SECTION && !finished)
{ // If we're in the middle of writing a section...
// Go back to the section's header
// Get the length of the section we've written thus far
u32 section_length = buffer_offset - CurSection;
// Write the length in the section's header
// (specifically the first 4 bytes after the magic number)
memcpy(buffer + CurSection + 4, &section_length, sizeof(section_length));
CurSection = NO_SECTION;
}
}
bool Savestate::Resize(u32 new_length)
{
if (!buffer_owned)
{ // If we're not allowed to resize this buffer...
Log(LogLevel::Error, "savestate: Buffer is externally-owned, cannot resize it\n");
return false;
}
u32 old_length = buffer_length;
void* resized = realloc(buffer, new_length);
if (!resized)
{ // If the buffer couldn't be expanded...
Log(LogLevel::Error, "savestate: Failed to resize owned savestate buffer from %dB to %dB\n", old_length, new_length);
return false;
}
u32 length_diff = new_length - old_length;
buffer = static_cast<u8 *>(resized);
buffer_length = new_length;
Log(LogLevel::Debug, "savestate: Expanded %uB savestate buffer to %uB\n", old_length, new_length);
// Zero out the newly-allocated memory (to ensure we don't introduce a security hole)
memset(buffer + old_length, 0, length_diff);
return true;
}
void Savestate::WriteSavestateHeader()
{
// The magic number
VarArray((void *) SAVESTATE_MAGIC, 4);
// The major and minor versions
u16 major = SAVESTATE_MAJOR;
Var16(&major);
u16 minor = SAVESTATE_MINOR;
Var16(&minor);
// The next 4 bytes are the file's length, which will be filled in at the end
u32 zero = 0;
Var32(&zero);
// The following 4 bytes are reserved
Var32(&zero);
}
void Savestate::WriteStateLength()
{
// Not to be confused with the buffer length.
// The buffer might not be full,
// so we don't want to write out the extra stuff.
u32 state_length = buffer_offset;
// Write the length in the header
memcpy(buffer + 0x08, &state_length, sizeof(state_length));
}
u32 Savestate::FindSection(const char* magic) const
{
if (!magic) return NO_SECTION;
// Start looking at the savestate's beginning, right after its global header
// (we can't start from the current offset because then we'd lose the ability to rearrange sections)
for (u32 offset = 0x10; offset < buffer_length;)
{ // Until we've found the desired section...
// Get this section's magic number
char read_magic[4] = {0};
memcpy(read_magic, buffer + offset, sizeof(read_magic));
if (memcmp(read_magic, magic, sizeof(read_magic)) == 0)
{ // If this is the right section...
return offset + 16; // ...return the offset of the first byte of the section after the header
}
// Haven't found our section yet. Let's move on to the next one.
u32 section_length_offset = offset + sizeof(read_magic);
if (section_length_offset >= buffer_length)
{ // If trying to read the section length would take us past the file's end...
break;
}
// First we need to find out how big this section is...
u32 section_length = 0;
memcpy(&section_length, buffer + section_length_offset, sizeof(section_length));
// ...then skip it. (The section length includes the 16-byte header.)
offset += section_length;
}
// We've reached the end of the file without finding the requested section...
Log(LogLevel::Error, "savestate: section %s not found. blarg\n", magic);
return NO_SECTION;
}