From a487016cb42d0d36ed8706909f4423571141fcaf Mon Sep 17 00:00:00 2001 From: zhupengfei Date: Tue, 18 Feb 2020 13:19:52 +0800 Subject: [PATCH] core, citra_qt: Implement a save states file format and slot UI 10 slots are offered along with 'Save to Oldest Slot' and 'Load from Newest Slot'. The savestate format is similar to the movie file format. It is called CST (Citra SavesTate), and is basically a 0x100 byte header (consisting of magic, revision, creation time and title ID) followed by Zstd compressed raw savestate data. The savestate files are saved to the `states` folder in Citra's user folder. The files are named like `.<Slot ID>.cst`. --- src/citra_qt/main.cpp | 111 +++++++++++++++++++++--- src/citra_qt/main.h | 16 +++- src/citra_qt/main.ui | 29 ++++++- src/common/common_paths.h | 1 + src/common/file_util.cpp | 1 + src/common/file_util.h | 1 + src/core/CMakeLists.txt | 2 + src/core/core.cpp | 85 ++++++------------- src/core/core.h | 14 ++- src/core/savestate.cpp | 174 ++++++++++++++++++++++++++++++++++++++ src/core/savestate.h | 27 ++++++ 11 files changed, 384 insertions(+), 77 deletions(-) create mode 100644 src/core/savestate.cpp create mode 100644 src/core/savestate.h diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp index 7fe4936f5f..de66e14ac0 100644 --- a/src/citra_qt/main.cpp +++ b/src/citra_qt/main.cpp @@ -79,6 +79,7 @@ #include "core/hle/service/nfc/nfc.h" #include "core/loader/loader.h" #include "core/movie.h" +#include "core/savestate.h" #include "core/settings.h" #include "game_list_p.h" #include "video_core/renderer_base.h" @@ -166,6 +167,7 @@ GMainWindow::GMainWindow() : config(new Config()), emu_thread(nullptr) { InitializeWidgets(); InitializeDebugWidgets(); InitializeRecentFileMenuActions(); + InitializeSaveStateMenuActions(); InitializeHotkeys(); ShowUpdaterWidgets(); @@ -383,6 +385,32 @@ void GMainWindow::InitializeRecentFileMenuActions() { UpdateRecentFiles(); } +void GMainWindow::InitializeSaveStateMenuActions() { + for (u32 i = 0; i < Core::SaveStateSlotCount; ++i) { + actions_load_state[i] = new QAction(this); + actions_load_state[i]->setData(i + 1); + connect(actions_load_state[i], &QAction::triggered, this, &GMainWindow::OnLoadState); + ui.menu_Load_State->addAction(actions_load_state[i]); + + actions_save_state[i] = new QAction(this); + actions_save_state[i]->setData(i + 1); + connect(actions_save_state[i], &QAction::triggered, this, &GMainWindow::OnSaveState); + ui.menu_Save_State->addAction(actions_save_state[i]); + } + + connect(ui.action_Load_from_Newest_Slot, &QAction::triggered, + [this] { actions_load_state[newest_slot - 1]->trigger(); }); + connect(ui.action_Save_to_Oldest_Slot, &QAction::triggered, + [this] { actions_save_state[oldest_slot - 1]->trigger(); }); + + connect(ui.menu_Load_State->menuAction(), &QAction::hovered, this, + &GMainWindow::UpdateSaveStates); + connect(ui.menu_Save_State->menuAction(), &QAction::hovered, this, + &GMainWindow::UpdateSaveStates); + + UpdateSaveStates(); +} + void GMainWindow::InitializeHotkeys() { hotkey_registry.LoadHotkeys(); @@ -607,8 +635,6 @@ void GMainWindow::ConnectMenuEvents() { &GMainWindow::OnMenuReportCompatibility); connect(ui.action_Configure, &QAction::triggered, this, &GMainWindow::OnConfigure); connect(ui.action_Cheats, &QAction::triggered, this, &GMainWindow::OnCheats); - connect(ui.action_Save, &QAction::triggered, this, &GMainWindow::OnSave); - connect(ui.action_Load, &QAction::triggered, this, &GMainWindow::OnLoad); // View connect(ui.action_Single_Window_Mode, &QAction::triggered, this, @@ -1036,8 +1062,6 @@ void GMainWindow::ShutdownGame() { ui.action_Stop->setEnabled(false); ui.action_Restart->setEnabled(false); ui.action_Cheats->setEnabled(false); - ui.action_Save->setEnabled(false); - ui.action_Load->setEnabled(false); ui.action_Load_Amiibo->setEnabled(false); ui.action_Remove_Amiibo->setEnabled(false); ui.action_Report_Compatibility->setEnabled(false); @@ -1061,6 +1085,8 @@ void GMainWindow::ShutdownGame() { game_fps_label->setVisible(false); emu_frametime_label->setVisible(false); + UpdateSaveStates(); + emulation_running = false; if (defer_update_prompt) { @@ -1107,6 +1133,62 @@ void GMainWindow::UpdateRecentFiles() { ui.menu_recent_files->setEnabled(num_recent_files != 0); } +void GMainWindow::UpdateSaveStates() { + if (!Core::System::GetInstance().IsPoweredOn()) { + ui.menu_Load_State->setEnabled(false); + ui.menu_Save_State->setEnabled(false); + return; + } + + ui.menu_Load_State->setEnabled(true); + ui.menu_Save_State->setEnabled(true); + ui.action_Load_from_Newest_Slot->setEnabled(false); + + oldest_slot = newest_slot = 0; + oldest_slot_time = std::numeric_limits<u64>::max(); + newest_slot_time = 0; + + u64 title_id; + if (Core::System::GetInstance().GetAppLoader().ReadProgramId(title_id) != + Loader::ResultStatus::Success) { + return; + } + auto savestates = Core::ListSaveStates(title_id); + for (u32 i = 0; i < Core::SaveStateSlotCount; ++i) { + actions_load_state[i]->setEnabled(false); + actions_load_state[i]->setText(tr("Slot %1").arg(i + 1)); + actions_save_state[i]->setText(tr("Slot %1").arg(i + 1)); + } + for (const auto& savestate : savestates) { + const auto text = tr("Slot %1 - %2") + .arg(savestate.slot) + .arg(QDateTime::fromSecsSinceEpoch(savestate.time) + .toString(QStringLiteral("yyyy-MM-dd hh:mm:ss"))); + actions_load_state[savestate.slot - 1]->setEnabled(true); + actions_load_state[savestate.slot - 1]->setText(text); + actions_save_state[savestate.slot - 1]->setText(text); + + ui.action_Load_from_Newest_Slot->setEnabled(true); + + if (savestate.time > newest_slot_time) { + newest_slot = savestate.slot; + newest_slot_time = savestate.time; + } + if (savestate.time < oldest_slot_time) { + oldest_slot = savestate.slot; + oldest_slot_time = savestate.time; + } + } + for (u32 i = 0; i < Core::SaveStateSlotCount; ++i) { + if (!actions_load_state[i]->isEnabled()) { + // Prefer empty slot + oldest_slot = i + 1; + oldest_slot_time = 0; + break; + } + } +} + void GMainWindow::OnGameListLoadFile(QString game_path) { BootGame(game_path); } @@ -1348,14 +1430,14 @@ void GMainWindow::OnStartGame() { ui.action_Stop->setEnabled(true); ui.action_Restart->setEnabled(true); ui.action_Cheats->setEnabled(true); - ui.action_Save->setEnabled(true); - ui.action_Load->setEnabled(true); ui.action_Load_Amiibo->setEnabled(true); ui.action_Report_Compatibility->setEnabled(true); ui.action_Enable_Frame_Advancing->setEnabled(true); ui.action_Capture_Screenshot->setEnabled(true); discord_rpc->Update(); + + UpdateSaveStates(); } void GMainWindow::OnPauseGame() { @@ -1503,14 +1585,19 @@ void GMainWindow::OnCheats() { cheat_dialog.exec(); } -void GMainWindow::OnSave() { - Core::System::GetInstance().SendSignal(Core::System::Signal::Save); +void GMainWindow::OnSaveState() { + QAction* action = qobject_cast<QAction*>(sender()); + assert(action); + + Core::System::GetInstance().SendSignal(Core::System::Signal::Save, action->data().toUInt()); + UpdateSaveStates(); } -void GMainWindow::OnLoad() { - if (QFileInfo("save0.citrasave").exists()) { - Core::System::GetInstance().SendSignal(Core::System::Signal::Load); - } +void GMainWindow::OnLoadState() { + QAction* action = qobject_cast<QAction*>(sender()); + assert(action); + + Core::System::GetInstance().SendSignal(Core::System::Signal::Load, action->data().toUInt()); } void GMainWindow::OnConfigure() { diff --git a/src/citra_qt/main.h b/src/citra_qt/main.h index 1858d5988f..ebe1a013af 100644 --- a/src/citra_qt/main.h +++ b/src/citra_qt/main.h @@ -4,6 +4,7 @@ #pragma once +#include <array> #include <memory> #include <QLabel> #include <QMainWindow> @@ -14,6 +15,7 @@ #include "common/announce_multiplayer_room.h" #include "core/core.h" #include "core/hle/service/am/am.h" +#include "core/savestate.h" #include "ui_main.h" class AboutDialog; @@ -106,6 +108,7 @@ private: void InitializeWidgets(); void InitializeDebugWidgets(); void InitializeRecentFileMenuActions(); + void InitializeSaveStateMenuActions(); void SetDefaultUIGeometry(); void SyncMenuUISettings(); @@ -149,6 +152,8 @@ private: */ void UpdateRecentFiles(); + void UpdateSaveStates(); + /** * If the emulation is running, * asks the user if he really want to close the emulator @@ -163,8 +168,8 @@ private slots: void OnStartGame(); void OnPauseGame(); void OnStopGame(); - void OnSave(); - void OnLoad(); + void OnSaveState(); + void OnLoadState(); void OnMenuReportCompatibility(); /// Called whenever a user selects a game in the game list widget. void OnGameListLoadFile(QString game_path); @@ -276,6 +281,13 @@ private: bool defer_update_prompt = false; QAction* actions_recent_files[max_recent_files_item]; + std::array<QAction*, Core::SaveStateSlotCount> actions_load_state; + std::array<QAction*, Core::SaveStateSlotCount> actions_save_state; + + u32 oldest_slot; + u64 oldest_slot_time; + u32 newest_slot; + u64 newest_slot_time; QTranslator translator; diff --git a/src/citra_qt/main.ui b/src/citra_qt/main.ui index c0c38e4c8b..2eff98083b 100644 --- a/src/citra_qt/main.ui +++ b/src/citra_qt/main.ui @@ -79,17 +79,32 @@ <property name="title"> <string>&Emulation</string> </property> + <widget class="QMenu" name="menu_Save_State"> + <property name="title"> + <string>Save State</string> + </property> + <addaction name="action_Save_to_Oldest_Slot"/> + <addaction name="separator"/> + </widget> + <widget class="QMenu" name="menu_Load_State"> + <property name="title"> + <string>Load State</string> + </property> + <addaction name="action_Load_from_Newest_Slot"/> + <addaction name="separator"/> + </widget> <addaction name="action_Start"/> <addaction name="action_Pause"/> <addaction name="action_Stop"/> <addaction name="action_Restart"/> <addaction name="separator"/> + <addaction name="menu_Load_State"/> + <addaction name="menu_Save_State"/> + <addaction name="separator"/> <addaction name="action_Report_Compatibility"/> <addaction name="separator"/> <addaction name="action_Configure"/> <addaction name="action_Cheats"/> - <addaction name="action_Save"/> - <addaction name="action_Load"/> </widget> <widget class="QMenu" name="menu_View"> <property name="title"> @@ -253,6 +268,16 @@ <string>Single Window Mode</string> </property> </action> + <action name="action_Save_to_Oldest_Slot"> + <property name="text"> + <string>Save to Oldest Slot</string> + </property> + </action> + <action name="action_Load_from_Newest_Slot"> + <property name="text"> + <string>Load from Newest Slot</string> + </property> + </action> <action name="action_Configure"> <property name="text"> <string>Configure...</string> diff --git a/src/common/common_paths.h b/src/common/common_paths.h index 13e71615e6..eec4dde9c6 100644 --- a/src/common/common_paths.h +++ b/src/common/common_paths.h @@ -47,6 +47,7 @@ #define DUMP_DIR "dump" #define LOAD_DIR "load" #define SHADER_DIR "shaders" +#define STATES_DIR "states" // Filenames // Files in the directory returned by GetUserPath(UserPath::LogDir) diff --git a/src/common/file_util.cpp b/src/common/file_util.cpp index c5da82973c..cd3f4e1027 100644 --- a/src/common/file_util.cpp +++ b/src/common/file_util.cpp @@ -725,6 +725,7 @@ void SetUserPath(const std::string& path) { g_paths.emplace(UserPath::ShaderDir, user_path + SHADER_DIR DIR_SEP); g_paths.emplace(UserPath::DumpDir, user_path + DUMP_DIR DIR_SEP); g_paths.emplace(UserPath::LoadDir, user_path + LOAD_DIR DIR_SEP); + g_paths.emplace(UserPath::StatesDir, user_path + STATES_DIR DIR_SEP); } const std::string& GetUserPath(UserPath path) { diff --git a/src/common/file_util.h b/src/common/file_util.h index 0368d36658..8af5a2a613 100644 --- a/src/common/file_util.h +++ b/src/common/file_util.h @@ -36,6 +36,7 @@ enum class UserPath { RootDir, SDMCDir, ShaderDir, + StatesDir, SysDataDir, UserDir, }; diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index c6908c59a6..2b02ffc416 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -447,6 +447,8 @@ add_library(core STATIC rpc/server.h rpc/udp_server.cpp rpc/udp_server.h + savestate.cpp + savestate.h settings.cpp settings.h telemetry_session.cpp diff --git a/src/core/core.cpp b/src/core/core.cpp index 0bbb79aead..f17474a85e 100644 --- a/src/core/core.cpp +++ b/src/core/core.cpp @@ -10,10 +10,8 @@ #include "audio_core/dsp_interface.h" #include "audio_core/hle/hle.h" #include "audio_core/lle/lle.h" -#include "common/archives.h" #include "common/logging/log.h" #include "common/texture.h" -#include "common/zstd_compression.h" #include "core/arm/arm_interface.h" #ifdef ARCHITECTURE_x86_64 #include "core/arm/dynarmic/arm_dynarmic.h" @@ -63,6 +61,8 @@ Kernel::KernelSystem& Global() { return System::GetInstance().Kernel(); } +System::~System() = default; + System::ResultStatus System::RunLoop(bool tight_loop) { status = ResultStatus::Success; if (!cpu_core) { @@ -106,7 +106,16 @@ System::ResultStatus System::RunLoop(bool tight_loop) { HW::Update(); Reschedule(); - auto signal = current_signal.exchange(Signal::None); + Signal signal{Signal::None}; + u32 param{}; + { + std::lock_guard lock{signal_mutex}; + if (current_signal != Signal::None) { + signal = current_signal; + param = signal_param; + current_signal = Signal::None; + } + } switch (signal) { case Signal::Reset: Reset(); @@ -116,14 +125,16 @@ System::ResultStatus System::RunLoop(bool tight_loop) { break; case Signal::Load: { LOG_INFO(Core, "Begin load"); - auto stream = std::ifstream("save0.citrasave", std::fstream::binary); - System::Load(stream, FileUtil::GetSize("save0.citrasave")); + System::LoadState(param); + // auto stream = std::ifstream("save0.citrasave", std::fstream::binary); + // System::Load(stream, FileUtil::GetSize("save0.citrasave")); LOG_INFO(Core, "Load completed"); } break; case Signal::Save: { LOG_INFO(Core, "Begin save"); - auto stream = std::ofstream("save0.citrasave", std::fstream::binary); - System::Save(stream); + System::SaveState(param); + // auto stream = std::ofstream("save0.citrasave", std::fstream::binary); + // System::Save(stream); LOG_INFO(Core, "Save completed"); } break; default: @@ -133,12 +144,14 @@ System::ResultStatus System::RunLoop(bool tight_loop) { return status; } -bool System::SendSignal(System::Signal signal) { - auto prev = System::Signal::None; - if (!current_signal.compare_exchange_strong(prev, signal)) { - LOG_ERROR(Core, "Unable to {} as {} is ongoing", signal, prev); +bool System::SendSignal(System::Signal signal, u32 param) { + std::lock_guard lock{signal_mutex}; + if (current_signal != signal && current_signal != Signal::None) { + LOG_ERROR(Core, "Unable to {} as {} is ongoing", signal, current_signal); return false; } + current_signal = signal; + signal_param = param; return true; } @@ -196,7 +209,7 @@ System::ResultStatus System::Load(Frontend::EmuWindow& emu_window, const std::st } } cheat_engine = std::make_unique<Cheats::CheatEngine>(*this); - u64 title_id{0}; + title_id = 0; if (app_loader->ReadProgramId(title_id) != Loader::ResultStatus::Success) { LOG_ERROR(Core, "Failed to find title id for ROM (Error {})", static_cast<u32>(load_result)); @@ -246,8 +259,8 @@ System::ResultStatus System::Init(Frontend::EmuWindow& emu_window, u32 system_mo timing = std::make_unique<Timing>(); - kernel = std::make_unique<Kernel::KernelSystem>( - *memory, *timing, [this] { PrepareReschedule(); }, system_mode); + kernel = std::make_unique<Kernel::KernelSystem>(*memory, *timing, + [this] { PrepareReschedule(); }, system_mode); if (Settings::values.use_cpu_jit) { #ifdef ARCHITECTURE_x86_64 @@ -464,48 +477,6 @@ void System::serialize(Archive& ar, const unsigned int file_version) { } } -void System::Save(std::ostream& stream) const { - std::ostringstream sstream{std::ios_base::binary}; - try { - - { - oarchive oa{sstream}; - oa&* this; - } - VideoCore::Save(sstream); - - } catch (const std::exception& e) { - LOG_ERROR(Core, "Error saving: {}", e.what()); - } - const std::string& str{sstream.str()}; - auto buffer = Common::Compression::CompressDataZSTDDefault( - reinterpret_cast<const u8*>(str.data()), str.size()); - stream.write(reinterpret_cast<const char*>(buffer.data()), buffer.size()); -} - -void System::Load(std::istream& stream, std::size_t size) { - std::vector<u8> decompressed; - { - std::vector<u8> buffer(size); - stream.read(reinterpret_cast<char*>(buffer.data()), size); - decompressed = Common::Compression::DecompressDataZSTD(buffer); - } - std::istringstream sstream{ - std::string{reinterpret_cast<char*>(decompressed.data()), decompressed.size()}, - std::ios_base::binary}; - decompressed.clear(); - - try { - - { - iarchive ia{sstream}; - ia&* this; - } - VideoCore::Load(sstream); - - } catch (const std::exception& e) { - LOG_ERROR(Core, "Error loading: {}", e.what()); - } -} +SERIALIZE_IMPL(System) } // namespace Core diff --git a/src/core/core.h b/src/core/core.h index 80c1505b5d..0ce6924cd0 100644 --- a/src/core/core.h +++ b/src/core/core.h @@ -5,6 +5,7 @@ #pragma once #include <memory> +#include <mutex> #include <string> #include "boost/serialization/access.hpp" #include "common/common_types.h" @@ -92,6 +93,8 @@ public: ErrorUnknown ///< Any other error }; + ~System(); + /** * Run the core CPU loop * This function runs the core for the specified number of CPU instructions before trying to @@ -118,7 +121,7 @@ public: enum class Signal : u32 { None, Shutdown, Reset, Save, Load }; - bool SendSignal(Signal signal); + bool SendSignal(Signal signal, u32 param = 0); /// Request reset of the system void RequestReset() { @@ -276,9 +279,9 @@ public: return registered_image_interface; } - void Save(std::ostream& stream) const; + void SaveState(u32 slot) const; - void Load(std::istream& stream, std::size_t size); + void LoadState(u32 slot); private: /** @@ -344,8 +347,11 @@ private: /// Saved variables for reset Frontend::EmuWindow* m_emu_window; std::string m_filepath; + u64 title_id; - std::atomic<Signal> current_signal; + std::mutex signal_mutex; + Signal current_signal; + u32 signal_param; friend class boost::serialization::access; template <typename Archive> diff --git a/src/core/savestate.cpp b/src/core/savestate.cpp new file mode 100644 index 0000000000..d52789b2c2 --- /dev/null +++ b/src/core/savestate.cpp @@ -0,0 +1,174 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include <chrono> +#include <cryptopp/hex.h> +#include "common/archives.h" +#include "common/logging/log.h" +#include "common/scm_rev.h" +#include "common/zstd_compression.h" +#include "core/core.h" +#include "core/savestate.h" +#include "video_core/video_core.h" + +namespace Core { + +#pragma pack(push, 1) +struct CSTHeader { + std::array<u8, 4> filetype; /// Unique Identifier to check the file type (always "CST"0x1B) + u64_le program_id; /// ID of the ROM being executed. Also called title_id + std::array<u8, 20> revision; /// Git hash of the revision this savestate was created with + u64_le time; /// The time when this save state was created + + std::array<u8, 216> reserved; /// Make heading 256 bytes so it has consistent size +}; +static_assert(sizeof(CSTHeader) == 256, "CSTHeader should be 256 bytes"); +#pragma pack(pop) + +constexpr std::array<u8, 4> header_magic_bytes{{'C', 'S', 'T', 0x1B}}; + +std::string GetSaveStatePath(u64 program_id, u32 slot) { + return fmt::format("{}{:016X}.{:02d}.cst", FileUtil::GetUserPath(FileUtil::UserPath::StatesDir), + program_id, slot); +} + +std::vector<SaveStateInfo> ListSaveStates(u64 program_id) { + std::vector<SaveStateInfo> result; + for (u32 slot = 1; slot <= SaveStateSlotCount; ++slot) { + const auto path = GetSaveStatePath(program_id, slot); + if (!FileUtil::Exists(path)) { + continue; + } + + SaveStateInfo info; + info.slot = slot; + + FileUtil::IOFile file(path, "rb"); + if (!file) { + LOG_ERROR(Core, "Could not open file {}", path); + continue; + } + CSTHeader header; + if (file.GetSize() < sizeof(header)) { + LOG_ERROR(Core, "File too small {}", path); + continue; + } + if (file.ReadBytes(&header, sizeof(header)) != sizeof(header)) { + LOG_ERROR(Core, "Could not read from file {}", path); + continue; + } + if (header.filetype != header_magic_bytes) { + LOG_WARNING(Core, "Invalid save state file {}", path); + continue; + } + info.time = header.time; + + if (header.program_id != program_id) { + LOG_WARNING(Core, "Save state file isn't for the current game {}", path); + continue; + } + std::string revision = fmt::format("{:02x}", fmt::join(header.revision, "")); + if (revision == Common::g_scm_rev) { + info.status = SaveStateInfo::ValidationStatus::OK; + } else { + LOG_WARNING(Core, "Save state file created from a different revision {}", path); + info.status = SaveStateInfo::ValidationStatus::RevisionDismatch; + } + result.emplace_back(std::move(info)); + } + return result; +} + +void System::SaveState(u32 slot) const { + std::ostringstream sstream{std::ios_base::binary}; + try { + + { + oarchive oa{sstream}; + oa&* this; + } + VideoCore::Save(sstream); + + } catch (const std::exception& e) { + LOG_ERROR(Core, "Error saving: {}", e.what()); + } + const std::string& str{sstream.str()}; + auto buffer = Common::Compression::CompressDataZSTDDefault( + reinterpret_cast<const u8*>(str.data()), str.size()); + + const auto path = GetSaveStatePath(title_id, slot); + if (!FileUtil::CreateFullPath(path)) { + LOG_ERROR(Core, "Could not create path {}", path); + return; + } + + FileUtil::IOFile file(path, "wb"); + if (!file) { + LOG_ERROR(Core, "Could not open file {}", path); + return; + } + + CSTHeader header{}; + header.filetype = header_magic_bytes; + header.program_id = title_id; + std::string rev_bytes; + CryptoPP::StringSource(Common::g_scm_rev, true, + new CryptoPP::HexDecoder(new CryptoPP::StringSink(rev_bytes))); + std::memcpy(header.revision.data(), rev_bytes.data(), sizeof(header.revision)); + header.time = std::chrono::duration_cast<std::chrono::seconds>( + std::chrono::system_clock::now().time_since_epoch()) + .count(); + + if (file.WriteBytes(&header, sizeof(header)) != sizeof(header)) { + LOG_ERROR(Core, "Could not write to file {}", path); + return; + } + if (file.WriteBytes(buffer.data(), buffer.size()) != buffer.size()) { + LOG_ERROR(Core, "Could not write to file {}", path); + return; + } +} + +void System::LoadState(u32 slot) { + const auto path = GetSaveStatePath(title_id, slot); + if (!FileUtil::Exists(path)) { + LOG_ERROR(Core, "File not exist {}", path); + return; + } + + std::vector<u8> decompressed; + { + std::vector<u8> buffer(FileUtil::GetSize(path) - sizeof(CSTHeader)); + + FileUtil::IOFile file(path, "rb"); + if (!file) { + LOG_ERROR(Core, "Could not open file {}", path); + return; + } + file.Seek(sizeof(CSTHeader), SEEK_SET); // Skip header + if (file.ReadBytes(buffer.data(), buffer.size()) != buffer.size()) { + LOG_ERROR(Core, "Could not read from file {}", path); + return; + } + decompressed = Common::Compression::DecompressDataZSTD(buffer); + } + std::istringstream sstream{ + std::string{reinterpret_cast<char*>(decompressed.data()), decompressed.size()}, + std::ios_base::binary}; + decompressed.clear(); + + try { + + { + iarchive ia{sstream}; + ia&* this; + } + VideoCore::Load(sstream); + + } catch (const std::exception& e) { + LOG_ERROR(Core, "Error loading: {}", e.what()); + } +} + +} // namespace Core diff --git a/src/core/savestate.h b/src/core/savestate.h new file mode 100644 index 0000000000..f67bee22f8 --- /dev/null +++ b/src/core/savestate.h @@ -0,0 +1,27 @@ +// Copyright 2020 Citra Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#pragma once + +#include <vector> +#include "common/common_types.h" + +namespace Core { + +struct CSTHeader; + +struct SaveStateInfo { + u32 slot; + u64 time; + enum class ValidationStatus { + OK, + RevisionDismatch, + } status; +}; + +constexpr u32 SaveStateSlotCount = 10; // Maximum count of savestate slots + +std::vector<SaveStateInfo> ListSaveStates(u64 program_id); + +} // namespace Core