2012-12-27 18:34:22 +00:00
|
|
|
// Copyright (c) 2012- PPSSPP Project.
|
|
|
|
|
|
|
|
// This program is free software: you can redistribute it and/or modify
|
|
|
|
// it under the terms of the GNU General Public License as published by
|
|
|
|
// the Free Software Foundation, version 2.0 or later versions.
|
|
|
|
|
|
|
|
// This program is distributed in the hope that it will be useful,
|
|
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
// GNU General Public License 2.0 for more details.
|
|
|
|
|
|
|
|
// A copy of the GPL 2.0 should have been included with the program.
|
|
|
|
// If not, see http://www.gnu.org/licenses/
|
|
|
|
|
|
|
|
// Official git repository and contact information can be found at
|
|
|
|
// https://github.com/hrydgard/ppsspp and http://www.ppsspp.org/.
|
|
|
|
|
2016-03-06 21:30:31 +00:00
|
|
|
#include <algorithm>
|
2012-12-27 19:28:12 +00:00
|
|
|
#include <vector>
|
2017-02-27 19:51:36 +00:00
|
|
|
#include <thread>
|
2017-02-27 20:57:46 +00:00
|
|
|
#include <mutex>
|
2012-12-27 19:28:12 +00:00
|
|
|
|
2013-12-29 22:28:31 +00:00
|
|
|
#include "base/timeutil.h"
|
|
|
|
#include "i18n/i18n.h"
|
2016-09-24 17:37:30 +00:00
|
|
|
#include "thread/threadutil.h"
|
2013-12-29 22:28:31 +00:00
|
|
|
|
2013-05-22 16:00:06 +00:00
|
|
|
#include "Common/FileUtil.h"
|
2013-12-29 22:28:31 +00:00
|
|
|
#include "Common/ChunkFile.h"
|
2013-05-22 16:00:06 +00:00
|
|
|
|
|
|
|
#include "Core/SaveState.h"
|
2013-09-15 03:28:41 +00:00
|
|
|
#include "Core/Config.h"
|
2013-05-22 16:00:06 +00:00
|
|
|
#include "Core/Core.h"
|
|
|
|
#include "Core/CoreTiming.h"
|
2013-09-15 04:19:10 +00:00
|
|
|
#include "Core/Host.h"
|
2014-12-21 07:14:46 +00:00
|
|
|
#include "Core/Screenshot.h"
|
2013-09-15 04:19:10 +00:00
|
|
|
#include "Core/System.h"
|
2013-12-29 22:28:31 +00:00
|
|
|
#include "Core/FileSystems/MetaFileSystem.h"
|
2013-12-29 23:11:29 +00:00
|
|
|
#include "Core/ELF/ParamSFO.h"
|
2013-05-22 16:00:06 +00:00
|
|
|
#include "Core/HLE/HLE.h"
|
2014-06-14 21:55:16 +00:00
|
|
|
#include "Core/HLE/sceDisplay.h"
|
2014-05-27 14:50:08 +00:00
|
|
|
#include "Core/HLE/ReplaceTables.h"
|
2013-05-22 16:00:06 +00:00
|
|
|
#include "Core/HLE/sceKernel.h"
|
|
|
|
#include "Core/MemMap.h"
|
|
|
|
#include "Core/MIPS/MIPS.h"
|
2016-04-30 23:20:21 +00:00
|
|
|
#include "Core/MIPS/JitCommon/JitBlockCache.h"
|
2013-12-29 22:28:31 +00:00
|
|
|
#include "HW/MemoryStick.h"
|
2013-09-30 02:57:51 +00:00
|
|
|
#include "GPU/GPUState.h"
|
2012-12-27 18:34:22 +00:00
|
|
|
|
2017-11-14 04:12:27 +00:00
|
|
|
#ifndef MOBILE_DEVICE
|
|
|
|
#include "Core/AVIDump.h"
|
|
|
|
#include "Core/HLE/__sceAudio.h"
|
|
|
|
|
|
|
|
AVIDump video;
|
|
|
|
WAVDump audio;
|
|
|
|
#endif
|
|
|
|
|
2012-12-27 19:28:12 +00:00
|
|
|
namespace SaveState
|
2012-12-27 18:34:22 +00:00
|
|
|
{
|
2012-12-27 19:28:12 +00:00
|
|
|
struct SaveStart
|
|
|
|
{
|
|
|
|
void DoState(PointerWrap &p);
|
|
|
|
};
|
2012-12-27 18:34:22 +00:00
|
|
|
|
2012-12-27 19:28:12 +00:00
|
|
|
enum OperationType
|
|
|
|
{
|
|
|
|
SAVESTATE_SAVE,
|
|
|
|
SAVESTATE_LOAD,
|
|
|
|
SAVESTATE_VERIFY,
|
2013-09-30 02:57:51 +00:00
|
|
|
SAVESTATE_REWIND,
|
2014-12-21 07:14:46 +00:00
|
|
|
SAVESTATE_SAVE_SCREENSHOT,
|
2012-12-27 19:28:12 +00:00
|
|
|
};
|
2012-12-27 18:34:22 +00:00
|
|
|
|
2012-12-27 19:28:12 +00:00
|
|
|
struct Operation
|
|
|
|
{
|
2013-01-02 20:00:10 +00:00
|
|
|
Operation(OperationType t, const std::string &f, Callback cb, void *cbUserData_)
|
|
|
|
: type(t), filename(f), callback(cb), cbUserData(cbUserData_)
|
2012-12-27 19:28:12 +00:00
|
|
|
{
|
|
|
|
}
|
2012-12-27 18:34:22 +00:00
|
|
|
|
2012-12-27 19:28:12 +00:00
|
|
|
OperationType type;
|
|
|
|
std::string filename;
|
|
|
|
Callback callback;
|
2013-01-02 20:00:10 +00:00
|
|
|
void *cbUserData;
|
2012-12-27 19:28:12 +00:00
|
|
|
};
|
|
|
|
|
2013-11-15 12:11:44 +00:00
|
|
|
CChunkFileReader::Error SaveToRam(std::vector<u8> &data) {
|
|
|
|
SaveStart state;
|
|
|
|
size_t sz = CChunkFileReader::MeasurePtr(state);
|
|
|
|
if (data.size() < sz)
|
|
|
|
data.resize(sz);
|
|
|
|
return CChunkFileReader::SavePtr(&data[0], state);
|
|
|
|
}
|
|
|
|
|
|
|
|
CChunkFileReader::Error LoadFromRam(std::vector<u8> &data) {
|
|
|
|
SaveStart state;
|
|
|
|
return CChunkFileReader::LoadPtr(&data[0], state);
|
|
|
|
}
|
|
|
|
|
2013-09-30 02:57:51 +00:00
|
|
|
struct StateRingbuffer
|
|
|
|
{
|
2014-01-18 18:18:47 +00:00
|
|
|
StateRingbuffer(int size) : first_(0), next_(0), size_(size), base_(-1)
|
2013-09-30 02:57:51 +00:00
|
|
|
{
|
|
|
|
states_.resize(size);
|
2014-01-18 18:18:47 +00:00
|
|
|
baseMapping_.resize(size);
|
2013-09-30 02:57:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
CChunkFileReader::Error Save()
|
|
|
|
{
|
2017-02-27 20:57:46 +00:00
|
|
|
std::lock_guard<std::mutex> guard(lock_);
|
2016-09-24 17:37:30 +00:00
|
|
|
|
2013-09-30 02:57:51 +00:00
|
|
|
int n = next_++ % size_;
|
2013-11-03 01:32:34 +00:00
|
|
|
if ((next_ % size_) == first_)
|
2013-09-30 02:57:51 +00:00
|
|
|
++first_;
|
|
|
|
|
2014-01-18 18:18:47 +00:00
|
|
|
static std::vector<u8> buffer;
|
|
|
|
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)
|
2016-09-24 17:37:30 +00:00
|
|
|
ScheduleCompress(&states_[n], compressBuffer, &bases_[base_]);
|
2014-01-18 18:18:47 +00:00
|
|
|
else
|
|
|
|
states_[n].clear();
|
|
|
|
baseMapping_[n] = base_;
|
|
|
|
return err;
|
2013-09-30 02:57:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
CChunkFileReader::Error Restore()
|
|
|
|
{
|
2017-02-27 20:57:46 +00:00
|
|
|
std::lock_guard<std::mutex> guard(lock_);
|
2016-09-24 17:37:30 +00:00
|
|
|
|
2013-09-30 02:57:51 +00:00
|
|
|
// No valid states left.
|
2013-11-03 01:32:34 +00:00
|
|
|
if (Empty())
|
2013-09-30 02:57:51 +00:00
|
|
|
return CChunkFileReader::ERROR_BAD_FILE;
|
|
|
|
|
2014-01-18 18:18:47 +00:00
|
|
|
int n = (--next_ + size_) % size_;
|
2013-09-30 02:57:51 +00:00
|
|
|
if (states_[n].empty())
|
|
|
|
return CChunkFileReader::ERROR_BAD_FILE;
|
2013-10-23 14:10:11 +00:00
|
|
|
|
2014-01-18 18:18:47 +00:00
|
|
|
static std::vector<u8> buffer;
|
2017-04-09 07:19:35 +00:00
|
|
|
LockedDecompress(buffer, states_[n], bases_[baseMapping_[n]]);
|
2014-01-18 18:18:47 +00:00
|
|
|
return LoadFromRam(buffer);
|
|
|
|
}
|
|
|
|
|
2016-09-24 17:37:30 +00:00
|
|
|
void ScheduleCompress(std::vector<u8> *result, const std::vector<u8> *state, const std::vector<u8> *base)
|
|
|
|
{
|
|
|
|
auto th = new std::thread([=]{
|
|
|
|
setCurrentThreadName("SaveStateCompress");
|
|
|
|
Compress(*result, *state, *base);
|
|
|
|
});
|
|
|
|
th->detach();
|
|
|
|
}
|
|
|
|
|
2014-01-18 18:18:47 +00:00
|
|
|
void Compress(std::vector<u8> &result, const std::vector<u8> &state, const std::vector<u8> &base)
|
|
|
|
{
|
2017-02-27 20:57:46 +00:00
|
|
|
std::lock_guard<std::mutex> guard(lock_);
|
2016-09-24 17:37:30 +00:00
|
|
|
// Bail if we were cleared before locking.
|
|
|
|
if (first_ == 0 && next_ == 0)
|
|
|
|
return;
|
|
|
|
|
2014-01-18 18:18:47 +00:00
|
|
|
result.clear();
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-04-09 07:19:35 +00:00
|
|
|
void LockedDecompress(std::vector<u8> &result, const std::vector<u8> &compressed, const std::vector<u8> &base)
|
2014-01-18 18:18:47 +00:00
|
|
|
{
|
|
|
|
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;
|
|
|
|
basePos += blockSize;
|
|
|
|
}
|
|
|
|
}
|
2013-09-30 02:57:51 +00:00
|
|
|
}
|
|
|
|
|
2013-11-03 01:32:34 +00:00
|
|
|
void Clear()
|
|
|
|
{
|
2016-09-24 17:37:30 +00:00
|
|
|
// This lock is mainly for shutdown.
|
2017-02-27 20:57:46 +00:00
|
|
|
std::lock_guard<std::mutex> guard(lock_);
|
2013-11-03 01:32:34 +00:00
|
|
|
first_ = 0;
|
|
|
|
next_ = 0;
|
|
|
|
}
|
|
|
|
|
2014-12-03 20:16:11 +00:00
|
|
|
bool Empty() const
|
2013-11-03 01:32:34 +00:00
|
|
|
{
|
|
|
|
return next_ == first_;
|
|
|
|
}
|
|
|
|
|
2014-01-18 22:05:32 +00:00
|
|
|
static const int BLOCK_SIZE;
|
2014-01-18 18:18:47 +00:00
|
|
|
// TODO: Instead, based on size of compressed state?
|
2014-01-18 22:05:32 +00:00
|
|
|
static const int BASE_USAGE_INTERVAL;
|
2016-09-24 17:37:30 +00:00
|
|
|
|
2013-09-30 02:57:51 +00:00
|
|
|
typedef std::vector<u8> StateBuffer;
|
2016-09-24 17:37:30 +00:00
|
|
|
|
2013-09-30 02:57:51 +00:00
|
|
|
int first_;
|
|
|
|
int next_;
|
|
|
|
int size_;
|
2016-09-24 17:37:30 +00:00
|
|
|
|
2013-09-30 02:57:51 +00:00
|
|
|
std::vector<StateBuffer> states_;
|
2014-01-18 18:18:47 +00:00
|
|
|
StateBuffer bases_[2];
|
|
|
|
std::vector<int> baseMapping_;
|
2017-02-27 20:57:46 +00:00
|
|
|
std::mutex lock_;
|
2016-09-24 17:37:30 +00:00
|
|
|
|
2014-01-18 18:18:47 +00:00
|
|
|
int base_;
|
|
|
|
int baseUsage_;
|
2013-09-30 02:57:51 +00:00
|
|
|
};
|
|
|
|
|
2013-01-02 22:25:35 +00:00
|
|
|
static bool needsProcess = false;
|
2012-12-27 19:28:12 +00:00
|
|
|
static std::vector<Operation> pending;
|
2017-02-27 20:57:46 +00:00
|
|
|
static std::mutex mutex;
|
2014-02-09 21:45:51 +00:00
|
|
|
static bool hasLoadedState = false;
|
2012-12-27 19:28:12 +00:00
|
|
|
|
2013-09-30 02:57:51 +00:00
|
|
|
// TODO: Should this be configurable?
|
2014-01-18 18:18:47 +00:00
|
|
|
static const int REWIND_NUM_STATES = 20;
|
2013-09-30 02:57:51 +00:00
|
|
|
static StateRingbuffer rewindStates(REWIND_NUM_STATES);
|
2013-09-30 03:04:23 +00:00
|
|
|
// TODO: Any reason for this to be configurable?
|
|
|
|
const static float rewindMaxWallFrequency = 1.0f;
|
|
|
|
static float rewindLastTime = 0.0f;
|
2014-01-18 22:05:32 +00:00
|
|
|
const int StateRingbuffer::BLOCK_SIZE = 8192;
|
|
|
|
const int StateRingbuffer::BASE_USAGE_INTERVAL = 15;
|
2013-09-30 02:57:51 +00:00
|
|
|
|
2012-12-27 19:28:12 +00:00
|
|
|
void SaveStart::DoState(PointerWrap &p)
|
|
|
|
{
|
2013-09-15 03:23:03 +00:00
|
|
|
auto s = p.Section("SaveStart", 1);
|
|
|
|
if (!s)
|
|
|
|
return;
|
|
|
|
|
2012-12-27 19:58:15 +00:00
|
|
|
// Gotta do CoreTiming first since we'll restore into it.
|
2012-12-27 21:08:58 +00:00
|
|
|
CoreTiming::DoState(p);
|
2012-12-27 19:58:15 +00:00
|
|
|
|
2013-09-29 20:51:09 +00:00
|
|
|
// Memory is a bit tricky when jit is enabled, since there's emuhacks in it.
|
2014-05-31 06:29:03 +00:00
|
|
|
auto savedReplacements = SaveAndClearReplacements();
|
2013-09-29 20:51:09 +00:00
|
|
|
if (MIPSComp::jit && p.mode == p.MODE_WRITE)
|
|
|
|
{
|
2016-05-12 10:18:12 +00:00
|
|
|
std::vector<u32> savedBlocks;
|
2016-05-14 15:57:52 +00:00
|
|
|
savedBlocks = MIPSComp::jit->SaveAndClearEmuHackOps();
|
2013-09-29 20:51:09 +00:00
|
|
|
Memory::DoState(p);
|
2016-05-14 15:57:52 +00:00
|
|
|
MIPSComp::jit->RestoreSavedEmuHackOps(savedBlocks);
|
2013-09-29 20:51:09 +00:00
|
|
|
}
|
|
|
|
else
|
|
|
|
Memory::DoState(p);
|
2014-05-31 06:29:03 +00:00
|
|
|
RestoreSavedReplacements(savedReplacements);
|
2013-09-29 20:51:09 +00:00
|
|
|
|
2012-12-27 19:58:15 +00:00
|
|
|
MemoryStick_DoState(p);
|
2012-12-28 04:33:10 +00:00
|
|
|
currentMIPS->DoState(p);
|
2012-12-28 06:14:31 +00:00
|
|
|
HLEDoState(p);
|
2012-12-27 19:28:12 +00:00
|
|
|
__KernelDoState(p);
|
2013-02-08 16:10:20 +00:00
|
|
|
// Kernel object destructors might close open files, so do the filesystem last.
|
|
|
|
pspFileSystem.DoState(p);
|
2012-12-27 19:28:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void Enqueue(SaveState::Operation op)
|
|
|
|
{
|
2017-02-27 20:57:46 +00:00
|
|
|
std::lock_guard<std::mutex> guard(mutex);
|
2012-12-27 19:28:12 +00:00
|
|
|
pending.push_back(op);
|
|
|
|
|
2013-09-15 01:43:23 +00:00
|
|
|
// Don't actually run it until next frame.
|
2012-12-27 19:28:12 +00:00
|
|
|
// It's possible there might be a duplicate but it won't hurt us.
|
2013-09-15 01:43:23 +00:00
|
|
|
needsProcess = true;
|
|
|
|
Core_UpdateSingleStep();
|
2012-12-27 19:28:12 +00:00
|
|
|
}
|
|
|
|
|
2013-01-02 20:00:10 +00:00
|
|
|
void Load(const std::string &filename, Callback callback, void *cbUserData)
|
2012-12-27 19:28:12 +00:00
|
|
|
{
|
2013-01-02 20:00:10 +00:00
|
|
|
Enqueue(Operation(SAVESTATE_LOAD, filename, callback, cbUserData));
|
2012-12-27 19:28:12 +00:00
|
|
|
}
|
|
|
|
|
2013-01-02 20:00:10 +00:00
|
|
|
void Save(const std::string &filename, Callback callback, void *cbUserData)
|
2012-12-27 19:28:12 +00:00
|
|
|
{
|
2013-01-02 20:00:10 +00:00
|
|
|
Enqueue(Operation(SAVESTATE_SAVE, filename, callback, cbUserData));
|
2012-12-27 19:28:12 +00:00
|
|
|
}
|
|
|
|
|
2013-04-13 08:13:28 +00:00
|
|
|
void Verify(Callback callback, void *cbUserData)
|
|
|
|
{
|
|
|
|
Enqueue(Operation(SAVESTATE_VERIFY, std::string(""), callback, cbUserData));
|
|
|
|
}
|
|
|
|
|
2013-09-30 02:57:51 +00:00
|
|
|
void Rewind(Callback callback, void *cbUserData)
|
|
|
|
{
|
|
|
|
Enqueue(Operation(SAVESTATE_REWIND, std::string(""), callback, cbUserData));
|
|
|
|
}
|
|
|
|
|
2014-12-21 07:14:46 +00:00
|
|
|
void SaveScreenshot(const std::string &filename, Callback callback, void *cbUserData)
|
|
|
|
{
|
|
|
|
Enqueue(Operation(SAVESTATE_SAVE_SCREENSHOT, filename, callback, cbUserData));
|
|
|
|
}
|
|
|
|
|
2013-11-03 01:32:34 +00:00
|
|
|
bool CanRewind()
|
|
|
|
{
|
|
|
|
return !rewindStates.Empty();
|
|
|
|
}
|
|
|
|
|
2013-01-02 20:00:10 +00:00
|
|
|
// Slot utilities
|
|
|
|
|
2016-01-23 21:06:30 +00:00
|
|
|
std::string AppendSlotTitle(const std::string &filename, const std::string &title) {
|
|
|
|
if (!endsWith(filename, std::string(".") + STATE_EXTENSION)) {
|
|
|
|
return title + " (" + filename + ")";
|
|
|
|
}
|
|
|
|
|
|
|
|
// Usually these are slots, let's check the slot # after the last '_'.
|
|
|
|
size_t slotNumPos = filename.find_last_of('_');
|
|
|
|
if (slotNumPos == filename.npos) {
|
|
|
|
return title + " (" + filename + ")";
|
|
|
|
}
|
|
|
|
|
|
|
|
const size_t extLength = strlen(STATE_EXTENSION) + 1;
|
|
|
|
// If we take out the extension, '_', etc. we should be left with only a single digit.
|
|
|
|
if (slotNumPos + 1 + extLength != filename.length() - 1) {
|
|
|
|
return title + " (" + filename + ")";
|
|
|
|
}
|
|
|
|
|
|
|
|
std::string slot = filename.substr(slotNumPos + 1, 1);
|
|
|
|
if (slot[0] < '0' || slot[0] > '8') {
|
|
|
|
return title + " (" + filename + ")";
|
|
|
|
}
|
|
|
|
|
|
|
|
// Change from zero indexed to human friendly.
|
|
|
|
slot[0]++;
|
|
|
|
return title + " (" + slot + ")";
|
|
|
|
}
|
|
|
|
|
|
|
|
std::string GetTitle(const std::string &filename) {
|
|
|
|
std::string title;
|
|
|
|
if (CChunkFileReader::GetFileTitle(filename, &title) == CChunkFileReader::ERROR_NONE) {
|
|
|
|
if (title.empty()) {
|
|
|
|
return File::GetFilename(filename);
|
|
|
|
}
|
|
|
|
|
|
|
|
return AppendSlotTitle(filename, title);
|
|
|
|
}
|
|
|
|
|
|
|
|
// The file can't be loaded - let's note that.
|
|
|
|
I18NCategory *sy = GetI18NCategory("System");
|
|
|
|
return File::GetFilename(filename) + " " + sy->T("(broken)");
|
|
|
|
}
|
|
|
|
|
2015-09-23 17:29:39 +00:00
|
|
|
std::string GenerateSaveSlotFilename(const std::string &gameFilename, int slot, const char *extension)
|
2013-01-02 20:00:10 +00:00
|
|
|
{
|
2015-09-23 17:29:39 +00:00
|
|
|
std::string discId = g_paramSFO.GetValueString("DISC_ID");
|
2017-05-29 01:42:45 +00:00
|
|
|
std::string discVer = g_paramSFO.GetValueString("DISC_VERSION");
|
2015-09-23 17:29:39 +00:00
|
|
|
std::string fullDiscId;
|
2017-05-29 01:42:45 +00:00
|
|
|
if (discId.empty()) {
|
|
|
|
discId = g_paramSFO.GenerateFakeID();
|
|
|
|
discVer = "1.00";
|
2015-09-23 17:29:39 +00:00
|
|
|
}
|
2017-05-29 01:42:45 +00:00
|
|
|
fullDiscId = StringFromFormat("%s_%s", discId.c_str(), discVer.c_str());
|
2015-09-23 17:29:39 +00:00
|
|
|
|
2017-05-30 11:52:29 +00:00
|
|
|
std::string temp = StringFromFormat("ms0:/PSP/PPSSPP_STATE/%s_%d.%s", fullDiscId.c_str(), slot, extension);
|
2013-01-02 20:00:10 +00:00
|
|
|
std::string hostPath;
|
2015-09-23 17:29:39 +00:00
|
|
|
if (pspFileSystem.GetHostPath(temp, hostPath)) {
|
2013-01-02 20:00:10 +00:00
|
|
|
return hostPath;
|
|
|
|
} else {
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-02-01 17:04:06 +00:00
|
|
|
int GetCurrentSlot()
|
|
|
|
{
|
|
|
|
return g_Config.iCurrentStateSlot;
|
|
|
|
}
|
|
|
|
|
2014-01-07 14:56:04 +00:00
|
|
|
void NextSlot()
|
|
|
|
{
|
2016-02-29 02:21:57 +00:00
|
|
|
g_Config.iCurrentStateSlot = (g_Config.iCurrentStateSlot + 1) % NUM_SLOTS;
|
2014-01-07 14:56:04 +00:00
|
|
|
}
|
|
|
|
|
2015-09-23 17:29:39 +00:00
|
|
|
void LoadSlot(const std::string &gameFilename, int slot, Callback callback, void *cbUserData)
|
2013-01-02 20:00:10 +00:00
|
|
|
{
|
2015-09-23 17:29:39 +00:00
|
|
|
std::string fn = GenerateSaveSlotFilename(gameFilename, slot, STATE_EXTENSION);
|
2013-07-17 05:33:26 +00:00
|
|
|
if (!fn.empty()) {
|
2013-01-02 20:00:10 +00:00
|
|
|
Load(fn, callback, cbUserData);
|
2013-07-17 05:33:26 +00:00
|
|
|
} else {
|
2015-01-29 15:32:13 +00:00
|
|
|
I18NCategory *sy = GetI18NCategory("System");
|
2013-09-11 20:21:15 +00:00
|
|
|
if (callback)
|
2016-05-28 04:25:05 +00:00
|
|
|
callback(false, sy->T("Failed to load state. Error in the file system."), cbUserData);
|
2013-07-17 05:33:26 +00:00
|
|
|
}
|
2013-01-02 20:00:10 +00:00
|
|
|
}
|
|
|
|
|
2015-09-23 17:29:39 +00:00
|
|
|
void SaveSlot(const std::string &gameFilename, int slot, Callback callback, void *cbUserData)
|
2013-01-02 20:00:10 +00:00
|
|
|
{
|
2015-09-23 17:29:39 +00:00
|
|
|
std::string fn = GenerateSaveSlotFilename(gameFilename, slot, STATE_EXTENSION);
|
|
|
|
std::string shot = GenerateSaveSlotFilename(gameFilename, slot, SCREENSHOT_EXTENSION);
|
2013-07-17 05:33:26 +00:00
|
|
|
if (!fn.empty()) {
|
2016-05-28 04:25:05 +00:00
|
|
|
auto renameCallback = [=](bool status, const std::string &message, void *data) {
|
2014-12-21 06:24:02 +00:00
|
|
|
if (status) {
|
|
|
|
if (File::Exists(fn)) {
|
|
|
|
File::Delete(fn);
|
|
|
|
}
|
|
|
|
File::Rename(fn + ".tmp", fn);
|
|
|
|
}
|
|
|
|
if (callback) {
|
2016-05-28 04:25:05 +00:00
|
|
|
callback(status, message, data);
|
2014-12-21 06:24:02 +00:00
|
|
|
}
|
|
|
|
};
|
2014-12-21 07:14:46 +00:00
|
|
|
// Let's also create a screenshot.
|
2014-12-21 07:34:11 +00:00
|
|
|
SaveScreenshot(shot, Callback(), 0);
|
2014-12-21 06:24:02 +00:00
|
|
|
Save(fn + ".tmp", renameCallback, cbUserData);
|
2013-07-17 05:33:26 +00:00
|
|
|
} else {
|
2016-05-28 04:25:05 +00:00
|
|
|
I18NCategory *sy = GetI18NCategory("System");
|
2013-09-11 20:21:15 +00:00
|
|
|
if (callback)
|
2016-05-28 04:25:05 +00:00
|
|
|
callback(false, sy->T("Failed to save state. Error in the file system."), cbUserData);
|
2013-07-17 05:33:26 +00:00
|
|
|
}
|
2013-01-02 20:00:10 +00:00
|
|
|
}
|
|
|
|
|
2015-09-23 17:29:39 +00:00
|
|
|
bool HasSaveInSlot(const std::string &gameFilename, int slot)
|
2013-01-02 20:00:10 +00:00
|
|
|
{
|
2015-09-23 17:29:39 +00:00
|
|
|
std::string fn = GenerateSaveSlotFilename(gameFilename, slot, STATE_EXTENSION);
|
2013-12-02 16:24:20 +00:00
|
|
|
return File::Exists(fn);
|
|
|
|
}
|
|
|
|
|
2015-09-23 17:29:39 +00:00
|
|
|
bool HasScreenshotInSlot(const std::string &gameFilename, int slot)
|
2013-12-02 16:24:20 +00:00
|
|
|
{
|
2015-09-23 17:29:39 +00:00
|
|
|
std::string fn = GenerateSaveSlotFilename(gameFilename, slot, SCREENSHOT_EXTENSION);
|
2013-04-13 08:13:28 +00:00
|
|
|
return File::Exists(fn);
|
2013-01-02 20:00:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
bool operator < (const tm &t1, const tm &t2) {
|
|
|
|
if (t1.tm_year < t2.tm_year) return true;
|
|
|
|
if (t1.tm_year > t2.tm_year) return false;
|
|
|
|
if (t1.tm_mon < t2.tm_mon) return true;
|
|
|
|
if (t1.tm_mon > t2.tm_mon) return false;
|
|
|
|
if (t1.tm_mday < t2.tm_mday) return true;
|
|
|
|
if (t1.tm_mday > t2.tm_mday) return false;
|
|
|
|
if (t1.tm_hour < t2.tm_hour) return true;
|
|
|
|
if (t1.tm_hour > t2.tm_hour) return false;
|
|
|
|
if (t1.tm_min < t2.tm_min) return true;
|
|
|
|
if (t1.tm_min > t2.tm_min) return false;
|
|
|
|
if (t1.tm_sec < t2.tm_sec) return true;
|
|
|
|
if (t1.tm_sec > t2.tm_sec) return false;
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2015-09-23 17:29:39 +00:00
|
|
|
int GetNewestSlot(const std::string &gameFilename) {
|
2013-01-02 20:00:10 +00:00
|
|
|
int newestSlot = -1;
|
|
|
|
tm newestDate = {0};
|
2016-02-29 02:21:57 +00:00
|
|
|
for (int i = 0; i < NUM_SLOTS; i++) {
|
2015-09-23 17:29:39 +00:00
|
|
|
std::string fn = GenerateSaveSlotFilename(gameFilename, i, STATE_EXTENSION);
|
2013-01-02 20:00:10 +00:00
|
|
|
if (File::Exists(fn)) {
|
2015-02-01 18:54:07 +00:00
|
|
|
tm time;
|
|
|
|
bool success = File::GetModifTime(fn, time);
|
|
|
|
if (success && newestDate < time) {
|
2013-01-02 20:00:10 +00:00
|
|
|
newestDate = time;
|
|
|
|
newestSlot = i;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return newestSlot;
|
|
|
|
}
|
|
|
|
|
2015-09-23 17:29:39 +00:00
|
|
|
std::string GetSlotDateAsString(const std::string &gameFilename, int slot) {
|
|
|
|
std::string fn = GenerateSaveSlotFilename(gameFilename, slot, STATE_EXTENSION);
|
2014-12-31 19:42:28 +00:00
|
|
|
if (File::Exists(fn)) {
|
2015-02-01 18:54:07 +00:00
|
|
|
tm time;
|
|
|
|
if (File::GetModifTime(fn, time)) {
|
|
|
|
char buf[256];
|
|
|
|
// TODO: Use local time format? Americans and some others might not like ISO standard :)
|
|
|
|
strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &time);
|
|
|
|
return std::string(buf);
|
|
|
|
}
|
2014-12-31 19:42:28 +00:00
|
|
|
}
|
|
|
|
return "";
|
|
|
|
}
|
2013-01-02 20:00:10 +00:00
|
|
|
|
2012-12-27 19:28:12 +00:00
|
|
|
std::vector<Operation> Flush()
|
|
|
|
{
|
2017-02-27 20:57:46 +00:00
|
|
|
std::lock_guard<std::mutex> guard(mutex);
|
2012-12-27 19:28:12 +00:00
|
|
|
std::vector<Operation> copy = pending;
|
|
|
|
pending.clear();
|
|
|
|
|
|
|
|
return copy;
|
|
|
|
}
|
|
|
|
|
2013-09-30 03:20:20 +00:00
|
|
|
bool HandleFailure()
|
2013-09-15 04:19:10 +00:00
|
|
|
{
|
2013-09-30 03:20:20 +00:00
|
|
|
// Okay, first, let's give the rewind state a shot - maybe we can at least not reset entirely.
|
|
|
|
// Even if this was a rewind, maybe we can still load a previous one.
|
|
|
|
CChunkFileReader::Error result;
|
|
|
|
do
|
|
|
|
result = rewindStates.Restore();
|
|
|
|
while (result == CChunkFileReader::ERROR_BROKEN_STATE);
|
|
|
|
|
|
|
|
if (result == CChunkFileReader::ERROR_NONE) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
// We tried, our only remaining option is to reset the game.
|
2013-09-15 04:19:10 +00:00
|
|
|
PSP_Shutdown();
|
|
|
|
std::string resetError;
|
|
|
|
if (!PSP_Init(PSP_CoreParameter(), &resetError))
|
|
|
|
{
|
|
|
|
ERROR_LOG(BOOT, "Error resetting: %s", resetError.c_str());
|
|
|
|
// TODO: This probably doesn't clean up well enough.
|
|
|
|
Core_Stop();
|
2013-09-30 03:20:20 +00:00
|
|
|
return false;
|
2013-09-15 04:19:10 +00:00
|
|
|
}
|
|
|
|
host->BootDone();
|
|
|
|
host->UpdateDisassembly();
|
2013-09-30 03:20:20 +00:00
|
|
|
return false;
|
2013-09-15 04:19:10 +00:00
|
|
|
}
|
|
|
|
|
2013-09-30 03:04:23 +00:00
|
|
|
static inline void CheckRewindState()
|
|
|
|
{
|
2013-11-03 01:33:23 +00:00
|
|
|
if (gpuStats.numFlips % g_Config.iRewindFlipFrequency != 0)
|
2013-09-30 03:04:23 +00:00
|
|
|
return;
|
|
|
|
|
|
|
|
// For fast-forwarding, otherwise they may be useless and too close.
|
|
|
|
time_update();
|
|
|
|
float diff = time_now() - rewindLastTime;
|
|
|
|
if (diff < rewindMaxWallFrequency)
|
|
|
|
return;
|
|
|
|
|
|
|
|
rewindLastTime = time_now();
|
2013-12-05 15:15:33 +00:00
|
|
|
DEBUG_LOG(BOOT, "saving rewind state");
|
2013-09-30 03:04:23 +00:00
|
|
|
rewindStates.Save();
|
|
|
|
}
|
|
|
|
|
2014-02-09 21:45:51 +00:00
|
|
|
bool HasLoadedState()
|
|
|
|
{
|
|
|
|
return hasLoadedState;
|
|
|
|
}
|
|
|
|
|
2013-08-07 06:00:20 +00:00
|
|
|
void Process()
|
2012-12-27 19:28:12 +00:00
|
|
|
{
|
2014-02-08 18:29:22 +00:00
|
|
|
#ifndef MOBILE_DEVICE
|
2013-11-03 01:33:23 +00:00
|
|
|
if (g_Config.iRewindFlipFrequency != 0 && gpuStats.numFlips != 0)
|
2013-09-30 03:04:23 +00:00
|
|
|
CheckRewindState();
|
2013-12-05 15:15:33 +00:00
|
|
|
#endif
|
2013-09-30 02:57:51 +00:00
|
|
|
|
2013-08-07 06:00:20 +00:00
|
|
|
if (!needsProcess)
|
|
|
|
return;
|
|
|
|
needsProcess = false;
|
|
|
|
|
2012-12-29 01:23:05 +00:00
|
|
|
if (!__KernelIsRunning())
|
|
|
|
{
|
2017-03-06 12:10:23 +00:00
|
|
|
ERROR_LOG(SAVESTATE, "Savestate failure: Unable to load without kernel, this should never happen.");
|
2012-12-29 01:23:05 +00:00
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2012-12-27 19:28:12 +00:00
|
|
|
std::vector<Operation> operations = Flush();
|
|
|
|
SaveStart state;
|
|
|
|
|
|
|
|
for (size_t i = 0, n = operations.size(); i < n; ++i)
|
|
|
|
{
|
|
|
|
Operation &op = operations[i];
|
2013-09-15 04:19:10 +00:00
|
|
|
CChunkFileReader::Error result;
|
|
|
|
bool callbackResult;
|
2016-05-28 04:25:05 +00:00
|
|
|
std::string callbackMessage;
|
2013-06-24 08:24:34 +00:00
|
|
|
std::string reason;
|
2017-05-30 00:18:09 +00:00
|
|
|
std::string title;
|
2013-06-24 08:24:34 +00:00
|
|
|
|
2015-07-01 22:50:07 +00:00
|
|
|
I18NCategory *sc = GetI18NCategory("Screen");
|
|
|
|
const char *i18nLoadFailure = sc->T("Load savestate failed", "");
|
|
|
|
const char *i18nSaveFailure = sc->T("Save State Failed", "");
|
2013-09-30 03:24:23 +00:00
|
|
|
if (strlen(i18nLoadFailure) == 0)
|
2015-07-01 22:50:07 +00:00
|
|
|
i18nLoadFailure = sc->T("Failed to load state");
|
2013-09-30 03:24:23 +00:00
|
|
|
if (strlen(i18nSaveFailure) == 0)
|
2015-07-01 22:50:07 +00:00
|
|
|
i18nSaveFailure = sc->T("Failed to save state");
|
2013-06-24 07:58:29 +00:00
|
|
|
|
2012-12-27 19:28:12 +00:00
|
|
|
switch (op.type)
|
|
|
|
{
|
|
|
|
case SAVESTATE_LOAD:
|
2017-03-06 12:10:23 +00:00
|
|
|
INFO_LOG(SAVESTATE, "Loading state from %s", op.filename.c_str());
|
2016-01-23 20:53:03 +00:00
|
|
|
result = CChunkFileReader::Load(op.filename, PPSSPP_GIT_VERSION, state, &reason);
|
2013-09-15 04:19:10 +00:00
|
|
|
if (result == CChunkFileReader::ERROR_NONE) {
|
2016-05-28 04:25:05 +00:00
|
|
|
callbackMessage = sc->T("Loaded State");
|
2013-09-15 04:19:10 +00:00
|
|
|
callbackResult = true;
|
2014-02-09 21:45:51 +00:00
|
|
|
hasLoadedState = true;
|
2017-11-14 04:12:27 +00:00
|
|
|
#ifndef MOBILE_DEVICE
|
|
|
|
if (g_Config.bSaveLoadResetsAVdumping) {
|
|
|
|
if (g_Config.bDumpFrames) {
|
|
|
|
video.Stop();
|
|
|
|
video.Start(PSP_CoreParameter().renderWidth, PSP_CoreParameter().renderHeight);
|
|
|
|
}
|
|
|
|
if (g_Config.bDumpAudio) {
|
|
|
|
audio.Reset();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
#endif
|
2013-09-15 04:19:10 +00:00
|
|
|
} else if (result == CChunkFileReader::ERROR_BROKEN_STATE) {
|
|
|
|
HandleFailure();
|
2016-05-28 04:25:05 +00:00
|
|
|
callbackMessage = i18nLoadFailure;
|
2017-03-06 12:10:23 +00:00
|
|
|
ERROR_LOG(SAVESTATE, "Load state failure: %s", reason.c_str());
|
2013-09-15 04:19:10 +00:00
|
|
|
callbackResult = false;
|
2013-09-11 20:21:15 +00:00
|
|
|
} else {
|
2016-05-28 04:25:05 +00:00
|
|
|
callbackMessage = sc->T(reason.c_str(), i18nLoadFailure);
|
2013-09-15 04:19:10 +00:00
|
|
|
callbackResult = false;
|
2013-06-24 08:24:34 +00:00
|
|
|
}
|
2012-12-27 19:28:12 +00:00
|
|
|
break;
|
|
|
|
|
|
|
|
case SAVESTATE_SAVE:
|
2017-03-06 12:10:23 +00:00
|
|
|
INFO_LOG(SAVESTATE, "Saving state to %s", op.filename.c_str());
|
2017-05-30 00:18:09 +00:00
|
|
|
title = g_paramSFO.GetValueString("TITLE");
|
|
|
|
if (title.empty()) {
|
|
|
|
// Homebrew title
|
|
|
|
title = PSP_CoreParameter().fileToStart;
|
|
|
|
std::size_t lslash = title.find_last_of("/");
|
|
|
|
title = title.substr(lslash + 1);
|
|
|
|
}
|
|
|
|
result = CChunkFileReader::Save(op.filename, title, PPSSPP_GIT_VERSION, state);
|
2013-09-15 04:19:10 +00:00
|
|
|
if (result == CChunkFileReader::ERROR_NONE) {
|
2016-05-28 04:25:05 +00:00
|
|
|
callbackMessage = sc->T("Saved State");
|
2013-09-15 04:19:10 +00:00
|
|
|
callbackResult = true;
|
2017-11-14 04:12:27 +00:00
|
|
|
#ifndef MOBILE_DEVICE
|
|
|
|
if (g_Config.bSaveLoadResetsAVdumping) {
|
|
|
|
if (g_Config.bDumpFrames) {
|
|
|
|
video.Stop();
|
|
|
|
video.Start(PSP_CoreParameter().renderWidth, PSP_CoreParameter().renderHeight);
|
|
|
|
}
|
|
|
|
if (g_Config.bDumpAudio) {
|
|
|
|
audio.Reset();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
#endif
|
2013-09-15 04:19:10 +00:00
|
|
|
} else if (result == CChunkFileReader::ERROR_BROKEN_STATE) {
|
|
|
|
HandleFailure();
|
2016-05-28 04:25:05 +00:00
|
|
|
callbackMessage = i18nSaveFailure;
|
2017-03-06 12:10:23 +00:00
|
|
|
ERROR_LOG(SAVESTATE, "Save state failure: %s", reason.c_str());
|
2013-09-15 04:19:10 +00:00
|
|
|
callbackResult = false;
|
2013-09-11 20:21:15 +00:00
|
|
|
} else {
|
2016-05-28 04:25:05 +00:00
|
|
|
callbackMessage = i18nSaveFailure;
|
2013-09-15 04:19:10 +00:00
|
|
|
callbackResult = false;
|
2013-09-11 20:21:15 +00:00
|
|
|
}
|
2012-12-27 19:28:12 +00:00
|
|
|
break;
|
|
|
|
|
|
|
|
case SAVESTATE_VERIFY:
|
2013-09-15 04:19:10 +00:00
|
|
|
callbackResult = CChunkFileReader::Verify(state) == CChunkFileReader::ERROR_NONE;
|
2016-05-28 04:25:05 +00:00
|
|
|
if (callbackResult) {
|
2017-03-06 12:10:23 +00:00
|
|
|
INFO_LOG(SAVESTATE, "Verified save state system");
|
2016-05-28 04:25:05 +00:00
|
|
|
} else {
|
2017-03-06 12:10:23 +00:00
|
|
|
ERROR_LOG(SAVESTATE, "Save state system verification failed");
|
2016-05-28 04:25:05 +00:00
|
|
|
}
|
2012-12-27 19:28:12 +00:00
|
|
|
break;
|
|
|
|
|
2013-09-30 02:57:51 +00:00
|
|
|
case SAVESTATE_REWIND:
|
2017-03-06 12:10:23 +00:00
|
|
|
INFO_LOG(SAVESTATE, "Rewinding to recent savestate snapshot");
|
2013-09-30 02:57:51 +00:00
|
|
|
result = rewindStates.Restore();
|
|
|
|
if (result == CChunkFileReader::ERROR_NONE) {
|
2016-05-28 04:25:05 +00:00
|
|
|
callbackMessage = sc->T("Loaded State");
|
2013-09-30 02:57:51 +00:00
|
|
|
callbackResult = true;
|
2014-02-09 21:45:51 +00:00
|
|
|
hasLoadedState = true;
|
2013-09-30 02:57:51 +00:00
|
|
|
} else if (result == CChunkFileReader::ERROR_BROKEN_STATE) {
|
|
|
|
// Cripes. Good news is, we might have more. Let's try those too, better than a reset.
|
2013-09-30 03:20:20 +00:00
|
|
|
if (HandleFailure()) {
|
|
|
|
// Well, we did rewind, even if too much...
|
2016-05-28 04:25:05 +00:00
|
|
|
callbackMessage = sc->T("Loaded State");
|
2013-09-30 02:57:51 +00:00
|
|
|
callbackResult = true;
|
2014-02-09 21:45:51 +00:00
|
|
|
hasLoadedState = true;
|
2013-09-30 02:57:51 +00:00
|
|
|
} else {
|
2016-05-28 04:25:05 +00:00
|
|
|
callbackMessage = i18nLoadFailure;
|
2013-09-30 02:57:51 +00:00
|
|
|
callbackResult = false;
|
|
|
|
}
|
|
|
|
} else {
|
2016-05-28 04:25:05 +00:00
|
|
|
callbackMessage = i18nLoadFailure;
|
2013-09-30 02:57:51 +00:00
|
|
|
callbackResult = false;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
|
2014-12-21 07:14:46 +00:00
|
|
|
case SAVESTATE_SAVE_SCREENSHOT:
|
2016-09-25 23:35:43 +00:00
|
|
|
callbackResult = TakeGameScreenshot(op.filename.c_str(), SCREENSHOT_JPG, SCREENSHOT_DISPLAY);
|
2016-03-20 21:17:51 +00:00
|
|
|
if (!callbackResult) {
|
2017-03-06 12:10:23 +00:00
|
|
|
ERROR_LOG(SAVESTATE, "Failed to take a screenshot for the savestate! %s", op.filename.c_str());
|
2014-12-31 19:42:28 +00:00
|
|
|
}
|
2014-12-21 07:14:46 +00:00
|
|
|
break;
|
|
|
|
|
2012-12-27 19:28:12 +00:00
|
|
|
default:
|
2017-03-06 12:10:23 +00:00
|
|
|
ERROR_LOG(SAVESTATE, "Savestate failure: unknown operation type %d", op.type);
|
2013-09-15 04:19:10 +00:00
|
|
|
callbackResult = false;
|
2012-12-27 19:28:12 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2013-09-11 20:21:15 +00:00
|
|
|
if (op.callback)
|
2016-05-28 04:25:05 +00:00
|
|
|
op.callback(callbackResult, callbackMessage, op.cbUserData);
|
2012-12-27 19:28:12 +00:00
|
|
|
}
|
2014-06-14 21:55:16 +00:00
|
|
|
if (operations.size()) {
|
|
|
|
// Avoid triggering frame skipping due to slowdown
|
|
|
|
__DisplaySetWasPaused();
|
|
|
|
}
|
2012-12-27 19:28:12 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
void Init()
|
|
|
|
{
|
2013-01-02 20:00:10 +00:00
|
|
|
// Make sure there's a directory for save slots
|
|
|
|
pspFileSystem.MkDir("ms0:/PSP/PPSSPP_STATE");
|
2013-01-02 22:25:35 +00:00
|
|
|
|
2017-02-27 20:57:46 +00:00
|
|
|
std::lock_guard<std::mutex> guard(mutex);
|
2013-11-03 01:32:34 +00:00
|
|
|
rewindStates.Clear();
|
2014-02-09 21:45:51 +00:00
|
|
|
|
|
|
|
hasLoadedState = false;
|
2012-12-27 19:28:12 +00:00
|
|
|
}
|
2016-09-24 17:37:30 +00:00
|
|
|
|
|
|
|
void Shutdown()
|
|
|
|
{
|
2017-02-27 20:57:46 +00:00
|
|
|
std::lock_guard<std::mutex> guard(mutex);
|
2016-09-24 17:37:30 +00:00
|
|
|
rewindStates.Clear();
|
|
|
|
}
|
2012-12-27 19:28:12 +00:00
|
|
|
}
|