Sunday, June 27, 2010

Fun with Timers

I've been cleaning up the timer code (I may have mentioned that recently) and have now gotten rid of a lot of the nasty hacks I had done (I may have also mentioned that too), and the whole thing has got me thinking about DirectQ's timer in general.

The key problem with timers is that ever since hardware stopped being shit some years back we're in a position where the traditional recommended way of getting high-precison timing info on Windows (QueryPerformanceFrequency and QueryPerformanceCounter) no longer cuts it. There are a few things wrong with it; firstly an overflow problem (that ID to their credit anticipated and worked around), secondly a problem with multi-core machines (where your app may be switched from one core to another, and incur subtle speed variations as a result) and thirdly a problem with power-saving modes (where the CPU may be now running faster than it was when the app started, and so the info you're basing your timing on is no longer valid).

The recommended way to resolve these is to use timeBeginPeriod and timeGetTime instead, which returns time in one millisecond increments. However, there is an even more insidious problem with this, and it goes something like:

"72 FPS was good enough for my grandfather, it was good enough for my father, it's good enough for me, and it will be dang-well good enough for my children too! Now gettouttahere with your fancy new ideas afore I sets the Sabre-Tooth Cat on ye!"
You see, the interesting thing about working with integers instead of floats is that sometimes numbers don't divide evenly. In this case, 1000 milliseconds at 72 FPS gives you 13.888888888888888888888888888888 milliseconds per frame. But because we're working with integers, sometimes it will be 13, other times it will be 14.

So now that we know why Quake II switched to 83.33333 FPS (a nice even 12 milliseconds per frame), what are the consequences for DirectQ? Probably none. That scary man with the Sabre-Tooth Cat is rather big, has a very loud voice and can wield a lot of influence (most of which one would hope he uses for good). However, it does make the problem of obtaining smooth even timing somewhat more complex than it should be, and sometimes when I'm sleeping in my cave at night, after a hard day's work banging stones together to make fire, I really do wish he would go away.

Update:

The current solution looks something like this:
  • cl_maxfps (or host_maxfps, whichever you prefer) is now clamped to 500. At values above 500, frame times will drop to 1 millisecond and rounding errors will be enough to cause accuracy to fall off. I'd actually make this 250 if I thought I could get away with it. Welcome to integer land!
  • Instead of cl_maxfps (or host_maxfps - you get the idea) being an absolute upper limit, timings will average around that value. Sometimes they will be slightly higher, sometimes they will be slightly lower.
  • The net effect is that gameplay is smoothed out. It doesn't feel a single bit rough or raggy in practice. Rounding errors now average themselves out instead of accumulating.

0 comments: