mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-07-11 01:25:07 +02:00
Extract the rewind code from SaveState.cpp, to make it more managable.
This commit is contained in:
@@ -2475,6 +2475,8 @@ add_library(${CoreLibName} ${CoreLinkType}
|
||||
Core/Replay.h
|
||||
Core/SaveState.cpp
|
||||
Core/SaveState.h
|
||||
Core/SaveStateRewind.cpp
|
||||
Core/SaveStateRewind.h
|
||||
Core/Screenshot.cpp
|
||||
Core/Screenshot.h
|
||||
Core/System.cpp
|
||||
|
||||
@@ -517,7 +517,7 @@ bool IsDirectory(const Path &path) {
|
||||
|
||||
// Deletes a given filename, return true on success
|
||||
// Doesn't supports deleting a directory
|
||||
bool Delete(const Path &filename) {
|
||||
bool Delete(const Path &filename, bool quiet) {
|
||||
if (SIMULATE_SLOW_IO) {
|
||||
sleep_ms(200, "slow-io-sim");
|
||||
}
|
||||
@@ -533,7 +533,9 @@ bool Delete(const Path &filename) {
|
||||
// Return true because we care about the file no
|
||||
// being there, not the actual delete.
|
||||
if (!Exists(filename)) {
|
||||
WARN_LOG(Log::IO, "Delete: '%s' already does not exist", filename.c_str());
|
||||
if (!quiet) {
|
||||
WARN_LOG(Log::IO, "Delete: '%s' already does not exist", filename.c_str());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ bool CreateFullPath(const Path &fullPath);
|
||||
|
||||
// Deletes a given file by name, return true on success
|
||||
// Doesn't support deleting a directory (although it will work on some platforms - ideally shouldn't)
|
||||
bool Delete(const Path &filename);
|
||||
bool Delete(const Path &filename, bool quiet = false);
|
||||
|
||||
// Deletes a directory by name, returns true on success
|
||||
// Directory must be empty.
|
||||
|
||||
@@ -926,6 +926,7 @@
|
||||
<ClCompile Include="RetroAchievements.cpp" />
|
||||
<ClCompile Include="SaveState.cpp" />
|
||||
<ClCompile Include="MIPS\MIPSStackWalk.cpp" />
|
||||
<ClCompile Include="SaveStateRewind.cpp" />
|
||||
<ClCompile Include="Screenshot.cpp" />
|
||||
<ClCompile Include="System.cpp" />
|
||||
<ClCompile Include="TiltEventProcessor.cpp" />
|
||||
@@ -1295,6 +1296,7 @@
|
||||
<ClInclude Include="RetroAchievements.h" />
|
||||
<ClInclude Include="SaveState.h" />
|
||||
<ClInclude Include="MIPS\MIPSStackWalk.h" />
|
||||
<ClInclude Include="SaveStateRewind.h" />
|
||||
<ClInclude Include="Screenshot.h" />
|
||||
<ClInclude Include="System.h" />
|
||||
<ClInclude Include="TiltEventProcessor.h" />
|
||||
|
||||
@@ -1396,6 +1396,9 @@
|
||||
<ClCompile Include="Util\PathUtil.cpp">
|
||||
<Filter>Util</Filter>
|
||||
</ClCompile>
|
||||
<ClCompile Include="SaveStateRewind.cpp">
|
||||
<Filter>Core</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="ELF\ElfReader.h">
|
||||
@@ -2256,6 +2259,9 @@
|
||||
<ClInclude Include="Util\PathUtil.h">
|
||||
<Filter>Util</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="SaveStateRewind.h">
|
||||
<Filter>Core</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="..\LICENSE.TXT" />
|
||||
|
||||
+61
-291
@@ -37,6 +37,7 @@
|
||||
#include "Core/CoreTiming.h"
|
||||
#include "Core/Screenshot.h"
|
||||
#include "Core/System.h"
|
||||
#include "Core/SaveStateRewind.h"
|
||||
#include "Core/FileSystems/MetaFileSystem.h"
|
||||
#include "Core/ELF/ParamSFO.h"
|
||||
#include "Core/HLE/HLE.h"
|
||||
@@ -46,11 +47,9 @@
|
||||
#include "Core/HLE/sceKernel.h"
|
||||
#include "Core/HLE/sceUtility.h"
|
||||
#include "Core/MemMap.h"
|
||||
#include "Core/MIPS/MIPS.h"
|
||||
#include "Core/MIPS/JitCommon/JitBlockCache.h"
|
||||
#include "Core/RetroAchievements.h"
|
||||
#include "HW/MemoryStick.h"
|
||||
#include "GPU/GPUState.h"
|
||||
|
||||
#ifndef MOBILE_DEVICE
|
||||
#include "Core/AVIDump.h"
|
||||
@@ -59,24 +58,42 @@
|
||||
|
||||
// Slot number is visual only, -2 will display special message
|
||||
constexpr int LOAD_UNDO_SLOT = -2;
|
||||
constexpr int SCREENSHOT_FAILURE_RETRIES = 6;
|
||||
|
||||
static const char * const STATE_EXTENSION = "ppst";
|
||||
static const char * const UNDO_STATE_EXTENSION = "undo.ppst";
|
||||
static const char * const UNDO_SCREENSHOT_EXTENSION = "undo.jpg";
|
||||
|
||||
static const char * const LOAD_UNDO_NAME = "load_undo.ppst";
|
||||
|
||||
namespace SaveState {
|
||||
|
||||
double g_lastSaveTime = -1.0;
|
||||
static bool needsProcess = false;
|
||||
static bool needsRestart = false;
|
||||
static std::mutex mutex;
|
||||
static int screenshotFailures = 0;
|
||||
static bool hasLoadedState = false;
|
||||
static const int STALE_STATE_USES = 2;
|
||||
// 4 hours of total gameplay since the virtual PSP started the game.
|
||||
static const u64 STALE_STATE_TIME = 4 * 3600 * 1000000ULL;
|
||||
static int saveStateGeneration = 0;
|
||||
static int saveDataGeneration = 0;
|
||||
static int lastSaveDataGeneration = 0;
|
||||
static std::string saveStateInitialGitVersion = "";
|
||||
static StateRingbuffer rewindStates;
|
||||
|
||||
struct SaveStart
|
||||
{
|
||||
void DoState(PointerWrap &p);
|
||||
};
|
||||
struct SaveStart {
|
||||
void DoState(PointerWrap &p);
|
||||
};
|
||||
|
||||
enum OperationType
|
||||
{
|
||||
SAVESTATE_SAVE,
|
||||
SAVESTATE_LOAD,
|
||||
SAVESTATE_VERIFY,
|
||||
SAVESTATE_REWIND,
|
||||
SAVESTATE_SAVE_SCREENSHOT,
|
||||
};
|
||||
enum class OperationType {
|
||||
Save,
|
||||
Load,
|
||||
Verify,
|
||||
Rewind,
|
||||
SaveScreenshot,
|
||||
};
|
||||
|
||||
struct Operation {
|
||||
// The slot number is for visual purposes only. Set to -1 for operations where we don't display a message for example.
|
||||
@@ -89,6 +106,8 @@ double g_lastSaveTime = -1.0;
|
||||
int slot;
|
||||
};
|
||||
|
||||
static std::vector<Operation> pending;
|
||||
|
||||
CChunkFileReader::Error SaveToRam(std::vector<u8> &data) {
|
||||
SaveStart state;
|
||||
return CChunkFileReader::MeasureAndSavePtr(state, &data);
|
||||
@@ -99,248 +118,12 @@ double g_lastSaveTime = -1.0;
|
||||
return CChunkFileReader::LoadPtr(&data[0], state, errorString);
|
||||
}
|
||||
|
||||
// This ring buffer of states is for rewind save states, which are kept in RAM.
|
||||
// Save states are compressed against one of two reference saves (bases_), and the reference
|
||||
// is switched to a fresh save every N saves, where N is BASE_USAGE_INTERVAL.
|
||||
// The compression is a simple block based scheme where 0 means to copy a block from the base,
|
||||
// and 1 means that the following bytes are the next block. See Compress/LockedDecompress.
|
||||
class StateRingbuffer {
|
||||
public:
|
||||
StateRingbuffer() {
|
||||
size_ = REWIND_NUM_STATES;
|
||||
states_.resize(size_);
|
||||
baseMapping_.resize(size_);
|
||||
}
|
||||
|
||||
~StateRingbuffer() {
|
||||
if (compressThread_.joinable()) {
|
||||
compressThread_.join();
|
||||
}
|
||||
}
|
||||
|
||||
CChunkFileReader::Error Save()
|
||||
{
|
||||
rewindLastTime_ = time_now_d();
|
||||
|
||||
// Make sure we're not processing a previous save. That'll cause a hitch though, but at least won't
|
||||
// crash due to contention over buffer_.
|
||||
if (compressThread_.joinable())
|
||||
compressThread_.join();
|
||||
|
||||
std::lock_guard<std::mutex> guard(lock_);
|
||||
|
||||
int n = next_++ % size_;
|
||||
if ((next_ % size_) == first_)
|
||||
++first_;
|
||||
|
||||
std::vector<u8> *compressBuffer = &buffer_;
|
||||
CChunkFileReader::Error err;
|
||||
|
||||
if (base_ == -1 || ++baseUsage_ > BASE_USAGE_INTERVAL)
|
||||
{
|
||||
base_ = (base_ + 1) % ARRAY_SIZE(bases_);
|
||||
baseUsage_ = 0;
|
||||
err = SaveToRam(bases_[base_]);
|
||||
// Let's not bother savestating twice.
|
||||
compressBuffer = &bases_[base_];
|
||||
}
|
||||
else
|
||||
err = SaveToRam(buffer_);
|
||||
|
||||
if (err == CChunkFileReader::ERROR_NONE)
|
||||
ScheduleCompress(&states_[n], compressBuffer, &bases_[base_]);
|
||||
else
|
||||
states_[n].clear();
|
||||
|
||||
baseMapping_[n] = base_;
|
||||
return err;
|
||||
}
|
||||
|
||||
CChunkFileReader::Error Restore(std::string *errorString)
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(lock_);
|
||||
|
||||
// No valid states left.
|
||||
if (Empty())
|
||||
return CChunkFileReader::ERROR_BAD_FILE;
|
||||
|
||||
int n = (--next_ + size_) % size_;
|
||||
if (states_[n].empty())
|
||||
return CChunkFileReader::ERROR_BAD_FILE;
|
||||
|
||||
static std::vector<u8> buffer;
|
||||
LockedDecompress(buffer, states_[n], bases_[baseMapping_[n]]);
|
||||
CChunkFileReader::Error error = LoadFromRam(buffer, errorString);
|
||||
rewindLastTime_ = time_now_d();
|
||||
return error;
|
||||
}
|
||||
|
||||
void ScheduleCompress(std::vector<u8> *result, const std::vector<u8> *state, const std::vector<u8> *base)
|
||||
{
|
||||
if (compressThread_.joinable())
|
||||
compressThread_.join();
|
||||
compressThread_ = std::thread([=]{
|
||||
SetCurrentThreadName("SaveStateCompress");
|
||||
|
||||
// Should do no I/O, so no JNI thread context needed.
|
||||
Compress(*result, *state, *base);
|
||||
});
|
||||
}
|
||||
|
||||
void Compress(std::vector<u8> &result, const std::vector<u8> &state, const std::vector<u8> &base)
|
||||
{
|
||||
std::lock_guard<std::mutex> guard(lock_);
|
||||
// Bail if we were cleared before locking.
|
||||
if (first_ == 0 && next_ == 0)
|
||||
return;
|
||||
|
||||
double start_time = time_now_d();
|
||||
result.clear();
|
||||
result.reserve(512 * 1024);
|
||||
for (size_t i = 0; i < state.size(); i += BLOCK_SIZE)
|
||||
{
|
||||
int blockSize = std::min(BLOCK_SIZE, (int)(state.size() - i));
|
||||
if (i + blockSize > base.size() || memcmp(&state[i], &base[i], blockSize) != 0)
|
||||
{
|
||||
result.push_back(1);
|
||||
result.insert(result.end(), state.begin() + i, state.begin() + i + blockSize);
|
||||
}
|
||||
else
|
||||
result.push_back(0);
|
||||
}
|
||||
|
||||
double taken_s = time_now_d() - start_time;
|
||||
DEBUG_LOG(Log::SaveState, "Rewind: Compressed save from %d bytes to %d in %0.2f ms.", (int)state.size(), (int)result.size(), taken_s * 1000.0);
|
||||
}
|
||||
|
||||
void LockedDecompress(std::vector<u8> &result, const std::vector<u8> &compressed, const std::vector<u8> &base)
|
||||
{
|
||||
result.clear();
|
||||
result.reserve(base.size());
|
||||
auto basePos = base.begin();
|
||||
for (size_t i = 0; i < compressed.size(); )
|
||||
{
|
||||
if (compressed[i] == 0)
|
||||
{
|
||||
++i;
|
||||
int blockSize = std::min(BLOCK_SIZE, (int)(base.size() - result.size()));
|
||||
result.insert(result.end(), basePos, basePos + blockSize);
|
||||
basePos += blockSize;
|
||||
}
|
||||
else
|
||||
{
|
||||
++i;
|
||||
int blockSize = std::min(BLOCK_SIZE, (int)(compressed.size() - i));
|
||||
result.insert(result.end(), compressed.begin() + i, compressed.begin() + i + blockSize);
|
||||
i += blockSize;
|
||||
// This check is to avoid advancing basePos out of range, which MSVC catches.
|
||||
// When this happens, we're at the end of decoding anyway.
|
||||
if (base.end() - basePos >= blockSize) {
|
||||
basePos += blockSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Clear()
|
||||
{
|
||||
if (compressThread_.joinable())
|
||||
compressThread_.join();
|
||||
|
||||
// This lock is mainly for shutdown.
|
||||
std::lock_guard<std::mutex> guard(lock_);
|
||||
first_ = 0;
|
||||
next_ = 0;
|
||||
for (auto &b : bases_) {
|
||||
b.clear();
|
||||
}
|
||||
baseMapping_.clear();
|
||||
baseMapping_.resize(size_);
|
||||
for (auto &s : states_) {
|
||||
s.clear();
|
||||
}
|
||||
buffer_.clear();
|
||||
base_ = -1;
|
||||
baseUsage_ = 0;
|
||||
rewindLastTime_ = time_now_d();
|
||||
}
|
||||
|
||||
bool Empty() const
|
||||
{
|
||||
return next_ == first_;
|
||||
}
|
||||
|
||||
void Process() {
|
||||
if (g_Config.iRewindSnapshotInterval <= 0) {
|
||||
return;
|
||||
}
|
||||
if (coreState != CORE_RUNNING_CPU) {
|
||||
return;
|
||||
}
|
||||
|
||||
// For fast-forwarding, otherwise they may be useless and too close.
|
||||
double now = time_now_d();
|
||||
double diff = now - rewindLastTime_;
|
||||
if (diff < g_Config.iRewindSnapshotInterval)
|
||||
return;
|
||||
|
||||
DEBUG_LOG(Log::SaveState, "Saving rewind state");
|
||||
Save();
|
||||
}
|
||||
|
||||
void NotifyState() {
|
||||
// Prevent saving snapshots immediately after loading or saving a state.
|
||||
rewindLastTime_ = time_now_d();
|
||||
}
|
||||
|
||||
private:
|
||||
const int BLOCK_SIZE = 8192;
|
||||
const int REWIND_NUM_STATES = 20;
|
||||
// TODO: Instead, based on size of compressed state?
|
||||
const int BASE_USAGE_INTERVAL = 15;
|
||||
|
||||
typedef std::vector<u8> StateBuffer;
|
||||
|
||||
int first_ = 0;
|
||||
int next_ = 0;
|
||||
int size_;
|
||||
|
||||
std::vector<StateBuffer> states_;
|
||||
StateBuffer bases_[2];
|
||||
std::vector<int> baseMapping_;
|
||||
std::mutex lock_;
|
||||
std::thread compressThread_;
|
||||
std::vector<u8> buffer_;
|
||||
|
||||
int base_ = -1;
|
||||
int baseUsage_ = 0;
|
||||
|
||||
double rewindLastTime_ = 0.0f;
|
||||
};
|
||||
|
||||
static bool needsProcess = false;
|
||||
static bool needsRestart = false;
|
||||
static std::vector<Operation> pending;
|
||||
static std::mutex mutex;
|
||||
static int screenshotFailures = 0;
|
||||
static bool hasLoadedState = false;
|
||||
static const int STALE_STATE_USES = 2;
|
||||
// 4 hours of total gameplay since the virtual PSP started the game.
|
||||
static const u64 STALE_STATE_TIME = 4 * 3600 * 1000000ULL;
|
||||
static int saveStateGeneration = 0;
|
||||
static int saveDataGeneration = 0;
|
||||
static int lastSaveDataGeneration = 0;
|
||||
static std::string saveStateInitialGitVersion = "";
|
||||
|
||||
// TODO: Should this be configurable?
|
||||
// Should only fail if the game hasn't created a framebuffer yet. Some games play video without a framebuffer,
|
||||
// we should probably just read back memory then instead (GTA for example). But this is a really unimportant edge case,
|
||||
// when in-game it's just not an issue.
|
||||
static const int SCREENSHOT_FAILURE_RETRIES = 6;
|
||||
static StateRingbuffer rewindStates;
|
||||
|
||||
void SaveStart::DoState(PointerWrap &p)
|
||||
{
|
||||
void SaveStart::DoState(PointerWrap &p) {
|
||||
auto s = p.Section("SaveStart", 1, 3);
|
||||
if (!s)
|
||||
return;
|
||||
@@ -404,13 +187,12 @@ double g_lastSaveTime = -1.0;
|
||||
pspFileSystem.DoState(p);
|
||||
}
|
||||
|
||||
void Enqueue(const SaveState::Operation &op)
|
||||
{
|
||||
void Enqueue(const SaveState::Operation &op) {
|
||||
if (!NetworkAllowSaveState()) {
|
||||
return;
|
||||
}
|
||||
if (Achievements::HardcoreModeActive()) {
|
||||
if (g_Config.bAchievementsSaveStateInHardcoreMode && ((op.type == SaveState::SAVESTATE_SAVE) || (op.type == SAVESTATE_SAVE_SCREENSHOT))) {
|
||||
if (g_Config.bAchievementsSaveStateInHardcoreMode && ((op.type == SaveState::OperationType::Save) || (op.type == OperationType::SaveScreenshot))) {
|
||||
// We allow saving in hardcore mode if this setting is on.
|
||||
} else {
|
||||
// Operation not allowed
|
||||
@@ -434,7 +216,7 @@ double g_lastSaveTime = -1.0;
|
||||
rewindStates.NotifyState();
|
||||
if (coreState == CoreState::CORE_RUNTIME_ERROR)
|
||||
Core_Break(BreakReason::SavestateLoad, 0);
|
||||
Enqueue(Operation(SAVESTATE_LOAD, filename, slot, callback));
|
||||
Enqueue(Operation(OperationType::Load, filename, slot, callback));
|
||||
}
|
||||
|
||||
void Save(const Path &filename, int slot, Callback callback) {
|
||||
@@ -445,11 +227,11 @@ double g_lastSaveTime = -1.0;
|
||||
rewindStates.NotifyState();
|
||||
if (coreState == CoreState::CORE_RUNTIME_ERROR)
|
||||
Core_Break(BreakReason::SavestateSave, 0);
|
||||
Enqueue(Operation(SAVESTATE_SAVE, filename, slot, callback));
|
||||
Enqueue(Operation(OperationType::Save, filename, slot, callback));
|
||||
}
|
||||
|
||||
void Verify(Callback callback) {
|
||||
Enqueue(Operation(SAVESTATE_VERIFY, Path(), -1, callback));
|
||||
Enqueue(Operation(OperationType::Verify, Path(), -1, callback));
|
||||
}
|
||||
|
||||
void Rewind(Callback callback) {
|
||||
@@ -458,12 +240,12 @@ double g_lastSaveTime = -1.0;
|
||||
}
|
||||
if (coreState == CoreState::CORE_RUNTIME_ERROR)
|
||||
Core_Break(BreakReason::SavestateRewind, 0);
|
||||
Enqueue(Operation(SAVESTATE_REWIND, Path(), -1, callback));
|
||||
Enqueue(Operation(OperationType::Rewind, Path(), -1, callback));
|
||||
}
|
||||
|
||||
static void SaveScreenshot(const Path &filename) {
|
||||
screenshotFailures = 0;
|
||||
Enqueue(Operation(SAVESTATE_SAVE_SCREENSHOT, filename, -1, nullptr));
|
||||
Enqueue(Operation(OperationType::SaveScreenshot, filename, -1, nullptr));
|
||||
}
|
||||
|
||||
bool CanRewind() {
|
||||
@@ -547,26 +329,21 @@ double g_lastSaveTime = -1.0;
|
||||
return GetSysDirectory(DIRECTORY_SAVESTATE) / filename;
|
||||
}
|
||||
|
||||
int GetCurrentSlot()
|
||||
{
|
||||
int GetCurrentSlot() {
|
||||
return g_Config.iCurrentStateSlot;
|
||||
}
|
||||
|
||||
void PrevSlot()
|
||||
{
|
||||
void PrevSlot() {
|
||||
g_Config.iCurrentStateSlot = (g_Config.iCurrentStateSlot - 1 + Config::iSaveStateSlotCount) % Config::iSaveStateSlotCount;
|
||||
}
|
||||
|
||||
void NextSlot()
|
||||
{
|
||||
void NextSlot() {
|
||||
g_Config.iCurrentStateSlot = (g_Config.iCurrentStateSlot + 1) % Config::iSaveStateSlotCount;
|
||||
}
|
||||
|
||||
static void DeleteIfExists(const Path &fn) {
|
||||
// Just avoiding error messages.
|
||||
if (File::Exists(fn)) {
|
||||
File::Delete(fn);
|
||||
}
|
||||
File::Delete(fn, true);
|
||||
}
|
||||
|
||||
static void RenameIfExists(const Path &from, const Path &to) {
|
||||
@@ -735,34 +512,29 @@ double g_lastSaveTime = -1.0;
|
||||
return UndoSaveSlot(gameFilename, g_Config.iStateUndoLastSaveSlot);
|
||||
}
|
||||
|
||||
bool HasSaveInSlot(const Path &gameFilename, int slot)
|
||||
{
|
||||
bool HasSaveInSlot(const Path &gameFilename, int slot) {
|
||||
Path fn = GenerateSaveSlotFilename(gameFilename, slot, STATE_EXTENSION);
|
||||
return File::Exists(fn);
|
||||
}
|
||||
|
||||
bool HasUndoSaveInSlot(const Path &gameFilename, int slot)
|
||||
{
|
||||
bool HasUndoSaveInSlot(const Path &gameFilename, int slot) {
|
||||
Path fn = GenerateSaveSlotFilename(gameFilename, slot, UNDO_STATE_EXTENSION);
|
||||
return File::Exists(fn);
|
||||
}
|
||||
|
||||
bool HasUndoLastSave(const Path &gameFilename)
|
||||
{
|
||||
bool HasUndoLastSave(const Path &gameFilename) {
|
||||
if (g_Config.sStateUndoLastSaveGame != GenerateFullDiscId(gameFilename))
|
||||
return false;
|
||||
|
||||
return HasUndoSaveInSlot(gameFilename, g_Config.iStateUndoLastSaveSlot);
|
||||
}
|
||||
|
||||
bool HasScreenshotInSlot(const Path &gameFilename, int slot)
|
||||
{
|
||||
bool HasScreenshotInSlot(const Path &gameFilename, int slot) {
|
||||
Path fn = GenerateSaveSlotFilename(gameFilename, slot, SCREENSHOT_EXTENSION);
|
||||
return File::Exists(fn);
|
||||
}
|
||||
|
||||
bool HasUndoLoad(const Path &gameFilename)
|
||||
{
|
||||
bool HasUndoLoad(const Path &gameFilename) {
|
||||
Path fn = GetSysDirectory(DIRECTORY_SAVESTATE) / LOAD_UNDO_NAME;
|
||||
return File::Exists(fn) && g_Config.sStateLoadUndoGame == GenerateFullDiscId(gameFilename);
|
||||
}
|
||||
@@ -950,6 +722,8 @@ double g_lastSaveTime = -1.0;
|
||||
}
|
||||
|
||||
// NOTE: This can cause ending of the current renderpass, due to the readback needed for the screenshot.
|
||||
// TODO: This should run the actual operations on a thread. While this returns true (for example), emulation
|
||||
// *must* not run further, in order not to disturb the current state operation.
|
||||
bool Process() {
|
||||
rewindStates.Process();
|
||||
|
||||
@@ -957,8 +731,7 @@ double g_lastSaveTime = -1.0;
|
||||
return false;
|
||||
needsProcess = false;
|
||||
|
||||
if (!__KernelIsRunning())
|
||||
{
|
||||
if (!__KernelIsRunning()) {
|
||||
ERROR_LOG(Log::SaveState, "Savestate failure: Unable to load without kernel, this should never happen.");
|
||||
return false;
|
||||
}
|
||||
@@ -981,9 +754,8 @@ double g_lastSaveTime = -1.0;
|
||||
std::string slot_prefix = op.slot >= 0 ? StringFromFormat("(%d) ", op.slot + 1) : "";
|
||||
std::string errorString;
|
||||
|
||||
switch (op.type)
|
||||
{
|
||||
case SAVESTATE_LOAD:
|
||||
switch (op.type) {
|
||||
case OperationType::Load:
|
||||
INFO_LOG(Log::SaveState, "Loading state from '%s'", op.filename.c_str());
|
||||
// Use the state's latest version as a guess for saveStateInitialGitVersion.
|
||||
result = CChunkFileReader::Load(op.filename, &saveStateInitialGitVersion, state, &errorString);
|
||||
@@ -1019,7 +791,7 @@ double g_lastSaveTime = -1.0;
|
||||
}
|
||||
break;
|
||||
|
||||
case SAVESTATE_SAVE:
|
||||
case OperationType::Save:
|
||||
INFO_LOG(Log::SaveState, "Saving state to '%s'", op.filename.c_str());
|
||||
title = g_paramSFO.GetValueString("TITLE");
|
||||
if (title.empty()) {
|
||||
@@ -1055,7 +827,7 @@ double g_lastSaveTime = -1.0;
|
||||
}
|
||||
break;
|
||||
|
||||
case SAVESTATE_VERIFY:
|
||||
case OperationType::Verify:
|
||||
{
|
||||
int tempResult = CChunkFileReader::Verify(state) == CChunkFileReader::ERROR_NONE;
|
||||
callbackResult = tempResult ? Status::SUCCESS : Status::FAILURE;
|
||||
@@ -1067,7 +839,7 @@ double g_lastSaveTime = -1.0;
|
||||
break;
|
||||
}
|
||||
|
||||
case SAVESTATE_REWIND:
|
||||
case OperationType::Rewind:
|
||||
INFO_LOG(Log::SaveState, "Rewinding to recent savestate snapshot");
|
||||
result = rewindStates.Restore(&errorString);
|
||||
if (result == CChunkFileReader::ERROR_NONE) {
|
||||
@@ -1093,7 +865,7 @@ double g_lastSaveTime = -1.0;
|
||||
}
|
||||
break;
|
||||
|
||||
case SAVESTATE_SAVE_SCREENSHOT:
|
||||
case OperationType::SaveScreenshot:
|
||||
{
|
||||
_dbg_assert_(!op.callback);
|
||||
|
||||
@@ -1158,8 +930,7 @@ double g_lastSaveTime = -1.0;
|
||||
return false;
|
||||
}
|
||||
|
||||
void Init()
|
||||
{
|
||||
void Init() {
|
||||
// Make sure there's a directory for save slots
|
||||
File::CreateFullPath(GetSysDirectory(DIRECTORY_SAVESTATE));
|
||||
|
||||
@@ -1175,8 +946,7 @@ double g_lastSaveTime = -1.0;
|
||||
g_lastSaveTime = time_now_d();
|
||||
}
|
||||
|
||||
void Shutdown()
|
||||
{
|
||||
void Shutdown() {
|
||||
std::lock_guard<std::mutex> guard(mutex);
|
||||
rewindStates.Clear();
|
||||
}
|
||||
|
||||
@@ -33,12 +33,7 @@ namespace SaveState {
|
||||
};
|
||||
typedef std::function<void(Status status, std::string_view message)> Callback;
|
||||
|
||||
static const char * const STATE_EXTENSION = "ppst";
|
||||
static const char * const SCREENSHOT_EXTENSION = "jpg";
|
||||
static const char * const UNDO_STATE_EXTENSION = "undo.ppst";
|
||||
static const char * const UNDO_SCREENSHOT_EXTENSION = "undo.jpg";
|
||||
|
||||
static const char * const LOAD_UNDO_NAME = "load_undo.ppst";
|
||||
|
||||
void Init();
|
||||
void Shutdown();
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
#include "Common/Thread/ThreadUtil.h"
|
||||
#include "Core/SaveState.h"
|
||||
#include "Core/SaveStateRewind.h"
|
||||
#include "Core/Core.h"
|
||||
#include "Core/Config.h"
|
||||
|
||||
namespace SaveState {
|
||||
|
||||
CChunkFileReader::Error StateRingbuffer::Save() {
|
||||
rewindLastTime_ = time_now_d();
|
||||
|
||||
// Make sure we're not processing a previous save. That'll cause a hitch though, but at least won't
|
||||
// crash due to contention over buffer_.
|
||||
if (compressThread_.joinable())
|
||||
compressThread_.join();
|
||||
|
||||
std::lock_guard<std::mutex> guard(lock_);
|
||||
|
||||
int n = next_++ % size_;
|
||||
if ((next_ % size_) == first_)
|
||||
++first_;
|
||||
|
||||
std::vector<u8> *compressBuffer = &buffer_;
|
||||
CChunkFileReader::Error err;
|
||||
|
||||
if (base_ == -1 || ++baseUsage_ > BASE_USAGE_INTERVAL)
|
||||
{
|
||||
base_ = (base_ + 1) % ARRAY_SIZE(bases_);
|
||||
baseUsage_ = 0;
|
||||
err = SaveToRam(bases_[base_]);
|
||||
// Let's not bother savestating twice.
|
||||
compressBuffer = &bases_[base_];
|
||||
} else
|
||||
err = SaveToRam(buffer_);
|
||||
|
||||
if (err == CChunkFileReader::ERROR_NONE)
|
||||
ScheduleCompress(&states_[n], compressBuffer, &bases_[base_]);
|
||||
else
|
||||
states_[n].clear();
|
||||
|
||||
baseMapping_[n] = base_;
|
||||
return err;
|
||||
}
|
||||
|
||||
CChunkFileReader::Error StateRingbuffer::Restore(std::string *errorString) {
|
||||
std::lock_guard<std::mutex> guard(lock_);
|
||||
|
||||
// No valid states left.
|
||||
if (Empty())
|
||||
return CChunkFileReader::ERROR_BAD_FILE;
|
||||
|
||||
int n = (--next_ + size_) % size_;
|
||||
if (states_[n].empty())
|
||||
return CChunkFileReader::ERROR_BAD_FILE;
|
||||
|
||||
static std::vector<u8> buffer;
|
||||
LockedDecompress(buffer, states_[n], bases_[baseMapping_[n]]);
|
||||
CChunkFileReader::Error error = LoadFromRam(buffer, errorString);
|
||||
rewindLastTime_ = time_now_d();
|
||||
return error;
|
||||
}
|
||||
|
||||
void StateRingbuffer::ScheduleCompress(std::vector<u8> *result, const std::vector<u8> *state, const std::vector<u8> *base) {
|
||||
if (compressThread_.joinable())
|
||||
compressThread_.join();
|
||||
compressThread_ = std::thread([=] {
|
||||
SetCurrentThreadName("SaveStateCompress");
|
||||
|
||||
// Should do no I/O, so no JNI thread context needed.
|
||||
Compress(*result, *state, *base);
|
||||
});
|
||||
}
|
||||
|
||||
void StateRingbuffer::Compress(std::vector<u8> &result, const std::vector<u8> &state, const std::vector<u8> &base) {
|
||||
std::lock_guard<std::mutex> guard(lock_);
|
||||
// Bail if we were cleared before locking.
|
||||
if (first_ == 0 && next_ == 0)
|
||||
return;
|
||||
|
||||
double start_time = time_now_d();
|
||||
result.clear();
|
||||
result.reserve(512 * 1024);
|
||||
for (size_t i = 0; i < state.size(); i += BLOCK_SIZE)
|
||||
{
|
||||
int blockSize = std::min(BLOCK_SIZE, (int)(state.size() - i));
|
||||
if (i + blockSize > base.size() || memcmp(&state[i], &base[i], blockSize) != 0) {
|
||||
result.push_back(1);
|
||||
result.insert(result.end(), state.begin() + i, state.begin() + i + blockSize);
|
||||
} else {
|
||||
result.push_back(0);
|
||||
}
|
||||
}
|
||||
|
||||
double taken_s = time_now_d() - start_time;
|
||||
DEBUG_LOG(Log::SaveState, "Rewind: Compressed save from %d bytes to %d in %0.2f ms.", (int)state.size(), (int)result.size(), taken_s * 1000.0);
|
||||
}
|
||||
|
||||
void StateRingbuffer::LockedDecompress(std::vector<u8> &result, const std::vector<u8> &compressed, const std::vector<u8> &base) {
|
||||
result.clear();
|
||||
result.reserve(base.size());
|
||||
auto basePos = base.begin();
|
||||
for (size_t i = 0; i < compressed.size(); )
|
||||
{
|
||||
if (compressed[i] == 0)
|
||||
{
|
||||
++i;
|
||||
int blockSize = std::min(BLOCK_SIZE, (int)(base.size() - result.size()));
|
||||
result.insert(result.end(), basePos, basePos + blockSize);
|
||||
basePos += blockSize;
|
||||
} else
|
||||
{
|
||||
++i;
|
||||
int blockSize = std::min(BLOCK_SIZE, (int)(compressed.size() - i));
|
||||
result.insert(result.end(), compressed.begin() + i, compressed.begin() + i + blockSize);
|
||||
i += blockSize;
|
||||
// This check is to avoid advancing basePos out of range, which MSVC catches.
|
||||
// When this happens, we're at the end of decoding anyway.
|
||||
if (base.end() - basePos >= blockSize) {
|
||||
basePos += blockSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void StateRingbuffer::Clear() {
|
||||
if (compressThread_.joinable())
|
||||
compressThread_.join();
|
||||
|
||||
// This lock is mainly for shutdown.
|
||||
std::lock_guard<std::mutex> guard(lock_);
|
||||
first_ = 0;
|
||||
next_ = 0;
|
||||
for (auto &b : bases_) {
|
||||
b.clear();
|
||||
}
|
||||
baseMapping_.clear();
|
||||
baseMapping_.resize(size_);
|
||||
for (auto &s : states_) {
|
||||
s.clear();
|
||||
}
|
||||
buffer_.clear();
|
||||
base_ = -1;
|
||||
baseUsage_ = 0;
|
||||
rewindLastTime_ = time_now_d();
|
||||
}
|
||||
|
||||
void StateRingbuffer::Process() {
|
||||
if (g_Config.iRewindSnapshotInterval <= 0) {
|
||||
return;
|
||||
}
|
||||
if (coreState != CORE_RUNNING_CPU) {
|
||||
return;
|
||||
}
|
||||
|
||||
// For fast-forwarding, otherwise they may be useless and too close.
|
||||
double now = time_now_d();
|
||||
double diff = now - rewindLastTime_;
|
||||
if (diff < g_Config.iRewindSnapshotInterval)
|
||||
return;
|
||||
|
||||
DEBUG_LOG(Log::SaveState, "Saving rewind state");
|
||||
Save();
|
||||
}
|
||||
|
||||
void StateRingbuffer::NotifyState() {
|
||||
// Prevent saving snapshots immediately after loading or saving a state.
|
||||
rewindLastTime_ = time_now_d();
|
||||
}
|
||||
|
||||
} // namespace SaveState
|
||||
@@ -0,0 +1,69 @@
|
||||
#pragma once
|
||||
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include "Common/Serialize/Serializer.h"
|
||||
#include "Common/CommonTypes.h"
|
||||
#include "Common/TimeUtil.h"
|
||||
|
||||
namespace SaveState {
|
||||
|
||||
// This ring buffer of states is for rewind save states, which are kept in RAM.
|
||||
// Save states are compressed against one of two reference saves (bases_), and the reference
|
||||
// is switched to a fresh save every N saves, where N is BASE_USAGE_INTERVAL.
|
||||
// The compression is a simple block based scheme where 0 means to copy a block from the base,
|
||||
// and 1 means that the following bytes are the next block. See Compress/LockedDecompress.
|
||||
class StateRingbuffer {
|
||||
public:
|
||||
StateRingbuffer() {
|
||||
size_ = REWIND_NUM_STATES;
|
||||
states_.resize(size_);
|
||||
baseMapping_.resize(size_);
|
||||
}
|
||||
|
||||
~StateRingbuffer() {
|
||||
if (compressThread_.joinable()) {
|
||||
compressThread_.join();
|
||||
}
|
||||
}
|
||||
|
||||
CChunkFileReader::Error Save();
|
||||
CChunkFileReader::Error Restore(std::string *errorString);
|
||||
void ScheduleCompress(std::vector<u8> *result, const std::vector<u8> *state, const std::vector<u8> *base);
|
||||
void Compress(std::vector<u8> &result, const std::vector<u8> &state, const std::vector<u8> &base);
|
||||
void LockedDecompress(std::vector<u8> &result, const std::vector<u8> &compressed, const std::vector<u8> &base);
|
||||
void Clear();
|
||||
|
||||
bool Empty() const {
|
||||
return next_ == first_;
|
||||
}
|
||||
|
||||
void Process();
|
||||
void NotifyState();
|
||||
|
||||
private:
|
||||
const int BLOCK_SIZE = 8192;
|
||||
const int REWIND_NUM_STATES = 20;
|
||||
// TODO: Instead, based on size of compressed state?
|
||||
const int BASE_USAGE_INTERVAL = 15;
|
||||
|
||||
typedef std::vector<u8> StateBuffer;
|
||||
|
||||
int first_ = 0;
|
||||
int next_ = 0;
|
||||
int size_;
|
||||
|
||||
std::vector<StateBuffer> states_;
|
||||
StateBuffer bases_[2];
|
||||
std::vector<int> baseMapping_;
|
||||
std::mutex lock_;
|
||||
std::thread compressThread_;
|
||||
std::vector<u8> buffer_;
|
||||
|
||||
int base_ = -1;
|
||||
int baseUsage_ = 0;
|
||||
|
||||
double rewindLastTime_ = 0.0f;
|
||||
};
|
||||
|
||||
} // namespace SaveState
|
||||
@@ -309,6 +309,7 @@
|
||||
<ClInclude Include="..\..\Core\HLE\Plugins.h" />
|
||||
<ClInclude Include="..\..\Core\RetroAchievements.h" />
|
||||
<ClInclude Include="..\..\Core\SaveState.h" />
|
||||
<ClInclude Include="..\..\Core\SaveStateRewind.h" />
|
||||
<ClInclude Include="..\..\Core\Screenshot.h" />
|
||||
<ClInclude Include="..\..\Core\System.h" />
|
||||
<ClInclude Include="..\..\Core\TiltEventProcessor.h" />
|
||||
@@ -616,6 +617,7 @@
|
||||
<ClCompile Include="..\..\Core\HLE\Plugins.cpp" />
|
||||
<ClCompile Include="..\..\Core\RetroAchievements.cpp" />
|
||||
<ClCompile Include="..\..\Core\SaveState.cpp" />
|
||||
<ClCompile Include="..\..\Core\SaveStateRewind.cpp" />
|
||||
<ClCompile Include="..\..\Core\Screenshot.cpp" />
|
||||
<ClCompile Include="..\..\Core\System.cpp" />
|
||||
<ClCompile Include="..\..\Core\TiltEventProcessor.cpp" />
|
||||
|
||||
@@ -411,6 +411,7 @@
|
||||
<ClCompile Include="..\..\ext\xxhash.c" />
|
||||
<ClCompile Include="pch.cpp" />
|
||||
<ClCompile Include="..\..\Core\Util\PathUtil.cpp" />
|
||||
<ClCompile Include="..\..\Core\SaveStateRewind.cpp" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="..\..\Core\AVIDump.h" />
|
||||
@@ -683,6 +684,7 @@
|
||||
<ClInclude Include="pch.h" />
|
||||
<ClInclude Include="targetver.h" />
|
||||
<ClInclude Include="..\..\Core\Util\PathUtil.h" />
|
||||
<ClInclude Include="..\..\Core\SaveStateRewind.h" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="..\..\ext\gason\LICENSE" />
|
||||
|
||||
@@ -617,6 +617,7 @@ EXEC_AND_LIB_FILES := \
|
||||
$(SRC)/Core/Replay.cpp \
|
||||
$(SRC)/Core/RetroAchievements.cpp \
|
||||
$(SRC)/Core/SaveState.cpp \
|
||||
$(SRC)/Core/SaveStateRewind.cpp \
|
||||
$(SRC)/Core/Screenshot.cpp \
|
||||
$(SRC)/Core/System.cpp \
|
||||
$(SRC)/Core/TiltEventProcessor.cpp \
|
||||
|
||||
@@ -879,6 +879,7 @@ SOURCES_CXX += \
|
||||
$(COREDIR)/Reporting.cpp \
|
||||
$(COREDIR)/RetroAchievements.cpp \
|
||||
$(COREDIR)/SaveState.cpp \
|
||||
$(COREDIR)/SaveStateRewind.cpp \
|
||||
$(COREDIR)/Screenshot.cpp \
|
||||
$(COREDIR)/System.cpp \
|
||||
$(COREDIR)/Util/AtracTrack.cpp \
|
||||
|
||||
Reference in New Issue
Block a user