diff --git a/Common/TimeUtil.cpp b/Common/TimeUtil.cpp index 344990ae2b..2279f11d77 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 84fe433ee5..27adb8ade2 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 837ce22dea..344343f3d2 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 c61570bb40..8eabc7585b 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 5c6cb3a83e..16eddec118 100644 --- a/Core/SaveStateRewind.h +++ b/Core/SaveStateRewind.h @@ -41,6 +41,8 @@ public: void Process(); void NotifyState(); + double NextStateTimestamp() const; + private: const int BLOCK_SIZE = 8192; const int REWIND_NUM_STATES = 20; @@ -49,11 +51,19 @@ private: 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_;