ft2-clone

Fasttracker 2 clone
Log | Files | Refs | README | LICENSE

commit f7063748617e4dc6742294e8a471383e7b2ba497
parent 14c45c48c360db0aa89bda32c450a710bf94a79c
Author: Olav Sørensen <olav.sorensen@live.no>
Date:   Tue, 24 Jan 2023 22:45:33 +0100

Fixing of frame delay/wait routines

Diffstat:
Msrc/ft2_events.c | 2+-
Msrc/ft2_hpc.c | 106+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Msrc/ft2_hpc.h | 11++++++-----
Msrc/ft2_main.c | 4++--
Msrc/ft2_video.c | 2+-
Msrc/scopes/ft2_scopes.c | 2+-
6 files changed, 82 insertions(+), 45 deletions(-)

diff --git a/src/ft2_events.c b/src/ft2_events.c @@ -368,7 +368,7 @@ void handleWaitVblQuirk(SDL_Event *event) // reset vblank end time if we minimize window if (event->window.event == SDL_WINDOWEVENT_MINIMIZED || event->window.event == SDL_WINDOWEVENT_FOCUS_LOST) - hpc_ResetEndTime(&video.vblankHpc); + hpc_ResetCounters(&video.vblankHpc); } } diff --git a/src/ft2_hpc.c b/src/ft2_hpc.c @@ -13,7 +13,7 @@ #include <stdbool.h> #include "ft2_hpc.h" -#define FRAC_BITS 53 +#define FRAC_BITS 63 #define FRAC_SCALE (1ULL << FRAC_BITS) #define FRAC_MASK (FRAC_SCALE-1) @@ -21,6 +21,10 @@ hpcFreq_t hpcFreq; #ifdef _WIN32 // Windows usleep() implementation +#define STATUS_SUCCESS 0 + +static bool canAdjustTimerResolution; + static NTSTATUS (__stdcall *NtDelayExecution)(BOOL Alertable, PLARGE_INTEGER DelayInterval); static NTSTATUS (__stdcall *NtQueryTimerResolution)(PULONG MinimumResolution, PULONG MaximumResolution, PULONG ActualResolution); static NTSTATUS (__stdcall *NtSetTimerResolution)(ULONG DesiredResolution, BOOLEAN SetResolution, PULONG CurrentResolution); @@ -31,7 +35,7 @@ static void usleepGood(int32_t usec) { LARGE_INTEGER delayInterval; - // NtDelayExecution() delays in 100ns-units, and negative value = delay from current time + // NtDelayExecution() delays in 100ns-units, and a negative value means to delay from current time usec *= -10; delayInterval.HighPart = 0xFFFFFFFF; // negative 64-bit value, we only set the lower dword @@ -39,7 +43,7 @@ static void usleepGood(int32_t usec) NtDelayExecution(false, &delayInterval); } -static void usleepWeak(int32_t usec) // fallback if no NtDelayExecution() +static void usleepPoor(int32_t usec) // fallback if no NtDelayExecution() { Sleep((usec + 500) / 1000); } @@ -47,10 +51,11 @@ static void usleepWeak(int32_t usec) // fallback if no NtDelayExecution() static void windowsSetupUsleep(void) { NtDelayExecution = (NTSTATUS (__stdcall *)(BOOL, PLARGE_INTEGER))GetProcAddress(GetModuleHandle("ntdll.dll"), "NtDelayExecution"); + usleep = (NtDelayExecution != NULL) ? usleepGood : usleepPoor; + NtQueryTimerResolution = (NTSTATUS (__stdcall *)(PULONG, PULONG, PULONG))GetProcAddress(GetModuleHandle("ntdll.dll"), "NtQueryTimerResolution"); NtSetTimerResolution = (NTSTATUS (__stdcall *)(ULONG, BOOLEAN, PULONG))GetProcAddress(GetModuleHandle("ntdll.dll"), "NtSetTimerResolution"); - - usleep = (NtDelayExecution != NULL) ? usleepGood : usleepWeak; + canAdjustTimerResolution = (NtQueryTimerResolution != NULL && NtSetTimerResolution != NULL); } #endif @@ -60,49 +65,67 @@ void hpc_Init(void) windowsSetupUsleep(); #endif hpcFreq.freq64 = SDL_GetPerformanceFrequency(); - hpcFreq.dFreq = (double)hpcFreq.freq64; - hpcFreq.dFreqMulMs = 1000.0 / hpcFreq.dFreq; - hpcFreq.dFreqMulMicro = (1000.0 * 1000.0) / hpcFreq.dFreq; + + double dFreq = (double)hpcFreq.freq64; + + hpcFreq.dFreqMulMs = 1000.0 / dFreq; + hpcFreq.dFreqMulMicro = (1000.0 * 1000.0) / dFreq; } -void hpc_SetDurationInHz(hpc_t *hpc, const double dHz) +// returns 64-bit fractional part of u64 divided by u32 +static uint64_t getFrac64FromU64DivU32(uint64_t dividend, uint32_t divisor) { - const double dDuration = hpcFreq.dFreq / dHz; + if (dividend == 0 || divisor == 0 || divisor >= dividend) + return 0; + + dividend %= divisor; + + if (dividend == 0) + return 0; - // break down duration into integer and frac parts - double dDurationInt; - double dDurationFrac = modf(dDuration, &dDurationInt); + const uint32_t quotient = (uint32_t)((dividend << 32) / divisor); + const uint32_t remainder = (uint32_t)((dividend << 32) % divisor); - // set 64:53fp values - hpc->duration64Int = (uint64_t)dDurationInt; - hpc->duration64Frac = (uint64_t)round(dDurationFrac * FRAC_SCALE); + const uint32_t resultHi = quotient; + const uint32_t resultLo = (uint32_t)(((uint64_t)remainder << 32) / divisor); + + return ((uint64_t)resultHi << 32) | resultLo; } -void hpc_ResetEndTime(hpc_t *hpc) +void hpc_SetDurationInHz(hpc_t *hpc, uint32_t hz) { - hpc->endTime64Int = SDL_GetPerformanceCounter() + hpc->duration64Int; - hpc->endTime64Frac = hpc->duration64Frac; + // set 64:63fp value + hpc->durationInt = hpcFreq.freq64 / hz; + hpc->durationFrac = getFrac64FromU64DivU32(hpcFreq.freq64, hz) >> 1; + + hpc->resetFrame = hz * 3600; // reset counters every hour + } -void hpc_Wait(hpc_t *hpc) +void hpc_ResetCounters(hpc_t *hpc) { -#ifdef _WIN32 // set resolution to 0.5ms (safest minium) - this is confirmed to improve NtDelayExecution() and Sleep() - ULONG originalTimerResolution, minRes, maxRes, curRes; + hpc->endTimeInt = SDL_GetPerformanceCounter() + hpc->durationInt; + hpc->endTimeFrac = hpc->durationFrac; +} - if (NtQueryTimerResolution != NULL && NtSetTimerResolution != NULL) +void hpc_Wait(hpc_t *hpc) +{ +#ifdef _WIN32 + /* Make sure resolution is set to 0.5ms (safest minimum) - this is confirmed to improve + ** NtDelayExecution() and Sleep(). This will only be changed when needed, not per frame. + */ + ULONG curRes, minRes, maxRes, junk; + if (canAdjustTimerResolution && NtQueryTimerResolution(&minRes, &maxRes, &curRes) == STATUS_SUCCESS) { - if (!NtQueryTimerResolution(&minRes, &maxRes, &originalTimerResolution)) - { - if (originalTimerResolution != 5000 && maxRes <= 5000) - NtSetTimerResolution(5000, TRUE, &curRes); // set to 0.5ms (safest minimum) - } + if (curRes != 5000 && maxRes <= 5000) + NtSetTimerResolution(5000, TRUE, &junk); // 0.5ms } #endif const uint64_t currTime64 = SDL_GetPerformanceCounter(); - if (currTime64 < hpc->endTime64Int) + if (currTime64 < hpc->endTimeInt) { - uint64_t timeLeft64 = hpc->endTime64Int - currTime64; + uint64_t timeLeft64 = hpc->endTimeInt - currTime64; // convert to int32_t for fast SSE2 SIMD usage lateron if (timeLeft64 > INT32_MAX) @@ -117,12 +140,25 @@ void hpc_Wait(hpc_t *hpc) // set next end time - hpc->endTime64Int += hpc->duration64Int; + hpc->endTimeInt += hpc->durationInt; + + // handle fractional part + hpc->endTimeFrac += hpc->durationFrac; + if (hpc->endTimeFrac >= FRAC_SCALE) + { + hpc->endTimeFrac &= FRAC_MASK; + hpc->endTimeInt++; + } - hpc->endTime64Frac += hpc->duration64Frac; - if (hpc->endTime64Frac >= FRAC_SCALE) + /* The counter ("endTimeInt") can accumulate major errors after a couple of hours, + ** since each frame is not happening at perfect intervals. + ** To fix this, reset the counter's int & frac once every hour. We should only get + ** up to one frame of stutter while they are resetting, then it's back to normal. + */ + hpc->frameCounter++; + if (hpc->frameCounter >= hpc->resetFrame) { - hpc->endTime64Frac &= FRAC_MASK; - hpc->endTime64Int++; + hpc->frameCounter = 0; + hpc_ResetCounters(hpc); } } diff --git a/src/ft2_hpc.h b/src/ft2_hpc.h @@ -6,18 +6,19 @@ typedef struct { uint64_t freq64; - double dFreq, dFreqMulMicro, dFreqMulMs; + double dFreqMulMicro, dFreqMulMs; } hpcFreq_t; typedef struct { - uint64_t duration64Int, duration64Frac; - uint64_t endTime64Int, endTime64Frac; + uint64_t durationInt, durationFrac; + uint64_t endTimeInt, endTimeFrac; + uint64_t frameCounter, resetFrame; } hpc_t; extern hpcFreq_t hpcFreq; void hpc_Init(void); -void hpc_SetDurationInHz(hpc_t *hpc, double dHz); -void hpc_ResetEndTime(hpc_t *hpc); +void hpc_SetDurationInHz(hpc_t *hpc, uint32_t dHz); +void hpc_ResetCounters(hpc_t *hpc); void hpc_Wait(hpc_t *hpc); diff --git a/src/ft2_main.c b/src/ft2_main.c @@ -233,11 +233,11 @@ int main(int argc, char *argv[]) SDL_DetachThread(initMidiThread); // don't wait for this thread, let it clean up when done #endif - hpc_ResetEndTime(&video.vblankHpc); // this is needed for potential okBox() calls in handleModuleLoadFromArg() + hpc_ResetCounters(&video.vblankHpc); // quirk: this is needed for potential okBox() calls in handleModuleLoadFromArg() handleModuleLoadFromArg(argc, argv); editor.mainLoopOngoing = true; - hpc_ResetEndTime(&video.vblankHpc); // this must be the very last thing done before entering the main loop + hpc_ResetCounters(&video.vblankHpc); // this must be the last thing we do before entering the main loop while (editor.programRunning) { diff --git a/src/ft2_video.c b/src/ft2_video.c @@ -187,7 +187,7 @@ void endFPSCounter(void) if (frameTimeDiff64 > INT32_MAX) frameTimeDiff64 = INT32_MAX; - dRunningFrameDuration += (int32_t)frameTimeDiff64 / (hpcFreq.dFreq / 1000.0); + dRunningFrameDuration += (int32_t)frameTimeDiff64 * hpcFreq.dFreqMulMs; } } diff --git a/src/scopes/ft2_scopes.c b/src/scopes/ft2_scopes.c @@ -521,7 +521,7 @@ static int32_t SDLCALL scopeThreadFunc(void *ptr) SDL_SetThreadPriority(SDL_THREAD_PRIORITY_HIGH); hpc_SetDurationInHz(&scopeHpc, SCOPE_HZ); - hpc_ResetEndTime(&scopeHpc); + hpc_ResetCounters(&scopeHpc); while (editor.programRunning) {