From 231aefe47624dc3835184cedda9ad380b5103ffc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 19 May 2026 11:07:07 +0200 Subject: [PATCH 1/7] Code cleanup for save state notification, add metadata field --- Core/SaveState.cpp | 27 ++++++++++++++------------- Core/SaveState.h | 2 +- Core/SaveStateRewind.cpp | 6 +++++- Core/SaveStateRewind.h | 2 +- UI/EmuScreen.cpp | 19 ++++++------------- UI/NativeApp.cpp | 8 ++------ UI/PauseScreen.cpp | 18 +++++++++--------- UI/PauseScreen.h | 2 ++ Windows/MainWindowMenu.cpp | 8 ++++---- 9 files changed, 44 insertions(+), 48 deletions(-) diff --git a/Core/SaveState.cpp b/Core/SaveState.cpp index a8300543d97b..837ce22deac8 100644 --- a/Core/SaveState.cpp +++ b/Core/SaveState.cpp @@ -414,7 +414,7 @@ int g_screenshotFailures; Path backup = GetSysDirectory(DIRECTORY_SAVESTATE) / LOAD_UNDO_NAME; std::string prefix(gamePrefix); - auto saveCallback = [prefix, fn, backup, slot, callback](Status status, std::string_view message) { + auto saveCallback = [prefix, fn, backup, slot, callback](Status status, std::string_view message, std::string_view metadata) { if (status != Status::FAILURE) { DeleteIfExists(backup); File::Rename(backup.WithExtraExtension(".tmp"), backup); @@ -438,7 +438,7 @@ int g_screenshotFailures; } else { if (callback) { auto sy = GetI18NCategory(I18NCat::SYSTEM); - callback(Status::FAILURE, sy->T("Failed to load state. Error in the file system.")); + callback(Status::FAILURE, sy->T("Failed to load state. Error in the file system."), ""); } } } @@ -451,7 +451,7 @@ int g_screenshotFailures; 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")); + callback(Status::FAILURE, sy->T("Error: load undo state is from a different game"), ""); } return false; } @@ -463,7 +463,7 @@ int g_screenshotFailures; } else { if (callback) { auto sy = GetI18NCategory(I18NCat::SYSTEM); - callback(Status::FAILURE, sy->T("Failed to load state for load undo. Error in the file system.")); + callback(Status::FAILURE, sy->T("Failed to load state for load undo. Error in the file system."), ""); } return false; } @@ -480,7 +480,7 @@ int g_screenshotFailures; Path shot = GenerateSaveSlotPath(gamePrefix, slot, SCREENSHOT_EXTENSION); std::string prefix(gamePrefix); - auto renameCallback = [fn, fnUndo, prefix, slot, callback](Status status, std::string_view message) { + auto renameCallback = [fn, fnUndo, prefix, slot, callback](Status status, std::string_view message, std::string_view metadata) { if (status != Status::FAILURE) { if (g_Config.bEnableStateUndo) { DeleteIfExists(fnUndo); @@ -494,7 +494,7 @@ int g_screenshotFailures; File::Rename(fn.WithExtraExtension(".tmp"), fn); } if (callback) { - callback(status, message); + callback(status, message, ""); } }; // Let's also create a screenshot. @@ -508,7 +508,7 @@ int g_screenshotFailures; } else { if (callback) { auto sy = GetI18NCategory(I18NCat::SYSTEM); - callback(Status::FAILURE, sy->T("Failed to save state. Error in the file system.")); + callback(Status::FAILURE, sy->T("Failed to save state. Error in the file system."), ""); } } Rescan(gamePrefix); @@ -714,7 +714,7 @@ int g_screenshotFailures; return copy; } - bool HandleLoadFailure(bool wasRewinding) { + static bool HandleLoadFailure(bool wasRewinding, std::string *metadata) { if (wasRewinding) { WARN_LOG(Log::SaveState, "HandleLoadFailure - trying a rewind state."); // Okay, first, let's give the next rewind state a shot - maybe we can at least not reset entirely. @@ -722,7 +722,7 @@ int g_screenshotFailures; CChunkFileReader::Error result; do { std::string errorString; - result = rewindStates.Restore(&errorString); + result = rewindStates.Restore(&errorString, metadata); } while (result == CChunkFileReader::ERROR_BROKEN_STATE); if (result == CChunkFileReader::ERROR_NONE) { @@ -813,6 +813,7 @@ int g_screenshotFailures; CChunkFileReader::Error result; Status callbackResult; std::string callbackMessage; + std::string callbackMetadata; std::string title; auto sc = GetI18NCategory(I18NCat::SCREEN); @@ -849,7 +850,7 @@ int g_screenshotFailures; #endif g_lastSaveTime = time_now_d(); } else if (result == CChunkFileReader::ERROR_BROKEN_STATE) { - HandleLoadFailure(false); + HandleLoadFailure(false, &callbackMetadata); callbackMessage = std::string(i18nLoadFailure) + ": " + errorString; ERROR_LOG(Log::SaveState, "Load state failure: %s", errorString.c_str()); callbackResult = Status::FAILURE; @@ -909,7 +910,7 @@ int g_screenshotFailures; case OperationType::Rewind: INFO_LOG(Log::SaveState, "Rewinding to recent savestate snapshot"); - result = rewindStates.Restore(&errorString); + result = rewindStates.Restore(&errorString, &callbackMetadata); if (result == CChunkFileReader::ERROR_NONE) { callbackMessage = sc->T("Loaded State"); callbackResult = Status::SUCCESS; @@ -917,7 +918,7 @@ int g_screenshotFailures; Core_ResetException(); } else if (result == CChunkFileReader::ERROR_BROKEN_STATE) { // Cripes. Good news is, we might have more. Let's try those too, better than a reset. - if (HandleLoadFailure(true)) { + if (HandleLoadFailure(true, &callbackMetadata)) { // Well, we did rewind, even if too much... callbackMessage = sc->T("Loaded State"); callbackResult = Status::SUCCESS; @@ -940,7 +941,7 @@ int g_screenshotFailures; } if (op.callback) { - op.callback(callbackResult, callbackMessage); + op.callback(callbackResult, callbackMessage, callbackMetadata); } } if (operations.size()) { diff --git a/Core/SaveState.h b/Core/SaveState.h index c8400bafe69b..ba5ae44f0b00 100644 --- a/Core/SaveState.h +++ b/Core/SaveState.h @@ -34,7 +34,7 @@ namespace SaveState { WARNING, SUCCESS, }; - typedef std::function Callback; + typedef std::function Callback; static const char * const SCREENSHOT_EXTENSION = "jpg"; diff --git a/Core/SaveStateRewind.cpp b/Core/SaveStateRewind.cpp index 414886d43603..c61570bb4052 100644 --- a/Core/SaveStateRewind.cpp +++ b/Core/SaveStateRewind.cpp @@ -1,4 +1,5 @@ #include "Common/Thread/ThreadUtil.h" +#include "Common/Data/Text/I18n.h" #include "Core/SaveState.h" #include "Core/SaveStateRewind.h" #include "Core/Core.h" @@ -42,7 +43,7 @@ CChunkFileReader::Error StateRingbuffer::Save() { return err; } -CChunkFileReader::Error StateRingbuffer::Restore(std::string *errorString) { +CChunkFileReader::Error StateRingbuffer::Restore(std::string *errorString, std::string *metadata) { std::lock_guard guard(lock_); // No valid states left. @@ -53,9 +54,12 @@ CChunkFileReader::Error StateRingbuffer::Restore(std::string *errorString) { if (states_[n].empty()) return CChunkFileReader::ERROR_BAD_FILE; + auto pa = GetI18NCategory(I18NCat::PAUSE); + static std::vector buffer; LockedDecompress(buffer, states_[n], bases_[baseMapping_[n]]); CChunkFileReader::Error error = LoadFromRam(buffer, errorString); + *metadata = pa->T("Rewind"); rewindLastTime_ = time_now_d(); return error; } diff --git a/Core/SaveStateRewind.h b/Core/SaveStateRewind.h index a20837ddd3b8..5c6cb3a83e75 100644 --- a/Core/SaveStateRewind.h +++ b/Core/SaveStateRewind.h @@ -28,7 +28,7 @@ class StateRingbuffer { } CChunkFileReader::Error Save(); - CChunkFileReader::Error Restore(std::string *errorString); + CChunkFileReader::Error Restore(std::string *errorString, std::string *metadata); 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); diff --git a/UI/EmuScreen.cpp b/UI/EmuScreen.cpp index b294842044ee..67c25d9dace0 100644 --- a/UI/EmuScreen.cpp +++ b/UI/EmuScreen.cpp @@ -74,7 +74,6 @@ using namespace std::placeholders; #include "Core/Screenshot.h" #include "UI/ImDebugger/ImDebugger.h" #include "Core/HLE/__sceAudio.h" -// #include "Core/HLE/proAdhoc.h" #include "Core/HW/Display.h" #include "UI/BackgroundAudio.h" @@ -533,12 +532,6 @@ void EmuScreen::dialogFinished(const Screen *dialog, DialogResult result) { lastImguiEnabled_ = false; } -static void AfterSaveStateAction(SaveState::Status status, std::string_view message) { - if (!message.empty() && (!g_Config.bDumpFrames || !g_Config.bDumpVideoOutput)) { - g_OSD.Show(status == SaveState::Status::SUCCESS ? OSDType::MESSAGE_SUCCESS : OSDType::MESSAGE_ERROR, message, status == SaveState::Status::SUCCESS ? 2.0 : 5.0); - } -} - void EmuScreen::focusChanged(ScreenFocusChange focusChange) { Screen::focusChanged(focusChange); @@ -609,7 +602,7 @@ void EmuScreen::sendMessage(UIMessage message, const char *value) { if (newGamePath.GetFileExtension() == ".ppst") { // TODO: Should verify that it's for the correct game.... INFO_LOG(Log::Loader, "New game is a save state - just load it."); - SaveState::Load(newGamePath, -1, [](SaveState::Status status, std::string_view message) { + SaveState::Load(newGamePath, -1, [](SaveState::Status status, std::string_view message, std::string_view metadata) { Core_Resume(); System_Notify(SystemNotification::DISASSEMBLY); }); @@ -965,7 +958,7 @@ void EmuScreen::ProcessVKey(VirtKey virtKey) { case VIRTKEY_REWIND: if (!Achievements::WarnUserIfHardcoreModeActive(false) && !NetworkWarnUserIfOnlineAndCantSavestate() && !bootPending_) { if (SaveState::CanRewind()) { - SaveState::Rewind(&AfterSaveStateAction); + SaveState::Rewind(&ShowMessageAfterSaveStateAction); } else { g_OSD.Show(OSDType::MESSAGE_WARNING, sc->T("norewind", "No rewind save states available"), 2.0); } @@ -1058,12 +1051,12 @@ void EmuScreen::ProcessVKey(VirtKey virtKey) { case VIRTKEY_SAVE_STATE: if (!Achievements::WarnUserIfHardcoreModeActive(true) && !NetworkWarnUserIfOnlineAndCantSavestate() && !bootPending_) { - SaveState::SaveSlot(SaveState::GetGamePrefix(g_paramSFO), g_Config.iCurrentStateSlot, &AfterSaveStateAction); + SaveState::SaveSlot(SaveState::GetGamePrefix(g_paramSFO), g_Config.iCurrentStateSlot, &ShowMessageAfterSaveStateAction); } break; case VIRTKEY_LOAD_STATE: if (!Achievements::WarnUserIfHardcoreModeActive(false) && !NetworkWarnUserIfOnlineAndCantSavestate() && !bootPending_) { - SaveState::LoadSlot(SaveState::GetGamePrefix(g_paramSFO), g_Config.iCurrentStateSlot, &AfterSaveStateAction); + SaveState::LoadSlot(SaveState::GetGamePrefix(g_paramSFO), g_Config.iCurrentStateSlot, &ShowMessageAfterSaveStateAction); } break; case VIRTKEY_PREVIOUS_SLOT: @@ -2053,8 +2046,8 @@ void EmuScreen::AutoLoadSaveState() { } if (g_Config.iAutoLoadSaveState && autoSlot != -1) { - SaveState::LoadSlot(gamePrefix, autoSlot, [this, autoSlot](SaveState::Status status, std::string_view message) { - AfterSaveStateAction(status, message); + SaveState::LoadSlot(gamePrefix, autoSlot, [this, autoSlot](SaveState::Status status, std::string_view message, std::string_view metadata) { + ShowMessageAfterSaveStateAction(status, message, metadata); auto sy = GetI18NCategory(I18NCat::SYSTEM); if (status == SaveState::Status::FAILURE) { autoLoadFailed_ = true; diff --git a/UI/NativeApp.cpp b/UI/NativeApp.cpp index 913068a78220..ab3b410d3d1b 100644 --- a/UI/NativeApp.cpp +++ b/UI/NativeApp.cpp @@ -133,6 +133,7 @@ #include "UI/OnScreenDisplay.h" #include "UI/RemoteISOScreen.h" #include "UI/Theme.h" +#include "UI/PauseScreen.h" #include "UI/UIAtlas.h" #if PPSSPP_PLATFORM(UWP) @@ -693,12 +694,7 @@ void NativeInit(int argc, const char *argv[], const char *savegame_dir, const ch g_BackgroundAudio.SFX().Init(); if (!boot_filename.empty() && stateToLoad.Valid()) { - SaveState::Load(stateToLoad, -1, [](SaveState::Status status, std::string_view message) { - if (!message.empty() && (!g_Config.bDumpFrames || !g_Config.bDumpVideoOutput)) { - g_OSD.Show(status == SaveState::Status::SUCCESS ? OSDType::MESSAGE_SUCCESS : OSDType::MESSAGE_ERROR, - message, status == SaveState::Status::SUCCESS ? 2.0 : 5.0); - } - }); + SaveState::Load(stateToLoad, -1, &ShowMessageAfterSaveStateAction); } if (g_Config.bAchievementsEnable) { diff --git a/UI/PauseScreen.cpp b/UI/PauseScreen.cpp index 12b9d00a02fb..802145d6c28e 100644 --- a/UI/PauseScreen.cpp +++ b/UI/PauseScreen.cpp @@ -76,10 +76,10 @@ void copyDeepLinkForPath(std::string_view filePath); #endif -static void AfterSaveStateAction(SaveState::Status status, std::string_view message) { - if (!message.empty() && (!g_Config.bDumpFrames || !g_Config.bDumpVideoOutput)) { +void ShowMessageAfterSaveStateAction(SaveState::Status status, std::string_view message, std::string_view metadata) { + if (!message.empty()) { g_OSD.Show(status == SaveState::Status::SUCCESS ? OSDType::MESSAGE_SUCCESS : OSDType::MESSAGE_ERROR, - message, status == SaveState::Status::SUCCESS ? 2.0 : 5.0); + message, metadata, status == SaveState::Status::SUCCESS ? 2.0 : 5.0); } } @@ -153,7 +153,7 @@ class ScreenshotViewScreen : public UI::PopupScreen { void ScreenshotViewScreen::OnSaveState(UI::EventParams &e) { if (!NetworkWarnUserIfOnlineAndCantSavestate()) { g_Config.iCurrentStateSlot = slot_; - SaveState::SaveSlot(saveStatePrefix_, slot_, &AfterSaveStateAction); + SaveState::SaveSlot(saveStatePrefix_, slot_, &ShowMessageAfterSaveStateAction); TriggerFinish(DR_OK); //OK will close the pause screen as well } } @@ -161,7 +161,7 @@ void ScreenshotViewScreen::OnSaveState(UI::EventParams &e) { void ScreenshotViewScreen::OnLoadState(UI::EventParams &e) { if (!NetworkWarnUserIfOnlineAndCantSavestate()) { g_Config.iCurrentStateSlot = slot_; - SaveState::LoadSlot(saveStatePrefix_, slot_, &AfterSaveStateAction); + SaveState::LoadSlot(saveStatePrefix_, slot_, &ShowMessageAfterSaveStateAction); TriggerFinish(DR_OK); } } @@ -303,7 +303,7 @@ void SaveSlotView::Draw(UIContext &dc) { void SaveSlotView::OnLoadState(UI::EventParams &e) { if (!NetworkWarnUserIfOnlineAndCantSavestate()) { g_Config.iCurrentStateSlot = slot_; - SaveState::LoadSlot(saveStatePrefix_, slot_, &AfterSaveStateAction); + SaveState::LoadSlot(saveStatePrefix_, slot_, &ShowMessageAfterSaveStateAction); UI::EventParams e2{}; e2.v = this; OnStateLoaded.Trigger(e2); @@ -313,7 +313,7 @@ void SaveSlotView::OnLoadState(UI::EventParams &e) { void SaveSlotView::OnSaveState(UI::EventParams &e) { if (!NetworkWarnUserIfOnlineAndCantSavestate()) { g_Config.iCurrentStateSlot = slot_; - SaveState::SaveSlot(saveStatePrefix_, slot_, &AfterSaveStateAction); + SaveState::SaveSlot(saveStatePrefix_, slot_, &ShowMessageAfterSaveStateAction); UI::EventParams e2{}; e2.v = this; OnStateSaved.Trigger(e2); @@ -426,7 +426,7 @@ void GamePauseScreen::CreateSavestateControls(UI::LinearLayout *leftColumnItems, UI::Choice *loadUndoButton = buttonRow->Add(new Choice(pa->T("Undo last load"), ImageID("I_NAVIGATE_BACK"), new LinearLayoutParams(WRAP_CONTENT, WRAP_CONTENT))); loadUndoButton->SetEnabled(SaveState::HasUndoLoad(saveStatePrefix_)); loadUndoButton->OnClick.Add([this](UI::EventParams &e) { - SaveState::UndoLoad(saveStatePrefix_, &AfterSaveStateAction); + SaveState::UndoLoad(saveStatePrefix_, &ShowMessageAfterSaveStateAction); TriggerFinish(DR_CANCEL); }); } @@ -435,7 +435,7 @@ void GamePauseScreen::CreateSavestateControls(UI::LinearLayout *leftColumnItems, UI::Choice *rewindButton = buttonRow->Add(new Choice(pa->T("Rewind"), ImageID("I_REWIND"), new LinearLayoutParams(WRAP_CONTENT, WRAP_CONTENT))); rewindButton->SetEnabled(SaveState::CanRewind()); rewindButton->OnClick.Add([this](UI::EventParams &e) { - SaveState::Rewind(&AfterSaveStateAction); + SaveState::Rewind(&ShowMessageAfterSaveStateAction); TriggerFinish(DR_CANCEL); }); } diff --git a/UI/PauseScreen.h b/UI/PauseScreen.h index 5cf57ac5f3f4..e7115c445814 100644 --- a/UI/PauseScreen.h +++ b/UI/PauseScreen.h @@ -23,6 +23,7 @@ #include "Common/File/Path.h" #include "Common/UI/UIScreen.h" #include "Common/UI/ViewGroup.h" +#include "Core/SaveState.h" #include "Core/ControlMapper.h" #include "UI/BaseScreens.h" #include "UI/Screen.h" @@ -85,3 +86,4 @@ class GamePauseScreen : public UIBaseDialogScreen, protected ControlListener { }; std::string GetConfirmExitMessage(); +void ShowMessageAfterSaveStateAction(SaveState::Status status, std::string_view message, std::string_view metadata); diff --git a/Windows/MainWindowMenu.cpp b/Windows/MainWindowMenu.cpp index 0c684163d015..89831211ca7d 100644 --- a/Windows/MainWindowMenu.cpp +++ b/Windows/MainWindowMenu.cpp @@ -45,6 +45,7 @@ #include "Core/SaveState.h" #include "Core/Core.h" #include "Core/RetroAchievements.h" +#include "UI/PauseScreen.h" #ifdef RC_CLIENT_SUPPORTS_RAINTEGRATION #include "ext/rcheevos/include/rc_client_raintegration.h" @@ -394,10 +395,9 @@ namespace MainWindow { }); } - static void SaveStateActionFinished(SaveState::Status status, std::string_view message) { - if (!message.empty() && (!g_Config.bDumpFrames || !g_Config.bDumpVideoOutput)) { - g_OSD.Show(status == SaveState::Status::SUCCESS ? OSDType::MESSAGE_SUCCESS : OSDType::MESSAGE_ERROR, message, status == SaveState::Status::SUCCESS ? 2.0 : 5.0); - } + static void SaveStateActionFinished(SaveState::Status status, std::string_view message, std::string_view metadata) { + // Reuse the message from the pause screen. + ShowMessageAfterSaveStateAction(status, message, metadata); PostMessage(MainWindow::GetHWND(), WM_USER_SAVESTATE_FINISH, 0, 0); } From 8dad5a5c57deeb9930b674d887c329571fb90745 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 19 May 2026 11:09:53 +0200 Subject: [PATCH 2/7] Remove asserts in GPUBufferSubscriber Fixes #21683 --- Core/Debugger/WebSocket/GPUBufferSubscriber.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Core/Debugger/WebSocket/GPUBufferSubscriber.cpp b/Core/Debugger/WebSocket/GPUBufferSubscriber.cpp index ce53edda9df8..2cbab16c991e 100644 --- a/Core/Debugger/WebSocket/GPUBufferSubscriber.cpp +++ b/Core/Debugger/WebSocket/GPUBufferSubscriber.cpp @@ -248,14 +248,16 @@ static void GenericStreamBuffer(DebuggerRequest &req, std::function Date: Tue, 19 May 2026 11:53:16 +0200 Subject: [PATCH 3/7] Show timestamps when loading rewind states --- Common/TimeUtil.cpp | 84 ++++++++++++++++++++++++++++++++++++++-- Common/TimeUtil.h | 2 + Core/SaveState.cpp | 3 -- Core/SaveStateRewind.cpp | 33 +++++++++++----- Core/SaveStateRewind.h | 12 +++++- 5 files changed, 116 insertions(+), 18 deletions(-) diff --git a/Common/TimeUtil.cpp b/Common/TimeUtil.cpp index 344990ae2bf5..2279f11d775c 100644 --- a/Common/TimeUtil.cpp +++ b/Common/TimeUtil.cpp @@ -36,21 +36,32 @@ constexpr double micros = 1000000.0; constexpr double nanos = 1000000000.0; + #if PPSSPP_PLATFORM(WINDOWS) +constexpr int64_t UNIX_TIME_START = 0x019DB1DED53E8000; //January 1, 1970 (start of Unix epoch) in "ticks" +constexpr double TICKS_PER_SECOND = 10000000; //a tick is 100ns + static LARGE_INTEGER frequency; static double frequencyMult; static LARGE_INTEGER startTime; +static LARGE_INTEGER startFileTime; HANDLE Timer; int SchedulerPeriodMs = 10; INT64 QpcPerSecond; void TimeInit() { + FILETIME ft; + GetSystemTimeAsFileTime(&ft); //returns ticks in UTC + // Copy the low and high parts of FILETIME into a LARGE_INTEGER + startFileTime.LowPart = ft.dwLowDateTime; + startFileTime.HighPart = ft.dwHighDateTime; + QueryPerformanceFrequency(&frequency); QueryPerformanceCounter(&startTime); QpcPerSecond = frequency.QuadPart; - frequencyMult = 1.0 / static_cast(frequency.QuadPart); + frequencyMult = 1.0 / frequency.QuadPart; // The timer will be automatically deleted on process destruction. Don't need to CloseHandle. Timer = CreateWaitableTimerExW(NULL, NULL, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS); @@ -85,9 +96,6 @@ double from_time_raw_relative(uint64_t raw_time) { } double time_now_unix_utc() { - const int64_t UNIX_TIME_START = 0x019DB1DED53E8000; //January 1, 1970 (start of Unix epoch) in "ticks" - const double TICKS_PER_SECOND = 10000000; //a tick is 100ns - FILETIME ft; GetSystemTimeAsFileTime(&ft); //returns ticks in UTC // Copy the low and high parts of FILETIME into a LARGE_INTEGER @@ -98,6 +106,15 @@ double time_now_unix_utc() { return (double)(li.QuadPart - UNIX_TIME_START) / TICKS_PER_SECOND; } +// Adds the timestamp to startTime, and converts to seconds from the unix epoch. +double time_to_unix_utc(double timestamp) { + // Copy the low and high parts of FILETIME into a LARGE_INTEGER + LARGE_INTEGER li; + li.LowPart = startFileTime.LowPart; + li.HighPart = startFileTime.HighPart; + return (double)(li.QuadPart - UNIX_TIME_START + static_cast(timestamp * TICKS_PER_SECOND)) / TICKS_PER_SECOND; +} + void yield() { YieldProcessor(); } @@ -227,6 +244,10 @@ double time_now_unix_utc() { return time_now_raw(); } +double time_to_unix_utc(double t) { + return (double)tv.tv_sec + (double)tv.tv_usec * (1.0 / micros) + t; +} + Instant::Instant() { struct timeval tv; gettimeofday(&tv, nullptr); @@ -364,6 +385,61 @@ void GetCurrentTimeFormatted(char formattedTime[13]) { #endif } +void FormatUnixTime(double unixTimeSeconds, char *formatted, size_t bufSize, bool includeDate) { +#ifdef _WIN32 + ULARGE_INTEGER uli; + uli.QuadPart = (ULONGLONG)(unixTimeSeconds * TICKS_PER_SECOND) + UNIX_TIME_START; // Convert seconds to ticks and add the offset to get FILETIME ticks. + FILETIME ft; + ft.dwLowDateTime = uli.LowPart; + ft.dwHighDateTime = uli.HighPart; + + // Convert UTC FILETIME to local FILETIME + FILETIME localFt; + FileTimeToLocalFileTime(&ft, &localFt); + + SYSTEMTIME st; + FileTimeToSystemTime(&localFt, &st); + + // Use system locale for date/time formatting + wchar_t dateStr[256]; + wchar_t timeStr[256]; + + // Get localized date string (short date format) + GetDateFormatW(LOCALE_USER_DEFAULT, DATE_SHORTDATE, &st, nullptr, dateStr, 256); + + // Get localized time string (without seconds by default, but we'll add them) + GetTimeFormatW(LOCALE_USER_DEFAULT, 0, &st, nullptr, timeStr, 256); + + // Convert to char and combine + char timeMb[256]; + char dateMb[256]; + WideCharToMultiByte(CP_UTF8, 0, timeStr, -1, timeMb, 256, nullptr, nullptr); + + if (includeDate) { + WideCharToMultiByte(CP_UTF8, 0, dateStr, -1, dateMb, 256, nullptr, nullptr); + snprintf(formatted, bufSize, "%s %s", dateMb, timeMb); + } else { + snprintf(formatted, bufSize, "%s", timeMb); + } + +#else + struct timespec ts; + ts.tv_sec = (time_t)unixTimeSeconds; + ts.tv_nsec = (long)((unixTimeSeconds - ts.tv_sec) * 1000000000.0); + struct tm tm; + localtime_r(&ts.tv_sec, &tm); + + // Use strftime with locale-specific formatting + if (includeDate) { + // %x is locale-specific date, %X is locale-specific time + strftime(formatted, bufSize, "%x %X", &tm); + } else { + // Just time + strftime(formatted, bufSize, "%X", &tm); + } +#endif +} + // We don't even bother synchronizing this, it's fine if threads stomp a bit. static GMRng g_sleepRandom; diff --git a/Common/TimeUtil.h b/Common/TimeUtil.h index 84fe433ee5b5..27adb8ade2b9 100644 --- a/Common/TimeUtil.h +++ b/Common/TimeUtil.h @@ -16,6 +16,7 @@ double from_time_raw_relative(uint64_t raw_time); // Seconds, Unix UTC time double time_now_unix_utc(); +double time_to_unix_utc(double timeNowSeconds); // Sleep for milliseconds. Does not necessarily have millisecond granularity, especially on Windows. // Requires a "reason" since sleeping generally should be very sparingly used. This @@ -33,6 +34,7 @@ void sleep_random(double minSeconds, double maxSeconds, const char *reason); void yield(); void GetCurrentTimeFormatted(char formattedTime[13]); +void FormatUnixTime(double unixTimeSeconds, char *formatted, size_t bufSize, bool includeDate = true); // Most accurate timer possible - no extra double conversions. Only for spans. class Instant { diff --git a/Core/SaveState.cpp b/Core/SaveState.cpp index 837ce22deac8..344343f3d287 100644 --- a/Core/SaveState.cpp +++ b/Core/SaveState.cpp @@ -245,9 +245,6 @@ int g_screenshotFailures; } void Rewind(Callback callback) { - if (g_netInited) { - return; - } if (coreState == CoreState::CORE_RUNTIME_ERROR) Core_Break(BreakReason::SavestateRewind, 0); Enqueue(Operation(OperationType::Rewind, Path(), -1, callback)); diff --git a/Core/SaveStateRewind.cpp b/Core/SaveStateRewind.cpp index c61570bb4052..8eabc7585b0a 100644 --- a/Core/SaveStateRewind.cpp +++ b/Core/SaveStateRewind.cpp @@ -34,10 +34,12 @@ CChunkFileReader::Error StateRingbuffer::Save() { } else err = SaveToRam(buffer_); - if (err == CChunkFileReader::ERROR_NONE) - ScheduleCompress(&states_[n], compressBuffer, &bases_[base_]); - else + if (err == CChunkFileReader::ERROR_NONE) { + ScheduleCompress(&states_[n].stateBuffer, compressBuffer, &bases_[base_]); + states_[n].savedTime = time_now_d(); + } else { states_[n].clear(); + } baseMapping_[n] = base_; return err; @@ -57,9 +59,19 @@ CChunkFileReader::Error StateRingbuffer::Restore(std::string *errorString, std:: auto pa = GetI18NCategory(I18NCat::PAUSE); static std::vector buffer; - LockedDecompress(buffer, states_[n], bases_[baseMapping_[n]]); + LockedDecompress(buffer, states_[n].stateBuffer, bases_[baseMapping_[n]]); CChunkFileReader::Error error = LoadFromRam(buffer, errorString); *metadata = pa->T("Rewind"); + + if (states_[n].savedTime) { + metadata->append(" ("); + char buffer[26]; + double unixTime = time_to_unix_utc(states_[n].savedTime); + FormatUnixTime(unixTime, buffer, sizeof(buffer), false); + metadata->append(buffer); + metadata->append(")"); + } + rewindLastTime_ = time_now_d(); return error; } @@ -103,16 +115,13 @@ void StateRingbuffer::LockedDecompress(std::vector &result, const std::vecto result.clear(); result.reserve(base.size()); auto basePos = base.begin(); - for (size_t i = 0; i < compressed.size(); ) - { - if (compressed[i] == 0) - { + 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 - { + } else { ++i; int blockSize = std::min(BLOCK_SIZE, (int)(compressed.size() - i)); result.insert(result.end(), compressed.begin() + i, compressed.begin() + i + blockSize); @@ -171,4 +180,8 @@ void StateRingbuffer::NotifyState() { rewindLastTime_ = time_now_d(); } +double StateRingbuffer::NextStateTimestamp() const { + return rewindLastTime_ + g_Config.iRewindSnapshotInterval; +} + } // namespace SaveState diff --git a/Core/SaveStateRewind.h b/Core/SaveStateRewind.h index 5c6cb3a83e75..16eddec11860 100644 --- a/Core/SaveStateRewind.h +++ b/Core/SaveStateRewind.h @@ -41,6 +41,8 @@ class StateRingbuffer { void Process(); void NotifyState(); + double NextStateTimestamp() const; + private: const int BLOCK_SIZE = 8192; const int REWIND_NUM_STATES = 20; @@ -49,11 +51,19 @@ class StateRingbuffer { typedef std::vector StateBuffer; + struct RewindState { + StateBuffer stateBuffer; + double savedTime; + + bool empty() const { return stateBuffer.empty(); } + void clear() { stateBuffer.clear(); savedTime = 0.0; } + }; + int first_ = 0; int next_ = 0; int size_; - std::vector states_; + std::vector states_; StateBuffer bases_[2]; std::vector baseMapping_; std::mutex lock_; From affdeb827e597d52366d6e56bf554af74f10600d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 19 May 2026 12:04:32 +0200 Subject: [PATCH 4/7] Change rewindstate metadata to say X seconds ago --- Core/SaveStateRewind.cpp | 5 +++++ Tools/langtool/Cargo.lock | 4 ++-- assets/lang/ar_AE.ini | 1 + assets/lang/az_AZ.ini | 1 + assets/lang/be_BY.ini | 1 + assets/lang/bg_BG.ini | 1 + assets/lang/ca_ES.ini | 1 + assets/lang/cz_CZ.ini | 1 + assets/lang/da_DK.ini | 1 + assets/lang/de_DE.ini | 1 + assets/lang/dr_ID.ini | 1 + assets/lang/en_US.ini | 1 + assets/lang/es_ES.ini | 1 + assets/lang/es_LA.ini | 1 + assets/lang/fa_IR.ini | 1 + assets/lang/fi_FI.ini | 1 + assets/lang/fr_FR.ini | 1 + assets/lang/gl_ES.ini | 1 + assets/lang/gr_EL.ini | 1 + assets/lang/he_IL.ini | 1 + assets/lang/he_IL_invert.ini | 1 + assets/lang/hr_HR.ini | 1 + assets/lang/hu_HU.ini | 1 + assets/lang/id_ID.ini | 1 + assets/lang/it_IT.ini | 1 + assets/lang/ja_JP.ini | 1 + assets/lang/jv_ID.ini | 1 + assets/lang/ko_KR.ini | 1 + assets/lang/ku_SO.ini | 1 + assets/lang/lo_LA.ini | 1 + assets/lang/lt-LT.ini | 1 + assets/lang/ms_MY.ini | 1 + assets/lang/nl_NL.ini | 1 + assets/lang/no_NO.ini | 1 + assets/lang/pl_PL.ini | 1 + assets/lang/pt_BR.ini | 1 + assets/lang/pt_PT.ini | 1 + assets/lang/ro_RO.ini | 1 + assets/lang/ru_RU.ini | 1 + assets/lang/sv_SE.ini | 1 + assets/lang/tg_PH.ini | 1 + assets/lang/th_TH.ini | 1 + assets/lang/tr_TR.ini | 1 + assets/lang/uk_UA.ini | 1 + assets/lang/vi_VN.ini | 1 + assets/lang/zh_CN.ini | 1 + assets/lang/zh_TW.ini | 1 + 47 files changed, 52 insertions(+), 2 deletions(-) diff --git a/Core/SaveStateRewind.cpp b/Core/SaveStateRewind.cpp index 8eabc7585b0a..aa4cc62926ab 100644 --- a/Core/SaveStateRewind.cpp +++ b/Core/SaveStateRewind.cpp @@ -1,5 +1,6 @@ #include "Common/Thread/ThreadUtil.h" #include "Common/Data/Text/I18n.h" +#include "Common/StringUtils.h" #include "Core/SaveState.h" #include "Core/SaveStateRewind.h" #include "Core/Core.h" @@ -64,11 +65,15 @@ CChunkFileReader::Error StateRingbuffer::Restore(std::string *errorString, std:: *metadata = pa->T("Rewind"); if (states_[n].savedTime) { + auto di = GetI18NCategory(I18NCat::DIALOG); metadata->append(" ("); + metadata->append(ApplySafeSubstitutions(di->T("%1 seconds ago"), static_cast(time_now_d() - states_[n].savedTime))); + /* char buffer[26]; double unixTime = time_to_unix_utc(states_[n].savedTime); FormatUnixTime(unixTime, buffer, sizeof(buffer), false); metadata->append(buffer); + */ metadata->append(")"); } diff --git a/Tools/langtool/Cargo.lock b/Tools/langtool/Cargo.lock index bbdfbfe047fa..91d519458e23 100644 --- a/Tools/langtool/Cargo.lock +++ b/Tools/langtool/Cargo.lock @@ -1727,9 +1727,9 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.10" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68d6fdd9f81c2819c9a8b0e0cd91660e7746a8e6ea2ba7c6b2b057985f6bcb51" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ "bitflags 2.11.1", "bytes", diff --git a/assets/lang/ar_AE.ini b/assets/lang/ar_AE.ini index 2b8f3bc24bf4..bb192ba9089d 100644 --- a/assets/lang/ar_AE.ini +++ b/assets/lang/ar_AE.ini @@ -400,6 +400,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = منذ %1 ثانية # AI translated %d hours = %d hours %d minutes = %d minutes %d ms = %d ms diff --git a/assets/lang/az_AZ.ini b/assets/lang/az_AZ.ini index 32d733ca71d2..837b93b953d1 100644 --- a/assets/lang/az_AZ.ini +++ b/assets/lang/az_AZ.ini @@ -392,6 +392,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = %1 saniyə əvvəl # AI translated %d hours = %d saat %d minutes = %d dəqiqə %d ms = %d ms diff --git a/assets/lang/be_BY.ini b/assets/lang/be_BY.ini index b050790e7b0d..93b76d553709 100644 --- a/assets/lang/be_BY.ini +++ b/assets/lang/be_BY.ini @@ -392,6 +392,7 @@ Vertex = Вяршыня VFPU = VFPU [Dialog] +%1 seconds ago = прыкладна %1 секунд таму # AI translated %d hours = %d г %d minutes = %d хв %d ms = %d мс diff --git a/assets/lang/bg_BG.ini b/assets/lang/bg_BG.ini index 1cbbe17765f7..b194e9d71cc8 100644 --- a/assets/lang/bg_BG.ini +++ b/assets/lang/bg_BG.ini @@ -392,6 +392,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = преди %1 секунди # AI translated %d hours = %d часове %d minutes = %d минути %d ms = %d мс diff --git a/assets/lang/ca_ES.ini b/assets/lang/ca_ES.ini index 1daf258d4b72..4d35ece24d6c 100644 --- a/assets/lang/ca_ES.ini +++ b/assets/lang/ca_ES.ini @@ -392,6 +392,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = fa %1 segons # AI translated %d hours = %d hours %d minutes = %d minutes %d ms = %d ms diff --git a/assets/lang/cz_CZ.ini b/assets/lang/cz_CZ.ini index 2d137701cde4..ada38878128c 100644 --- a/assets/lang/cz_CZ.ini +++ b/assets/lang/cz_CZ.ini @@ -392,6 +392,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = před %1 sekundami # AI translated %d hours = %d hours %d minutes = %d minutes %d ms = %d ms diff --git a/assets/lang/da_DK.ini b/assets/lang/da_DK.ini index 1297c309b595..d70675d1f447 100644 --- a/assets/lang/da_DK.ini +++ b/assets/lang/da_DK.ini @@ -392,6 +392,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = for %1 sekunder siden # AI translated %d hours = %d hours %d minutes = %d minutes %d ms = %d ms diff --git a/assets/lang/de_DE.ini b/assets/lang/de_DE.ini index 9d3c51975316..a01db48faee1 100644 --- a/assets/lang/de_DE.ini +++ b/assets/lang/de_DE.ini @@ -391,6 +391,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = vor %1 Sekunden # AI translated %d hours = %d Stunden %d minutes = %d Minuten %d ms = %d ms diff --git a/assets/lang/dr_ID.ini b/assets/lang/dr_ID.ini index c0290b7928a0..870df3a4b6c5 100644 --- a/assets/lang/dr_ID.ini +++ b/assets/lang/dr_ID.ini @@ -392,6 +392,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = %1 detik yang lalu # AI translated %d hours = %d hours %d minutes = %d minutes %d ms = %d ms diff --git a/assets/lang/en_US.ini b/assets/lang/en_US.ini index f60c941f47a7..a2c2c53ee640 100644 --- a/assets/lang/en_US.ini +++ b/assets/lang/en_US.ini @@ -416,6 +416,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = %1 seconds ago %d hours = %d hours %d minutes = %d minutes %d ms = %d ms diff --git a/assets/lang/es_ES.ini b/assets/lang/es_ES.ini index e983e385c321..f5e7b6ee1f6b 100644 --- a/assets/lang/es_ES.ini +++ b/assets/lang/es_ES.ini @@ -393,6 +393,7 @@ Vertex = Vértices VFPU = VFPU [Dialog] +%1 seconds ago = hace %1 segundos # AI translated %d hours = %d horas %d minutes = %d minutos %d ms = %d ms diff --git a/assets/lang/es_LA.ini b/assets/lang/es_LA.ini index dbf3cb31521a..fdf0c6c5d079 100644 --- a/assets/lang/es_LA.ini +++ b/assets/lang/es_LA.ini @@ -392,6 +392,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = hace %1 segundos # AI translated %d hours = %d horas %d minutes = %d minutos %d ms = %d ms diff --git a/assets/lang/fa_IR.ini b/assets/lang/fa_IR.ini index 5c8373f8e7b0..4bde39d3463f 100644 --- a/assets/lang/fa_IR.ini +++ b/assets/lang/fa_IR.ini @@ -392,6 +392,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = قبل از %1 ثانیه # AI translated %d hours = %d hours %d minutes = %d minutes %d ms = %d میلی‌ثانیه diff --git a/assets/lang/fi_FI.ini b/assets/lang/fi_FI.ini index 0241885b079d..eabc310cd21f 100644 --- a/assets/lang/fi_FI.ini +++ b/assets/lang/fi_FI.ini @@ -392,6 +392,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = %1 sekuntia sitten # AI translated %d hours = %d tuntia %d minutes = %d minuuttia %d ms = %d ms diff --git a/assets/lang/fr_FR.ini b/assets/lang/fr_FR.ini index 8efc6ec288ab..1c61b7280bf8 100644 --- a/assets/lang/fr_FR.ini +++ b/assets/lang/fr_FR.ini @@ -416,6 +416,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = il y a %1 secondes # AI translated %d hours = %d heures %d minutes = %d minutes %d ms = %d ms diff --git a/assets/lang/gl_ES.ini b/assets/lang/gl_ES.ini index 2b3f1e598a50..c61bac47afbd 100644 --- a/assets/lang/gl_ES.ini +++ b/assets/lang/gl_ES.ini @@ -392,6 +392,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = hai %1 segundos # AI translated %d hours = %d hours %d minutes = %d minutes %d ms = %d ms diff --git a/assets/lang/gr_EL.ini b/assets/lang/gr_EL.ini index d3ddeba23f7c..80743c9060b4 100644 --- a/assets/lang/gr_EL.ini +++ b/assets/lang/gr_EL.ini @@ -392,6 +392,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = πριν από %1 δευτερόλεπτα # AI translated %d hours = %d hours %d minutes = %d minutes %d ms = %d ms diff --git a/assets/lang/he_IL.ini b/assets/lang/he_IL.ini index 3ade3a70b25d..d1598c727833 100644 --- a/assets/lang/he_IL.ini +++ b/assets/lang/he_IL.ini @@ -392,6 +392,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = לפני %1 שניות # AI translated %d hours = %d hours %d minutes = %d minutes %d ms = %d ms diff --git a/assets/lang/he_IL_invert.ini b/assets/lang/he_IL_invert.ini index 16a70df8f2e6..5e08433a6070 100644 --- a/assets/lang/he_IL_invert.ini +++ b/assets/lang/he_IL_invert.ini @@ -392,6 +392,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = לפני %1 שניות # AI translated %d hours = %d hours %d minutes = %d minutes %d ms = %d ms diff --git a/assets/lang/hr_HR.ini b/assets/lang/hr_HR.ini index ff5f4551420b..8ab11f19f49a 100644 --- a/assets/lang/hr_HR.ini +++ b/assets/lang/hr_HR.ini @@ -392,6 +392,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = prije %1 sekundi # AI translated %d hours = %d hours %d minutes = %d minutes %d ms = %d ms diff --git a/assets/lang/hu_HU.ini b/assets/lang/hu_HU.ini index 6384a42a1efe..91574ba6752f 100644 --- a/assets/lang/hu_HU.ini +++ b/assets/lang/hu_HU.ini @@ -392,6 +392,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = %1 másodperce # AI translated %d hours = %d hours %d minutes = %d minutes %d ms = %d ms diff --git a/assets/lang/id_ID.ini b/assets/lang/id_ID.ini index 50aea553e7fa..4554527ba3c2 100644 --- a/assets/lang/id_ID.ini +++ b/assets/lang/id_ID.ini @@ -392,6 +392,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = sejak %1 detik yang lalu # AI translated %d hours = %d jam %d minutes = %d menit %d ms = %d ms diff --git a/assets/lang/it_IT.ini b/assets/lang/it_IT.ini index 93acd4f2920b..3e42a0bcb7e8 100644 --- a/assets/lang/it_IT.ini +++ b/assets/lang/it_IT.ini @@ -392,6 +392,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = fa %1 secondi # AI translated %d hours = %d ore %d minutes = %d minuti %d ms = %d ms diff --git a/assets/lang/ja_JP.ini b/assets/lang/ja_JP.ini index 668ede31987c..d9805bdae80c 100644 --- a/assets/lang/ja_JP.ini +++ b/assets/lang/ja_JP.ini @@ -392,6 +392,7 @@ Vertex = 頂点 VFPU = VFPU [Dialog] +%1 seconds ago = %1 秒前 # AI translated %d hours = %d 時間 %d minutes = %d 分 %d ms = %d ms diff --git a/assets/lang/jv_ID.ini b/assets/lang/jv_ID.ini index f9bce51608da..bae709408c2a 100644 --- a/assets/lang/jv_ID.ini +++ b/assets/lang/jv_ID.ini @@ -392,6 +392,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = saka %1 detik kepungkur # AI translated %d hours = %d hours %d minutes = %d minutes %d ms = %d ms diff --git a/assets/lang/ko_KR.ini b/assets/lang/ko_KR.ini index ca855d8a9faa..700f0a77066c 100644 --- a/assets/lang/ko_KR.ini +++ b/assets/lang/ko_KR.ini @@ -392,6 +392,7 @@ Vertex = 꼭짓점 VFPU = VFPU [Dialog] +%1 seconds ago = %1초 전 # AI translated %d hours = %d 시간 %d minutes = %d 분 %d ms = %d ms diff --git a/assets/lang/ku_SO.ini b/assets/lang/ku_SO.ini index 13fdb8d9b018..5f8ea73409be 100644 --- a/assets/lang/ku_SO.ini +++ b/assets/lang/ku_SO.ini @@ -406,6 +406,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = bori %1 çirkan # AI translated %d hours = %d hours %d minutes = %d minutes %d ms = %d ms diff --git a/assets/lang/lo_LA.ini b/assets/lang/lo_LA.ini index 7977e1426e82..65ceaefbb5d7 100644 --- a/assets/lang/lo_LA.ini +++ b/assets/lang/lo_LA.ini @@ -392,6 +392,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = ຕ໭ວບ %1 ວິນາທີ # AI translated %d hours = %d hours %d minutes = %d minutes %d ms = %d ms diff --git a/assets/lang/lt-LT.ini b/assets/lang/lt-LT.ini index 80c955f756cb..eb55dcca266a 100644 --- a/assets/lang/lt-LT.ini +++ b/assets/lang/lt-LT.ini @@ -392,6 +392,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = prieš %1 sekundes # AI translated %d hours = %d hours %d minutes = %d minutes %d ms = %d ms diff --git a/assets/lang/ms_MY.ini b/assets/lang/ms_MY.ini index 3e679dc3196b..b404f11ec629 100644 --- a/assets/lang/ms_MY.ini +++ b/assets/lang/ms_MY.ini @@ -392,6 +392,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = lebih %1 saat yang lalu # AI translated %d hours = %d hours %d minutes = %d minutes %d ms = %d ms diff --git a/assets/lang/nl_NL.ini b/assets/lang/nl_NL.ini index aa0f2a678d8d..23567e00be9a 100644 --- a/assets/lang/nl_NL.ini +++ b/assets/lang/nl_NL.ini @@ -392,6 +392,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = ongeveer %1 seconden geleden # AI translated %d hours = %d hours %d minutes = %d minutes %d ms = %d ms diff --git a/assets/lang/no_NO.ini b/assets/lang/no_NO.ini index 740615776976..ab21b7a95b60 100644 --- a/assets/lang/no_NO.ini +++ b/assets/lang/no_NO.ini @@ -392,6 +392,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = for %1 sekunder siden # AI translated %d hours = %d timer %d minutes = %d minutter %d ms = %d ms diff --git a/assets/lang/pl_PL.ini b/assets/lang/pl_PL.ini index bde823aae0bc..06962d68ea21 100644 --- a/assets/lang/pl_PL.ini +++ b/assets/lang/pl_PL.ini @@ -392,6 +392,7 @@ Vertex = Wierzchołka VFPU = VFPU [Dialog] +%1 seconds ago = przed %1 sekundami # AI translated %d hours = %d godzin %d minutes = %d minut %d ms = %d milisekund diff --git a/assets/lang/pt_BR.ini b/assets/lang/pt_BR.ini index ac7bf3881117..a4eb698d7f92 100644 --- a/assets/lang/pt_BR.ini +++ b/assets/lang/pt_BR.ini @@ -416,6 +416,7 @@ Vertex = Vértice VFPU = VFPU [Dialog] +%1 seconds ago = há %1 segundos # AI translated %d hours = %d horas %d minutes = %d minutos %d ms = %d ms diff --git a/assets/lang/pt_PT.ini b/assets/lang/pt_PT.ini index 27aa60d51a67..bde2c0a51b22 100644 --- a/assets/lang/pt_PT.ini +++ b/assets/lang/pt_PT.ini @@ -416,6 +416,7 @@ Vertex = Vértice VFPU = VFPU [Dialog] +%1 seconds ago = há %1 segundos # AI translated %d hours = %d horas %d minutes = %d minutos %d ms = %d ms diff --git a/assets/lang/ro_RO.ini b/assets/lang/ro_RO.ini index 7765f0686b2c..dc6d4f1cd85e 100644 --- a/assets/lang/ro_RO.ini +++ b/assets/lang/ro_RO.ini @@ -393,6 +393,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = acum %1 secunde # AI translated %d hours = %d hours %d minutes = %d minutes %d ms = %d ms diff --git a/assets/lang/ru_RU.ini b/assets/lang/ru_RU.ini index 54a78243be4f..7178870c16ae 100644 --- a/assets/lang/ru_RU.ini +++ b/assets/lang/ru_RU.ini @@ -392,6 +392,7 @@ Vertex = Вершина VFPU = VFPU [Dialog] +%1 seconds ago = %1 секунду назад # AI translated %d hours = %d ч %d minutes = %d мин %d ms = %d мс diff --git a/assets/lang/sv_SE.ini b/assets/lang/sv_SE.ini index e1d6f6712216..4460c4c2febe 100644 --- a/assets/lang/sv_SE.ini +++ b/assets/lang/sv_SE.ini @@ -392,6 +392,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = för %1 sekunder sedan # AI translated %d hours = %d timmar %d minutes = %d minuter %d ms = %d ms diff --git a/assets/lang/tg_PH.ini b/assets/lang/tg_PH.ini index ebc8fc4ad6c6..a82c7b58195d 100644 --- a/assets/lang/tg_PH.ini +++ b/assets/lang/tg_PH.ini @@ -393,6 +393,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = ҳоло %1 сония пеш # AI translated %d hours = %d hours %d minutes = %d minutes %d ms = %d ms diff --git a/assets/lang/th_TH.ini b/assets/lang/th_TH.ini index c421c0601907..0b806ec06c94 100644 --- a/assets/lang/th_TH.ini +++ b/assets/lang/th_TH.ini @@ -408,6 +408,7 @@ Vulkan = วัลแคน VFPU = VFPU [Dialog] +%1 seconds ago = เมื่อ %1 วินาทีที่แล้ว # AI translated %d hours = %d ชั่วโมง %d minutes = %d นาที %d ms = %d ms diff --git a/assets/lang/tr_TR.ini b/assets/lang/tr_TR.ini index 66a379b7eeb9..ef8f3960aaf6 100644 --- a/assets/lang/tr_TR.ini +++ b/assets/lang/tr_TR.ini @@ -394,6 +394,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = %1 saniye önce # AI translated %d hours = %d saat %d minutes = %d dakika %d ms = %d ms diff --git a/assets/lang/uk_UA.ini b/assets/lang/uk_UA.ini index eda0d05ed883..d3b876b103fe 100644 --- a/assets/lang/uk_UA.ini +++ b/assets/lang/uk_UA.ini @@ -392,6 +392,7 @@ Vertex = Вершина VFPU = VFPU [Dialog] +%1 seconds ago = %1 секунд тому # AI translated %d hours = %d год %d minutes = %d хв %d ms = %d мс diff --git a/assets/lang/vi_VN.ini b/assets/lang/vi_VN.ini index 11873c79bde7..60587210ac09 100644 --- a/assets/lang/vi_VN.ini +++ b/assets/lang/vi_VN.ini @@ -392,6 +392,7 @@ Vertex = Vertex VFPU = VFPU [Dialog] +%1 seconds ago = trước %1 giây # AI translated %d hours = %d hours %d minutes = %d minutes %d ms = %d ms diff --git a/assets/lang/zh_CN.ini b/assets/lang/zh_CN.ini index efed768b1f9f..94f68bb6c9d1 100644 --- a/assets/lang/zh_CN.ini +++ b/assets/lang/zh_CN.ini @@ -392,6 +392,7 @@ Vertex = 顶点着色器 VFPU = VFPU [Dialog] +%1 seconds ago = %1秒前 # AI translated %d hours = %d 小时 %d minutes = %d 分 %d ms = %d 毫秒 diff --git a/assets/lang/zh_TW.ini b/assets/lang/zh_TW.ini index 46c4c1d39191..93ddf851e65d 100644 --- a/assets/lang/zh_TW.ini +++ b/assets/lang/zh_TW.ini @@ -392,6 +392,7 @@ Vertex = 頂點 VFPU = VFPU [Dialog] +%1 seconds ago = %1 秒鐘前 # AI translated %d hours = %d 小時 %d minutes = %d 分 %d ms = %d 毫秒 From fb7ce84edbb6dccf4e5c3b48de3f71e9b17ebdff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 19 May 2026 12:19:13 +0200 Subject: [PATCH 5/7] SaveState: Show time/date when loading states --- Core/SaveState.cpp | 22 ++++++++++++++-------- Core/SaveStateRewind.cpp | 6 ------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Core/SaveState.cpp b/Core/SaveState.cpp index 344343f3d287..521c6057c1ea 100644 --- a/Core/SaveState.cpp +++ b/Core/SaveState.cpp @@ -106,10 +106,10 @@ enum class OperationType { struct Operation { // The slot number is for visual purposes only. Set to -1 for operations where we don't display a message for example. Operation(OperationType t, const Path &f, int slot_, Callback cb) - : type(t), filename(f), callback(cb), slot(slot_) {} + : type(t), path(f), callback(cb), slot(slot_) {} OperationType type; - Path filename; + Path path; Callback callback; int slot; }; @@ -675,8 +675,7 @@ int g_screenshotFailures; return oldestSlot; } - std::string GetSlotDateAsString(std::string_view gamePrefix, int slot) { - std::string fn = GenerateSaveSlotFilename(gamePrefix, slot, STATE_EXTENSION); + static std::string GetSaveFileDateAsString(const std::string &fn) { auto iter = g_files.find(fn); if (iter == g_files.end()) { return ""; @@ -703,6 +702,11 @@ int g_screenshotFailures; return buf; } + std::string GetSlotDateAsString(std::string_view gamePrefix, int slot) { + std::string fn = GenerateSaveSlotFilename(gamePrefix, slot, STATE_EXTENSION); + return GetSaveFileDateAsString(fn); + } + std::vector Flush() { std::lock_guard guard(mutex); std::vector copy = g_pendingOperations; @@ -822,12 +826,14 @@ int g_screenshotFailures; switch (op.type) { case OperationType::Load: - INFO_LOG(Log::SaveState, "Loading state from '%s'", op.filename.c_str()); + INFO_LOG(Log::SaveState, "Loading state from '%s'", op.path.c_str()); // Use the state's latest version as a guess for saveStateInitialGitVersion. - result = CChunkFileReader::Load(op.filename, &saveStateInitialGitVersion, state, &errorString); + result = CChunkFileReader::Load(op.path, &saveStateInitialGitVersion, state, &errorString); if (result == CChunkFileReader::ERROR_NONE) { callbackMessage = op.slot != LOAD_UNDO_SLOT ? sc->T("Loaded State") : sc->T("State load undone"); callbackResult = TriggerLoadWarnings(callbackMessage); + callbackMetadata = SaveState::GetSaveFileDateAsString(op.path.GetFilename().c_str()); + hasLoadedState = true; Core_ResetException(); @@ -858,7 +864,7 @@ int g_screenshotFailures; break; case OperationType::Save: - INFO_LOG(Log::SaveState, "Saving state to '%s'", op.filename.c_str()); + INFO_LOG(Log::SaveState, "Saving state to '%s'", op.path.c_str()); title = g_paramSFO.GetValueString("TITLE"); if (title.empty()) { // Homebrew title @@ -866,7 +872,7 @@ int g_screenshotFailures; std::size_t lslash = title.find_last_of('/'); title = title.substr(lslash + 1); } - result = CChunkFileReader::Save(op.filename, title, PPSSPP_GIT_VERSION, state); + result = CChunkFileReader::Save(op.path, title, PPSSPP_GIT_VERSION, state); if (result == CChunkFileReader::ERROR_NONE) { callbackMessage = slot_prefix + std::string(sc->T("Saved State")); callbackResult = Status::SUCCESS; diff --git a/Core/SaveStateRewind.cpp b/Core/SaveStateRewind.cpp index aa4cc62926ab..dc7eaf0c7a53 100644 --- a/Core/SaveStateRewind.cpp +++ b/Core/SaveStateRewind.cpp @@ -68,12 +68,6 @@ CChunkFileReader::Error StateRingbuffer::Restore(std::string *errorString, std:: auto di = GetI18NCategory(I18NCat::DIALOG); metadata->append(" ("); metadata->append(ApplySafeSubstitutions(di->T("%1 seconds ago"), static_cast(time_now_d() - states_[n].savedTime))); - /* - char buffer[26]; - double unixTime = time_to_unix_utc(states_[n].savedTime); - FormatUnixTime(unixTime, buffer, sizeof(buffer), false); - metadata->append(buffer); - */ metadata->append(")"); } From 07efc7f7a3b4271dbf5489e21ec6ccf3f6d7cece Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 19 May 2026 13:59:04 +0200 Subject: [PATCH 6/7] Buildfix, remove some .c_str() --- Common/TimeUtil.cpp | 2 +- Common/TimeUtil.h | 2 +- Core/SaveState.cpp | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Common/TimeUtil.cpp b/Common/TimeUtil.cpp index 2279f11d775c..1178e4e21395 100644 --- a/Common/TimeUtil.cpp +++ b/Common/TimeUtil.cpp @@ -385,7 +385,7 @@ void GetCurrentTimeFormatted(char formattedTime[13]) { #endif } -void FormatUnixTime(double unixTimeSeconds, char *formatted, size_t bufSize, bool includeDate) { +void FormatUnixTime(double unixTimeSeconds, char *formatted, int bufSize, bool includeDate) { #ifdef _WIN32 ULARGE_INTEGER uli; uli.QuadPart = (ULONGLONG)(unixTimeSeconds * TICKS_PER_SECOND) + UNIX_TIME_START; // Convert seconds to ticks and add the offset to get FILETIME ticks. diff --git a/Common/TimeUtil.h b/Common/TimeUtil.h index 27adb8ade2b9..30f4802ffa44 100644 --- a/Common/TimeUtil.h +++ b/Common/TimeUtil.h @@ -34,7 +34,7 @@ void sleep_random(double minSeconds, double maxSeconds, const char *reason); void yield(); void GetCurrentTimeFormatted(char formattedTime[13]); -void FormatUnixTime(double unixTimeSeconds, char *formatted, size_t bufSize, bool includeDate = true); +void FormatUnixTime(double unixTimeSeconds, char *formatted, int bufSize, bool includeDate = true); // Most accurate timer possible - no extra double conversions. Only for spans. class Instant { diff --git a/Core/SaveState.cpp b/Core/SaveState.cpp index 521c6057c1ea..cdc7843f5d1e 100644 --- a/Core/SaveState.cpp +++ b/Core/SaveState.cpp @@ -832,7 +832,7 @@ int g_screenshotFailures; if (result == CChunkFileReader::ERROR_NONE) { callbackMessage = op.slot != LOAD_UNDO_SLOT ? sc->T("Loaded State") : sc->T("State load undone"); callbackResult = TriggerLoadWarnings(callbackMessage); - callbackMetadata = SaveState::GetSaveFileDateAsString(op.path.GetFilename().c_str()); + callbackMetadata = SaveState::GetSaveFileDateAsString(op.path.GetFilename()); hasLoadedState = true; Core_ResetException(); @@ -858,7 +858,7 @@ int g_screenshotFailures; ERROR_LOG(Log::SaveState, "Load state failure: %s", errorString.c_str()); callbackResult = Status::FAILURE; } else { - callbackMessage = sc->T(errorString.c_str(), i18nLoadFailure); + callbackMessage = sc->T(errorString, i18nLoadFailure); callbackResult = Status::FAILURE; } break; From 992bd1b12d70c3d053a4e1508ee5c7b54ebe5384 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 19 May 2026 14:36:47 +0200 Subject: [PATCH 7/7] Qt buildfix, remove unused function --- Common/TimeUtil.cpp | 55 --------------------------------------------- Common/TimeUtil.h | 1 - Qt/mainwindow.cpp | 2 +- 3 files changed, 1 insertion(+), 57 deletions(-) diff --git a/Common/TimeUtil.cpp b/Common/TimeUtil.cpp index 1178e4e21395..ff34dac7de67 100644 --- a/Common/TimeUtil.cpp +++ b/Common/TimeUtil.cpp @@ -385,61 +385,6 @@ void GetCurrentTimeFormatted(char formattedTime[13]) { #endif } -void FormatUnixTime(double unixTimeSeconds, char *formatted, int bufSize, bool includeDate) { -#ifdef _WIN32 - ULARGE_INTEGER uli; - uli.QuadPart = (ULONGLONG)(unixTimeSeconds * TICKS_PER_SECOND) + UNIX_TIME_START; // Convert seconds to ticks and add the offset to get FILETIME ticks. - FILETIME ft; - ft.dwLowDateTime = uli.LowPart; - ft.dwHighDateTime = uli.HighPart; - - // Convert UTC FILETIME to local FILETIME - FILETIME localFt; - FileTimeToLocalFileTime(&ft, &localFt); - - SYSTEMTIME st; - FileTimeToSystemTime(&localFt, &st); - - // Use system locale for date/time formatting - wchar_t dateStr[256]; - wchar_t timeStr[256]; - - // Get localized date string (short date format) - GetDateFormatW(LOCALE_USER_DEFAULT, DATE_SHORTDATE, &st, nullptr, dateStr, 256); - - // Get localized time string (without seconds by default, but we'll add them) - GetTimeFormatW(LOCALE_USER_DEFAULT, 0, &st, nullptr, timeStr, 256); - - // Convert to char and combine - char timeMb[256]; - char dateMb[256]; - WideCharToMultiByte(CP_UTF8, 0, timeStr, -1, timeMb, 256, nullptr, nullptr); - - if (includeDate) { - WideCharToMultiByte(CP_UTF8, 0, dateStr, -1, dateMb, 256, nullptr, nullptr); - snprintf(formatted, bufSize, "%s %s", dateMb, timeMb); - } else { - snprintf(formatted, bufSize, "%s", timeMb); - } - -#else - struct timespec ts; - ts.tv_sec = (time_t)unixTimeSeconds; - ts.tv_nsec = (long)((unixTimeSeconds - ts.tv_sec) * 1000000000.0); - struct tm tm; - localtime_r(&ts.tv_sec, &tm); - - // Use strftime with locale-specific formatting - if (includeDate) { - // %x is locale-specific date, %X is locale-specific time - strftime(formatted, bufSize, "%x %X", &tm); - } else { - // Just time - strftime(formatted, bufSize, "%X", &tm); - } -#endif -} - // We don't even bother synchronizing this, it's fine if threads stomp a bit. static GMRng g_sleepRandom; diff --git a/Common/TimeUtil.h b/Common/TimeUtil.h index 30f4802ffa44..6f1a30a14c46 100644 --- a/Common/TimeUtil.h +++ b/Common/TimeUtil.h @@ -34,7 +34,6 @@ void sleep_random(double minSeconds, double maxSeconds, const char *reason); void yield(); void GetCurrentTimeFormatted(char formattedTime[13]); -void FormatUnixTime(double unixTimeSeconds, char *formatted, int bufSize, bool includeDate = true); // Most accurate timer possible - no extra double conversions. Only for spans. class Instant { diff --git a/Qt/mainwindow.cpp b/Qt/mainwindow.cpp index cbd9fdafce16..c499de440cbe 100644 --- a/Qt/mainwindow.cpp +++ b/Qt/mainwindow.cpp @@ -153,7 +153,7 @@ void MainWindow::openmsAct() QDesktopServices::openUrl(QUrl(memorystick)); } -static void SaveStateActionFinished(SaveState::Status status, std::string_view message) +static void SaveStateActionFinished(SaveState::Status status, std::string_view message, std::string_view metadata) { // TODO: Improve messaging? if (status == SaveState::Status::FAILURE)