StrikeBox
StrikeBox copied to clipboard
Fix timing accuracy
The method currently in use by timing-sensitive threads such as the i8254 timer loop (which needs to trigger once every millisecond) is not accurate enough on Windows. In both computers I have access to, the loop does trigger 1000 times per second as expected, but it fires two events every ~2ms instead of one every 1ms.
Windows offers the following timer and timing-related APIs:
- QueryPerformanceCounter and QueryPerformanceFrequency: Useful for accurate time interval measurements, but not for triggering events as they require a busy loop that pegs down a CPU core to 100%.
- std::this_thread::sleep_until and std::chrono::high_resolution_clock: The technique currently in use. Cross-platform, but doesn't provide millisecond accuracy (on Windows at least).
- timeBeginPeriod and timeSetEvent: The most accurate of all methods I've tested, but only works if
timeBeginPeriod(1)succeeds. (It should succeed on basically any modern computer using at least Windows 7 which is what StrikeBox targets.) - CreateWaitableTimer and SetWaitableTimer: A synchronization object that fires periodically. Misses a lot of events.
- CreateTimerQueue and CreateTimerQueueTimer: Microsoft says this is a replacement for the deprecated
timeSetEventAPI. My tests argue otherwise.
Here's a snapshot of 100 milliseconds of execution using each technique. Time is measured in microseconds with QueryPerformanceCounter. Each dot represents an event fired by one of the methods at a given moment. The system was under light load at the time of the test, which should provide a realistic environment similar to the expected usage of the emulator. The code was compiled in 64-bit Release mode.

The graph describes exactly what the issue is with std::this_thread::sleep_until: it doesn't trigger frequently enough.
QPC triggers almost perfectly in sync with the expected tick rate, but it costs 100% of a CPU core and is still subject to thread preemption (as seen in the last tick).
CreateWaitableTimer/SetWaitableTimer and CreateTimerQueue/CreateTimerQueueTimer are unsuited to the task. They missed a lot of ticks and drifted away from the desired tick rate.
timeSetEvent is the best option out of all these. It's not perfect either, but we can't expect perfect accuracy from a non-realtime operating system. However, it is much better than the current technique.
I haven't tested this on Linux yet. In any case, if the std::this_thread::sleep_until method is not accurate enough, timer_create seems to be the solution.
Historically, there's been done more research on the topic; Here a few similar reports from around the world:
http://www.geisswerks.com/ryan/FAQS/timing.html http://www.virtualdub.org/blog/pivot/entry.php?id=272 https://omeg.pl/blog/2011/11/on-winapi-timers-and-their-resolution/
What it boils down to, is that timeSetPeriod(1)+timeSetEvent() and busy-wait loops (using QPF+QPC) are most reliable, combining them together makes for the most accurate and precise timing
Also related : https://en.wikipedia.org/wiki/Accuracy_and_precision#ISO_definition_(ISO_5725) https://stackoverflow.com/a/29183085/12170 https://stackoverflow.com/questions/7685762/windows-7-timing-functions-how-to-use-getsystemtimeadjustment-correctly http://blog.nuclex-games.com/2012/04/perfectly-accurate-game-timing/ https://redream.io/posts/improving-audio-video-synchronization-multi-sync https://www.gamasutra.com/view/feature/171774/getting_high_precision_timing_on_.php?print=1
EDIT Oh, and : https://docs.microsoft.com/en-us/windows-hardware/drivers/kernel/timer-accuracy