mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-07-11 01:25:07 +02:00
Show timestamps when loading rewind states
This commit is contained in:
+80
-4
@@ -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;
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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_;
|
||||
|
||||
Reference in New Issue
Block a user