diff options
author | Jesse Talavera-Greenberg <jesse@jesse.tg> | 2023-06-12 17:56:09 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-06-12 23:56:09 +0200 |
commit | 391ad8c95e9b942ff39705f2c3cd5359aef633b3 (patch) | |
tree | 7718e0b201ebddf66ec1ca74f0866fe9adbeb32c /src/Savestate.cpp | |
parent | ca7fb4f55e8fdad53993ba279b073f97f453c13c (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.cpp | 399 |
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, §ion_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(§ion_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 |