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/hle/service/nfc/nfc.h"
|
||||||
#include "core/loader/loader.h"
|
#include "core/loader/loader.h"
|
||||||
#include "core/movie.h"
|
#include "core/movie.h"
|
||||||
|
#include "core/savestate.h"
|
||||||
#include "core/settings.h"
|
#include "core/settings.h"
|
||||||
#include "game_list_p.h"
|
#include "game_list_p.h"
|
||||||
#include "video_core/renderer_base.h"
|
#include "video_core/renderer_base.h"
|
||||||
|
@ -166,6 +167,7 @@ GMainWindow::GMainWindow() : config(new Config()), emu_thread(nullptr) {
|
||||||
InitializeWidgets();
|
InitializeWidgets();
|
||||||
InitializeDebugWidgets();
|
InitializeDebugWidgets();
|
||||||
InitializeRecentFileMenuActions();
|
InitializeRecentFileMenuActions();
|
||||||
|
InitializeSaveStateMenuActions();
|
||||||
InitializeHotkeys();
|
InitializeHotkeys();
|
||||||
ShowUpdaterWidgets();
|
ShowUpdaterWidgets();
|
||||||
|
|
||||||
|
@ -383,6 +385,32 @@ void GMainWindow::InitializeRecentFileMenuActions() {
|
||||||
UpdateRecentFiles();
|
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() {
|
void GMainWindow::InitializeHotkeys() {
|
||||||
hotkey_registry.LoadHotkeys();
|
hotkey_registry.LoadHotkeys();
|
||||||
|
|
||||||
|
@ -607,8 +635,6 @@ void GMainWindow::ConnectMenuEvents() {
|
||||||
&GMainWindow::OnMenuReportCompatibility);
|
&GMainWindow::OnMenuReportCompatibility);
|
||||||
connect(ui.action_Configure, &QAction::triggered, this, &GMainWindow::OnConfigure);
|
connect(ui.action_Configure, &QAction::triggered, this, &GMainWindow::OnConfigure);
|
||||||
connect(ui.action_Cheats, &QAction::triggered, this, &GMainWindow::OnCheats);
|
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
|
// View
|
||||||
connect(ui.action_Single_Window_Mode, &QAction::triggered, this,
|
connect(ui.action_Single_Window_Mode, &QAction::triggered, this,
|
||||||
|
@ -1036,8 +1062,6 @@ void GMainWindow::ShutdownGame() {
|
||||||
ui.action_Stop->setEnabled(false);
|
ui.action_Stop->setEnabled(false);
|
||||||
ui.action_Restart->setEnabled(false);
|
ui.action_Restart->setEnabled(false);
|
||||||
ui.action_Cheats->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_Load_Amiibo->setEnabled(false);
|
||||||
ui.action_Remove_Amiibo->setEnabled(false);
|
ui.action_Remove_Amiibo->setEnabled(false);
|
||||||
ui.action_Report_Compatibility->setEnabled(false);
|
ui.action_Report_Compatibility->setEnabled(false);
|
||||||
|
@ -1061,6 +1085,8 @@ void GMainWindow::ShutdownGame() {
|
||||||
game_fps_label->setVisible(false);
|
game_fps_label->setVisible(false);
|
||||||
emu_frametime_label->setVisible(false);
|
emu_frametime_label->setVisible(false);
|
||||||
|
|
||||||
|
UpdateSaveStates();
|
||||||
|
|
||||||
emulation_running = false;
|
emulation_running = false;
|
||||||
|
|
||||||
if (defer_update_prompt) {
|
if (defer_update_prompt) {
|
||||||
|
@ -1107,6 +1133,62 @@ void GMainWindow::UpdateRecentFiles() {
|
||||||
ui.menu_recent_files->setEnabled(num_recent_files != 0);
|
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) {
|
void GMainWindow::OnGameListLoadFile(QString game_path) {
|
||||||
BootGame(game_path);
|
BootGame(game_path);
|
||||||
}
|
}
|
||||||
|
@ -1348,14 +1430,14 @@ void GMainWindow::OnStartGame() {
|
||||||
ui.action_Stop->setEnabled(true);
|
ui.action_Stop->setEnabled(true);
|
||||||
ui.action_Restart->setEnabled(true);
|
ui.action_Restart->setEnabled(true);
|
||||||
ui.action_Cheats->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_Load_Amiibo->setEnabled(true);
|
||||||
ui.action_Report_Compatibility->setEnabled(true);
|
ui.action_Report_Compatibility->setEnabled(true);
|
||||||
ui.action_Enable_Frame_Advancing->setEnabled(true);
|
ui.action_Enable_Frame_Advancing->setEnabled(true);
|
||||||
ui.action_Capture_Screenshot->setEnabled(true);
|
ui.action_Capture_Screenshot->setEnabled(true);
|
||||||
|
|
||||||
discord_rpc->Update();
|
discord_rpc->Update();
|
||||||
|
|
||||||
|
UpdateSaveStates();
|
||||||
}
|
}
|
||||||
|
|
||||||
void GMainWindow::OnPauseGame() {
|
void GMainWindow::OnPauseGame() {
|
||||||
|
@ -1503,14 +1585,19 @@ void GMainWindow::OnCheats() {
|
||||||
cheat_dialog.exec();
|
cheat_dialog.exec();
|
||||||
}
|
}
|
||||||
|
|
||||||
void GMainWindow::OnSave() {
|
void GMainWindow::OnSaveState() {
|
||||||
Core::System::GetInstance().SendSignal(Core::System::Signal::Save);
|
QAction* action = qobject_cast<QAction*>(sender());
|
||||||
|
assert(action);
|
||||||
|
|
||||||
|
Core::System::GetInstance().SendSignal(Core::System::Signal::Save, action->data().toUInt());
|
||||||
|
UpdateSaveStates();
|
||||||
}
|
}
|
||||||
|
|
||||||
void GMainWindow::OnLoad() {
|
void GMainWindow::OnLoadState() {
|
||||||
if (QFileInfo("save0.citrasave").exists()) {
|
QAction* action = qobject_cast<QAction*>(sender());
|
||||||
Core::System::GetInstance().SendSignal(Core::System::Signal::Load);
|
assert(action);
|
||||||
}
|
|
||||||
|
Core::System::GetInstance().SendSignal(Core::System::Signal::Load, action->data().toUInt());
|
||||||
}
|
}
|
||||||
|
|
||||||
void GMainWindow::OnConfigure() {
|
void GMainWindow::OnConfigure() {
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <QLabel>
|
#include <QLabel>
|
||||||
#include <QMainWindow>
|
#include <QMainWindow>
|
||||||
|
@ -14,6 +15,7 @@
|
||||||
#include "common/announce_multiplayer_room.h"
|
#include "common/announce_multiplayer_room.h"
|
||||||
#include "core/core.h"
|
#include "core/core.h"
|
||||||
#include "core/hle/service/am/am.h"
|
#include "core/hle/service/am/am.h"
|
||||||
|
#include "core/savestate.h"
|
||||||
#include "ui_main.h"
|
#include "ui_main.h"
|
||||||
|
|
||||||
class AboutDialog;
|
class AboutDialog;
|
||||||
|
@ -106,6 +108,7 @@ private:
|
||||||
void InitializeWidgets();
|
void InitializeWidgets();
|
||||||
void InitializeDebugWidgets();
|
void InitializeDebugWidgets();
|
||||||
void InitializeRecentFileMenuActions();
|
void InitializeRecentFileMenuActions();
|
||||||
|
void InitializeSaveStateMenuActions();
|
||||||
|
|
||||||
void SetDefaultUIGeometry();
|
void SetDefaultUIGeometry();
|
||||||
void SyncMenuUISettings();
|
void SyncMenuUISettings();
|
||||||
|
@ -149,6 +152,8 @@ private:
|
||||||
*/
|
*/
|
||||||
void UpdateRecentFiles();
|
void UpdateRecentFiles();
|
||||||
|
|
||||||
|
void UpdateSaveStates();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If the emulation is running,
|
* If the emulation is running,
|
||||||
* asks the user if he really want to close the emulator
|
* asks the user if he really want to close the emulator
|
||||||
|
@ -163,8 +168,8 @@ private slots:
|
||||||
void OnStartGame();
|
void OnStartGame();
|
||||||
void OnPauseGame();
|
void OnPauseGame();
|
||||||
void OnStopGame();
|
void OnStopGame();
|
||||||
void OnSave();
|
void OnSaveState();
|
||||||
void OnLoad();
|
void OnLoadState();
|
||||||
void OnMenuReportCompatibility();
|
void OnMenuReportCompatibility();
|
||||||
/// Called whenever a user selects a game in the game list widget.
|
/// Called whenever a user selects a game in the game list widget.
|
||||||
void OnGameListLoadFile(QString game_path);
|
void OnGameListLoadFile(QString game_path);
|
||||||
|
@ -276,6 +281,13 @@ private:
|
||||||
bool defer_update_prompt = false;
|
bool defer_update_prompt = false;
|
||||||
|
|
||||||
QAction* actions_recent_files[max_recent_files_item];
|
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;
|
QTranslator translator;
|
||||||
|
|
||||||
|
|
|
@ -79,17 +79,32 @@
|
||||||
<property name="title">
|
<property name="title">
|
||||||
<string>&Emulation</string>
|
<string>&Emulation</string>
|
||||||
</property>
|
</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_Start"/>
|
||||||
<addaction name="action_Pause"/>
|
<addaction name="action_Pause"/>
|
||||||
<addaction name="action_Stop"/>
|
<addaction name="action_Stop"/>
|
||||||
<addaction name="action_Restart"/>
|
<addaction name="action_Restart"/>
|
||||||
<addaction name="separator"/>
|
<addaction name="separator"/>
|
||||||
|
<addaction name="menu_Load_State"/>
|
||||||
|
<addaction name="menu_Save_State"/>
|
||||||
|
<addaction name="separator"/>
|
||||||
<addaction name="action_Report_Compatibility"/>
|
<addaction name="action_Report_Compatibility"/>
|
||||||
<addaction name="separator"/>
|
<addaction name="separator"/>
|
||||||
<addaction name="action_Configure"/>
|
<addaction name="action_Configure"/>
|
||||||
<addaction name="action_Cheats"/>
|
<addaction name="action_Cheats"/>
|
||||||
<addaction name="action_Save"/>
|
|
||||||
<addaction name="action_Load"/>
|
|
||||||
</widget>
|
</widget>
|
||||||
<widget class="QMenu" name="menu_View">
|
<widget class="QMenu" name="menu_View">
|
||||||
<property name="title">
|
<property name="title">
|
||||||
|
@ -253,6 +268,16 @@
|
||||||
<string>Single Window Mode</string>
|
<string>Single Window Mode</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</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">
|
<action name="action_Configure">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Configure...</string>
|
<string>Configure...</string>
|
||||||
|
|
|
@ -47,6 +47,7 @@
|
||||||
#define DUMP_DIR "dump"
|
#define DUMP_DIR "dump"
|
||||||
#define LOAD_DIR "load"
|
#define LOAD_DIR "load"
|
||||||
#define SHADER_DIR "shaders"
|
#define SHADER_DIR "shaders"
|
||||||
|
#define STATES_DIR "states"
|
||||||
|
|
||||||
// Filenames
|
// Filenames
|
||||||
// Files in the directory returned by GetUserPath(UserPath::LogDir)
|
// 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::ShaderDir, user_path + SHADER_DIR DIR_SEP);
|
||||||
g_paths.emplace(UserPath::DumpDir, user_path + DUMP_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::LoadDir, user_path + LOAD_DIR DIR_SEP);
|
||||||
|
g_paths.emplace(UserPath::StatesDir, user_path + STATES_DIR DIR_SEP);
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::string& GetUserPath(UserPath path) {
|
const std::string& GetUserPath(UserPath path) {
|
||||||
|
|
|
@ -36,6 +36,7 @@ enum class UserPath {
|
||||||
RootDir,
|
RootDir,
|
||||||
SDMCDir,
|
SDMCDir,
|
||||||
ShaderDir,
|
ShaderDir,
|
||||||
|
StatesDir,
|
||||||
SysDataDir,
|
SysDataDir,
|
||||||
UserDir,
|
UserDir,
|
||||||
};
|
};
|
||||||
|
|
|
@ -447,6 +447,8 @@ add_library(core STATIC
|
||||||
rpc/server.h
|
rpc/server.h
|
||||||
rpc/udp_server.cpp
|
rpc/udp_server.cpp
|
||||||
rpc/udp_server.h
|
rpc/udp_server.h
|
||||||
|
savestate.cpp
|
||||||
|
savestate.h
|
||||||
settings.cpp
|
settings.cpp
|
||||||
settings.h
|
settings.h
|
||||||
telemetry_session.cpp
|
telemetry_session.cpp
|
||||||
|
|
|
@ -10,10 +10,8 @@
|
||||||
#include "audio_core/dsp_interface.h"
|
#include "audio_core/dsp_interface.h"
|
||||||
#include "audio_core/hle/hle.h"
|
#include "audio_core/hle/hle.h"
|
||||||
#include "audio_core/lle/lle.h"
|
#include "audio_core/lle/lle.h"
|
||||||
#include "common/archives.h"
|
|
||||||
#include "common/logging/log.h"
|
#include "common/logging/log.h"
|
||||||
#include "common/texture.h"
|
#include "common/texture.h"
|
||||||
#include "common/zstd_compression.h"
|
|
||||||
#include "core/arm/arm_interface.h"
|
#include "core/arm/arm_interface.h"
|
||||||
#ifdef ARCHITECTURE_x86_64
|
#ifdef ARCHITECTURE_x86_64
|
||||||
#include "core/arm/dynarmic/arm_dynarmic.h"
|
#include "core/arm/dynarmic/arm_dynarmic.h"
|
||||||
|
@ -63,6 +61,8 @@ Kernel::KernelSystem& Global() {
|
||||||
return System::GetInstance().Kernel();
|
return System::GetInstance().Kernel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
System::~System() = default;
|
||||||
|
|
||||||
System::ResultStatus System::RunLoop(bool tight_loop) {
|
System::ResultStatus System::RunLoop(bool tight_loop) {
|
||||||
status = ResultStatus::Success;
|
status = ResultStatus::Success;
|
||||||
if (!cpu_core) {
|
if (!cpu_core) {
|
||||||
|
@ -106,7 +106,16 @@ System::ResultStatus System::RunLoop(bool tight_loop) {
|
||||||
HW::Update();
|
HW::Update();
|
||||||
Reschedule();
|
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) {
|
switch (signal) {
|
||||||
case Signal::Reset:
|
case Signal::Reset:
|
||||||
Reset();
|
Reset();
|
||||||
|
@ -116,14 +125,16 @@ System::ResultStatus System::RunLoop(bool tight_loop) {
|
||||||
break;
|
break;
|
||||||
case Signal::Load: {
|
case Signal::Load: {
|
||||||
LOG_INFO(Core, "Begin load");
|
LOG_INFO(Core, "Begin load");
|
||||||
auto stream = std::ifstream("save0.citrasave", std::fstream::binary);
|
System::LoadState(param);
|
||||||
System::Load(stream, FileUtil::GetSize("save0.citrasave"));
|
// auto stream = std::ifstream("save0.citrasave", std::fstream::binary);
|
||||||
|
// System::Load(stream, FileUtil::GetSize("save0.citrasave"));
|
||||||
LOG_INFO(Core, "Load completed");
|
LOG_INFO(Core, "Load completed");
|
||||||
} break;
|
} break;
|
||||||
case Signal::Save: {
|
case Signal::Save: {
|
||||||
LOG_INFO(Core, "Begin save");
|
LOG_INFO(Core, "Begin save");
|
||||||
auto stream = std::ofstream("save0.citrasave", std::fstream::binary);
|
System::SaveState(param);
|
||||||
System::Save(stream);
|
// auto stream = std::ofstream("save0.citrasave", std::fstream::binary);
|
||||||
|
// System::Save(stream);
|
||||||
LOG_INFO(Core, "Save completed");
|
LOG_INFO(Core, "Save completed");
|
||||||
} break;
|
} break;
|
||||||
default:
|
default:
|
||||||
|
@ -133,12 +144,14 @@ System::ResultStatus System::RunLoop(bool tight_loop) {
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool System::SendSignal(System::Signal signal) {
|
bool System::SendSignal(System::Signal signal, u32 param) {
|
||||||
auto prev = System::Signal::None;
|
std::lock_guard lock{signal_mutex};
|
||||||
if (!current_signal.compare_exchange_strong(prev, signal)) {
|
if (current_signal != signal && current_signal != Signal::None) {
|
||||||
LOG_ERROR(Core, "Unable to {} as {} is ongoing", signal, prev);
|
LOG_ERROR(Core, "Unable to {} as {} is ongoing", signal, current_signal);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
current_signal = signal;
|
||||||
|
signal_param = param;
|
||||||
return true;
|
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);
|
cheat_engine = std::make_unique<Cheats::CheatEngine>(*this);
|
||||||
u64 title_id{0};
|
title_id = 0;
|
||||||
if (app_loader->ReadProgramId(title_id) != Loader::ResultStatus::Success) {
|
if (app_loader->ReadProgramId(title_id) != Loader::ResultStatus::Success) {
|
||||||
LOG_ERROR(Core, "Failed to find title id for ROM (Error {})",
|
LOG_ERROR(Core, "Failed to find title id for ROM (Error {})",
|
||||||
static_cast<u32>(load_result));
|
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>();
|
timing = std::make_unique<Timing>();
|
||||||
|
|
||||||
kernel = std::make_unique<Kernel::KernelSystem>(
|
kernel = std::make_unique<Kernel::KernelSystem>(*memory, *timing,
|
||||||
*memory, *timing, [this] { PrepareReschedule(); }, system_mode);
|
[this] { PrepareReschedule(); }, system_mode);
|
||||||
|
|
||||||
if (Settings::values.use_cpu_jit) {
|
if (Settings::values.use_cpu_jit) {
|
||||||
#ifdef ARCHITECTURE_x86_64
|
#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 {
|
SERIALIZE_IMPL(System)
|
||||||
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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Core
|
} // namespace Core
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include "boost/serialization/access.hpp"
|
#include "boost/serialization/access.hpp"
|
||||||
#include "common/common_types.h"
|
#include "common/common_types.h"
|
||||||
|
@ -92,6 +93,8 @@ public:
|
||||||
ErrorUnknown ///< Any other error
|
ErrorUnknown ///< Any other error
|
||||||
};
|
};
|
||||||
|
|
||||||
|
~System();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run the core CPU loop
|
* Run the core CPU loop
|
||||||
* This function runs the core for the specified number of CPU instructions before trying to
|
* 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 };
|
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
|
/// Request reset of the system
|
||||||
void RequestReset() {
|
void RequestReset() {
|
||||||
|
@ -276,9 +279,9 @@ public:
|
||||||
return registered_image_interface;
|
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:
|
private:
|
||||||
/**
|
/**
|
||||||
|
@ -344,8 +347,11 @@ private:
|
||||||
/// Saved variables for reset
|
/// Saved variables for reset
|
||||||
Frontend::EmuWindow* m_emu_window;
|
Frontend::EmuWindow* m_emu_window;
|
||||||
std::string m_filepath;
|
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;
|
friend class boost::serialization::access;
|
||||||
template <typename Archive>
|
template <typename Archive>
|
||||||
|
|
|
@ -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
|
|
@ -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
|
Reference in New Issue