diff --git a/src/citra_qt/main.cpp b/src/citra_qt/main.cpp
index 7fe4936f5..de66e14ac 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 1858d5988..ebe1a013a 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 c0c38e4c8..2eff98083 100644
--- a/src/citra_qt/main.ui
+++ b/src/citra_qt/main.ui
@@ -79,17 +79,32 @@
     <property name="title">
      <string>&amp;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 13e71615e..eec4dde9c 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 c5da82973..cd3f4e102 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 0368d3665..8af5a2a61 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 c6908c59a..2b02ffc41 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 0bbb79aea..f17474a85 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 80c1505b5..0ce6924cd 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 000000000..d52789b2c
--- /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 000000000..f67bee22f
--- /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