Files
ppsspp/Core/SaveState.cpp
2026-05-19 13:59:04 +02:00

999 lines
31 KiB
C++

// 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/.
#include <algorithm>
#include <vector>
#include <thread>
#include <mutex>
#include <string>
#include <set>
#include "Common/Data/Text/I18n.h"
#include "Common/Thread/ThreadUtil.h"
#include "Common/Data/Text/Parsers.h"
#include "Common/System/System.h"
#include "Common/File/FileUtil.h"
#include "Common/File/DirListing.h"
#include "Common/Serialize/Serializer.h"
#include "Common/Serialize/SerializeFuncs.h"
#include "Common/StringUtils.h"
#include "Common/TimeUtil.h"
#include "Core/SaveState.h"
#include "Core/Config.h"
#include "Core/Core.h"
#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"
#include "Core/HLE/sceNet.h"
#include "Core/HLE/ReplaceTables.h"
#include "Core/HLE/sceDisplay.h"
#include "Core/HLE/sceKernel.h"
#include "Core/HLE/sceUtility.h"
#include "Core/MemMap.h"
#include "Core/MIPS/JitCommon/JitBlockCache.h"
#include "Core/RetroAchievements.h"
#include "HW/MemoryStick.h"
#ifndef MOBILE_DEVICE
#include "Core/AVIDump.h"
#include "Core/HLE/__sceAudio.h"
#endif
// 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 {
// Used for "confirm exit if you haven't saved in a while"
double g_lastSaveTime = -1.0;
static bool needsProcess = false;
static bool needsRestart = false;
static std::mutex mutex;
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;
// Updated by Rescan. This is to avoid calling Exists a lot of times, which could be slow
// on some Android devices when we allow many save states.
static double g_lastRescan = 0.0;
static std::map<std::string, uint64_t, std::less<>> g_files; // the value is the mod-time.
struct SaveStart {
void DoState(PointerWrap &p);
};
enum class OperationType {
Save,
Load,
Verify,
Rewind,
};
struct Operation {
// The slot number is for visual purposes only. Set to -1 for operations where we don't display a message for example.
Operation(OperationType t, const Path &f, int slot_, Callback cb)
: type(t), path(f), callback(cb), slot(slot_) {}
OperationType type;
Path path;
Callback callback;
int slot;
};
static std::vector<Operation> g_pendingOperations;
int g_screenshotFailures;
CChunkFileReader::Error SaveToRam(std::vector<u8> &data) {
SaveStart state;
return CChunkFileReader::MeasureAndSavePtr(state, &data);
}
CChunkFileReader::Error LoadFromRam(std::vector<u8> &data, std::string *errorString) {
SaveStart state;
return CChunkFileReader::LoadPtr(&data[0], state, errorString);
}
// 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.
void SaveStart::DoState(PointerWrap &p) {
auto s = p.Section("SaveStart", 1, 3);
if (!s)
return;
if (s >= 2) {
// This only increments on save, of course.
++saveStateGeneration;
Do(p, saveStateGeneration);
// This saves the first git version to create this save state (or generation of save states.)
if (saveStateInitialGitVersion.empty())
saveStateInitialGitVersion = PPSSPP_GIT_VERSION;
Do(p, saveStateInitialGitVersion);
} else {
saveStateGeneration = 1;
}
if (s >= 3) {
// Keep track of savedata (not save states) too.
Do(p, saveDataGeneration);
} else {
saveDataGeneration = 0;
}
// Gotta do CoreTiming before HLE, but from v3 we've moved it after the memory stuff.
if (s <= 2) {
CoreTiming::DoState(p);
}
// Memory is a bit tricky when jit is enabled, since there's emuhacks in it.
// These must be saved before copying out memory and restored after.
auto savedReplacements = SaveAndClearReplacements();
if (MIPSComp::jit && p.mode == p.MODE_WRITE) {
std::lock_guard<std::recursive_mutex> guard(MIPSComp::jitLock);
if (MIPSComp::jit) {
std::vector<u32> savedBlocks;
savedBlocks = MIPSComp::jit->SaveAndClearEmuHackOps();
Memory::DoState(p);
MIPSComp::jit->RestoreSavedEmuHackOps(savedBlocks);
} else {
Memory::DoState(p);
}
} else {
Memory::DoState(p);
}
if (s >= 3) {
CoreTiming::DoState(p);
}
// Don't bother restoring if reading, we'll deal with that in KernelModuleDoState.
// In theory, different functions might have been runtime loaded in the state.
if (p.mode != p.MODE_READ) {
RestoreSavedReplacements(savedReplacements);
}
MemoryStick_DoState(p);
currentMIPS->DoState(p);
HLEDoState(p);
__KernelDoState(p);
Achievements::DoState(p);
// Kernel object destructors might close open files, so do the filesystem last.
pspFileSystem.DoState(p);
}
void Enqueue(const SaveState::Operation &op) {
if (!NetworkAllowSaveState()) {
return;
}
if (Achievements::HardcoreModeActive()) {
if (g_Config.bAchievementsSaveStateInHardcoreMode && ((op.type == SaveState::OperationType::Save))) {
// We allow saving in hardcore mode if this setting is on.
} else {
// Operation not allowed
return;
}
}
std::lock_guard<std::mutex> guard(mutex);
g_pendingOperations.push_back(op);
// Don't actually run it until next frame.
// It's possible there might be a duplicate but it won't hurt us.
needsProcess = true;
}
void Load(const Path &filename, int slot, Callback callback) {
if (!NetworkAllowSaveState()) {
return;
}
rewindStates.NotifyState();
if (coreState == CoreState::CORE_RUNTIME_ERROR)
Core_Break(BreakReason::SavestateLoad, 0);
Enqueue(Operation(OperationType::Load, filename, slot, callback));
}
void Save(const Path &filename, int slot, Callback callback) {
if (!NetworkAllowSaveState()) {
return;
}
rewindStates.NotifyState();
if (coreState == CoreState::CORE_RUNTIME_ERROR)
Core_Break(BreakReason::SavestateSave, 0);
Enqueue(Operation(OperationType::Save, filename, slot, callback));
}
void Verify(Callback callback) {
Enqueue(Operation(OperationType::Verify, Path(), -1, callback));
}
void Rewind(Callback callback) {
if (coreState == CoreState::CORE_RUNTIME_ERROR)
Core_Break(BreakReason::SavestateRewind, 0);
Enqueue(Operation(OperationType::Rewind, Path(), -1, callback));
}
bool CanRewind() {
return !rewindStates.Empty();
}
// Slot utilities
std::string AppendSlotTitle(const std::string &filename, const std::string &title) {
char slotChar = 0;
auto detectSlot = [&slotChar, filename](const std::string &ext) {
if (!endsWith(filename, std::string(".") + ext)) {
return false;
}
// Usually these are slots, let's check the slot # after the last '_'.
size_t slotNumPos = filename.find_last_of('_');
if (slotNumPos == filename.npos) {
return false;
}
const size_t extLength = ext.length() + 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 false;
}
slotChar = filename[slotNumPos + 1];
if (slotChar < '0' || slotChar > '8') {
return false;
}
// Change from zero indexed to human friendly.
slotChar++;
return true;
};
if (detectSlot(STATE_EXTENSION)) {
return StringFromFormat("%s (%c)", title.c_str(), slotChar);
}
if (detectSlot(UNDO_STATE_EXTENSION)) {
auto sy = GetI18NCategory(I18NCat::SYSTEM);
// Allow the number to be positioned where it makes sense.
std::string undo(sy->T("undo %c"));
return title + " (" + StringFromFormat(undo.c_str(), slotChar) + ")";
}
// Couldn't detect, use the filename.
return title + " (" + filename + ")";
}
std::string GetTitle(const Path &filename) {
std::string title;
if (CChunkFileReader::GetFileTitle(filename, &title) == CChunkFileReader::ERROR_NONE) {
if (title.empty()) {
return filename.GetFilename();
}
return AppendSlotTitle(filename.GetFilename(), title);
}
// The file can't be loaded - let's note that.
auto sy = GetI18NCategory(I18NCat::SYSTEM);
return filename.GetFilename() + " " + std::string(sy->T("(broken)"));
}
// TODO: gameFilename is ignored!
// This means that this must always be called in-game, otherwise it will not work!
//
// This particular ID string generation is specific for save states.
static std::string GenerateFullDiscId(const ParamSFOData &paramSFO) {
std::string discId = paramSFO.GetValueString("DISC_ID");
std::string discVer = paramSFO.GetValueString("DISC_VERSION");
if (discId.empty()) {
// Should never happen.
discId = g_paramSFO.GenerateFakeID(Path());
discVer = "1.00";
}
return StringFromFormat("%s_%s", discId.c_str(), discVer.c_str());
}
std::string GetGamePrefix(const ParamSFOData &paramSFO) {
return GenerateFullDiscId(paramSFO);
}
// The prefix is always based on GenerateFullDiscId. So we can find these by scanning, too.
std::string GenerateSaveSlotFilename(std::string_view gamePrefix, int slot, const char *extension) {
return StringFromFormat("%.*s_%d.%s", STR_VIEW(gamePrefix), slot, extension);
}
Path GenerateSaveSlotPath(std::string_view gamePrefix, int slot, const char *extension) {
std::string filename = GenerateSaveSlotFilename(gamePrefix, slot, extension);
return GetSysDirectory(DIRECTORY_SAVESTATE) / filename;
}
int GetCurrentSlot() {
return g_Config.iCurrentStateSlot;
}
void PrevSlot() {
g_Config.iCurrentStateSlot = (g_Config.iCurrentStateSlot - 1 + g_Config.iSaveStateSlotCount) % g_Config.iSaveStateSlotCount;
}
void NextSlot() {
g_Config.iCurrentStateSlot = (g_Config.iCurrentStateSlot + 1) % g_Config.iSaveStateSlotCount;
}
static void DeleteIfExists(const Path &fn) {
// Just avoiding error messages.
File::Delete(fn, true);
}
static void RenameIfExists(const Path &from, const Path &to) {
if (File::Exists(from)) {
File::Rename(from, to);
}
}
static void SwapIfExists(const Path &from, const Path &to) {
Path temp = from.WithExtraExtension(".tmp");
if (File::Exists(from)) {
File::Rename(from, temp);
File::Rename(to, from);
File::Rename(temp, to);
}
}
static void ScheduleSaveScreenshot(const Path &path) {
std::lock_guard<std::mutex> guard(mutex);
g_screenshotFailures = 0;
// Savestate thumbnails don't need to be bigger.
constexpr int maxResMultiplier = 2;
ScheduleScreenshot(path, ScreenshotFormat::JPG, ScreenshotType::Display, maxResMultiplier, [path](ScreenshotResult result) {
switch (result) {
case ScreenshotResult::ScreenshotNotPossible:
// Try again soon, for a short while.
WARN_LOG(Log::SaveState, "Failed to take a screenshot for the savestate! (%s) The savestate will lack an icon.", path.c_str());
if (coreState != CORE_STEPPING_CPU && g_screenshotFailures++ < SCREENSHOT_FAILURE_RETRIES) {
// Requeue for next frame (if we were stepping, no point, will just spam errors quickly).
ScheduleSaveScreenshot(path);
}
break;
case ScreenshotResult::FailedToWriteFile:
case ScreenshotResult::Success:
g_screenshotFailures = 0;
break;
}
});
}
void LoadSlot(std::string_view gamePrefix, int slot, Callback callback) {
if (!NetworkAllowSaveState()) {
return;
}
Path fn = GenerateSaveSlotPath(gamePrefix, slot, STATE_EXTENSION);
if (!fn.empty()) {
// This add only 1 extra state, should we just always enable it?
if (g_Config.bEnableStateUndo) {
Path backup = GetSysDirectory(DIRECTORY_SAVESTATE) / LOAD_UNDO_NAME;
std::string prefix(gamePrefix);
auto saveCallback = [prefix, fn, backup, slot, callback](Status status, std::string_view message, std::string_view metadata) {
if (status != Status::FAILURE) {
DeleteIfExists(backup);
File::Rename(backup.WithExtraExtension(".tmp"), backup);
g_Config.sStateLoadUndoGame = prefix;
g_Config.Save("Saving config for savestate last load undo");
} else {
ERROR_LOG(Log::SaveState, "Saving load undo state failed: %.*s", (int)message.size(), message.data());
}
Load(fn, slot, callback);
};
if (!backup.empty()) {
Save(backup.WithExtraExtension(".tmp"), LOAD_UNDO_SLOT, saveCallback);
} else {
ERROR_LOG(Log::SaveState, "Saving load undo state failed. Error in the file system.");
Load(fn, slot, callback);
}
} else {
Load(fn, slot, callback);
}
} else {
if (callback) {
auto sy = GetI18NCategory(I18NCat::SYSTEM);
callback(Status::FAILURE, sy->T("Failed to load state. Error in the file system."), "");
}
}
}
bool UndoLoad(std::string_view gamePrefix, Callback callback) {
if (!NetworkAllowSaveState()) {
return false;
}
if (g_Config.sStateLoadUndoGame != gamePrefix) {
if (callback) {
auto sy = GetI18NCategory(I18NCat::SYSTEM);
callback(Status::FAILURE, sy->T("Error: load undo state is from a different game"), "");
}
return false;
}
Path fn = GetSysDirectory(DIRECTORY_SAVESTATE) / LOAD_UNDO_NAME;
if (!fn.empty()) {
Load(fn, LOAD_UNDO_SLOT, callback);
return true;
} else {
if (callback) {
auto sy = GetI18NCategory(I18NCat::SYSTEM);
callback(Status::FAILURE, sy->T("Failed to load state for load undo. Error in the file system."), "");
}
return false;
}
}
void SaveSlot(std::string_view gamePrefix, int slot, Callback callback) {
if (!NetworkAllowSaveState()) {
return;
}
Path fn = GenerateSaveSlotPath(gamePrefix, slot, STATE_EXTENSION);
Path fnUndo = GenerateSaveSlotPath(gamePrefix, slot, UNDO_STATE_EXTENSION);
if (!fn.empty()) {
Path shot = GenerateSaveSlotPath(gamePrefix, slot, SCREENSHOT_EXTENSION);
std::string prefix(gamePrefix);
auto renameCallback = [fn, fnUndo, prefix, slot, callback](Status status, std::string_view message, std::string_view metadata) {
if (status != Status::FAILURE) {
if (g_Config.bEnableStateUndo) {
DeleteIfExists(fnUndo);
RenameIfExists(fn, fnUndo);
g_Config.sStateUndoLastSaveGame = prefix;
g_Config.iStateUndoLastSaveSlot = slot;
g_Config.Save("Saving config for savestate last save undo");
} else {
DeleteIfExists(fn);
}
File::Rename(fn.WithExtraExtension(".tmp"), fn);
}
if (callback) {
callback(status, message, "");
}
};
// Let's also create a screenshot.
if (g_Config.bEnableStateUndo) {
Path shotUndo = GenerateSaveSlotPath(gamePrefix, slot, UNDO_SCREENSHOT_EXTENSION);
DeleteIfExists(shotUndo);
RenameIfExists(shot, shotUndo);
}
ScheduleSaveScreenshot(shot);
Save(fn.WithExtraExtension(".tmp"), slot, renameCallback);
} else {
if (callback) {
auto sy = GetI18NCategory(I18NCat::SYSTEM);
callback(Status::FAILURE, sy->T("Failed to save state. Error in the file system."), "");
}
}
Rescan(gamePrefix);
}
bool UndoSaveSlot(std::string_view gamePrefix, int slot) {
if (!NetworkAllowSaveState()) {
return false;
}
Path fnUndo = GenerateSaveSlotPath(gamePrefix, slot, UNDO_STATE_EXTENSION);
// Do nothing if there's no undo.
if (File::Exists(fnUndo)) {
Path fn = GenerateSaveSlotPath(gamePrefix, slot, STATE_EXTENSION);
Path shot = GenerateSaveSlotPath(gamePrefix, slot, SCREENSHOT_EXTENSION);
Path shotUndo = GenerateSaveSlotPath(gamePrefix, slot, UNDO_SCREENSHOT_EXTENSION);
// Swap them so they can undo again to redo. Mistakes happen.
SwapIfExists(shotUndo, shot);
SwapIfExists(fnUndo, fn);
return true;
}
Rescan(gamePrefix);
return false;
}
void DeleteSlot(std::string_view gamePrefix, int slot) {
Path fn = GenerateSaveSlotPath(gamePrefix, slot, STATE_EXTENSION);
Path shot = GenerateSaveSlotPath(gamePrefix, slot, SCREENSHOT_EXTENSION);
if (File::Exists(fn)) {
DeleteIfExists(fn);
DeleteIfExists(shot);
}
Rescan(gamePrefix);
}
bool UndoLastSave(std::string_view gamePrefix) {
if (!NetworkAllowSaveState()) {
return false;
}
if (g_Config.sStateUndoLastSaveGame != gamePrefix)
return false;
bool retval = UndoSaveSlot(gamePrefix, g_Config.iStateUndoLastSaveSlot);
Rescan(gamePrefix);
return retval;
}
void Rescan(std::string_view gamePrefix) {
// Currently nothing to do here.
// On Android this would be important to rescan the save state directory.
std::vector<File::FileInfo> file;
std::string prefix(gamePrefix);
prefix += "_";
if (!File::GetFilesInDir(GetSysDirectory(DIRECTORY_SAVESTATE), &file, nullptr, 0, prefix)) {
ERROR_LOG(Log::System, "Failed to list savestate directory!");
return;
}
g_files.clear();
for (const auto &f : file) {
g_files[f.name] = f.mtime;
}
g_lastRescan = time_now_d();
}
static bool SaveStateFileExists(std::string_view gamePrefix, int slot, const char *extension) {
return g_files.find(GenerateSaveSlotFilename(gamePrefix, slot, extension)) != g_files.end();
}
bool HasSaveInSlot(std::string_view gamePrefix, int slot) {
return SaveStateFileExists(gamePrefix, slot, STATE_EXTENSION);
}
bool HasUndoSaveInSlot(std::string_view gamePrefix, int slot) {
return SaveStateFileExists(gamePrefix, slot, UNDO_STATE_EXTENSION);
}
bool HasUndoLastSave(std::string_view gamePrefix) {
if (g_Config.sStateUndoLastSaveGame != gamePrefix)
return false;
return HasUndoSaveInSlot(gamePrefix, g_Config.iStateUndoLastSaveSlot);
}
bool HasScreenshotInSlot(std::string_view gamePrefix, int slot) {
return SaveStateFileExists(gamePrefix, slot, SCREENSHOT_EXTENSION);
}
bool HasUndoLoad(std::string_view gamePrefix) {
Path fn = GetSysDirectory(DIRECTORY_SAVESTATE) / LOAD_UNDO_NAME;
return File::Exists(fn) && g_Config.sStateLoadUndoGame == gamePrefix;
}
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;
}
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;
}
bool operator ! (const tm &t1) {
if (t1.tm_year || t1.tm_mon || t1.tm_mday || t1.tm_hour || t1.tm_min || t1.tm_sec) return false;
return true;
}
int GetNewestSlot(std::string_view gamePrefix) {
int newestSlot = -1;
int64_t newestTime = 0;
for (int i = 0; i < g_Config.iSaveStateSlotCount; i++) {
auto iter = g_files.find(GenerateSaveSlotFilename(gamePrefix, i, STATE_EXTENSION));
if (iter != g_files.end()) {
int64_t mod = iter->second;
if (mod > newestTime) {
newestTime = mod;
newestSlot = i;
}
}
}
return newestSlot;
}
int GetOldestSlot(std::string_view gamePrefix) {
int oldestSlot = -1;
int64_t oldestTime = INT64_MAX;
for (int i = 0; i < g_Config.iSaveStateSlotCount; i++) {
auto iter = g_files.find(GenerateSaveSlotFilename(gamePrefix, i, STATE_EXTENSION));
if (iter != g_files.end()) {
int64_t mod = iter->second;
if (mod < oldestTime) {
oldestTime = mod;
oldestSlot = i;
}
}
}
return oldestSlot;
}
static std::string GetSaveFileDateAsString(const std::string &fn) {
auto iter = g_files.find(fn);
if (iter == g_files.end()) {
return "";
}
time_t t = iter->second;
tm time;
localtime_r((time_t*)&t, &time);
char buf[256];
// TODO: Use local time format instead of the configured PSP format?
// Americans and some others might not like ISO standard :)
switch (g_Config.iDateFormat) {
case PSP_SYSTEMPARAM_DATE_FORMAT_YYYYMMDD:
strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &time);
break;
case PSP_SYSTEMPARAM_DATE_FORMAT_MMDDYYYY:
strftime(buf, sizeof(buf), "%m-%d-%Y %H:%M:%S", &time);
break;
case PSP_SYSTEMPARAM_DATE_FORMAT_DDMMYYYY:
strftime(buf, sizeof(buf), "%d-%m-%Y %H:%M:%S", &time);
break;
default: // Should never happen
return "";
}
return buf;
}
std::string GetSlotDateAsString(std::string_view gamePrefix, int slot) {
std::string fn = GenerateSaveSlotFilename(gamePrefix, slot, STATE_EXTENSION);
return GetSaveFileDateAsString(fn);
}
std::vector<Operation> Flush() {
std::lock_guard<std::mutex> guard(mutex);
std::vector<Operation> copy = g_pendingOperations;
g_pendingOperations.clear();
return copy;
}
static bool HandleLoadFailure(bool wasRewinding, std::string *metadata) {
if (wasRewinding) {
WARN_LOG(Log::SaveState, "HandleLoadFailure - trying a rewind state.");
// Okay, first, let's give the next rewind state a shot - maybe we can at least not reset entirely.
// Actually, this seems like a bad thing - some systems can end up in bad states, like sceFont :(
CChunkFileReader::Error result;
do {
std::string errorString;
result = rewindStates.Restore(&errorString, metadata);
} while (result == CChunkFileReader::ERROR_BROKEN_STATE);
if (result == CChunkFileReader::ERROR_NONE) {
return true;
}
}
// We tried, our only remaining option is to reset the game.
needsRestart = true;
// Make sure we don't proceed to run anything yet.
coreState = CORE_NEXTFRAME;
return false;
}
bool HasLoadedState() {
return hasLoadedState;
}
bool IsStale() {
if (saveStateGeneration >= STALE_STATE_USES) {
return CoreTiming::GetGlobalTimeUs() > STALE_STATE_TIME;
}
return false;
}
bool IsOldVersion() {
if (saveStateInitialGitVersion.empty())
return false;
Version state(saveStateInitialGitVersion);
Version gitVer(PPSSPP_GIT_VERSION);
if (!state.IsValid() || !gitVer.IsValid())
return false;
return state < gitVer;
}
static Status TriggerLoadWarnings(std::string &callbackMessage) {
auto sc = GetI18NCategory(I18NCat::SCREEN);
if (g_Config.bHideStateWarnings)
return Status::SUCCESS;
if (IsStale()) {
// For anyone wondering why (too long to put on the screen in an osm):
// Using save states instead of saves simulates many hour play sessions.
// Sometimes this exposes game bugs that were rarely seen on real devices,
// because few people played on a real PSP for 10 hours straight.
callbackMessage = sc->T("Loaded. Save in game, restart, and load for less bugs.");
return Status::SUCCESS;
}
if (IsOldVersion()) {
// Save states also preserve bugs from old PPSSPP versions, so warn.
callbackMessage = sc->T("Loaded. Save in game, restart, and load for less bugs.");
return Status::SUCCESS;
}
// If the loaded state (saveDataGeneration) is older, the game may prevent saving again.
// This can happen with newer too, but ignore to/from 0 as a common likely safe case.
if (saveDataGeneration != lastSaveDataGeneration && saveDataGeneration != 0 && lastSaveDataGeneration != 0) {
if (saveDataGeneration < lastSaveDataGeneration)
callbackMessage = sc->T("Loaded. Game may refuse to save over newer savedata.");
else
callbackMessage = sc->T("Loaded. Game may refuse to save over different savedata.");
return Status::WARNING;
}
return Status::SUCCESS;
}
// 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.
void Process() {
rewindStates.Process();
if (!needsProcess)
return;
needsProcess = false;
if (!__KernelIsRunning()) {
ERROR_LOG(Log::SaveState, "Savestate failure: Unable to load without kernel, this should never happen.");
return;
}
std::vector<Operation> operations = Flush();
SaveStart state;
for (const auto &op : operations) {
CChunkFileReader::Error result;
Status callbackResult;
std::string callbackMessage;
std::string callbackMetadata;
std::string title;
auto sc = GetI18NCategory(I18NCat::SCREEN);
const char *i18nLoadFailure = sc->T_cstr("Failed to load state");
const char *i18nSaveFailure = sc->T_cstr("Failed to save state");
std::string slot_prefix = op.slot >= 0 ? StringFromFormat("(%d) ", op.slot + 1) : "";
std::string errorString;
switch (op.type) {
case OperationType::Load:
INFO_LOG(Log::SaveState, "Loading state from '%s'", op.path.c_str());
// Use the state's latest version as a guess for saveStateInitialGitVersion.
result = CChunkFileReader::Load(op.path, &saveStateInitialGitVersion, state, &errorString);
if (result == CChunkFileReader::ERROR_NONE) {
callbackMessage = op.slot != LOAD_UNDO_SLOT ? sc->T("Loaded State") : sc->T("State load undone");
callbackResult = TriggerLoadWarnings(callbackMessage);
callbackMetadata = SaveState::GetSaveFileDateAsString(op.path.GetFilename());
hasLoadedState = true;
Core_ResetException();
if (!slot_prefix.empty())
callbackMessage = slot_prefix + callbackMessage;
#ifndef MOBILE_DEVICE
if (g_Config.bSaveLoadResetsAVdumping) {
if (g_Config.bDumpFrames) {
AVIDump::Stop();
AVIDump::Start(PSP_CoreParameter().renderWidth, PSP_CoreParameter().renderHeight);
}
if (g_Config.bDumpAudio) {
WAVDump::Reset();
}
}
#endif
g_lastSaveTime = time_now_d();
} else if (result == CChunkFileReader::ERROR_BROKEN_STATE) {
HandleLoadFailure(false, &callbackMetadata);
callbackMessage = std::string(i18nLoadFailure) + ": " + errorString;
ERROR_LOG(Log::SaveState, "Load state failure: %s", errorString.c_str());
callbackResult = Status::FAILURE;
} else {
callbackMessage = sc->T(errorString, i18nLoadFailure);
callbackResult = Status::FAILURE;
}
break;
case OperationType::Save:
INFO_LOG(Log::SaveState, "Saving state to '%s'", op.path.c_str());
title = g_paramSFO.GetValueString("TITLE");
if (title.empty()) {
// Homebrew title
title = PSP_CoreParameter().fileToStart.ToVisualString();
std::size_t lslash = title.find_last_of('/');
title = title.substr(lslash + 1);
}
result = CChunkFileReader::Save(op.path, title, PPSSPP_GIT_VERSION, state);
if (result == CChunkFileReader::ERROR_NONE) {
callbackMessage = slot_prefix + std::string(sc->T("Saved State"));
callbackResult = Status::SUCCESS;
#ifndef MOBILE_DEVICE
if (g_Config.bSaveLoadResetsAVdumping) {
if (g_Config.bDumpFrames) {
AVIDump::Stop();
AVIDump::Start(PSP_CoreParameter().renderWidth, PSP_CoreParameter().renderHeight);
}
if (g_Config.bDumpAudio) {
WAVDump::Reset();
}
}
#endif
g_lastSaveTime = time_now_d();
} else if (result == CChunkFileReader::ERROR_BROKEN_STATE) {
// TODO: What else might we want to do here? This should be very unusual.
callbackMessage = i18nSaveFailure;
ERROR_LOG(Log::SaveState, "Save state failure");
callbackResult = Status::FAILURE;
} else {
callbackMessage = i18nSaveFailure;
callbackResult = Status::FAILURE;
}
break;
case OperationType::Verify:
{
int tempResult = CChunkFileReader::Verify(state) == CChunkFileReader::ERROR_NONE;
callbackResult = tempResult ? Status::SUCCESS : Status::FAILURE;
if (tempResult) {
INFO_LOG(Log::SaveState, "Verified save state system");
} else {
ERROR_LOG(Log::SaveState, "Save state system verification failed");
}
break;
}
case OperationType::Rewind:
INFO_LOG(Log::SaveState, "Rewinding to recent savestate snapshot");
result = rewindStates.Restore(&errorString, &callbackMetadata);
if (result == CChunkFileReader::ERROR_NONE) {
callbackMessage = sc->T("Loaded State");
callbackResult = Status::SUCCESS;
hasLoadedState = true;
Core_ResetException();
} else if (result == CChunkFileReader::ERROR_BROKEN_STATE) {
// Cripes. Good news is, we might have more. Let's try those too, better than a reset.
if (HandleLoadFailure(true, &callbackMetadata)) {
// Well, we did rewind, even if too much...
callbackMessage = sc->T("Loaded State");
callbackResult = Status::SUCCESS;
hasLoadedState = true;
Core_ResetException();
} else {
callbackMessage = std::string(i18nLoadFailure) + ": " + errorString;
callbackResult = Status::FAILURE;
}
} else {
callbackMessage = std::string(i18nLoadFailure) + ": " + errorString;
callbackResult = Status::FAILURE;
}
break;
default:
ERROR_LOG(Log::SaveState, "Savestate failure: unknown operation type %d", (int)op.type);
callbackResult = Status::FAILURE;
break;
}
if (op.callback) {
op.callback(callbackResult, callbackMessage, callbackMetadata);
}
}
if (operations.size()) {
// Avoid triggering frame skipping due to slowdown
__DisplaySetWasPaused();
}
}
void NotifySaveData() {
saveDataGeneration++;
lastSaveDataGeneration = saveDataGeneration;
}
bool PollRestartNeeded() {
if (needsRestart) {
needsRestart = false;
return true;
}
return false;
}
void Init() {
// Make sure there's a directory for save slots
File::CreateFullPath(GetSysDirectory(DIRECTORY_SAVESTATE));
std::lock_guard<std::mutex> guard(mutex);
rewindStates.Clear();
hasLoadedState = false;
saveStateGeneration = 0;
saveDataGeneration = 0;
lastSaveDataGeneration = 0;
saveStateInitialGitVersion.clear();
g_lastSaveTime = time_now_d();
}
void Shutdown() {
std::lock_guard<std::mutex> guard(mutex);
rewindStates.Clear();
}
double SecondsSinceLastSavestate() {
if (g_lastSaveTime < 0) {
return -1.0;
} else {
return time_now_d() - g_lastSaveTime;
}
}
} // namespace SaveState