// Copyright 2008 Dolphin Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "Core/State.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "Common/ChunkFile.h" #include "Common/CommonTypes.h" #include "Common/Event.h" #include "Common/FileUtil.h" #include "Common/IOFile.h" #include "Common/MsgHandler.h" #include "Common/Thread.h" #include "Common/Timer.h" #include "Common/Version.h" #include "Common/WorkQueueThread.h" #include "Core/ConfigManager.h" #include "Core/Core.h" #include "Core/CoreTiming.h" #include "Core/GeckoCode.h" #include "Core/HW/HW.h" #include "Core/HW/Memmap.h" #include "Core/HW/Wiimote.h" #include "Core/Host.h" #include "Core/Movie.h" #include "Core/NetPlayClient.h" #include "Core/PowerPC/PowerPC.h" #include "Core/System.h" #include "VideoCommon/FrameDumpFFMpeg.h" #include "VideoCommon/OnScreenDisplay.h" #include "VideoCommon/VideoBackendBase.h" namespace State { #if defined(__LZO_STRICT_16BIT) static const u32 IN_LEN = 8 * 1024u; #elif defined(LZO_ARCH_I086) && !defined(LZO_HAVE_MM_HUGE_ARRAY) static const u32 IN_LEN = 60 * 1024u; #else static const u32 IN_LEN = 128 * 1024u; #endif static const u32 OUT_LEN = IN_LEN + (IN_LEN / 16) + 64 + 3; static unsigned char __LZO_MMODEL out[OUT_LEN]; static AfterLoadCallbackFunc s_on_after_load_callback; // Temporary undo state buffer static std::vector s_undo_load_buffer; static std::mutex s_undo_load_buffer_mutex; static std::mutex s_load_or_save_in_progress_mutex; struct CompressAndDumpState_args { std::vector buffer_vector; std::string filename; std::shared_ptr state_write_done_event; }; // Protects against simultaneous reads and writes to the final savestate location from multiple // threads. static std::mutex s_save_thread_mutex; // Queue for compressing and writing savestates to disk. static Common::WorkQueueThread s_save_thread; // Keeps track of savestate writes that are currently happening, so we don't load a state while // another one is still saving. This is particularly important so if you save to a slot and then // immediately load from the same one, you don't accidentally load the state that's still at that // file path before the write is done. static std::mutex s_state_writes_in_queue_mutex; static size_t s_state_writes_in_queue; static std::condition_variable s_state_write_queue_is_empty; // Don't forget to increase this after doing changes on the savestate system constexpr u32 STATE_VERSION = 165; // Last changed in PR 12328 // Increase this if the StateExtendedHeader definition changes constexpr u32 EXTENDED_HEADER_VERSION = 1; // Last changed in PR 12217 // Change this if we ever need to store more data in the extended header constexpr u32 COMPRESSED_DATA_OFFSET = 0; constexpr u32 COOKIE_BASE = 0xBAADBABE; // Maps savestate versions to Dolphin versions. // Versions after 42 don't need to be added to this list, // because they save the exact Dolphin version to savestates. static const std::map> s_old_versions = { // The 16 -> 17 change modified the size of StateHeader, // so versions older than that can't even be decompressed anymore {17, {"3.5-1311", "3.5-1364"}}, {18, {"3.5-1366", "3.5-1371"}}, {19, {"3.5-1372", "3.5-1408"}}, {20, {"3.5-1409", "4.0-704"}}, {21, {"4.0-705", "4.0-889"}}, {22, {"4.0-905", "4.0-1871"}}, {23, {"4.0-1873", "4.0-1900"}}, {24, {"4.0-1902", "4.0-1919"}}, {25, {"4.0-1921", "4.0-1936"}}, {26, {"4.0-1939", "4.0-1959"}}, {27, {"4.0-1961", "4.0-2018"}}, {28, {"4.0-2020", "4.0-2291"}}, {29, {"4.0-2293", "4.0-2360"}}, {30, {"4.0-2362", "4.0-2628"}}, {31, {"4.0-2632", "4.0-3331"}}, {32, {"4.0-3334", "4.0-3340"}}, {33, {"4.0-3342", "4.0-3373"}}, {34, {"4.0-3376", "4.0-3402"}}, {35, {"4.0-3409", "4.0-3603"}}, {36, {"4.0-3610", "4.0-4480"}}, {37, {"4.0-4484", "4.0-4943"}}, {38, {"4.0-4963", "4.0-5267"}}, {39, {"4.0-5279", "4.0-5525"}}, {40, {"4.0-5531", "4.0-5809"}}, {41, {"4.0-5811", "4.0-5923"}}, {42, {"4.0-5925", "4.0-5946"}}}; enum { STATE_NONE = 0, STATE_SAVE = 1, STATE_LOAD = 2, }; static bool s_use_compression = true; void EnableCompression(bool compression) { s_use_compression = compression; } static void DoState(PointerWrap& p) { bool is_wii = SConfig::GetInstance().bWii || SConfig::GetInstance().m_is_mios; const bool is_wii_currently = is_wii; p.Do(is_wii); if (is_wii != is_wii_currently) { OSD::AddMessage(fmt::format("Cannot load a savestate created under {} mode in {} mode", is_wii ? "Wii" : "GC", is_wii_currently ? "Wii" : "GC"), OSD::Duration::NORMAL, OSD::Color::RED); p.SetMeasureMode(); return; } // Check to make sure the emulated memory sizes are the same as the savestate auto& system = Core::System::GetInstance(); auto& memory = system.GetMemory(); u32 state_mem1_size = memory.GetRamSizeReal(); u32 state_mem2_size = memory.GetExRamSizeReal(); p.Do(state_mem1_size); p.Do(state_mem2_size); if (state_mem1_size != memory.GetRamSizeReal() || state_mem2_size != memory.GetExRamSizeReal()) { OSD::AddMessage(fmt::format("Memory size mismatch!\n" "Current | MEM1 {:08X} ({:3}MB) MEM2 {:08X} ({:3}MB)\n" "State | MEM1 {:08X} ({:3}MB) MEM2 {:08X} ({:3}MB)", memory.GetRamSizeReal(), memory.GetRamSizeReal() / 0x100000U, memory.GetExRamSizeReal(), memory.GetExRamSizeReal() / 0x100000U, state_mem1_size, state_mem1_size / 0x100000U, state_mem2_size, state_mem2_size / 0x100000U)); p.SetMeasureMode(); return; } // Movie must be done before the video backend, because the window is redrawn in the video backend // state load, and the frame number must be up-to-date. Movie::DoState(p); p.DoMarker("Movie"); // Begin with video backend, so that it gets a chance to clear its caches and writeback modified // things to RAM g_video_backend->DoState(p); p.DoMarker("video_backend"); // CoreTiming needs to be restored before restoring Hardware because // the controller code might need to schedule an event if the controller has changed. system.GetCoreTiming().DoState(p); p.DoMarker("CoreTiming"); // HW needs to be restored before PowerPC because the data cache might need to be flushed. HW::DoState(system, p); p.DoMarker("HW"); system.GetPowerPC().DoState(p); p.DoMarker("PowerPC"); if (SConfig::GetInstance().bWii) Wiimote::DoState(p); p.DoMarker("Wiimote"); Gecko::DoState(p); p.DoMarker("Gecko"); } void LoadFromBuffer(std::vector& buffer) { if (NetPlay::IsNetPlayRunning()) { OSD::AddMessage("Loading savestates is disabled in Netplay to prevent desyncs"); return; } Core::RunOnCPUThread( [&] { u8* ptr = buffer.data(); PointerWrap p(&ptr, buffer.size(), PointerWrap::Mode::Read); DoState(p); }, true); } void SaveToBuffer(std::vector& buffer) { Core::RunOnCPUThread( [&] { u8* ptr = nullptr; PointerWrap p_measure(&ptr, 0, PointerWrap::Mode::Measure); DoState(p_measure); const size_t buffer_size = reinterpret_cast(ptr); buffer.resize(buffer_size); ptr = buffer.data(); PointerWrap p(&ptr, buffer_size, PointerWrap::Mode::Write); DoState(p); }, true); } namespace { struct SlotWithTimestamp { int slot; double timestamp; }; } // namespace // returns first slot number not in the vector, or -1 if all are in the vector static int GetEmptySlot(const std::vector& used_slots) { for (int i = 1; i <= (int)NUM_STATES; i++) { const auto it = std::find_if(used_slots.begin(), used_slots.end(), [i](const SlotWithTimestamp& slot) { return slot.slot == i; }); if (it == used_slots.end()) return i; } return -1; } // Arbitrarily chosen value (38 years) that is subtracted in GetSystemTimeAsDouble() // to increase sub-second precision of the resulting double timestamp static constexpr int DOUBLE_TIME_OFFSET = (38 * 365 * 24 * 60 * 60); static double GetSystemTimeAsDouble() { const auto since_epoch = std::chrono::system_clock::now().time_since_epoch(); const auto since_double_time_epoch = since_epoch - std::chrono::seconds(DOUBLE_TIME_OFFSET); return std::chrono::duration_cast>(since_double_time_epoch).count(); } static std::string SystemTimeAsDoubleToString(double time) { // revert adjustments from GetSystemTimeAsDouble() to get a normal Unix timestamp again time_t seconds = static_cast(time) + DOUBLE_TIME_OFFSET; errno = 0; tm* local_time = localtime(&seconds); if (errno != 0 || !local_time) return ""; #ifdef _WIN32 wchar_t tmp[32] = {}; wcsftime(tmp, std::size(tmp), L"%x %X", local_time); return WStringToUTF8(tmp); #else char tmp[32] = {}; strftime(tmp, sizeof(tmp), "%x %X", local_time); return tmp; #endif } static std::string MakeStateFilename(int number); static std::vector GetUsedSlotsWithTimestamp() { std::vector result; StateHeader header; for (int i = 1; i <= (int)NUM_STATES; i++) { std::string filename = MakeStateFilename(i); if (File::Exists(filename)) { if (ReadHeader(filename, header)) { result.emplace_back(SlotWithTimestamp{.slot = i, .timestamp = header.legacy_header.time}); } } } return result; } static bool CompareTimestamp(const SlotWithTimestamp& lhs, const SlotWithTimestamp& rhs) { return lhs.timestamp < rhs.timestamp; } static void CompressBufferToFile(const u8* raw_buffer, u64 size, File::IOFile& f) { u64 total_bytes_compressed = 0; while (true) { u64 bytes_left_to_compress = size - total_bytes_compressed; int bytes_to_compress = static_cast(std::min(static_cast(LZ4_MAX_INPUT_SIZE), bytes_left_to_compress)); int compressed_buffer_size = LZ4_compressBound(bytes_to_compress); auto compressed_buffer = std::make_unique(compressed_buffer_size); s32 compressed_len = LZ4_compress_default(reinterpret_cast(raw_buffer) + total_bytes_compressed, compressed_buffer.get(), bytes_to_compress, compressed_buffer_size); if (compressed_len == 0) { PanicAlertFmtT("Internal LZ4 Error - compression failed"); break; } // The size of the data to write is 'compressed_len' f.WriteArray(&compressed_len, 1); f.WriteBytes(compressed_buffer.get(), compressed_len); total_bytes_compressed += bytes_to_compress; if (total_bytes_compressed == size) break; } } static void CreateExtendedHeader(StateExtendedHeader& extended_header, size_t uncompressed_size) { StateExtendedBaseHeader& base_header = extended_header.base_header; base_header.header_version = EXTENDED_HEADER_VERSION; base_header.compression_type = s_use_compression ? CompressionType::LZ4 : CompressionType::Uncompressed; base_header.payload_offset = COMPRESSED_DATA_OFFSET; base_header.uncompressed_size = uncompressed_size; // If more fields are added to StateExtendedHeader, set them here. } static void WriteHeadersToFile(size_t uncompressed_size, File::IOFile& f) { StateHeader header{}; SConfig::GetInstance().GetGameID().copy(header.legacy_header.game_id, std::size(header.legacy_header.game_id)); header.legacy_header.time = GetSystemTimeAsDouble(); header.version_header.version_cookie = COOKIE_BASE + STATE_VERSION; header.version_string = Common::GetScmRevStr(); header.version_header.version_string_length = static_cast(header.version_string.length()); StateExtendedHeader extended_header{}; CreateExtendedHeader(extended_header, uncompressed_size); f.WriteArray(&header.legacy_header, 1); f.WriteArray(&header.version_header, 1); f.WriteString(header.version_string); f.WriteArray(&extended_header.base_header, 1); // If StateExtendedHeader is amended to include more than the base, add WriteBytes() calls here. } static void CompressAndDumpState(CompressAndDumpState_args& save_args) { const u8* const buffer_data = save_args.buffer_vector.data(); const size_t buffer_size = save_args.buffer_vector.size(); const std::string& filename = save_args.filename; // Find free temporary filename. // TODO: The file exists check and the actual opening of the file should be atomic, we don't have // functions for that. std::string temp_filename; size_t temp_counter = static_cast(Common::CurrentThreadId()); do { temp_filename = fmt::format("{}{}.tmp", filename, temp_counter); ++temp_counter; } while (File::Exists(temp_filename)); File::IOFile f(temp_filename, "wb"); if (!f) { Core::DisplayMessage("Could not save state", 2000); return; } WriteHeadersToFile(buffer_size, f); if (s_use_compression) CompressBufferToFile(buffer_data, buffer_size, f); else f.WriteBytes(buffer_data, buffer_size); const std::string last_state_filename = File::GetUserPath(D_STATESAVES_IDX) + "lastState.sav"; const std::string last_state_dtmname = last_state_filename + ".dtm"; const std::string dtmname = filename + ".dtm"; { std::lock_guard lk(s_save_thread_mutex); // Backup existing state (overwriting an existing backup, if any). if (File::Exists(filename)) { if (File::Exists(last_state_filename)) File::Delete((last_state_filename)); if (File::Exists(last_state_dtmname)) File::Delete((last_state_dtmname)); if (!File::Rename(filename, last_state_filename)) { Core::DisplayMessage("Failed to move previous state to state undo backup", 1000); } else if (File::Exists(dtmname)) { if (!File::Rename(dtmname, last_state_dtmname)) Core::DisplayMessage("Failed to move previous state's dtm to state undo backup", 1000); } } if ((Movie::IsMovieActive()) && !Movie::IsJustStartingRecordingInputFromSaveState()) Movie::SaveRecording(dtmname); else if (!Movie::IsMovieActive()) File::Delete(dtmname); // Move written state to final location. // TODO: This should also be atomic. This is possible on all systems, but needs a special // implementation of IOFile on Windows. f.Close(); File::Rename(temp_filename, filename); } std::filesystem::path tempfilename(filename); Core::DisplayMessage(fmt::format("Saved State to {}", tempfilename.filename().string()), 2000); Host_UpdateMainFrame(); } void SaveAs(const std::string& filename, bool wait) { std::unique_lock lk(s_load_or_save_in_progress_mutex, std::try_to_lock); if (!lk) return; Core::RunOnCPUThread( [&] { { std::lock_guard lk_(s_state_writes_in_queue_mutex); ++s_state_writes_in_queue; } // Measure the size of the buffer. u8* ptr = nullptr; PointerWrap p_measure(&ptr, 0, PointerWrap::Mode::Measure); DoState(p_measure); const size_t buffer_size = reinterpret_cast(ptr); // Then actually do the write. std::vector current_buffer; current_buffer.resize(buffer_size); ptr = current_buffer.data(); PointerWrap p(&ptr, buffer_size, PointerWrap::Mode::Write); DoState(p); if (p.IsWriteMode()) { Core::DisplayMessage("Saving State...", 1000); std::shared_ptr sync_event; CompressAndDumpState_args save_args; save_args.buffer_vector = std::move(current_buffer); save_args.filename = filename; if (wait) { sync_event = std::make_shared(); save_args.state_write_done_event = sync_event; } s_save_thread.EmplaceItem(std::move(save_args)); if (sync_event) sync_event->Wait(); } else { // someone aborted the save by changing the mode? { // Note: The worker thread takes care of this in the other branch. std::lock_guard lk_(s_state_writes_in_queue_mutex); if (--s_state_writes_in_queue == 0) s_state_write_queue_is_empty.notify_all(); } Core::DisplayMessage("Unable to save: Internal DoState Error", 4000); } }, true); } static bool GetVersionFromLZO(StateHeader& header, File::IOFile& f) { // Just read the first block, since it will contain the full revision string lzo_uint32 cur_len = 0; // size of compressed bytes lzo_uint new_len = 0; // size of uncompressed bytes std::vector buffer; buffer.resize(header.legacy_header.lzo_size); if (!f.ReadArray(&cur_len, 1) || !f.ReadBytes(out, cur_len)) return false; const int res = lzo1x_decompress(out, cur_len, buffer.data(), &new_len, nullptr); if (res != LZO_E_OK) { // This doesn't seem to happen anymore. PanicAlertFmtT("Internal LZO Error - decompression failed ({0}) ({1}) \n" "Unable to retrieve outdated savestate version info.", res, new_len); return false; } // Read in cookie and string length if (buffer.size() >= sizeof(StateHeaderVersion)) { memcpy(&header.version_header, buffer.data(), sizeof(StateHeaderVersion)); } else { PanicAlertFmtT("Internal LZO Error - failed to parse decompressed version cookie and version " "string length ({0})", buffer.size()); return false; } // Read in the string if (buffer.size() >= sizeof(StateHeaderVersion) + header.version_header.version_string_length) { auto version_buffer = std::make_unique(header.version_header.version_string_length); memcpy(version_buffer.get(), buffer.data() + sizeof(StateHeaderVersion), header.version_header.version_string_length); header.version_string = std::string(version_buffer.get(), header.version_header.version_string_length); } else { PanicAlertFmtT("Internal LZO Error - failed to parse decompressed version string ({0} / {1})", header.version_header.version_string_length, buffer.size()); return false; } return true; } static bool ReadStateHeaderFromFile(StateHeader& header, File::IOFile& f, bool get_version_header = true) { if (!f.IsOpen()) { Core::DisplayMessage("State not found", 2000); return false; } if (!f.ReadArray(&header.legacy_header, 1)) { Core::DisplayMessage("Failed to read state legacy header", 2000); return false; } // Bail out if we only care for retrieving the legacy header. // This is the case with ReadHeader() calls. if (!get_version_header) return true; if (header.legacy_header.lzo_size != 0) { // Parse out version from legacy LZO compressed states if (!GetVersionFromLZO(header, f)) return false; } else { if (!f.ReadArray(&header.version_header, 1)) { Core::DisplayMessage("Failed to read state version header", 2000); return false; } auto version_buffer = std::make_unique(header.version_header.version_string_length); if (!f.ReadBytes(version_buffer.get(), header.version_header.version_string_length)) { Core::DisplayMessage("Failed to read state version string", 2000); return false; } header.version_string = std::string(version_buffer.get(), header.version_header.version_string_length); } return true; } bool ReadHeader(const std::string& filename, StateHeader& header) { // ensure that the savestate write thread isn't moving around states while we do this std::lock_guard lk(s_save_thread_mutex); File::IOFile f(filename, "rb"); bool get_version_header = false; return ReadStateHeaderFromFile(header, f, get_version_header); } std::string GetInfoStringOfSlot(int slot, bool translate) { std::string filename = MakeStateFilename(slot); if (!File::Exists(filename)) return translate ? Common::GetStringT("Empty") : "Empty"; State::StateHeader header; if (!ReadHeader(filename, header)) return translate ? Common::GetStringT("Unknown") : "Unknown"; return SystemTimeAsDoubleToString(header.legacy_header.time); } u64 GetUnixTimeOfSlot(int slot) { State::StateHeader header; if (!ReadHeader(MakeStateFilename(slot), header)) return 0; constexpr u64 MS_PER_SEC = 1000; return static_cast(header.legacy_header.time * MS_PER_SEC) + (DOUBLE_TIME_OFFSET * MS_PER_SEC); } static bool DecompressLZ4(std::vector& raw_buffer, u64 size, File::IOFile& f) { raw_buffer.resize(size); u64 total_bytes_read = 0; while (true) { s32 compressed_data_len; if (!f.ReadArray(&compressed_data_len, 1)) { PanicAlertFmt("Could not read state data length"); return false; } if (compressed_data_len <= 0) { PanicAlertFmtT("Internal LZ4 Error - Tried decompressing {0} bytes", compressed_data_len); return false; } auto compressed_data = std::make_unique(compressed_data_len); if (!f.ReadBytes(compressed_data.get(), compressed_data_len)) { PanicAlertFmt("Could not read state data"); return false; } u32 max_decompress_size = static_cast(std::min((u64)LZ4_MAX_INPUT_SIZE, size - total_bytes_read)); int bytes_read = LZ4_decompress_safe( compressed_data.get(), reinterpret_cast(raw_buffer.data()) + total_bytes_read, compressed_data_len, max_decompress_size); if (bytes_read < 0) { PanicAlertFmtT("Internal LZ4 Error - decompression failed ({0}, {1}, {2})", bytes_read, compressed_data_len, max_decompress_size); return false; } total_bytes_read += static_cast(bytes_read); if (total_bytes_read == size) { return true; } else if (total_bytes_read > size) { PanicAlertFmtT("Internal LZ4 Error - payload size mismatch ({0} / {1}))", total_bytes_read, size); return false; } } } static bool ValidateHeaders(const StateHeader& header) { bool success = true; // Game ID if (strncmp(SConfig::GetInstance().GetGameID().c_str(), header.legacy_header.game_id, 6)) { Core::DisplayMessage(fmt::format("State belongs to a different game (ID {})", std::string_view{header.legacy_header.game_id, std::size(header.legacy_header.game_id)}), 2000); return false; } // Check both the state version and the revision string std::string current_str = Common::GetScmRevStr(); std::string loaded_str = header.version_string; const u32 loaded_version = header.version_header.version_cookie - COOKIE_BASE; if (s_old_versions.count(loaded_version)) { // This is a REALLY old version, before we started writing the version string to file success = false; std::pair version_range = s_old_versions.find(loaded_version)->second; std::string oldest_version = version_range.first; std::string newest_version = version_range.second; loaded_str = "Dolphin " + oldest_version + " - " + newest_version; } else if (loaded_version != STATE_VERSION) { success = false; } if (!success) { const std::string message = loaded_str.empty() ? "This savestate was created using an incompatible version of Dolphin" : "This savestate was created using the incompatible version " + loaded_str; Core::DisplayMessage(message, OSD::Duration::NORMAL); } return success; } static void LoadFileStateData(const std::string& filename, std::vector& ret_data) { File::IOFile f; { // If a state is currently saving, wait for that to end or time out. std::unique_lock lk(s_state_writes_in_queue_mutex); if (s_state_writes_in_queue != 0) { if (!s_state_write_queue_is_empty.wait_for(lk, std::chrono::seconds(3), []() { return s_state_writes_in_queue == 0; })) { Core::DisplayMessage( "A previous state saving operation is still in progress, cancelling load.", 2000); return; } } f.Open(filename, "rb"); } StateHeader header; if (!ReadStateHeaderFromFile(header, f) || !ValidateHeaders(header)) return; StateExtendedHeader extended_header; if (!f.ReadArray(&extended_header.base_header, 1)) { PanicAlertFmt("Unable to read state header"); return; } // If StateExtendedHeader is amended to include more than the base, add ReadBytes() calls here. if (extended_header.base_header.header_version != EXTENDED_HEADER_VERSION) { PanicAlertFmt("State header corrupted"); return; } std::vector buffer; switch (extended_header.base_header.compression_type) { case CompressionType::LZ4: { Core::DisplayMessage("Decompressing State...", 500); if (!DecompressLZ4(buffer, extended_header.base_header.uncompressed_size, f)) return; break; } case CompressionType::Uncompressed: { u64 header_len = sizeof(StateHeaderLegacy) + sizeof(StateHeaderVersion) + header.version_header.version_string_length + sizeof(StateExtendedBaseHeader) + extended_header.base_header.payload_offset; u64 file_size = f.GetSize(); if (file_size < header_len) { PanicAlertFmt("State header length corrupted"); return; } const auto size = static_cast(file_size - header_len); buffer.resize(size); if (!f.ReadBytes(buffer.data(), size)) { PanicAlertFmt("Error reading bytes: {0}", size); return; } break; } default: PanicAlertFmt("Unknown compression type {0}", extended_header.base_header.compression_type); return; } // all good ret_data.swap(buffer); } void LoadAs(const std::string& filename) { if (!Core::IsRunning()) return; if (NetPlay::IsNetPlayRunning()) { OSD::AddMessage("Loading savestates is disabled in Netplay to prevent desyncs"); return; } std::unique_lock lk(s_load_or_save_in_progress_mutex, std::try_to_lock); if (!lk) return; Core::RunOnCPUThread( [&] { // Save temp buffer for undo load state if (!Movie::IsJustStartingRecordingInputFromSaveState()) { std::lock_guard lk2(s_undo_load_buffer_mutex); SaveToBuffer(s_undo_load_buffer); const std::string dtmpath = File::GetUserPath(D_STATESAVES_IDX) + "undo.dtm"; if (Movie::IsMovieActive()) Movie::SaveRecording(dtmpath); else if (File::Exists(dtmpath)) File::Delete(dtmpath); } bool loaded = false; bool loadedSuccessfully = false; // brackets here are so buffer gets freed ASAP { std::vector buffer; LoadFileStateData(filename, buffer); if (!buffer.empty()) { u8* ptr = buffer.data(); PointerWrap p(&ptr, buffer.size(), PointerWrap::Mode::Read); DoState(p); loaded = true; loadedSuccessfully = p.IsReadMode(); } } if (loaded) { if (loadedSuccessfully) { std::filesystem::path tempfilename(filename); Core::DisplayMessage( fmt::format("Loaded State from {}", tempfilename.filename().string()), 2000); if (File::Exists(filename + ".dtm")) Movie::LoadInput(filename + ".dtm"); else if (!Movie::IsJustStartingRecordingInputFromSaveState() && !Movie::IsJustStartingPlayingInputFromSaveState()) Movie::EndPlayInput(false); } else { Core::DisplayMessage("The savestate could not be loaded", OSD::Duration::NORMAL); // since we could be in an inconsistent state now (and might crash or whatever), undo. UndoLoadState(); } } if (s_on_after_load_callback) s_on_after_load_callback(); }, true); } void SetOnAfterLoadCallback(AfterLoadCallbackFunc callback) { s_on_after_load_callback = std::move(callback); } void Init() { s_save_thread.Reset("Savestate Worker", [](CompressAndDumpState_args args) { CompressAndDumpState(args); { std::lock_guard lk(s_state_writes_in_queue_mutex); if (--s_state_writes_in_queue == 0) s_state_write_queue_is_empty.notify_all(); } if (args.state_write_done_event) args.state_write_done_event->Set(); }); } void Shutdown() { s_save_thread.Shutdown(); // swapping with an empty vector, rather than clear()ing // this gives a better guarantee to free the allocated memory right NOW (as opposed to, actually, // never) { std::lock_guard lk(s_undo_load_buffer_mutex); std::vector().swap(s_undo_load_buffer); } } static std::string MakeStateFilename(int number) { return fmt::format("{}{}.s{:02d}", File::GetUserPath(D_STATESAVES_IDX), SConfig::GetInstance().GetGameID(), number); } void Save(int slot, bool wait) { SaveAs(MakeStateFilename(slot), wait); } void Load(int slot) { LoadAs(MakeStateFilename(slot)); } void LoadLastSaved(int i) { if (i <= 0) { Core::DisplayMessage("State doesn't exist", 2000); return; } std::vector used_slots = GetUsedSlotsWithTimestamp(); if (static_cast(i) > used_slots.size()) { Core::DisplayMessage("State doesn't exist", 2000); return; } std::stable_sort(used_slots.begin(), used_slots.end(), CompareTimestamp); Load((used_slots.end() - i)->slot); } // must wait for state to be written because it must know if all slots are taken void SaveFirstSaved() { std::vector used_slots = GetUsedSlotsWithTimestamp(); if (used_slots.size() < NUM_STATES) { // save to an empty slot Save(GetEmptySlot(used_slots), true); return; } // overwrite the oldest state std::stable_sort(used_slots.begin(), used_slots.end(), CompareTimestamp); Save(used_slots.front().slot, true); } // Load the last state before loading the state void UndoLoadState() { std::lock_guard lk(s_undo_load_buffer_mutex); if (!s_undo_load_buffer.empty()) { if (Movie::IsMovieActive()) { const std::string dtmpath = File::GetUserPath(D_STATESAVES_IDX) + "undo.dtm"; if (File::Exists(dtmpath)) { LoadFromBuffer(s_undo_load_buffer); Movie::LoadInput(dtmpath); } else { PanicAlertFmtT("No undo.dtm found, aborting undo load state to prevent movie desyncs"); } } else { LoadFromBuffer(s_undo_load_buffer); } } else { PanicAlertFmtT("There is nothing to undo!"); } } // Load the state that the last save state overwritten on void UndoSaveState() { LoadAs(File::GetUserPath(D_STATESAVES_IDX) + "lastState.sav"); } } // namespace State