Frame timing hell


Over the last few days I've been performing a much needed restructure of the game loop, into 'do one update' and 'draw one frame' rather than an amalgamated mess.

As part of this I took the first serious look at timing: update timings, v-sync, frame rate limit etc.

Some of these things don't have very elegant solutions without adding a lot of complication (e.g. frame to frame interpolation) - despite the comments made by armchair programmers about the framerates/frame locks of their favorite games.

There are three big issues here:

  • Logic update frequency - so how frequently the game actually updates the world etc. or responds to input
  • Draw/render frequency (and limiting)
  • V-sync (which although has the same effect as a 60Hz limit, gets more complicated than that)

Logic updates are classically made independent of framerate by using a 'delta time' increment which everything is multiplied by - i.e. a variable timestep. This is pretty standard and the game already has this.

But: having that doesn't just allow you to have the framerate arbitrarily high without consequence. Some motion calculations are not amenable to being stepped forward with a potentially fluctuating timestep - sometimes this is just ugly (or jittery) - other times objects will 'explode'. The classic example here is fully physics engines which are extremely sensitive to timing variations, and often have to be fixed separately in some way. 

If you can have an unlocked timestep like this, you can often get away with performing updates in lockstep: i.e. you perform one logic update and one render per frame in a very simple setup. This is the classic arcade-game like game loop which is the type I'm most used to, frankly, having learned game programming in DOS.

And admittedly, that does lead to silky smooth performance - when it works.

Some frame timings locked at 90FPS. The blue bar is continuous here because a logic update occurs every frame using a variable timestep

But as the game gets more 'stuff' in it, the likelihood of erratic variations in that timestep increase, and the result will be weird bugs and behaviour (it also becomes harder to guarantee that every player's machine will be able to manage update and render in lockstep).

The next contender would be to support a variable timestep, but actually fix its value per update in almost all cases: classically 60Hz. i.e. you perform one game world update every 1/60 of a second, regardless of how fast everything is being rendered/shown.

This is fine, but now object positions need interpolating for display, because otherwise they will judder as they move. Although I've implemented the frame timing like this, I don't have the interpolation - and while I really want it, I'm not sure how practical it is to implement it right now (the game doesn't have a nice elegant blob of state to perform interpolation between right now).

Frame timings locked at 75FPs with logic updates at 60Hz. Note the gap in the blue bar after a 60Hz update before a 75Hz frame finishes

Making game logic and rendering independent also calls for deciding how to waste time if (when) you're rendering more quickly than expected: When totally unlocked to render as quickly as possible, a lot of modern machines will easily render the game at several thousand FPS, which is mostly pointless. And not just pointless - but also, often a crappy experience - because although the framerate is now free, it's very likely to vary quite a lot, which leads to perceived stutter and strobing even though the game is 'going fast' numerically.

A lot of suggestions online show doing a sleep to kill the surplus time per frame to meet a specific frame rate limit - which is not unreasonable but is a rabbithole of its own: Most operating systems guarantee a minimum that a sleep will occur for, but not a maximum. Which you means you have to be very careful which API you use and how long you actually sleep for (choosing an exact interval to try and get the frame timing dead-on is likely a mistake). As an aside, I've actually found so far that burning frames with empty loops - so like busy-waiting - actually results in much more consistent behavior than attempting to sleep (or worse, yield).

And then finally the elephant in the room: V-sync. Great for eliminating tearing, but far more subtle in frame timing than it looks on the surface. Because importantly it isn't really 60Hz lock - it's display lock. For many monitors that means a frequency that is often slightly above or below 60Hz - often leading to odd fractional numbers that are a pain to detect (for example my display is actually 59.97Hz). So even if you were to force v-sync on, lock game logic to 60Hz and call it a day, you have to contend with these unusual refresh rates not being precisely 60Hz - and then on top of that, bizarre driver oddities where users are permitted to just completely bypass v-sync (or worse: the opposite and v-sync is forced on inappropriately)

TLDR: Frame timing/pacing is hell, and it often seems like there is no single solution that will please all parties.

For the time being on this game, I've made it possible to switch between all combinations of settings (lockstep or unlocked rendering at a customizable frame rate limit, optionally with v-sync) - but as mentioned above, interpolation to eliminate judder requires more work and I'm not sure if (or when) I will return to add it

Leave a comment

Log in with itch.io to leave a comment.