From 391ad8c95e9b942ff39705f2c3cd5359aef633b3 Mon Sep 17 00:00:00 2001 From: Jesse Talavera-Greenberg Date: Mon, 12 Jun 2023 17:56:09 -0400 Subject: 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 in Savestate.h - This was causing a build error on Linux --- src/GPU3D.cpp | 2 +- src/NDS.cpp | 11 +- src/Savestate.cpp | 399 ++++++++++++++++++++++--------------- src/Savestate.h | 78 +++++++- src/frontend/qt_sdl/ROMManager.cpp | 121 ++++++++--- src/frontend/qt_sdl/ROMManager.h | 1 + 6 files changed, 412 insertions(+), 200 deletions(-) (limited to 'src') diff --git a/src/GPU3D.cpp b/src/GPU3D.cpp index 5c48575..145b689 100644 --- a/src/GPU3D.cpp +++ b/src/GPU3D.cpp @@ -562,7 +562,7 @@ void DoSavestate(Savestate* file) file->Bool32(&poly->IsShadowMask); file->Bool32(&poly->IsShadow); - if (file->IsAtleastVersion(4, 1)) + if (file->IsAtLeastVersion(4, 1)) file->Var32((u32*)&poly->Type); else poly->Type = 0; diff --git a/src/NDS.cpp b/src/NDS.cpp index b5c00db..e37a194 100644 --- a/src/NDS.cpp +++ b/src/NDS.cpp @@ -815,7 +815,10 @@ bool DoSavestate(Savestate* file) u32 console; file->Var32(&console); if (console != ConsoleType) + { + Log(LogLevel::Error, "savestate: Expected console type %d, got console type %d. cannot load.\n", ConsoleType, console); return false; + } } file->VarArray(MainRAM, MainRAMMaxSize); @@ -870,7 +873,11 @@ bool DoSavestate(Savestate* file) file->VarArray(DMA9Fill, 4*sizeof(u32)); - if (!DoSavestate_Scheduler(file)) return false; + if (!DoSavestate_Scheduler(file)) + { + Platform::Log(Platform::LogLevel::Error, "savestate: failed to %s scheduler state\n", file->Saving ? "save" : "load"); + return false; + } file->Var32(&SchedListMask); file->Var64(&ARM9Timestamp); file->Var64(&ARM9Target); @@ -937,6 +944,8 @@ bool DoSavestate(Savestate* file) } #endif + file->Finish(); + return true; } 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 +#include +#include #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(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(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(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 diff --git a/src/Savestate.h b/src/Savestate.h index a2216ce..0aef517 100644 --- a/src/Savestate.h +++ b/src/Savestate.h @@ -19,6 +19,7 @@ #ifndef SAVESTATE_H #define SAVESTATE_H +#include #include #include #include "types.h" @@ -29,37 +30,92 @@ class Savestate { public: - Savestate(const std::string& filename, bool save); + static constexpr u32 DEFAULT_SIZE = 32 * 1024 * 1024; // 32 MB + Savestate(void* buffer, u32 size, bool save); + explicit Savestate(u32 initial_size = DEFAULT_SIZE); + ~Savestate(); bool Error; bool Saving; - u32 VersionMajor; - u32 VersionMinor; u32 CurSection; void Section(const char* magic); - void Var8(u8* var); - void Var16(u16* var); - void Var32(u32* var); - void Var64(u64* var); + void Var8(u8* var) + { + VarArray(var, sizeof(*var)); + } + + void Var16(u16* var) + { + VarArray(var, sizeof(*var)); + } + + void Var32(u32* var) + { + VarArray(var, sizeof(*var)); + } + + void Var64(u64* var) + { + VarArray(var, sizeof(*var)); + } void Bool32(bool* var); void VarArray(void* data, u32 len); - bool IsAtleastVersion(u32 major, u32 minor) + void Finish(); + + // TODO rewinds the stream + void Rewind(bool save); + + bool IsAtLeastVersion(u32 major, u32 minor) { - if (VersionMajor > major) return true; - if (VersionMajor == major && VersionMinor >= minor) return true; + u16 major_version = MajorVersion(); + if (MajorVersion() > major) return true; + if (major_version == major && MinorVersion() >= minor) return true; return false; } + void* Buffer() { return buffer; } + [[nodiscard]] const void* Buffer() const { return buffer; } + + [[nodiscard]] u32 BufferLength() const { return buffer_length; } + + [[nodiscard]] u32 Length() const { return buffer_offset; } + + [[nodiscard]] u16 MajorVersion() const + { + // major version is stored at offset 0x04 + u16 major = 0; + memcpy(&major, buffer + 0x04, sizeof(major)); + return major; + } + + [[nodiscard]] u16 MinorVersion() const + { + // minor version is stored at offset 0x06 + u16 minor = 0; + memcpy(&minor, buffer + 0x06, sizeof(minor)); + return minor; + } + private: - FILE* file; + static constexpr u32 NO_SECTION = 0xffffffff; + void CloseCurrentSection(); + bool Resize(u32 new_length); + void WriteSavestateHeader(); + void WriteStateLength(); + u32 FindSection(const char* magic) const; + u8* buffer; + u32 buffer_offset; + u32 buffer_length; + bool buffer_owned; + bool finished; }; #endif // SAVESTATE_H diff --git a/src/frontend/qt_sdl/ROMManager.cpp b/src/frontend/qt_sdl/ROMManager.cpp index 95337e1..80f4652 100644 --- a/src/frontend/qt_sdl/ROMManager.cpp +++ b/src/frontend/qt_sdl/ROMManager.cpp @@ -21,6 +21,7 @@ #include #include +#include #include #ifdef ARCHIVE_SUPPORT_ENABLED @@ -52,6 +53,7 @@ std::string BaseGBAAssetName = ""; SaveManager* NDSSave = nullptr; SaveManager* GBASave = nullptr; +std::unique_ptr BackupState = nullptr; bool SavestateLoaded = false; std::string PreviousSaveFile = ""; @@ -304,35 +306,62 @@ bool SavestateExists(int slot) bool LoadState(const std::string& filename) { - // backup - Savestate* backup = new Savestate("timewarp.mln", true); - NDS::DoSavestate(backup); - delete backup; + FILE* file = fopen(filename.c_str(), "rb"); + if (file == nullptr) + { // If we couldn't open the state file... + Platform::Log(Platform::LogLevel::Error, "Failed to open state file \"%s\"\n", filename.c_str()); + return false; + } - bool failed = false; + std::unique_ptr backup = std::make_unique(Savestate::DEFAULT_SIZE); + if (backup->Error) + { // If we couldn't allocate memory for the backup... + Platform::Log(Platform::LogLevel::Error, "Failed to allocate memory for state backup\n"); + fclose(file); + return false; + } + + if (!NDS::DoSavestate(backup.get()) || backup->Error) + { // Back up the emulator's state. If that failed... + Platform::Log(Platform::LogLevel::Error, "Failed to back up state, aborting load (from \"%s\")\n", filename.c_str()); + fclose(file); + return false; + } + // We'll store the backup once we're sure that the state was loaded. + // Now that we know the file and backup are both good, let's load the new state. - Savestate* state = new Savestate(filename, false); - if (state->Error) + // Get the size of the file that we opened + if (fseek(file, 0, SEEK_END) != 0) { - delete state; + Platform::Log(Platform::LogLevel::Error, "Failed to seek to end of state file \"%s\"\n", filename.c_str()); + fclose(file); + return false; + } + size_t size = ftell(file); + rewind(file); // reset the filebuf's position - // current state might be crapoed, so restore from sane backup - state = new Savestate("timewarp.mln", false); - failed = true; + // Allocate exactly as much memory as we need for the savestate + std::vector buffer(size); + if (fread(buffer.data(), size, 1, file) == 0) + { // Read the state file into the buffer. If that failed... + Platform::Log(Platform::LogLevel::Error, "Failed to read %u-byte state file \"%s\"\n", size, filename.c_str()); + fclose(file); + return false; } + fclose(file); // done with the file now - bool res = NDS::DoSavestate(state); - delete state; + // Get ready to load the state from the buffer into the emulator + std::unique_ptr state = std::make_unique(buffer.data(), size, false); - if (!res) - { - failed = true; - state = new Savestate("timewarp.mln", false); - NDS::DoSavestate(state); - delete state; + if (!NDS::DoSavestate(state.get()) || state->Error) + { // If we couldn't load the savestate from the buffer... + Platform::Log(Platform::LogLevel::Error, "Failed to load state file \"%s\" into emulator\n", filename.c_str()); + return false; } - if (failed) return false; + // The backup was made and the state was loaded, so we can store the backup now. + BackupState = std::move(backup); // This will clean up any existing backup + assert(backup == nullptr); if (Config::SavestateRelocSRAM && NDSSave) { @@ -351,15 +380,41 @@ bool LoadState(const std::string& filename) bool SaveState(const std::string& filename) { - Savestate* state = new Savestate(filename, true); - if (state->Error) + FILE* file = fopen(filename.c_str(), "wb"); + + if (file == nullptr) + { // If the file couldn't be opened... + return false; + } + + Savestate state; + if (state.Error) + { // If there was an error creating the state (and allocating its memory)... + fclose(file); + return false; + } + + // Write the savestate to the in-memory buffer + NDS::DoSavestate(&state); + + if (state.Error) { - delete state; + fclose(file); + return false; + } + + if (fwrite(state.Buffer(), state.Length(), 1, file) == 0) + { // Write the Savestate buffer to the file. If that fails... + Platform::Log(Platform::Error, + "Failed to write %d-byte savestate to %s\n", + state.Length(), + filename.c_str() + ); + fclose(file); return false; } - NDS::DoSavestate(state); - delete state; + fclose(file); if (Config::SavestateRelocSRAM && NDSSave) { @@ -374,14 +429,14 @@ bool SaveState(const std::string& filename) void UndoStateLoad() { - if (!SavestateLoaded) return; + if (!SavestateLoaded || !BackupState) return; + // Rewind the backup state and put it in load mode + BackupState->Rewind(false); // pray that this works // what do we do if it doesn't??? // but it should work. - Savestate* backup = new Savestate("timewarp.mln", false); - NDS::DoSavestate(backup); - delete backup; + NDS::DoSavestate(BackupState.get()); if (NDSSave && (!PreviousSaveFile.empty())) { @@ -514,6 +569,14 @@ u32 DecompressROM(const u8* inContent, const u32 inSize, u8** outContent) return realSize; } +void ClearBackupState() +{ + if (BackupState != nullptr) + { + BackupState = nullptr; + } +} + bool LoadROM(QStringList filepath, bool reset) { if (filepath.empty()) return false; diff --git a/src/frontend/qt_sdl/ROMManager.h b/src/frontend/qt_sdl/ROMManager.h index efaed36..1ec0fe5 100644 --- a/src/frontend/qt_sdl/ROMManager.h +++ b/src/frontend/qt_sdl/ROMManager.h @@ -35,6 +35,7 @@ extern SaveManager* GBASave; QString VerifySetup(); void Reset(); bool LoadBIOS(); +void ClearBackupState(); bool LoadROM(QStringList filepath, bool reset); void EjectCart(); -- cgit v1.2.3