mirror of
https://git.h3cjp.net/H3cJP/citra.git
synced 2024-12-27 13:46:54 +00:00
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 `<Title ID>.<Slot ID>.cst`.
This commit is contained in:
parent
7d880f94db
commit
a487016cb4
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -36,6 +36,7 @@ enum class UserPath {
|
|||
RootDir,
|
||||
SDMCDir,
|
||||
ShaderDir,
|
||||
StatesDir,
|
||||
SysDataDir,
|
||||
UserDir,
|
||||
};
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
174
src/core/savestate.cpp
Normal file
174
src/core/savestate.cpp
Normal file
|
@ -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
|
27
src/core/savestate.h
Normal file
27
src/core/savestate.h
Normal file
|
@ -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
|
Loading…
Reference in a new issue