StrikerX3 / StrikeBox

StrikeBox - Original Xbox emulator
BSD 2-Clause "Simplified" License
52 stars 9 forks source link

Fix timing accuracy #24

Open StrikerX3 opened 4 years ago

StrikerX3 commented 4 years ago

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:

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.

image

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.

PatrickvL commented 4 years ago

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