diff --git a/CMakeLists.txt b/CMakeLists.txt index 007b6c846f..5ecab0a13d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/Common/File/AndroidStorage.cpp b/Common/File/AndroidStorage.cpp index 2798cb88f3..1163d50426 100644 --- a/Common/File/AndroidStorage.cpp +++ b/Common/File/AndroidStorage.cpp @@ -39,7 +39,7 @@ void Android_RegisterStorageCallbacks(JNIEnv * env, jobject obj) { openContentUri = env->GetStaticMethodID(localClass, "openContentUri", "(Landroid/app/Activity;Ljava/lang/String;Ljava/lang/String;)I"); _dbg_assert_(openContentUri); - listContentUriDir = env->GetStaticMethodID(localClass, "listContentUriDir", "(Landroid/app/Activity;Ljava/lang/String;)[Ljava/lang/String;"); + listContentUriDir = env->GetStaticMethodID(localClass, "listContentUriDir", "(Landroid/app/Activity;Ljava/lang/String;Ljava/lang/String;)[Ljava/lang/String;"); _dbg_assert_(listContentUriDir); contentUriCreateDirectory = env->GetStaticMethodID(localClass, "contentUriCreateDirectory", "(Landroid/app/Activity;Ljava/lang/String;Ljava/lang/String;)I"); _dbg_assert_(contentUriCreateDirectory); @@ -265,7 +265,8 @@ std::vector Android_ListContentUri(const std::string &uri, const double start = time_now_d(); jstring param = env->NewStringUTF(uri.c_str()); - jobject retval = env->CallStaticObjectMethod(g_classContentUri, listContentUriDir, g_nativeActivity, param); + jstring filenamePrefix = prefix.empty() ? nullptr : env->NewStringUTF(prefix.c_str()); + jobject retval = env->CallStaticObjectMethod(g_classContentUri, listContentUriDir, g_nativeActivity, param, filenamePrefix); jobjectArray fileList = (jobjectArray)retval; std::vector items; diff --git a/Common/File/FileUtil.cpp b/Common/File/FileUtil.cpp index 7a7c748008..292d0da7d2 100644 --- a/Common/File/FileUtil.cpp +++ b/Common/File/FileUtil.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; } diff --git a/Common/File/FileUtil.h b/Common/File/FileUtil.h index 56e07d76ca..aabbb0e34f 100644 --- a/Common/File/FileUtil.h +++ b/Common/File/FileUtil.h @@ -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. diff --git a/Common/UI/View.cpp b/Common/UI/View.cpp index e0e3891d0a..75fe18ba44 100644 --- a/Common/UI/View.cpp +++ b/Common/UI/View.cpp @@ -1070,7 +1070,7 @@ const FontStyle *SimpleTextView::ComputeStyle(const UIContext &dc) const { } } -const FontStyle *TextView::GetTextStyle(const UIContext &dc, TextSize size) { +const FontStyle *GetTextStyle(const UIContext &dc, TextSize size) { const FontStyle *style = &dc.GetTheme().uiFont; switch (size) { case TextSize::Tiny: diff --git a/Common/UI/View.h b/Common/UI/View.h index 4e02bc30ba..39108d6db4 100644 --- a/Common/UI/View.h +++ b/Common/UI/View.h @@ -1047,8 +1047,6 @@ public: bool CanBeFocused() const override { return focusable_; } private: - static const FontStyle *GetTextStyle(const UIContext &dc, TextSize size); - std::string text_; int textAlign_; uint32_t textColor_; @@ -1190,5 +1188,6 @@ bool IsTabRightKey(const KeyInput &key); // TODO: Doesn't really belong here. void DrawIconShine(UIContext &dc, const Bounds &bounds, float shine, bool animated); +const FontStyle *GetTextStyle(const UIContext &dc, TextSize size); } // namespace diff --git a/Core/Core.vcxproj b/Core/Core.vcxproj index 5224acdeca..56af3f33f4 100644 --- a/Core/Core.vcxproj +++ b/Core/Core.vcxproj @@ -926,6 +926,7 @@ + @@ -1295,6 +1296,7 @@ + diff --git a/Core/Core.vcxproj.filters b/Core/Core.vcxproj.filters index 17491dc488..6e08989137 100644 --- a/Core/Core.vcxproj.filters +++ b/Core/Core.vcxproj.filters @@ -1396,6 +1396,9 @@ Util + + Core + @@ -2256,6 +2259,9 @@ Util + + Core + diff --git a/Core/HLE/ErrorCodes.h b/Core/HLE/ErrorCodes.h index fbe2738b42..9d345ed2de 100644 --- a/Core/HLE/ErrorCodes.h +++ b/Core/HLE/ErrorCodes.h @@ -648,4 +648,8 @@ enum PSPErrorCode : u32 { SCE_NET_ADHOCCTL_ERROR_CHANNEL_NOT_MATCH = 0x80410b11, SCE_NET_ADHOCCTL_ERROR_TOO_MANY_HANDLERS = 0x80410b12, SCE_NET_ADHOCCTL_ERROR_STACKSIZE_TOO_SHORT = 0x80410B13, + + // Error codes matching JPCSP + SCE_NPDRM_ERROR_NO_K_LICENSEE_SET = 0x80550901, + SCE_NPDRM_ERROR_INVALID_FILE = 0x80550902, }; diff --git a/Core/HLE/sceIo.cpp b/Core/HLE/sceIo.cpp index ce7b93cd8e..4db3820ff5 100644 --- a/Core/HLE/sceIo.cpp +++ b/Core/HLE/sceIo.cpp @@ -2589,7 +2589,7 @@ int __IoIoctl(u32 id, u32 cmd, u32 indataPtr, u32 inlen, u32 outdataPtr, u32 out key_ptr = nullptr; } - DEBUG_LOG(Log::sceIo, "Decrypting PGD DRM files"); + INFO_LOG(Log::sceIo, "ioctl: Decrypting PGD DRM files from '%s'", f->fullpath.c_str()); pspFileSystem.SeekFile(f->handle, (s32)f->pgd_offset, FILEMOVE_BEGIN); pspFileSystem.ReadFile(f->handle, pgd_header, 0x90); KirkState *kirk = __ChnnlsvKirkState(); diff --git a/Core/HLE/sceKernel.cpp b/Core/HLE/sceKernel.cpp index 23ac6800b3..71f66b5f0b 100644 --- a/Core/HLE/sceKernel.cpp +++ b/Core/HLE/sceKernel.cpp @@ -1633,6 +1633,10 @@ const char *KernelErrorToString(u32 err) { case SCE_NET_ADHOCCTL_ERROR_TOO_MANY_HANDLERS: return "SCE_NET_ADHOCCTL_ERROR_TOO_MANY_HANDLERS"; case SCE_NET_ADHOCCTL_ERROR_STACKSIZE_TOO_SHORT: return "SCE_NET_ADHOCCTL_ERROR_STACKSIZE_TOO_SHORT"; + // NPDRM + case SCE_NPDRM_ERROR_NO_K_LICENSEE_SET: return "SCE_NPDRM_ERROR_NO_K_LICENSEE_SET"; + case SCE_NPDRM_ERROR_INVALID_FILE: return "SCE_NPDRM_ERROR_INVALID_FILE"; + default: return nullptr; } diff --git a/Core/HLE/scePspNpDrm_user.cpp b/Core/HLE/scePspNpDrm_user.cpp index 6e7317ecc2..fad8c9e402 100644 --- a/Core/HLE/scePspNpDrm_user.cpp +++ b/Core/HLE/scePspNpDrm_user.cpp @@ -1,5 +1,4 @@ -#include "scePspNpDrm_user.h" - +#include "Core/HLE/scePspNpDrm_user.h" #include "Core/MemMapHelpers.h" #include "Core/HLE/HLE.h" #include "Core/HLE/FunctionWrappers.h" @@ -13,10 +12,6 @@ static const int PSP_NPDRM_LICENSEE_KEY_LENGTH = 0x10; static u8 licenseeKey[PSP_NPDRM_LICENSEE_KEY_LENGTH]; static bool isLicenseeKeySet = false; -// Error codes matching JPCSP -static const int ERROR_NPDRM_NO_K_LICENSEE_SET = 0x80550901; -static const int ERROR_NPDRM_INVALID_FILE = 0x80550902; - // Check if the file is an encrypted EDAT file by reading the magic number static bool isEncrypted(u32 edataFd) { // Check for "\0PSPEDAT" magic number @@ -67,7 +62,7 @@ static int sceNpDrmSetLicenseeKey(u32 npDrmKeyAddr) { isLicenseeKeySet = true; return hleLogInfo(Log::sceIo, 0); } - return hleLogError(Log::sceIo, ERROR_NPDRM_INVALID_FILE, "Invalid address"); + return hleLogError(Log::sceIo, SCE_NPDRM_ERROR_INVALID_FILE, "Invalid address"); } static int sceNpDrmClearLicenseeKey() { @@ -85,7 +80,7 @@ static int sceNpDrmEdataSetupKey(u32 edataFd) { // Return an error if the key has not been set. // Note: An empty key is valid, as long as it was set with sceNpDrmSetLicenseeKey. if (!isLicenseeKeySet) { - return hleLogError(Log::sceIo, ERROR_NPDRM_NO_K_LICENSEE_SET, "Licensee key not set"); + return hleLogError(Log::sceIo, SCE_NPDRM_ERROR_NO_K_LICENSEE_SET, "Licensee key not set"); } // Get file info to validate the file descriptor @@ -129,8 +124,7 @@ static int sceNpDrmEdataGetDataSize(u32 edataFd) { } static int sceNpDrmOpen() { - ERROR_LOG(Log::sceIo, "UNIMPL: sceNpDrmOpen()"); - return 0; + return hleLogError(Log::sceIo, 0, "UNIMPL: sceNpDrmOpen()"); } const HLEFunction sceNpDrm[] = { diff --git a/Core/SaveState.cpp b/Core/SaveState.cpp index 09a6a887c4..e0cba7d934 100644 --- a/Core/SaveState.cpp +++ b/Core/SaveState.cpp @@ -26,6 +26,7 @@ #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" @@ -37,6 +38,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 +48,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 +59,44 @@ // 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; - struct SaveStart - { - void DoState(PointerWrap &p); - }; +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; - enum OperationType - { - SAVESTATE_SAVE, - SAVESTATE_LOAD, - SAVESTATE_VERIFY, - SAVESTATE_REWIND, - SAVESTATE_SAVE_SCREENSHOT, - }; +struct SaveStart { + void DoState(PointerWrap &p); +}; + +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 +109,8 @@ double g_lastSaveTime = -1.0; int slot; }; + static std::vector pending; + CChunkFileReader::Error SaveToRam(std::vector &data) { SaveStart state; return CChunkFileReader::MeasureAndSavePtr(state, &data); @@ -99,248 +121,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 guard(lock_); - - int n = next_++ % size_; - if ((next_ % size_) == first_) - ++first_; - - std::vector *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 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 buffer; - LockedDecompress(buffer, states_[n], bases_[baseMapping_[n]]); - CChunkFileReader::Error error = LoadFromRam(buffer, errorString); - rewindLastTime_ = time_now_d(); - return error; - } - - void ScheduleCompress(std::vector *result, const std::vector *state, const std::vector *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 &result, const std::vector &state, const std::vector &base) - { - std::lock_guard 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 &result, const std::vector &compressed, const std::vector &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 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 StateBuffer; - - int first_ = 0; - int next_ = 0; - int size_; - - std::vector states_; - StateBuffer bases_[2]; - std::vector baseMapping_; - std::mutex lock_; - std::thread compressThread_; - std::vector buffer_; - - int base_ = -1; - int baseUsage_ = 0; - - double rewindLastTime_ = 0.0f; - }; - - static bool needsProcess = false; - static bool needsRestart = false; - static std::vector 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 +190,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 +219,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 +230,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 +243,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() { @@ -530,9 +315,13 @@ double g_lastSaveTime = -1.0; return filename.GetFilename() + " " + std::string(sy->T("(broken)")); } - std::string GenerateFullDiscId(const Path &gameFilename) { - std::string discId = g_paramSFO.GetValueString("DISC_ID"); - std::string discVer = g_paramSFO.GetValueString("DISC_VERSION"); + // 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 ¶mSFO) { + 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()); @@ -541,32 +330,31 @@ double g_lastSaveTime = -1.0; return StringFromFormat("%s_%s", discId.c_str(), discVer.c_str()); } - Path GenerateSaveSlotFilename(const Path &gameFilename, int slot, const char *extension) - { - std::string filename = StringFromFormat("%s_%d.%s", GenerateFullDiscId(gameFilename).c_str(), slot, extension); + std::string GetGamePrefix(const ParamSFOData ¶mSFO) { + return GenerateFullDiscId(paramSFO); + } + + // The prefix is always based on GenerateFullDiscId. So we can find these by scanning, too. + Path GenerateSaveSlotFilename(std::string_view gamePrefix, int slot, const char *extension) { + std::string filename = StringFromFormat("%.*s_%d.%s", STR_VIEW(gamePrefix), slot, extension); 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) { @@ -584,13 +372,12 @@ double g_lastSaveTime = -1.0; } } - void LoadSlot(const Path &gameFilename, int slot, Callback callback) - { + void LoadSlot(std::string_view gamePrefix, int slot, Callback callback) { if (!NetworkAllowSaveState()) { return; } - Path fn = GenerateSaveSlotFilename(gameFilename, slot, STATE_EXTENSION); + Path fn = GenerateSaveSlotFilename(gamePrefix, slot, STATE_EXTENSION); if (!fn.empty()) { // This add only 1 extra state, should we just always enable it? if (g_Config.bEnableStateUndo) { @@ -600,7 +387,7 @@ double g_lastSaveTime = -1.0; if (status != Status::FAILURE) { DeleteIfExists(backup); File::Rename(backup.WithExtraExtension(".tmp"), backup); - g_Config.sStateLoadUndoGame = GenerateFullDiscId(gameFilename); + g_Config.sStateLoadUndoGame = gamePrefix; 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()); @@ -625,12 +412,12 @@ double g_lastSaveTime = -1.0; } } - bool UndoLoad(const Path &gameFilename, Callback callback) { + bool UndoLoad(std::string_view gamePrefix, Callback callback) { if (!NetworkAllowSaveState()) { return false; } - if (g_Config.sStateLoadUndoGame != GenerateFullDiscId(gameFilename)) { + 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")); @@ -651,21 +438,21 @@ double g_lastSaveTime = -1.0; } } - void SaveSlot(const Path &gameFilename, int slot, Callback callback) { + void SaveSlot(std::string_view gamePrefix, int slot, Callback callback) { if (!NetworkAllowSaveState()) { return; } - Path fn = GenerateSaveSlotFilename(gameFilename, slot, STATE_EXTENSION); - Path fnUndo = GenerateSaveSlotFilename(gameFilename, slot, UNDO_STATE_EXTENSION); + Path fn = GenerateSaveSlotFilename(gamePrefix, slot, STATE_EXTENSION); + Path fnUndo = GenerateSaveSlotFilename(gamePrefix, slot, UNDO_STATE_EXTENSION); if (!fn.empty()) { - Path shot = GenerateSaveSlotFilename(gameFilename, slot, SCREENSHOT_EXTENSION); + Path shot = GenerateSaveSlotFilename(gamePrefix, slot, SCREENSHOT_EXTENSION); auto renameCallback = [=](Status status, std::string_view message) { if (status != Status::FAILURE) { if (g_Config.bEnableStateUndo) { DeleteIfExists(fnUndo); RenameIfExists(fn, fnUndo); - g_Config.sStateUndoLastSaveGame = GenerateFullDiscId(gameFilename); + g_Config.sStateUndoLastSaveGame = gamePrefix; g_Config.iStateUndoLastSaveSlot = slot; g_Config.Save("Saving config for savestate last save undo"); } else { @@ -679,7 +466,7 @@ double g_lastSaveTime = -1.0; }; // Let's also create a screenshot. if (g_Config.bEnableStateUndo) { - Path shotUndo = GenerateSaveSlotFilename(gameFilename, slot, UNDO_SCREENSHOT_EXTENSION); + Path shotUndo = GenerateSaveSlotFilename(gamePrefix, slot, UNDO_SCREENSHOT_EXTENSION); DeleteIfExists(shotUndo); RenameIfExists(shot, shotUndo); } @@ -693,18 +480,18 @@ double g_lastSaveTime = -1.0; } } - bool UndoSaveSlot(const Path &gameFilename, int slot) { + bool UndoSaveSlot(std::string_view gamePrefix, int slot) { if (!NetworkAllowSaveState()) { return false; } - Path fnUndo = GenerateSaveSlotFilename(gameFilename, slot, UNDO_STATE_EXTENSION); + Path fnUndo = GenerateSaveSlotFilename(gamePrefix, slot, UNDO_STATE_EXTENSION); // Do nothing if there's no undo. if (File::Exists(fnUndo)) { - Path fn = GenerateSaveSlotFilename(gameFilename, slot, STATE_EXTENSION); - Path shot = GenerateSaveSlotFilename(gameFilename, slot, SCREENSHOT_EXTENSION); - Path shotUndo = GenerateSaveSlotFilename(gameFilename, slot, UNDO_SCREENSHOT_EXTENSION); + Path fn = GenerateSaveSlotFilename(gamePrefix, slot, STATE_EXTENSION); + Path shot = GenerateSaveSlotFilename(gamePrefix, slot, SCREENSHOT_EXTENSION); + Path shotUndo = GenerateSaveSlotFilename(gamePrefix, slot, UNDO_SCREENSHOT_EXTENSION); // Swap them so they can undo again to redo. Mistakes happen. SwapIfExists(shotUndo, shot); SwapIfExists(fnUndo, fn); @@ -714,9 +501,9 @@ double g_lastSaveTime = -1.0; return false; } - void DeleteSlot(const Path &gameFilename, int slot) { - Path fn = GenerateSaveSlotFilename(gameFilename, slot, STATE_EXTENSION); - Path shot = GenerateSaveSlotFilename(gameFilename, slot, SCREENSHOT_EXTENSION); + void DeleteSlot(std::string_view gamePrefix, int slot) { + Path fn = GenerateSaveSlotFilename(gamePrefix, slot, STATE_EXTENSION); + Path shot = GenerateSaveSlotFilename(gamePrefix, slot, SCREENSHOT_EXTENSION); if (File::Exists(fn)) { DeleteIfExists(fn); @@ -724,47 +511,57 @@ double g_lastSaveTime = -1.0; } } - bool UndoLastSave(const Path &gameFilename) { + bool UndoLastSave(std::string_view gamePrefix) { if (!NetworkAllowSaveState()) { return false; } - if (g_Config.sStateUndoLastSaveGame != GenerateFullDiscId(gameFilename)) + if (g_Config.sStateUndoLastSaveGame != gamePrefix) return false; - return UndoSaveSlot(gameFilename, g_Config.iStateUndoLastSaveSlot); + return UndoSaveSlot(gamePrefix, g_Config.iStateUndoLastSaveSlot); } - bool HasSaveInSlot(const Path &gameFilename, int slot) - { - Path fn = GenerateSaveSlotFilename(gameFilename, slot, STATE_EXTENSION); + 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; + + std::string prefix(gamePrefix); + prefix += "_"; + + File::GetFilesInDir(GetSysDirectory(DIRECTORY_SAVESTATE), &file, nullptr, 0, prefix); + + for (const auto &f : file) { + INFO_LOG(Log::System, "%s", f.name.c_str()); + } + } + + bool HasSaveInSlot(std::string_view gamePrefix, int slot) { + Path fn = GenerateSaveSlotFilename(gamePrefix, slot, STATE_EXTENSION); return File::Exists(fn); } - bool HasUndoSaveInSlot(const Path &gameFilename, int slot) - { - Path fn = GenerateSaveSlotFilename(gameFilename, slot, UNDO_STATE_EXTENSION); + bool HasUndoSaveInSlot(std::string_view gamePrefix, int slot) { + Path fn = GenerateSaveSlotFilename(gamePrefix, slot, UNDO_STATE_EXTENSION); return File::Exists(fn); } - bool HasUndoLastSave(const Path &gameFilename) - { - if (g_Config.sStateUndoLastSaveGame != GenerateFullDiscId(gameFilename)) + bool HasUndoLastSave(std::string_view gamePrefix) { + if (g_Config.sStateUndoLastSaveGame != gamePrefix) return false; - return HasUndoSaveInSlot(gameFilename, g_Config.iStateUndoLastSaveSlot); + return HasUndoSaveInSlot(gamePrefix, g_Config.iStateUndoLastSaveSlot); } - bool HasScreenshotInSlot(const Path &gameFilename, int slot) - { - Path fn = GenerateSaveSlotFilename(gameFilename, slot, SCREENSHOT_EXTENSION); + bool HasScreenshotInSlot(std::string_view gamePrefix, int slot) { + Path fn = GenerateSaveSlotFilename(gamePrefix, slot, SCREENSHOT_EXTENSION); return File::Exists(fn); } - bool HasUndoLoad(const Path &gameFilename) - { + bool HasUndoLoad(std::string_view gamePrefix) { Path fn = GetSysDirectory(DIRECTORY_SAVESTATE) / LOAD_UNDO_NAME; - return File::Exists(fn) && g_Config.sStateLoadUndoGame == GenerateFullDiscId(gameFilename); + return File::Exists(fn) && g_Config.sStateLoadUndoGame == gamePrefix; } bool operator < (const tm &t1, const tm &t2) { @@ -804,11 +601,11 @@ double g_lastSaveTime = -1.0; return true; } - int GetNewestSlot(const Path &gameFilename) { + int GetNewestSlot(std::string_view gamePrefix) { int newestSlot = -1; tm newestDate = {0}; for (int i = 0; i < Config::iSaveStateSlotCount; i++) { - Path fn = GenerateSaveSlotFilename(gameFilename, i, STATE_EXTENSION); + Path fn = GenerateSaveSlotFilename(gamePrefix, i, STATE_EXTENSION); if (File::Exists(fn)) { tm time; bool success = File::GetModifTime(fn, time); @@ -821,11 +618,11 @@ double g_lastSaveTime = -1.0; return newestSlot; } - int GetOldestSlot(const Path &gameFilename) { + int GetOldestSlot(std::string_view gamePrefix) { int oldestSlot = -1; tm oldestDate = {0}; for (int i = 0; i < Config::iSaveStateSlotCount; i++) { - Path fn = GenerateSaveSlotFilename(gameFilename, i, STATE_EXTENSION); + Path fn = GenerateSaveSlotFilename(gamePrefix, i, STATE_EXTENSION); if (File::Exists(fn)) { tm time; bool success = File::GetModifTime(fn, time); @@ -838,8 +635,8 @@ double g_lastSaveTime = -1.0; return oldestSlot; } - std::string GetSlotDateAsString(const Path &gameFilename, int slot) { - Path fn = GenerateSaveSlotFilename(gameFilename, slot, STATE_EXTENSION); + std::string GetSlotDateAsString(std::string_view gamePrefix, int slot) { + Path fn = GenerateSaveSlotFilename(gamePrefix, slot, STATE_EXTENSION); tm time; if (File::GetModifTime(fn, time)) { char buf[256]; @@ -862,8 +659,7 @@ double g_lastSaveTime = -1.0; return ""; } - std::vector Flush() - { + std::vector Flush() { std::lock_guard guard(mutex); std::vector copy = pending; pending.clear(); @@ -950,6 +746,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 +755,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 +778,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 +815,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 +851,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 +863,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 +889,7 @@ double g_lastSaveTime = -1.0; } break; - case SAVESTATE_SAVE_SCREENSHOT: + case OperationType::SaveScreenshot: { _dbg_assert_(!op.callback); @@ -1158,8 +954,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 +970,7 @@ double g_lastSaveTime = -1.0; g_lastSaveTime = time_now_d(); } - void Shutdown() - { + void Shutdown() { std::lock_guard guard(mutex); rewindStates.Clear(); } diff --git a/Core/SaveState.h b/Core/SaveState.h index 9073cba3cf..f2356b1f5d 100644 --- a/Core/SaveState.h +++ b/Core/SaveState.h @@ -25,6 +25,9 @@ #include "Common/File/Path.h" #include "Common/Serialize/Serializer.h" +class ParamSFOData; +#undef Process + namespace SaveState { enum class Status { FAILURE, @@ -33,12 +36,7 @@ namespace SaveState { }; typedef std::function 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(); @@ -46,29 +44,37 @@ namespace SaveState { // Cycle through the 5 savestate slots void PrevSlot(); void NextSlot(); - void SaveSlot(const Path &gameFilename, int slot, Callback callback); - void LoadSlot(const Path &gameFilename, int slot, Callback callback); - bool UndoSaveSlot(const Path &gameFilename, int slot); - bool UndoLastSave(const Path &gameFilename); - bool UndoLoad(const Path &gameFilename, Callback callback); - void DeleteSlot(const Path &gameFilename, int slot); + + std::string GetGamePrefix(const ParamSFOData ¶mSFO); + + // Run the various actions directly. + void SaveSlot(std::string_view gamePrefix, int slot, Callback callback); + void LoadSlot(std::string_view gamePrefix, int slot, Callback callback); + bool UndoSaveSlot(std::string_view gamePrefix, int slot); + bool UndoLastSave(std::string_view gamePrefix); + bool UndoLoad(std::string_view gamePrefix, Callback callback); + void DeleteSlot(std::string_view gamePrefix, int slot); + + // This will rescan the save state directory for files associated with the specified game. + // Note that when we have many save states, this is really important on Android. + void Rescan(std::string_view gamePrefix); // Checks whether there's an existing save in the specified slot. - bool HasSaveInSlot(const Path &gameFilename, int slot); - bool HasUndoSaveInSlot(const Path &gameFilename, int slot); - bool HasUndoLastSave(const Path &gameFilename); - bool HasUndoLoad(const Path &gameFilename); - bool HasScreenshotInSlot(const Path &gameFilename, int slot); + bool HasSaveInSlot(std::string_view gamePrefix, int slot); + bool HasUndoSaveInSlot(std::string_view gamePrefix, int slot); + bool HasUndoLastSave(std::string_view gamePrefix); + bool HasUndoLoad(std::string_view gamePrefix); + bool HasScreenshotInSlot(std::string_view gamePrefix, int slot); + // Just returns the current slot from config. int GetCurrentSlot(); // Returns -1 if there's no oldest/newest slot. - int GetNewestSlot(const Path &gameFilename); - int GetOldestSlot(const Path &gameFilename); + int GetNewestSlot(std::string_view gamePrefix); + int GetOldestSlot(std::string_view gamePrefix); - std::string GetSlotDateAsString(const Path &gameFilename, int slot); - std::string GenerateFullDiscId(const Path &gameFilename); - Path GenerateSaveSlotFilename(const Path &gameFilename, int slot, const char *extension); + std::string GetSlotDateAsString(std::string_view gamePrefix, int slot); + Path GenerateSaveSlotFilename(std::string_view gamePrefix, int slot, const char *extension); std::string GetTitle(const Path &filename); diff --git a/Core/SaveStateRewind.cpp b/Core/SaveStateRewind.cpp new file mode 100644 index 0000000000..414886d436 --- /dev/null +++ b/Core/SaveStateRewind.cpp @@ -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 guard(lock_); + + int n = next_++ % size_; + if ((next_ % size_) == first_) + ++first_; + + std::vector *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 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 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 *result, const std::vector *state, const std::vector *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 &result, const std::vector &state, const std::vector &base) { + std::lock_guard 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 &result, const std::vector &compressed, const std::vector &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 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 diff --git a/Core/SaveStateRewind.h b/Core/SaveStateRewind.h new file mode 100644 index 0000000000..a20837ddd3 --- /dev/null +++ b/Core/SaveStateRewind.h @@ -0,0 +1,69 @@ +#pragma once + +#include +#include +#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 *result, const std::vector *state, const std::vector *base); + void Compress(std::vector &result, const std::vector &state, const std::vector &base); + void LockedDecompress(std::vector &result, const std::vector &compressed, const std::vector &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 StateBuffer; + + int first_ = 0; + int next_ = 0; + int size_; + + std::vector states_; + StateBuffer bases_[2]; + std::vector baseMapping_; + std::mutex lock_; + std::thread compressThread_; + std::vector buffer_; + + int base_ = -1; + int baseUsage_ = 0; + + double rewindLastTime_ = 0.0f; +}; + +} // namespace SaveState diff --git a/Qt/mainwindow.cpp b/Qt/mainwindow.cpp index 6d63d21e02..948d797304 100644 --- a/Qt/mainwindow.cpp +++ b/Qt/mainwindow.cpp @@ -168,13 +168,13 @@ static void SaveStateActionFinished(SaveState::Status status, std::string_view m void MainWindow::qlstateAct() { Path gamePath = PSP_CoreParameter().fileToStart; - SaveState::LoadSlot(gamePath, 0, SaveStateActionFinished); + SaveState::LoadSlot(SaveState::GetGamePrefix(g_paramSFO), 0, SaveStateActionFinished); } void MainWindow::qsstateAct() { Path gamePath = PSP_CoreParameter().fileToStart; - SaveState::SaveSlot(gamePath, 0, SaveStateActionFinished); + SaveState::SaveSlot(SaveState::GetGamePrefix(g_paramSFO), 0, SaveStateActionFinished); } void MainWindow::lstateAct() diff --git a/UI/EmuScreen.cpp b/UI/EmuScreen.cpp index 9b20938092..07c64c8953 100644 --- a/UI/EmuScreen.cpp +++ b/UI/EmuScreen.cpp @@ -821,12 +821,12 @@ void EmuScreen::onVKey(VirtKey virtualKeyCode, bool down) { case VIRTKEY_SAVE_STATE: if (down && !Achievements::WarnUserIfHardcoreModeActive(true) && !NetworkWarnUserIfOnlineAndCantSavestate() && !bootPending_) { - SaveState::SaveSlot(gamePath_, g_Config.iCurrentStateSlot, &AfterSaveStateAction); + SaveState::SaveSlot(SaveState::GetGamePrefix(g_paramSFO), g_Config.iCurrentStateSlot, &AfterSaveStateAction); } break; case VIRTKEY_LOAD_STATE: if (down && !Achievements::WarnUserIfHardcoreModeActive(false) && !NetworkWarnUserIfOnlineAndCantSavestate() && !bootPending_) { - SaveState::LoadSlot(gamePath_, g_Config.iCurrentStateSlot, &AfterSaveStateAction); + SaveState::LoadSlot(SaveState::GetGamePrefix(g_paramSFO), g_Config.iCurrentStateSlot, &AfterSaveStateAction); } break; case VIRTKEY_PREVIOUS_SLOT: @@ -1481,9 +1481,11 @@ void EmuScreen::update() { if (saveStateSlot_ != currentSlot) { saveStateSlot_ = currentSlot; + const std::string gamePrefix = SaveState::GetGamePrefix(g_paramSFO); + Path fn; - if (SaveState::HasSaveInSlot(gamePath_, currentSlot)) { - fn = SaveState::GenerateSaveSlotFilename(gamePath_, currentSlot, SaveState::SCREENSHOT_EXTENSION); + if (SaveState::HasSaveInSlot(gamePrefix, currentSlot)) { + fn = SaveState::GenerateSaveSlotFilename(gamePrefix, currentSlot, SaveState::SCREENSHOT_EXTENSION); } saveStatePreview_->SetFilename(fn); @@ -2035,23 +2037,25 @@ void EmuScreen::AutoLoadSaveState() { int autoSlot = -1; + std::string gamePrefix = SaveState::GetGamePrefix(g_paramSFO); + //check if save state has save, if so, load switch (g_Config.iAutoLoadSaveState) { case (int)AutoLoadSaveState::OFF: // "AutoLoad Off" return; case (int)AutoLoadSaveState::OLDEST: // "Oldest Save" - autoSlot = SaveState::GetOldestSlot(gamePath_); + autoSlot = SaveState::GetOldestSlot(gamePrefix); break; case (int)AutoLoadSaveState::NEWEST: // "Newest Save" - autoSlot = SaveState::GetNewestSlot(gamePath_); + autoSlot = SaveState::GetNewestSlot(gamePrefix); break; default: // try the specific save state slot specified - autoSlot = (SaveState::HasSaveInSlot(gamePath_, g_Config.iAutoLoadSaveState - 3)) ? (g_Config.iAutoLoadSaveState - 3) : -1; + autoSlot = (SaveState::HasSaveInSlot(gamePrefix, g_Config.iAutoLoadSaveState - 3)) ? (g_Config.iAutoLoadSaveState - 3) : -1; break; } if (g_Config.iAutoLoadSaveState && autoSlot != -1) { - SaveState::LoadSlot(gamePath_, autoSlot, [this, autoSlot](SaveState::Status status, std::string_view message) { + SaveState::LoadSlot(gamePrefix, autoSlot, [this, autoSlot](SaveState::Status status, std::string_view message) { AfterSaveStateAction(status, message); auto sy = GetI18NCategory(I18NCat::SYSTEM); std::string msg = std::string(sy->T("Auto Load Savestate")) + ": " + StringFromFormat("%d", autoSlot); diff --git a/UI/PauseScreen.cpp b/UI/PauseScreen.cpp index d4ec91688b..8b5a717028 100644 --- a/UI/PauseScreen.cpp +++ b/UI/PauseScreen.cpp @@ -78,8 +78,8 @@ static void AfterSaveStateAction(SaveState::Status status, std::string_view mess class ScreenshotViewScreen : public UI::PopupScreen { public: - ScreenshotViewScreen(const Path &filename, std::string title, int slot, Path gamePath) - : PopupScreen(title), filename_(filename), slot_(slot), gamePath_(gamePath), title_(title) {} // PopupScreen will translate Back on its own + ScreenshotViewScreen(const Path &screenshotFilename, std::string_view saveStatePrefix, std::string_view title, int slot, Path gamePath) + : PopupScreen(title), screenshotFilename_(screenshotFilename), saveStatePrefix_(saveStatePrefix), slot_(slot), gamePath_(gamePath), title_(title) {} // PopupScreen will translate Back on its own int GetSlot() const { return slot_; @@ -100,7 +100,7 @@ protected: ScrollView *scroll = new ScrollView(ORIENT_VERTICAL, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, 1.0f)); LinearLayout *content = new LinearLayout(ORIENT_VERTICAL); Margins contentMargins(10, 0); - content->Add(new AsyncImageFileView(filename_, IS_KEEP_ASPECT, new LinearLayoutParams(480, 272, contentMargins)))->SetCanBeFocused(false); + content->Add(new AsyncImageFileView(screenshotFilename_, IS_KEEP_ASPECT, new LinearLayoutParams(480, 272, contentMargins)))->SetCanBeFocused(false); GridLayoutSettings gridsettings(240, 64, 5); gridsettings.fillCells = true; @@ -108,7 +108,7 @@ protected: Choice *back = new Choice(di->T("Back")); - const bool hasUndo = SaveState::HasUndoSaveInSlot(gamePath_, slot_); + const bool hasUndo = SaveState::HasUndoSaveInSlot(saveStatePrefix_, slot_); const bool undoEnabled = g_Config.bEnableStateUndo; Choice *undoButton = nullptr; @@ -138,8 +138,9 @@ private: void OnUndoState(UI::EventParams &e); void OnDeleteState(UI::EventParams &e); - Path filename_; + Path screenshotFilename_; Path gamePath_; + std::string saveStatePrefix_; std::string title_; int slot_; }; @@ -147,7 +148,7 @@ private: void ScreenshotViewScreen::OnSaveState(UI::EventParams &e) { if (!NetworkWarnUserIfOnlineAndCantSavestate()) { g_Config.iCurrentStateSlot = slot_; - SaveState::SaveSlot(gamePath_, slot_, &AfterSaveStateAction); + SaveState::SaveSlot(saveStatePrefix_, slot_, &AfterSaveStateAction); TriggerFinish(DR_OK); //OK will close the pause screen as well } } @@ -155,14 +156,14 @@ void ScreenshotViewScreen::OnSaveState(UI::EventParams &e) { void ScreenshotViewScreen::OnLoadState(UI::EventParams &e) { if (!NetworkWarnUserIfOnlineAndCantSavestate()) { g_Config.iCurrentStateSlot = slot_; - SaveState::LoadSlot(gamePath_, slot_, &AfterSaveStateAction); + SaveState::LoadSlot(saveStatePrefix_, slot_, &AfterSaveStateAction); TriggerFinish(DR_OK); } } void ScreenshotViewScreen::OnUndoState(UI::EventParams &e) { if (!NetworkWarnUserIfOnlineAndCantSavestate()) { - SaveState::UndoSaveSlot(gamePath_, slot_); + SaveState::UndoSaveSlot(saveStatePrefix_, slot_); TriggerFinish(DR_CANCEL); } } @@ -180,7 +181,7 @@ void ScreenshotViewScreen::OnDeleteState(UI::EventParams &e) { screenManager()->push(new UI::MessagePopupScreen(title, message, di->T("Delete"), di->T("Cancel"), [=](bool result) { if (result) { - SaveState::DeleteSlot(gamePath_, slot_); + SaveState::DeleteSlot(saveStatePrefix_, slot_); TriggerFinish(DR_CANCEL); } })); @@ -188,7 +189,7 @@ void ScreenshotViewScreen::OnDeleteState(UI::EventParams &e) { class SaveSlotView : public UI::LinearLayout { public: - SaveSlotView(const Path &gamePath, int slot, UI::LayoutParams *layoutParams = nullptr); + SaveSlotView(std::string_view saveStatePrefix, int slot, UI::LayoutParams *layoutParams = nullptr); void GetContentDimensions(const UIContext &dc, float &w, float &h) const override { w = 500; h = 90; @@ -205,12 +206,13 @@ public: } std::string GetScreenshotTitle() const { - return SaveState::GetSlotDateAsString(gamePath_, slot_); + return SaveState::GetSlotDateAsString(saveStatePrefix_, slot_); } UI::Event OnStateLoaded; UI::Event OnStateSaved; UI::Event OnScreenshotClicked; + UI::Event OnSelected; private: void OnSaveState(UI::EventParams &e); @@ -220,19 +222,25 @@ private: UI::Button *loadStateButton_ = nullptr; int slot_; - Path gamePath_; + std::string saveStatePrefix_; Path screenshotFilename_; }; -SaveSlotView::SaveSlotView(const Path &gameFilename, int slot, UI::LayoutParams *layoutParams) : UI::LinearLayout(ORIENT_HORIZONTAL, layoutParams), slot_(slot), gamePath_(gameFilename) { +SaveSlotView::SaveSlotView(std::string_view saveStatePrefix, int slot, UI::LayoutParams *layoutParams) : UI::LinearLayout(ORIENT_HORIZONTAL, layoutParams), slot_(slot), saveStatePrefix_(saveStatePrefix) { using namespace UI; - screenshotFilename_ = SaveState::GenerateSaveSlotFilename(gamePath_, slot, SaveState::SCREENSHOT_EXTENSION); + screenshotFilename_ = SaveState::GenerateSaveSlotFilename(saveStatePrefix_, slot, SaveState::SCREENSHOT_EXTENSION); std::string number = StringFromFormat("%d", slot + 1); Add(new Spacer(5)); - Add(new TextView(number, new LinearLayoutParams(40.0f, WRAP_CONTENT, 0.0f, Gravity::G_VCENTER)))->SetBig(true); + // TEMP HACK: use some other view, like a Choice, themed differently to enable keyboard access to selection. + ClickableTextView *numberView = Add(new ClickableTextView(number, new LinearLayoutParams(40.0f, WRAP_CONTENT, 0.0f, Gravity::G_VCENTER))); + numberView->SetBig(true); + numberView->OnClick.Add([this](UI::EventParams &e) { + e.v = this; + OnSelected.Trigger(e); + }); AsyncImageFileView *fv = Add(new AsyncImageFileView(screenshotFilename_, IS_DEFAULT, new UI::LayoutParams(82 * 2, 47 * 2))); @@ -256,13 +264,13 @@ SaveSlotView::SaveSlotView(const Path &gameFilename, int slot, UI::LayoutParams OnScreenshotClicked.Trigger(e); }); - if (SaveState::HasSaveInSlot(gamePath_, slot)) { + if (SaveState::HasSaveInSlot(saveStatePrefix_, slot)) { if (!Achievements::HardcoreModeActive()) { loadStateButton_ = buttons->Add(new Button(pa->T("Load State"), new LinearLayoutParams(0.0, Gravity::G_VCENTER))); loadStateButton_->OnClick.Handle(this, &SaveSlotView::OnLoadState); } - std::string dateStr = SaveState::GetSlotDateAsString(gamePath_, slot_); + std::string dateStr = SaveState::GetSlotDateAsString(saveStatePrefix_, slot_); if (!dateStr.empty()) { TextView *dateView = new TextView(dateStr, new LinearLayoutParams(0.0, Gravity::G_VCENTER)); dateView->SetSmall(true); @@ -284,7 +292,7 @@ void SaveSlotView::Draw(UIContext &dc) { void SaveSlotView::OnLoadState(UI::EventParams &e) { if (!NetworkWarnUserIfOnlineAndCantSavestate()) { g_Config.iCurrentStateSlot = slot_; - SaveState::LoadSlot(gamePath_, slot_, &AfterSaveStateAction); + SaveState::LoadSlot(saveStatePrefix_, slot_, &AfterSaveStateAction); UI::EventParams e2{}; e2.v = this; OnStateLoaded.Trigger(e2); @@ -294,7 +302,7 @@ void SaveSlotView::OnLoadState(UI::EventParams &e) { void SaveSlotView::OnSaveState(UI::EventParams &e) { if (!NetworkWarnUserIfOnlineAndCantSavestate()) { g_Config.iCurrentStateSlot = slot_; - SaveState::SaveSlot(gamePath_, slot_, &AfterSaveStateAction); + SaveState::SaveSlot(saveStatePrefix_, slot_, &AfterSaveStateAction); UI::EventParams e2{}; e2.v = this; OnStateSaved.Trigger(e2); @@ -335,6 +343,7 @@ GamePauseScreen::GamePauseScreen(const Path &filename, bool bootPending) // So we can tell if something blew up while on the pause screen. std::string assertStr = "PauseScreen: " + filename.GetFilename(); SetExtraAssertInfo(assertStr.c_str()); + saveStatePrefix_ = SaveState::GetGamePrefix(g_paramSFO); } GamePauseScreen::~GamePauseScreen() { @@ -366,26 +375,41 @@ void GamePauseScreen::CreateSavestateControls(UI::LinearLayout *leftColumnItems) leftColumnItems->SetSpacing(10.0); for (int i = 0; i < Config::iSaveStateSlotCount; i++) { - SaveSlotView *slot = leftColumnItems->Add(new SaveSlotView(gamePath_, i, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, Gravity::G_HCENTER, Margins(0,0,0,0)))); + SaveSlotView *slot = leftColumnItems->Add(new SaveSlotView(saveStatePrefix_, i, new LinearLayoutParams(FILL_PARENT, WRAP_CONTENT, Gravity::G_HCENTER, Margins(0,0,0,0)))); slot->OnStateLoaded.Handle(this, &GamePauseScreen::OnState); slot->OnStateSaved.Handle(this, &GamePauseScreen::OnState); - slot->OnScreenshotClicked.Handle(this, &GamePauseScreen::OnScreenshotClicked); + slot->OnScreenshotClicked.Add([this](UI::EventParams &e) { + SaveSlotView *v = static_cast(e.v); + int slot = v->GetSlot(); + g_Config.iCurrentStateSlot = v->GetSlot(); + if (SaveState::HasSaveInSlot(saveStatePrefix_, slot)) { + Path fn = v->GetScreenshotFilename(); + std::string title = v->GetScreenshotTitle(); + Screen *screen = new ScreenshotViewScreen(fn, saveStatePrefix_, title, v->GetSlot(), gamePath_); + screenManager()->push(screen); + } + }); + slot->OnSelected.Add([this](UI::EventParams &e) { + SaveSlotView *v = static_cast(e.v); + g_Config.iCurrentStateSlot = v->GetSlot(); + RecreateViews(); + }); } leftColumnItems->Add(new Spacer(0.0)); LinearLayout *buttonRow = leftColumnItems->Add(new LinearLayout(ORIENT_HORIZONTAL, new LinearLayoutParams(Margins(10, 0, 0, 0)))); if (g_Config.bEnableStateUndo && !Achievements::HardcoreModeActive() && NetworkAllowSaveState()) { - UI::Choice *loadUndoButton = buttonRow->Add(new Choice(pa->T("Undo last load"))); - loadUndoButton->SetEnabled(SaveState::HasUndoLoad(gamePath_)); + UI::Choice *loadUndoButton = buttonRow->Add(new Choice(pa->T("Undo last load"), ImageID("I_NAVIGATE_BACK"))); + loadUndoButton->SetEnabled(SaveState::HasUndoLoad(saveStatePrefix_)); loadUndoButton->OnClick.Handle(this, &GamePauseScreen::OnLoadUndo); - UI::Choice *saveUndoButton = buttonRow->Add(new Choice(pa->T("Undo last save"))); - saveUndoButton->SetEnabled(SaveState::HasUndoLastSave(gamePath_)); + UI::Choice *saveUndoButton = buttonRow->Add(new Choice(pa->T("Undo last save"), ImageID("I_NAVIGATE_BACK"))); + saveUndoButton->SetEnabled(SaveState::HasUndoLastSave(saveStatePrefix_)); saveUndoButton->OnClick.Handle(this, &GamePauseScreen::OnLastSaveUndo); } if (g_Config.iRewindSnapshotInterval > 0 && !Achievements::HardcoreModeActive() && NetworkAllowSaveState()) { - UI::Choice *rewindButton = buttonRow->Add(new Choice(pa->T("Rewind"))); + UI::Choice *rewindButton = buttonRow->Add(new Choice(pa->T("Rewind"), ImageID("I_REWIND"))); rewindButton->SetEnabled(SaveState::CanRewind()); rewindButton->OnClick.Handle(this, &GamePauseScreen::OnRewind); } @@ -721,18 +745,6 @@ void GamePauseScreen::dialogFinished(const Screen *dialog, DialogResult dr) { } } -void GamePauseScreen::OnScreenshotClicked(UI::EventParams &e) { - SaveSlotView *v = static_cast(e.v); - int slot = v->GetSlot(); - g_Config.iCurrentStateSlot = v->GetSlot(); - if (SaveState::HasSaveInSlot(gamePath_, slot)) { - Path fn = v->GetScreenshotFilename(); - std::string title = v->GetScreenshotTitle(); - Screen *screen = new ScreenshotViewScreen(fn, title, v->GetSlot(), gamePath_); - screenManager()->push(screen); - } -} - int GetUnsavedProgressSeconds() { const double timeSinceSaveState = SaveState::SecondsSinceLastSavestate(); const double timeSinceGameSave = SecondsSinceLastGameSave(); @@ -811,13 +823,13 @@ void GamePauseScreen::OnRewind(UI::EventParams &e) { } void GamePauseScreen::OnLoadUndo(UI::EventParams &e) { - SaveState::UndoLoad(gamePath_, &AfterSaveStateAction); + SaveState::UndoLoad(saveStatePrefix_, &AfterSaveStateAction); TriggerFinish(DR_CANCEL); } void GamePauseScreen::OnLastSaveUndo(UI::EventParams &e) { - SaveState::UndoLastSave(gamePath_); + SaveState::UndoLastSave(saveStatePrefix_); RecreateViews(); } diff --git a/UI/PauseScreen.h b/UI/PauseScreen.h index 27039c0596..8cad4a9997 100644 --- a/UI/PauseScreen.h +++ b/UI/PauseScreen.h @@ -53,8 +53,6 @@ private: void OnLoadUndo(UI::EventParams &e); void OnLastSaveUndo(UI::EventParams &e); - void OnScreenshotClicked(UI::EventParams &e); - void OnCreateConfig(UI::EventParams &e); void OnDeleteConfig(UI::EventParams &e); @@ -77,6 +75,8 @@ private: bool lastDNSConfigLoaded_ = false; bool bootPending_ = false; + + std::string saveStatePrefix_; }; std::string GetConfirmExitMessage(); diff --git a/UWP/CoreUWP/CoreUWP.vcxproj b/UWP/CoreUWP/CoreUWP.vcxproj index 8fc5b8a974..4d243f5ce9 100644 --- a/UWP/CoreUWP/CoreUWP.vcxproj +++ b/UWP/CoreUWP/CoreUWP.vcxproj @@ -309,6 +309,7 @@ + @@ -616,6 +617,7 @@ + diff --git a/UWP/CoreUWP/CoreUWP.vcxproj.filters b/UWP/CoreUWP/CoreUWP.vcxproj.filters index 816635bf1e..5a476d27ab 100644 --- a/UWP/CoreUWP/CoreUWP.vcxproj.filters +++ b/UWP/CoreUWP/CoreUWP.vcxproj.filters @@ -411,6 +411,7 @@ + @@ -683,6 +684,7 @@ + diff --git a/Windows/MainWindowMenu.cpp b/Windows/MainWindowMenu.cpp index 18669924f2..4a50cb104d 100644 --- a/Windows/MainWindowMenu.cpp +++ b/Windows/MainWindowMenu.cpp @@ -335,7 +335,7 @@ namespace MainWindow { RemoveMenu(saveStateSlots, 0, MF_BYPOSITION); } - auto dt = GetI18NCategory(I18NCat::DESKTOPUI); + auto di = GetI18NCategory(I18NCat::DIALOG); // Add new items for (int i = 0; i < Config::iSaveStateSlotCount; ++i) { std::string number = StringFromFormat("%d", i + 1); @@ -343,7 +343,7 @@ namespace MainWindow { // Add an accelerator for the first 10 slots. number = "&" + number; } - std::string label = ApplySafeSubstitutions(dt->T("Slot %1"), number); + std::string label = ApplySafeSubstitutions(di->T("Slot %1"), number); AppendMenu( saveStateSlots, MF_STRING, @@ -613,7 +613,7 @@ namespace MainWindow { case ID_FILE_QUICKLOADSTATE: if (!Achievements::WarnUserIfHardcoreModeActive(false) && !NetworkWarnUserIfOnlineAndCantSavestate()) { SetCursor(LoadCursor(0, IDC_WAIT)); - SaveState::LoadSlot(PSP_CoreParameter().fileToStart, g_Config.iCurrentStateSlot, SaveStateActionFinished); + SaveState::LoadSlot(SaveState::GetGamePrefix(g_paramSFO), g_Config.iCurrentStateSlot, SaveStateActionFinished); } break; @@ -622,7 +622,7 @@ namespace MainWindow { if (!Achievements::WarnUserIfHardcoreModeActive(false) && !NetworkWarnUserIfOnlineAndCantSavestate()) { if (!KeyMap::PspButtonHasMappings(VIRTKEY_LOAD_STATE)) { SetCursor(LoadCursor(0, IDC_WAIT)); - SaveState::LoadSlot(PSP_CoreParameter().fileToStart, g_Config.iCurrentStateSlot, SaveStateActionFinished); + SaveState::LoadSlot(SaveState::GetGamePrefix(g_paramSFO), g_Config.iCurrentStateSlot, SaveStateActionFinished); } } break; @@ -631,7 +631,7 @@ namespace MainWindow { { if (!Achievements::WarnUserIfHardcoreModeActive(true) && !NetworkWarnUserIfOnlineAndCantSavestate()) { SetCursor(LoadCursor(0, IDC_WAIT)); - SaveState::SaveSlot(PSP_CoreParameter().fileToStart, g_Config.iCurrentStateSlot, SaveStateActionFinished); + SaveState::SaveSlot(SaveState::GetGamePrefix(g_paramSFO), g_Config.iCurrentStateSlot, SaveStateActionFinished); } break; } @@ -642,7 +642,7 @@ namespace MainWindow { if (!KeyMap::PspButtonHasMappings(VIRTKEY_SAVE_STATE)) { SetCursor(LoadCursor(0, IDC_WAIT)); - SaveState::SaveSlot(PSP_CoreParameter().fileToStart, g_Config.iCurrentStateSlot, SaveStateActionFinished); + SaveState::SaveSlot(SaveState::GetGamePrefix(g_paramSFO), g_Config.iCurrentStateSlot, SaveStateActionFinished); break; } } diff --git a/android/jni/Android.mk b/android/jni/Android.mk index 33ad6789e2..10b64d6717 100644 --- a/android/jni/Android.mk +++ b/android/jni/Android.mk @@ -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 \ diff --git a/android/src/org/ppsspp/ppsspp/ContentUri.java b/android/src/org/ppsspp/ppsspp/ContentUri.java index c0e557c446..b25093def9 100644 --- a/android/src/org/ppsspp/ppsspp/ContentUri.java +++ b/android/src/org/ppsspp/ppsspp/ContentUri.java @@ -19,6 +19,7 @@ import androidx.documentfile.provider.DocumentFile; import java.io.File; import java.util.ArrayList; +import java.util.Locale; import java.util.UUID; public class ContentUri { @@ -65,7 +66,7 @@ public class ContentUri { DocumentsContract.Document.COLUMN_LAST_MODIFIED }; - private static String cursorToString(Cursor c) { + private static String cursorToString(Cursor c, String prefixLowercase) { final int flags = c.getInt(2); // Filter out any virtual or partial nonsense. // There's a bunch of potentially-interesting flags here btw, @@ -74,9 +75,16 @@ public class ContentUri { if ((flags & (DocumentsContract.Document.FLAG_PARTIAL | DocumentsContract.Document.FLAG_VIRTUAL_DOCUMENT)) != 0) { return null; } + final String documentName = c.getString(0); + if (prefixLowercase != null && !prefixLowercase.isEmpty()) { + // Filter by prefix, case insensitive. + if (!documentName.toLowerCase(Locale.ROOT).startsWith(prefixLowercase)) { + return null; + } + } + final String mimeType = c.getString(3); final boolean isDirectory = mimeType.equals(DocumentsContract.Document.MIME_TYPE_DIR); - final String documentName = c.getString(0); final long size = isDirectory ? 0 : c.getLong(1); final long lastModified = c.getLong(4); @@ -160,7 +168,7 @@ public class ContentUri { // uestions/42186820/documentfile-is-very-slow @Keep @SuppressWarnings("unused") - public static String[] listContentUriDir(Activity activity, String uriString) { + public static String[] listContentUriDir(Activity activity, String uriString, String prefix) { try { Uri uri = Uri.parse(uriString); final ContentResolver resolver = activity.getContentResolver(); @@ -176,12 +184,16 @@ public class ContentUri { DocumentsContract.Document.COLUMN_LAST_MODIFIED // index 4 }; + if (prefix != null) { + prefix = prefix.toLowerCase(Locale.ROOT); + } + try (Cursor c = resolver.query(childrenUri, projection, null, null, null)) { if (c == null) { return new String[]{"X"}; } while (c.moveToNext()) { - String str = cursorToString(c); + String str = cursorToString(c, prefix); if (str != null) { listing.add(str); } @@ -354,7 +366,7 @@ public class ContentUri { try (Cursor c = activity.getContentResolver().query(Uri.parse(fileName), projection, null, null, null)) { if (c != null && c.moveToFirst()) { - return cursorToString(c); + return cursorToString(c, null); } else { return null; } diff --git a/assets/lang/ar_AE.ini b/assets/lang/ar_AE.ini index bfea3cbe01..1dfe99bcd0 100644 --- a/assets/lang/ar_AE.ini +++ b/assets/lang/ar_AE.ini @@ -281,7 +281,6 @@ Savestate Slot = Saves&tate slot Screen Scaling Filter = Scr&een scaling filter Show Debug Statistics = ‎إظهار حالة التصحيح Show FPS Counter = ‎&أظهر عداد الإطارات -Slot %1 = فتحة %1 Smart 2D texture filtering = Smart 2D texture filtering Stop = ‎&توقف Switch UMD = ‎تغيير UMD @@ -484,6 +483,7 @@ Settings = Settings Shift = ‎شفت Show in folder = ‎أظهر في المجلد Skip = Skip +Slot %1 = فتحة %1 Snap = Snap Space = ‎مسافة SSID = SSID diff --git a/assets/lang/az_AZ.ini b/assets/lang/az_AZ.ini index 2ba0b357aa..41725e1d98 100644 --- a/assets/lang/az_AZ.ini +++ b/assets/lang/az_AZ.ini @@ -276,7 +276,6 @@ Show Debug Statistics = Yolaqoyuş Durumlarını &Göstər Show FPS Counter = &FPS Sayğacını Göstər Skip Number of Frames = Kadr buraxma sayı Skip Percent of FPS = FPS Buraxma Yüzdəsi -Slot %1 = Yuva %1 Smart 2D texture filtering = Ağıllı 2D toxuma süzgüsü Stop = &Dayan Switch UMD = UMD'ni Dəyiş @@ -480,6 +479,7 @@ Settings = Quruluşlar Shift = Shift Show in folder = Qovluqda göstər Skip = Burax +Slot %1 = Yuva %1 Snap = Oturuşma Space = Ara SSID = SSID diff --git a/assets/lang/be_BY.ini b/assets/lang/be_BY.ini index 415c072c1e..5b232c2c15 100644 --- a/assets/lang/be_BY.ini +++ b/assets/lang/be_BY.ini @@ -273,7 +273,6 @@ Savestate Slot = Saves&tate slot Screen Scaling Filter = Scr&een scaling filter Show Debug Statistics = Show debu&g statistics Show FPS Counter = Show &FPS counter -Slot %1 = Слот %1 Smart 2D texture filtering = Smart 2D texture filtering Stop = &Stop Switch UMD = Switch UMD @@ -476,6 +475,7 @@ Settings = Налады Shift = Shift Show in folder = Паказаць у тэчцы Skip = Прапусціць +Slot %1 = Слот %1 Snap = Snap Space = Прабел SSID = SSID diff --git a/assets/lang/bg_BG.ini b/assets/lang/bg_BG.ini index b761104f2e..8ded18d764 100644 --- a/assets/lang/bg_BG.ini +++ b/assets/lang/bg_BG.ini @@ -273,7 +273,6 @@ Savestate Slot = Брой на запазени състояния Screen Scaling Filter = Scr&een Scaling Filter Show Debug Statistics = Покажи Debu&g Statistics Show FPS Counter = Покажи &FPS -Slot %1 = Слот %1 Smart 2D texture filtering = Smart 2D texture filtering Stop = &Stop Switch UMD = Смени UMD @@ -476,6 +475,7 @@ Settings = Настройки Shift = Shift Show in folder = Покажи в папка Skip = Пропускане +Slot %1 = Слот %1 Snap = Snap Space = Space SSID = SSID diff --git a/assets/lang/ca_ES.ini b/assets/lang/ca_ES.ini index 9ca55309b1..68d599e0ca 100644 --- a/assets/lang/ca_ES.ini +++ b/assets/lang/ca_ES.ini @@ -273,7 +273,6 @@ Savestate Slot = Ranura de desa &d'estats Screen Scaling Filter = Filtre d'escalat de &pantalla Show Debug Statistics = Mostrar estadístiques de &depuació Show FPS Counter = Mostrar comptador de &FPS -Slot %1 = Slot %1 Smart 2D texture filtering = Smart 2D texture filtering Stop = &Aturar Switch UMD = Canvia UMD @@ -476,6 +475,7 @@ Settings = Settings Shift = Canviar Show in folder = Mostrar en carpeta Skip = Saltar +Slot %1 = Slot %1 Snap = Capturar Space = Espai SSID = SSID diff --git a/assets/lang/cz_CZ.ini b/assets/lang/cz_CZ.ini index ca29e30290..4a6dc255af 100644 --- a/assets/lang/cz_CZ.ini +++ b/assets/lang/cz_CZ.ini @@ -273,7 +273,6 @@ Savestate Slot = Po&zice uloženého stavu Screen Scaling Filter = Fil&tr změny velikosti obrazovky Show Debug Statistics = Zobrazit &statistiky ladění Show FPS Counter = Zobrazit počíta&dlo -Slot %1 = Slot %1 Smart 2D texture filtering = Smart 2D texture filtering Stop = &Zastavit Switch UMD = Přepnout UMD @@ -476,6 +475,7 @@ Settings = Settings Shift = Shift Show in folder = Zobrazit ve složce Skip = Skip +Slot %1 = Slot %1 Snap = Snap Space = Mezera SSID = SSID diff --git a/assets/lang/da_DK.ini b/assets/lang/da_DK.ini index 2c75e2af62..d0b30a249d 100644 --- a/assets/lang/da_DK.ini +++ b/assets/lang/da_DK.ini @@ -273,7 +273,6 @@ Savestate Slot = &Lagerplads for spil-status Screen Scaling Filter = Skæ&rmskaleringafilter Show Debug Statistics = &Vis fejlfindingsstatistik Show FPS Counter = Vis &FPS -Slot %1 = Slot %1 Smart 2D texture filtering = Smart 2D texture filtering Stop = &Stop Switch UMD = Skift UMD @@ -476,6 +475,7 @@ Settings = Settings Shift = Shift Show in folder = Vis i katalog Skip = Skip +Slot %1 = Slot %1 Snap = Snap Space = Space SSID = SSID diff --git a/assets/lang/de_DE.ini b/assets/lang/de_DE.ini index 8fa101855d..1e8c0b92ed 100644 --- a/assets/lang/de_DE.ini +++ b/assets/lang/de_DE.ini @@ -270,7 +270,6 @@ Savestate Slot = Speicherstand-Slot Screen Scaling Filter = Filter für Bildskalierung Show Debug Statistics = Fehlerbehebungs-Statistiken anzeigen Show FPS Counter = FPS anzeigen -Slot %1 = Slot %1 Smart 2D texture filtering = Intelligente 2D-Texturfilterung Stop = Stopp Switch UMD = UMD wechseln @@ -467,6 +466,7 @@ Settings = Einstellungen Shift = Umschalt Show in folder = Im Ordner anzeigen Skip = Überspringen +Slot %1 = Slot %1 Snap = Einrasten Space = Leertaste SSID = SSID diff --git a/assets/lang/dr_ID.ini b/assets/lang/dr_ID.ini index b1da199704..dad3ed8d3c 100644 --- a/assets/lang/dr_ID.ini +++ b/assets/lang/dr_ID.ini @@ -273,7 +273,6 @@ Savestate Slot = Saves&tate Slot Screen Scaling Filter = Scr&een Scaling Filter Show Debug Statistics = Pempakitan Debu&g Statistics Show FPS Counter = Pempakitan &FPS -Slot %1 = Slot %1 Smart 2D texture filtering = Smart 2D texture filtering Stop = &Kenden Switch UMD = Switch UMD @@ -476,6 +475,7 @@ Settings = Settings Shift = Angka'i Show in folder = Show in folder Skip = Skip +Slot %1 = Slot %1 Snap = Snap Space = Space SSID = SSID diff --git a/assets/lang/en_US.ini b/assets/lang/en_US.ini index df973db5d5..97e4976851 100644 --- a/assets/lang/en_US.ini +++ b/assets/lang/en_US.ini @@ -297,7 +297,6 @@ Savestate Slot = Saves&tate slot Screen Scaling Filter = Scr&een scaling filter Show Debug Statistics = Show debu&g statistics Show FPS Counter = Show &FPS counter -Slot %1 = Slot %1 Smart 2D texture filtering = Smart 2D texture filtering Stop = &Stop Switch UMD = Switch UMD @@ -500,6 +499,7 @@ Settings = Settings Shift = Shift Show in folder = Show in folder Skip = Skip +Slot %1 = Slot %1 Snap = Snap Space = Space SSID = SSID diff --git a/assets/lang/es_ES.ini b/assets/lang/es_ES.ini index 8c03d8ac2a..703779cbb0 100644 --- a/assets/lang/es_ES.ini +++ b/assets/lang/es_ES.ini @@ -274,7 +274,6 @@ Screen Scaling Filter = Filtro de escalado de &pantalla Show Debug Statistics = Mostrar &estadísticas de depuración Show FPS Counter = &Mostrar contador de FPS Skip Buffer Effects = &Saltar efectos de búfer (rápido) -Slot %1 = Ranura %1 Smart 2D texture filtering = Filtrado inteligente de texturas 2D Stop = Parar Switch UMD = Cambiar UMD @@ -477,6 +476,7 @@ Settings = Ajustes Shift = Alternar Show in folder = Mostrar carpeta Skip = Saltar +Slot %1 = Ranura %1 Snap = Capturar Space = Espacio SSID = SSID diff --git a/assets/lang/es_LA.ini b/assets/lang/es_LA.ini index 12262af3d4..b6ffdb7c3f 100644 --- a/assets/lang/es_LA.ini +++ b/assets/lang/es_LA.ini @@ -273,7 +273,6 @@ Savestate Slot = Ranura de guardar &estados Screen Scaling Filter = Filtro de escalado de &pantalla Show Debug Statistics = Mostrar &estadísticas de depuración Show FPS Counter = &Mostrar contador de FPS -Slot %1 = Ranura %1 Smart 2D texture filtering = Smart 2D texture filtering Stop = P&arar Switch UMD = Cambiar &UMD @@ -476,6 +475,7 @@ Settings = Ajustes Shift = Alternar Show in folder = Mostrar en carpeta Skip = Saltar +Slot %1 = Ranura %1 Snap = Capturar Space = Espacio SSID = SSID diff --git a/assets/lang/fa_IR.ini b/assets/lang/fa_IR.ini index 166ca7e534..3c844a6621 100644 --- a/assets/lang/fa_IR.ini +++ b/assets/lang/fa_IR.ini @@ -273,7 +273,6 @@ Savestate Slot = ‎مکان ذخیره سریع Screen Scaling Filter = ‎فیلتر تغییر سایز صفحه Show Debug Statistics = ‎نمایش وضعیت دیباگ Show FPS Counter = ‎نمایش شمارنده فریم بر ثانیه -Slot %1 = Slot %1 Smart 2D texture filtering = Smart 2D texture filtering Stop = ‎توقف Switch UMD = ‎تغییر دیسک @@ -476,6 +475,7 @@ Settings = تنظیمات Shift = ‎تغییر Show in folder = نمایش پوشه Skip = رد کردن +Slot %1 = Slot %1 Snap = Snap Space = فاصله SSID = SSID diff --git a/assets/lang/fi_FI.ini b/assets/lang/fi_FI.ini index 9edc762c01..5b882e266d 100644 --- a/assets/lang/fi_FI.ini +++ b/assets/lang/fi_FI.ini @@ -273,7 +273,6 @@ Savestate Slot = Tilatallennuksen &lohko Screen Scaling Filter = N&äytön skaalaussuodatin Show Debug Statistics = Näytä &vianetsinnän tiedot Show FPS Counter = Näytä kuvalaskuri (FPS) -Slot %1 = Lohko %1 Smart 2D texture filtering = Smart 2D texture filtering Stop = &Pysäytä Switch UMD = Vaihda UMD @@ -476,6 +475,7 @@ Settings = Asetukset Shift = Shift Show in folder = Näytä kansiossa Skip = Skip +Slot %1 = Lohko %1 Snap = Snap Space = Space SSID = SSID diff --git a/assets/lang/fr_FR.ini b/assets/lang/fr_FR.ini index 616764984c..0c6874c6d5 100644 --- a/assets/lang/fr_FR.ini +++ b/assets/lang/fr_FR.ini @@ -273,7 +273,6 @@ Savestate Slot = &Emplacement d'état Screen Scaling Filter = Filtre &de mise à l'échelle de l'écran Show Debug Statistics = &Montrer les statistiques de débogage Show FPS Counter = M&ontrer la vitesse d'émulation -Slot %1 = Emplacement %1 Smart 2D texture filtering = Smart 2D texture filtering Stop = &Arrêter Switch UMD = Changer d'UMD @@ -476,6 +475,7 @@ Settings = Settings Shift = Majuscule Show in folder = Montrer dans dossier Skip = Skip +Slot %1 = Emplacement %1 Snap = Aligner Space = Espace SSID = SSID diff --git a/assets/lang/gl_ES.ini b/assets/lang/gl_ES.ini index 1feff0f99e..8d3384776a 100644 --- a/assets/lang/gl_ES.ini +++ b/assets/lang/gl_ES.ini @@ -273,7 +273,6 @@ Savestate Slot = Ranura de gardar &estados Screen Scaling Filter = Filtro de escalado de &pantalla Show Debug Statistics = Mostrar &estadísticas de depuración Show FPS Counter = &Mostrar contador de FPS -Slot %1 = Slot %1 Smart 2D texture filtering = Smart 2D texture filtering Stop = P&arar Switch UMD = Cambiar &UMD @@ -476,6 +475,7 @@ Settings = Settings Shift = Alternar Show in folder = Mostrar en carpeta Skip = Skip +Slot %1 = Slot %1 Snap = Snap Space = Espacio SSID = SSID diff --git a/assets/lang/gr_EL.ini b/assets/lang/gr_EL.ini index d8fc4ba259..04af5f4e3f 100644 --- a/assets/lang/gr_EL.ini +++ b/assets/lang/gr_EL.ini @@ -273,7 +273,6 @@ Savestate Slot = Slot Σημείου Αποθήκευσης Screen Scaling Filter = Φίλτρο κλιμάκωσης οθόνης Show Debug Statistics = Εμφάνισ&η Στατιστικών Αποσφαλμάτωσης Show FPS Counter = Εμφάνι&ση μετρητή FPS -Slot %1 = Slot %1 Smart 2D texture filtering = Smart 2D texture filtering Stop = &Σταμάτημα Switch UMD = Αλλαγή UMD @@ -476,6 +475,7 @@ Settings = Settings Shift = Shift Show in folder = Εμφάνιση σε Φάκελο Skip = Skip +Slot %1 = Slot %1 Snap = Snap Space = ΚΕΝΟ SSID = SSID diff --git a/assets/lang/he_IL.ini b/assets/lang/he_IL.ini index 84fe5f2740..6edfca342f 100644 --- a/assets/lang/he_IL.ini +++ b/assets/lang/he_IL.ini @@ -273,7 +273,6 @@ Savestate Slot = Saves&tate Slot Screen Scaling Filter = Scr&een Scaling Filter Show Debug Statistics = Show Debu&g Statistics Show FPS Counter = Show &FPS Counter -Slot %1 = Slot %1 Smart 2D texture filtering = Smart 2D texture filtering Stop = &עצור Switch UMD = Switch UMD @@ -476,6 +475,7 @@ Settings = Settings Shift = העבר Show in folder = Show in folder Skip = Skip +Slot %1 = Slot %1 Snap = Snap Space = Space SSID = SSID diff --git a/assets/lang/he_IL_invert.ini b/assets/lang/he_IL_invert.ini index 6c9dc288f1..c27d0343e6 100644 --- a/assets/lang/he_IL_invert.ini +++ b/assets/lang/he_IL_invert.ini @@ -273,7 +273,6 @@ Savestate Slot = Saves&tate Slot Screen Scaling Filter = Scr&een Scaling Filter Show Debug Statistics = Show Debu&g Statistics Show FPS Counter = Show &FPS Counter -Slot %1 = Slot %1 Smart 2D texture filtering = Smart 2D texture filtering Stop = &Stop Switch UMD = Switch UMD @@ -473,6 +472,7 @@ Settings = Settings Shift = העבר Show in folder = Show in folder Skip = Skip +Slot %1 = Slot %1 Snap = Snap Space = Space SSID = SSID diff --git a/assets/lang/hr_HR.ini b/assets/lang/hr_HR.ini index 51ee5695fc..59770cde62 100644 --- a/assets/lang/hr_HR.ini +++ b/assets/lang/hr_HR.ini @@ -273,7 +273,6 @@ Savestate Slot = Saves&tate slot Screen Scaling Filter = Scr&een scaling filter Show Debug Statistics = Prikaži debu&g statistike Show FPS Counter = Prikaži &FPS broj -Slot %1 = Mjesto %1 Smart 2D texture filtering = Smart 2D texture filtering Stop = &Stopiraj Switch UMD = Promijeni UMD @@ -476,6 +475,7 @@ Settings = Settings Shift = Shift Show in folder = Prikaži u mapi Skip = Skip +Slot %1 = Mjesto %1 Snap = Snap Space = Space SSID = SSID diff --git a/assets/lang/hu_HU.ini b/assets/lang/hu_HU.ini index d16d967506..713e229e9f 100644 --- a/assets/lang/hu_HU.ini +++ b/assets/lang/hu_HU.ini @@ -273,7 +273,6 @@ Savestate Slot = Állapotmentés s&orszáma Screen Scaling Filter = Kép&ernyő skálázási szűrő Show Debug Statistics = &Hibakereső statisztikák mutatása Show FPS Counter = &FPS számláló mutatása -Slot %1 = Slot %1 Smart 2D texture filtering = &Okos 2D textúra szűrés Stop = &Leállítás Switch UMD = UMD &váltása @@ -476,6 +475,7 @@ Settings = Beállítások Shift = Shift Show in folder = Mutatás mappában Skip = Skip +Slot %1 = Slot %1 Snap = Igazít Space = Szóköz SSID = SSID diff --git a/assets/lang/id_ID.ini b/assets/lang/id_ID.ini index 779c57cfe3..684e9923b8 100644 --- a/assets/lang/id_ID.ini +++ b/assets/lang/id_ID.ini @@ -273,7 +273,6 @@ Savestate Slot = Slot saves&tate Screen Scaling Filter = Filter p&enskala layar Show Debug Statistics = Tampilkan statistik debug Show FPS Counter = Tampilkan penghitung &FPS -Slot %1 = Slot %1 Smart 2D texture filtering = Smart 2D texture filtering Stop = Hentikan Switch UMD = Ganti UMD @@ -476,6 +475,7 @@ Settings = Pengaturan Shift = Geser Show in folder = Tampilkan di berkas Skip = Lewati +Slot %1 = Slot %1 Snap = Garis pinggir Space = Spasi SSID = SSID diff --git a/assets/lang/it_IT.ini b/assets/lang/it_IT.ini index f5d98da0f2..292dcca520 100644 --- a/assets/lang/it_IT.ini +++ b/assets/lang/it_IT.ini @@ -273,7 +273,6 @@ Savestate Slot = Slot del salvataggio di stato Screen Scaling Filter = Filtro Scalatura Schermo Show Debug Statistics = Mostra Statistiche di Debug Show FPS Counter = Mostra FPS -Slot %1 = Slot %1 Smart 2D texture filtering = Filtro texture 2D intelligente Stop = &Interrompi Switch UMD = Cambia UMD @@ -476,6 +475,7 @@ Settings = Impostazioni Shift = Maiuscolo Show in folder = Mostra nella Cartella Skip = Salta +Slot %1 = Slot %1 Snap = Allinea Space = Spazio SSID = SSID diff --git a/assets/lang/ja_JP.ini b/assets/lang/ja_JP.ini index 6ed17304a8..2042675d96 100644 --- a/assets/lang/ja_JP.ini +++ b/assets/lang/ja_JP.ini @@ -273,7 +273,6 @@ Savestate Slot = セーブステートのスロット (&T) Screen Scaling Filter = 画像スケーリングのフィルタ (&E) Show Debug Statistics = デバッグ情報を表示する (&G) Show FPS Counter = FPSを表示する (&F) -Slot %1 = スロット%1 Smart 2D texture filtering = スマート2Dテクスチャフィルタリング Stop = 停止 (&S) Switch UMD = UMDを交換する @@ -476,6 +475,7 @@ Settings = 設定 Shift = シフト Show in folder = フォルダを表示する Skip = スキップ +Slot %1 = スロット%1 Snap = グリッドに吸着 Space = スペース SSID = SSID diff --git a/assets/lang/jv_ID.ini b/assets/lang/jv_ID.ini index 1d9e55c699..48c484ba17 100644 --- a/assets/lang/jv_ID.ini +++ b/assets/lang/jv_ID.ini @@ -273,7 +273,6 @@ Savestate Slot = Kalbu Slot Screen Scaling Filter = Layar njongko Filter Show Debug Statistics = Tampilno Statistik Debu&g Show FPS Counter = Tampilno Penghitung &FPS -Slot %1 = Slot %1 Smart 2D texture filtering = Smart 2D texture filtering Stop = &Mandek Switch UMD = Ngalih UMD @@ -476,6 +475,7 @@ Settings = Settings Shift = Shift Show in folder = Tampilno Nang Folder Skip = Skip +Slot %1 = Slot %1 Snap = Snap Space = Spasi SSID = SSID diff --git a/assets/lang/ko_KR.ini b/assets/lang/ko_KR.ini index a15c42c64f..5a7efc523a 100644 --- a/assets/lang/ko_KR.ini +++ b/assets/lang/ko_KR.ini @@ -273,7 +273,6 @@ Savestate Slot = 상태 저장 슬롯(&T) Screen Scaling Filter = 화면 크기 조정 필터(&E) Show Debug Statistics = 디버그 통계 표시(&G) Show FPS Counter = FPS 카운터 표시(&F) -Slot %1 = 슬롯 %1 Smart 2D texture filtering = 스마트 2D 텍스처 필터링 Stop = 정지(&S) Switch UMD = UMD 교체 @@ -476,6 +475,7 @@ Settings = 설정 Shift = 전환 Show in folder = 폴더에 표기 Skip = 건너뜀 +Slot %1 = 슬롯 %1 Snap = 스냅 Space = 공간 SSID = SSID diff --git a/assets/lang/ku_SO.ini b/assets/lang/ku_SO.ini index 8b1d7fe3ab..abb66e0d75 100644 --- a/assets/lang/ku_SO.ini +++ b/assets/lang/ku_SO.ini @@ -287,7 +287,6 @@ Savestate Slot = Saves&tate slot Screen Scaling Filter = Scr&een scaling filter Show Debug Statistics = Show debu&g statistics Show FPS Counter = Show &FPS counter -Slot %1 = Slot %1 Smart 2D texture filtering = Smart 2D texture filtering Stop = &Stop Switch UMD = Switch UMD @@ -490,6 +489,7 @@ Settings = سێتینگەکان Shift = Shift Show in folder = نیشاندانەوە لە فۆڵدەر Skip = پەڕاندن +Slot %1 = Slot %1 Snap = Snap Space = Space SSID = SSID diff --git a/assets/lang/lo_LA.ini b/assets/lang/lo_LA.ini index 4b3b1d4bdc..97503a7475 100644 --- a/assets/lang/lo_LA.ini +++ b/assets/lang/lo_LA.ini @@ -273,7 +273,6 @@ Savestate Slot = &ຊ່ອງບັນທຶກເກມ Screen Scaling Filter = &ໂຕຕອງຂະໜາດຂອງໜ້າຈໍ Show Debug Statistics = &ສະແດງຄ່າທາງສະຖິຕິແກ້ໄຂຈຸດບົກພ່ອງ Show FPS Counter = &ສະແດງໂຕນັບຄ່າເຟຣມເຣດ -Slot %1 = Slot %1 Smart 2D texture filtering = Smart 2D texture filtering Stop = &ຢຸດ Switch UMD = ສະຫຼັບແຜ່ນ UMD @@ -476,6 +475,7 @@ Settings = Settings Shift = ປ່ຽນ Show in folder = ສະແດງໃນໂຟນເດີ້ Skip = Skip +Slot %1 = Slot %1 Snap = Snap Space = ຍະວ່າງ SSID = SSID diff --git a/assets/lang/lt-LT.ini b/assets/lang/lt-LT.ini index 5e46187865..a61648cc9b 100644 --- a/assets/lang/lt-LT.ini +++ b/assets/lang/lt-LT.ini @@ -273,7 +273,6 @@ Savestate Slot = Iš&saugojimo statuso vieta Screen Scaling Filter = Scr&een Scaling Filter Show Debug Statistics = Rodyti Debu&g statistikas Show FPS Counter = Rodyti &kadrų per sekundę rodmenis -Slot %1 = Slot %1 Smart 2D texture filtering = Smart 2D texture filtering Stop = &Stabdyti Switch UMD = Perkeisti "UMD" @@ -476,6 +475,7 @@ Settings = Settings Shift = Shift Show in folder = Rodyti aplanke Skip = Skip +Slot %1 = Slot %1 Snap = Snap Space = Tarpas SSID = SSID diff --git a/assets/lang/ms_MY.ini b/assets/lang/ms_MY.ini index 6179370567..6141705a98 100644 --- a/assets/lang/ms_MY.ini +++ b/assets/lang/ms_MY.ini @@ -273,7 +273,6 @@ Savestate Slot = Slot Saves&tate Screen Scaling Filter = Scr&een Scaling Filter Show Debug Statistics = Papar Stati&stik Pepijat Show FPS Counter = Papar &penghitung FPS -Slot %1 = Slot %1 Smart 2D texture filtering = Smart 2D texture filtering Stop = &Henti Switch UMD = Tukar UMD @@ -476,6 +475,7 @@ Settings = Settings Shift = Shif Show in folder = Papar dalam folder Skip = Skip +Slot %1 = Slot %1 Snap = Snap Space = Space SSID = SSID diff --git a/assets/lang/nl_NL.ini b/assets/lang/nl_NL.ini index a06d250e7d..40bedd57bb 100644 --- a/assets/lang/nl_NL.ini +++ b/assets/lang/nl_NL.ini @@ -273,7 +273,6 @@ Savestate Slot = &Savestatesleuf Screen Scaling Filter = Bee&ldschalingsfilter Show Debug Statistics = &Foutopsporingsstatistieken tonen Show FPS Counter = FP&S-teller weergeven -Slot %1 = Slot %1 Smart 2D texture filtering = Smart 2D texture filtering Stop = &Stoppen Switch UMD = UMD verwisselen @@ -476,6 +475,7 @@ Settings = Settings Shift = Shift Show in folder = Weergeven in map Skip = Skip +Slot %1 = Slot %1 Snap = Snap Space = Spatie SSID = SSID diff --git a/assets/lang/no_NO.ini b/assets/lang/no_NO.ini index 3ccb155cc7..ce87de6d90 100644 --- a/assets/lang/no_NO.ini +++ b/assets/lang/no_NO.ini @@ -273,7 +273,6 @@ Savestate Slot = Saves&tate Slot Screen Scaling Filter = Scr&een Scaling Filter Show Debug Statistics = Show Debu&g Statistics Show FPS Counter = Vis &bildefrekvensteller -Slot %1 = Slot %1 Smart 2D texture filtering = Smart 2D texture filtering Stop = &Stopp Switch UMD = Bytt UMD @@ -476,6 +475,7 @@ Settings = Innstillinger Shift = Shift Show in folder = Show in folder Skip = Skip +Slot %1 = Slot %1 Snap = Snap Space = Space SSID = SSID diff --git a/assets/lang/pl_PL.ini b/assets/lang/pl_PL.ini index 277c257c03..fa48b479d7 100644 --- a/assets/lang/pl_PL.ini +++ b/assets/lang/pl_PL.ini @@ -272,7 +272,6 @@ Savestate Slot = Port zapisu stanu Screen Scaling Filter = Filtrowanie skalowania ekranu Show Debug Statistics = Pokaż statystyki debugowania Show FPS Counter = Pokaż licznik FPS -Slot %1 = Gniazdo %1 Smart 2D texture filtering = Inteligentne filtrowanie tekstur 2D Stop = Zatrzymaj Switch UMD = Podmień UMD @@ -474,6 +473,7 @@ Settings = Ustawienia Shift = Shift Show in folder = Pokaż w folderze Skip = Pomiń +Slot %1 = Gniazdo %1 Snap = Tryb siatki Space = Spacja SSID = SSID diff --git a/assets/lang/pt_BR.ini b/assets/lang/pt_BR.ini index 79cc971e6c..0402d8a17d 100644 --- a/assets/lang/pt_BR.ini +++ b/assets/lang/pt_BR.ini @@ -297,7 +297,6 @@ Savestate Slot = Slo&t do save state Screen Scaling Filter = Fi<ro do dimensionamento da tela Show Debug Statistics = Mostrar as estatíst&icas do debug Show FPS Counter = Mostrar o contador de &FPS -Slot %1 = Slot %1 Smart 2D texture filtering = Filtragem inteligente das texturas 2D Stop = &Parar Switch UMD = Trocar UMD @@ -499,6 +498,7 @@ Settings = Configurações Shift = Shift Show in folder = Mostrar na pasta Skip = Ignorar +Slot %1 = Slot %1 Snap = Grade Space = Espaço SSID = SSID diff --git a/assets/lang/pt_PT.ini b/assets/lang/pt_PT.ini index 097d9fc24d..f8496872ad 100644 --- a/assets/lang/pt_PT.ini +++ b/assets/lang/pt_PT.ini @@ -297,7 +297,6 @@ Savestate Slot = Espaç&o do Estado Salvo Screen Scaling Filter = Fi<ro do Dimensionamento da Tela Show Debug Statistics = Mostrar as estatíst&icas de depuração Show FPS Counter = Mostrar o contador de &FPS (Frames Por Segundo) -Slot %1 = Slot %1 Smart 2D texture filtering = Smart 2D texture filtering Stop = &Parar Switch UMD = Trocar UMD @@ -500,6 +499,7 @@ Settings = Definições Shift = Shift Show in folder = Mostrar na pasta Skip = Ignorar +Slot %1 = Slot %1 Snap = Grade Space = Espaço SSID = SSID diff --git a/assets/lang/ro_RO.ini b/assets/lang/ro_RO.ini index 5a282f681a..df3764c72a 100644 --- a/assets/lang/ro_RO.ini +++ b/assets/lang/ro_RO.ini @@ -274,7 +274,6 @@ Savestate Slot = Saves&tate Slot Screen Scaling Filter = Scr&een Scaling Filter Show Debug Statistics = Show Debu&g Statistics Show FPS Counter = Show &FPS Counter -Slot %1 = Slot %1 Smart 2D texture filtering = Smart 2D texture filtering Stop = &Stop Switch UMD = Switch UMD @@ -477,6 +476,7 @@ Settings = Settings Shift = Shift Show in folder = Arată în folder Skip = Skip +Slot %1 = Slot %1 Snap = Snap Space = Space SSID = SSID diff --git a/assets/lang/ru_RU.ini b/assets/lang/ru_RU.ini index e7fd11368d..668c6127e4 100644 --- a/assets/lang/ru_RU.ini +++ b/assets/lang/ru_RU.ini @@ -273,7 +273,6 @@ Savestate Slot = Слот состояния Screen Scaling Filter = Ф&ильтр масштабирования экрана Show Debug Statistics = Показывать &отладочную информацию Show FPS Counter = По&казывать FPS -Slot %1 = Слот %1 Smart 2D texture filtering = Умная фильтрация 2D-текстур Stop = Остановить Switch UMD = Сменить UMD @@ -476,6 +475,7 @@ Settings = Настройки Shift = Сдвинуть Show in folder = Показать в папке Skip = Пропустить +Slot %1 = Слот %1 Snap = Разметка Space = Пробел SSID = SSID diff --git a/assets/lang/sv_SE.ini b/assets/lang/sv_SE.ini index 5faec63faf..217ca5f5f4 100644 --- a/assets/lang/sv_SE.ini +++ b/assets/lang/sv_SE.ini @@ -273,7 +273,6 @@ Savestate Slot = Plats för sparat tillstånd Screen Scaling Filter = Skärmskalningsfilter Show Debug Statistics = Visa felsökningsstatistik Show FPS Counter = Visa räknare för bildfrekvens -Slot %1 = Plats %1 Smart 2D texture filtering = Smart 2D-texturfiltrering Stop = Stoppa Switch UMD = Växla UMD @@ -477,6 +476,7 @@ Settings = Inställningar Shift = Skift Show in folder = Visa i mapp Skip = Hoppa över +Slot %1 = Plats %1 Snap = Fäst Space = Mellanslag SSID = SSID diff --git a/assets/lang/tg_PH.ini b/assets/lang/tg_PH.ini index b6ff0de699..65a4c7a00b 100644 --- a/assets/lang/tg_PH.ini +++ b/assets/lang/tg_PH.ini @@ -273,7 +273,6 @@ Savestate Slot = Slot ng save state Screen Scaling Filter = Screen Scaling Filter Show Debug Statistics = Ipakita ang Debug Statistics Show FPS Counter = Ipakita ang FPS Counter -Slot %1 = Slot %1 Smart 2D texture filtering = Smart 2D texture filtering Stop = I-hinto Switch UMD = Magpalit ng UMD @@ -477,6 +476,7 @@ Settings = Mga Setting Shift = Shift Show in folder = Ipakita sa folder Skip = I-Skip +Slot %1 = Slot %1 Snap = Snap Space = Space SSID = SSID diff --git a/assets/lang/th_TH.ini b/assets/lang/th_TH.ini index 96516eede5..8b1a10b6d2 100644 --- a/assets/lang/th_TH.ini +++ b/assets/lang/th_TH.ini @@ -273,7 +273,6 @@ Savestate Slot = ช่องเซฟสเตทเกม Screen Scaling Filter = ตัวกรองปรับสเกลหน้าจอ Show Debug Statistics = แสดงค่าทางสถิติการแก้ไขจุดบกพร่อง Show FPS Counter = แสดงค่าเฟรมเรทต่อวินาที -Slot %1 = เซฟช่องที่ %1 Smart 2D texture filtering = ตัวกรองเท็คเจอร์ประเภท 2D แบบชาญฉลาด Stop = หยุด Switch UMD = สลับแผ่น UMD @@ -492,6 +491,7 @@ Settings = การตั้งค่า Shift = เปลี่ยน Show in folder = แสดงในโฟลเดอร์ Skip = ข้าม +Slot %1 = เซฟช่องที่ %1 Snap = ขยับ Space = เว้นวรรค SSID = SSID diff --git a/assets/lang/tr_TR.ini b/assets/lang/tr_TR.ini index 49b579ae1e..2aa20d5971 100644 --- a/assets/lang/tr_TR.ini +++ b/assets/lang/tr_TR.ini @@ -275,7 +275,6 @@ Savestate Slot = Durum Kaydı Yuvası Screen Scaling Filter = Ekran Ölçek Filtresi Show Debug Statistics = Hata Ayıklama İstatistiklerini Göster Show FPS Counter = FPS Sayacını Göster -Slot %1 = Yuva %1 Smart 2D texture filtering = Akıllı 2D doku filtresi Stop = Dur Switch UMD = UMD Değiştir @@ -478,6 +477,7 @@ Settings = Ayarlar Shift = Shift Show in folder = Klasörde göster Skip = Geç +Slot %1 = Yuva %1 Snap = Hizala Space = Boşluk SSID = SSID diff --git a/assets/lang/uk_UA.ini b/assets/lang/uk_UA.ini index a4e48e6cb3..f481ca781c 100644 --- a/assets/lang/uk_UA.ini +++ b/assets/lang/uk_UA.ini @@ -273,7 +273,6 @@ Savestate Slot = Слот збереження Screen Scaling Filter = Ф&ільтр масштабування екрану Show Debug Statistics = Показувати &статистику налагодження Show FPS Counter = По&казувати FPS -Slot %1 = Слот %1 Smart 2D texture filtering = розумна 2D фільтрація текстур Stop = Зупинити Switch UMD = Змінити UMD @@ -476,6 +475,7 @@ Settings = Параметри Shift = Shift Show in folder = Показати в теці Skip = Пропустити +Slot %1 = Слот %1 Snap = Знімок Space = Пробіл SSID = SSID diff --git a/assets/lang/vi_VN.ini b/assets/lang/vi_VN.ini index cf672952df..7deddf3151 100644 --- a/assets/lang/vi_VN.ini +++ b/assets/lang/vi_VN.ini @@ -273,7 +273,6 @@ Savestate Slot = Ô save Screen Scaling Filter = Bộ lọc họa tiết rộng Show Debug Statistics = Hiện thông số debug Show FPS Counter = Hiện thông số FPS -Slot %1 = Slot %1 Smart 2D texture filtering = Smart 2D texture filtering Stop = Dừng lại Switch UMD = Chuyển sang UMD @@ -476,6 +475,7 @@ Settings = Settings Shift = Shift Show in folder = Hiện trong thư mục Skip = Skip +Slot %1 = Slot %1 Snap = Snap Space = Space SSID = SSID diff --git a/assets/lang/zh_CN.ini b/assets/lang/zh_CN.ini index 284d16e8d7..4a5edf8bde 100644 --- a/assets/lang/zh_CN.ini +++ b/assets/lang/zh_CN.ini @@ -273,7 +273,6 @@ Savestate Slot = 即时存档插槽(&T) Screen Scaling Filter = 屏幕缩放算法(&E) Show Debug Statistics = 显示调试信息(&G) Show FPS Counter = 显示帧率(&F) -Slot %1 = 插槽%1 Smart 2D texture filtering = 自动保留2D纹理像素风格 Stop = 停止(&S) Switch UMD = 切换UMD光盘 @@ -476,6 +475,7 @@ Settings = 设置 Shift = 翻页 Show in folder = 打开文件夹 Skip = 是,跳过 +Slot %1 = 插槽%1 Snap = 网格对齐 Space = 空格键 SSID = 网络名称 diff --git a/assets/lang/zh_TW.ini b/assets/lang/zh_TW.ini index 0969557234..8dac4bc973 100644 --- a/assets/lang/zh_TW.ini +++ b/assets/lang/zh_TW.ini @@ -273,7 +273,6 @@ Savestate Slot = 存檔插槽(&T) Screen Scaling Filter = 螢幕縮放濾鏡(&E) Show Debug Statistics = 顯示偵錯統計資料(&G) Show FPS Counter = 顯示 FPS 計數器(&F) -Slot %1 = 插槽 %1 Smart 2D texture filtering = 智慧 2D 紋理過濾 Stop = 停止(&S) Switch UMD = 切換 UMD @@ -476,6 +475,7 @@ Settings = 設定 Shift = 移位 Show in folder = 在資料夾中顯示 Skip = 跳過 +Slot %1 = 插槽 %1 Snap = 貼齊 Space = 空白 SSID = SSID diff --git a/assets/ui_images/images.svg b/assets/ui_images/images.svg index 7bdf52d7e9..7237bc8168 100644 --- a/assets/ui_images/images.svg +++ b/assets/ui_images/images.svg @@ -7,7 +7,7 @@ viewBox="0 0 158.74998 185.20831" version="1.1" id="svg1" - inkscape:version="1.4.2 (f4327f4, 2025-05-13)" + inkscape:version="1.4.3 (0d15f75, 2025-12-25)" sodipodi:docname="images.svg" xml:space="preserve" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" @@ -24,9 +24,9 @@ inkscape:pagecheckerboard="true" inkscape:deskcolor="#d1d1d1" inkscape:document-units="px" - inkscape:zoom="16.000002" - inkscape:cx="338.78121" - inkscape:cy="626.46867" + inkscape:zoom="2.8284275" + inkscape:cx="267.81666" + inkscape:cy="394.74231" inkscape:window-width="3840" inkscape:window-height="2071" inkscape:window-x="-9" @@ -4429,7 +4429,23 @@ d="m 73.353766,158.91621 v 2.9068 h 0.968933 v -1.25315 l 1.513086,1.51308 0.685229,-0.68523 -1.513086,-1.51257 h 1.252637 v -0.96893 z m 5.412589,0 v 0.96893 h 1.252636 l -1.551843,1.55185 0.68523,0.68471 1.551843,-1.55184 v 1.25315 h 0.968933 v -2.9068 z m -2.896981,5.11803 -1.546675,1.54668 v -1.25264 h -0.968933 v 2.9068 h 2.906799 v -0.96893 h -1.252637 l 1.547193,-1.54668 z m 3.303674,0.0155 -0.685229,0.68523 1.531172,1.53117 h -1.252636 v 0.96893 h 2.906799 v -2.9068 h -0.968933 v 1.25264 z" />