aboutsummaryrefslogtreecommitdiff
path: root/src/Savestate.cpp
diff options
context:
space:
mode:
authorJesse Talavera-Greenberg <jesse@jesse.tg>2023-06-12 17:56:09 -0400
committerGitHub <noreply@github.com>2023-06-12 23:56:09 +0200
commit391ad8c95e9b942ff39705f2c3cd5359aef633b3 (patch)
tree7718e0b201ebddf66ec1ca74f0866fe9adbeb32c /src/Savestate.cpp
parentca7fb4f55e8fdad53993ba279b073f97f453c13c (diff)
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
Diffstat (limited to 'src/Savestate.cpp')
-rw-r--r--src/Savestate.cpp399
1 files changed, 241 insertions, 158 deletions
diff --git a/src/Savestate.cpp b/src/Savestate.cpp
index 0aa0ba3..546c16e 100644
--- a/src/Savestate.cpp
+++ b/src/Savestate.cpp
@@ -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,253 +50,332 @@ 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)
- {
- Log(LogLevel::Error, "savestate: file %s doesn't exist\n", filename.c_str());
- Error = true;
- return;
- }
-
- u32 len;
- fseek(file, 0, SEEK_END);
- len = (u32)ftell(file);
- fseek(file, 0, SEEK_SET);
+ // Ensure that the file starts with "MELN"
+ u32 read_magic = 0;
+ Var32(&read_magic);
- u32 buf = 0;
-
- fread(&buf, 4, 1, file);
- if (buf != ((u32*)magic)[0])
+ if (read_magic != *((u32*)SAVESTATE_MAGIC))
{
- Log(LogLevel::Error, "savestate: invalid magic %08X\n", buf);
+ Log(LogLevel::Error, "savestate: expected magic number %#08x (%s), got %#08x\n",
+ *((u32*)SAVESTATE_MAGIC),
+ SAVESTATE_MAGIC,
+ read_magic
+ );
Error = true;
return;
}
- VersionMajor = 0;
- VersionMinor = 0;
-
- fread(&VersionMajor, 2, 1, file);
- if (VersionMajor != SAVESTATE_MAJOR)
+ u16 major = 0;
+ Var16(&major);
+ if (major != SAVESTATE_MAJOR)
{
- Log(LogLevel::Error, "savestate: bad version major %d, expecting %d\n", VersionMajor, SAVESTATE_MAJOR);
+ Log(LogLevel::Error, "savestate: bad version major %d, expecting %d\n", major, SAVESTATE_MAJOR);
Error = true;
return;
}
- fread(&VersionMinor, 2, 1, file);
- if (VersionMinor > SAVESTATE_MINOR)
+ u16 minor = 0;
+ Var16(&minor);
+ if (minor > SAVESTATE_MINOR)
{
- Log(LogLevel::Error, "savestate: state from the future, %d > %d\n", VersionMinor, SAVESTATE_MINOR);
+ Log(LogLevel::Error, "savestate: state from the future, %d > %d\n", minor, SAVESTATE_MINOR);
Error = true;
return;
}
- buf = 0;
- fread(&buf, 4, 1, file);
- if (buf != len)
+ u32 read_length = 0;
+ Var32(&read_length);
+ if (read_length != buffer_length)
{
- Log(LogLevel::Error, "savestate: bad length %d\n", buf);
+ Log(LogLevel::Error, "savestate: expected a length of %d, got %d\n", buffer_length, read_length);
Error = true;
return;
}
- fseek(file, 4, SEEK_CUR);
+ // The next 4 bytes are reserved
+ buffer_offset += 4;
}
-
- CurSection = -1;
}
-Savestate::~Savestate()
+
+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)
{
- if (Error) return;
+ buffer = static_cast<u8 *>(malloc(buffer_length));
- if (Saving)
+ if (buffer == nullptr)
{
- if (CurSection != 0xFFFFFFFF)
- {
- u32 pos = (u32)ftell(file);
- fseek(file, CurSection+4, SEEK_SET);
-
- u32 len = pos - CurSection;
- fwrite(&len, 4, 1, file);
+ Log(LogLevel::Error, "savestate: failed to allocate %d bytes\n", buffer_length);
+ Error = true;
+ return;
+ }
- fseek(file, pos, SEEK_SET);
- }
+ WriteSavestateHeader();
+}
- fseek(file, 0, SEEK_END);
- u32 len = (u32)ftell(file);
- fseek(file, 8, SEEK_SET);
- fwrite(&len, 4, 1, file);
+Savestate::~Savestate()
+{
+ 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)
- {
- u32 pos = (u32)ftell(file);
- fseek(file, CurSection+4, SEEK_SET);
+ // Go back to the current section's header and write the length
+ CloseCurrentSection();
- u32 len = pos - CurSection;
- fwrite(&len, 4, 1, file);
+ CurSection = buffer_offset;
- fseek(file, pos, SEEK_SET);
- }
+ // Write the new section's magic number
+ VarArray((void*)magic, 4);
- CurSection = (u32)ftell(file);
+ // The next 4 bytes are the length, which we'll come back to later.
+ u32 zero = 0;
+ Var32(&zero);
- fwrite(magic, 4, 1, file);
- fseek(file, 12, SEEK_CUR);
+ // The 8 bytes afterward are reserved, so we skip them.
+ Var32(&zero);
+ Var32(&zero);
}
else
{
- fseek(file, 0x10, SEEK_SET);
+ u32 section_offset = FindSection(magic);
- for (;;)
+ if (section_offset != NO_SECTION)
{
- 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;
+ buffer_offset = section_offset;
+ }
+ else
+ {
+ Log(LogLevel::Error, "savestate: section %s not found. blarg\n", magic);
+ Error = true;
}
}
}
-void Savestate::Var8(u8* var)
+void Savestate::Bool32(bool* var)
{
- if (Error) return;
-
+ // for compatibility
if (Saving)
{
- fwrite(var, 1, 1, file);
+ u32 val = *var;
+ Var32(&val);
}
else
{
- fread(var, 1, 1, file);
+ u32 val;
+ Var32(&val);
+ *var = val != 0;
}
}
-void Savestate::Var16(u16* var)
+void Savestate::VarArray(void* data, u32 len)
{
- if (Error) return;
+ if (Error || finished) return;
+
+ assert(buffer_offset <= buffer_length);
if (Saving)
{
- fwrite(var, 2, 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(var, 2, 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::Var32(u32* var)
+void Savestate::Finish()
{
- if (Error) return;
+ if (Error || finished) return;
+ CloseCurrentSection();
+ WriteStateLength();
+ finished = true;
+}
- if (Saving)
- {
- fwrite(var, 4, 1, file);
- }
- else
- {
- fread(var, 4, 1, file);
- }
+void Savestate::Rewind(bool save)
+{
+ Error = false;
+ Saving = save;
+ CurSection = NO_SECTION;
+
+ buffer_offset = 0;
+ finished = false;
}
-void Savestate::Var64(u64* var)
+void Savestate::CloseCurrentSection()
{
- if (Error) return;
+ if (CurSection != NO_SECTION && !finished)
+ { // If we're in the middle of writing a section...
- if (Saving)
- {
- fwrite(var, 8, 1, file);
- }
- else
- {
- fread(var, 8, 1, file);
+ // 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;
}
}
-void Savestate::Bool32(bool* var)
+bool Savestate::Resize(u32 new_length)
{
- // for compability
- if (Saving)
- {
- u32 val = *var;
- Var32(&val);
+ 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;
}
- else
- {
- u32 val;
- Var32(&val);
- *var = val != 0;
+
+ 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::VarArray(void* data, u32 len)
+void Savestate::WriteSavestateHeader()
{
- if (Error) return;
+ // The magic number
+ VarArray((void *) SAVESTATE_MAGIC, 4);
- if (Saving)
- {
- fwrite(data, len, 1, file);
- }
- else
- {
- fread(data, len, 1, file);
- }
+ // 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;
+} \ No newline at end of file