Show timestamps when loading rewind states

This commit is contained in:
Henrik Rydgård
2026-05-19 11:53:16 +02:00
parent 8dad5a5c57
commit c20d20ccba
5 changed files with 116 additions and 18 deletions
+80 -4
View File
@@ -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<double>(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<int64_t>(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;
+2
View File
@@ -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 {
-3
View File
@@ -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));
+23 -10
View File
@@ -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<u8> 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<u8> &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
+11 -1
View File
@@ -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<u8> 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<StateBuffer> states_;
std::vector<RewindState> states_;
StateBuffer bases_[2];
std::vector<int> baseMapping_;
std::mutex lock_;