/ Game Programming

Achieving Consistent Frame Times

In writing my own game engine I came across the issue of trying to achieve consistent timing in my update loop. I'm trying to maintain 60 fps at all times, so the update loop should take 16.66 ms. My naive implementation was getting me close, but I was seeing several milliseconds in variation on either side. Initially this variance wasn't an issue, but it started to cause problems once I began introducing functionality that was sensitive to these small variations in framerate. In searching for a way to solve this problem I discovered that there are few articles that discuss it in detail. In fact, Handmade Hero is the only place that I could find that demonstrated exactly what I was looking for and it took me some digging to find the right episode. Therefore I'm putting this article together to help collect relevant information and document my own experiences, but if you have the time I highly recommend watching the Handmade Hero episode referenced above since Casey Muratori goes into more depth explaining this issue than I do.

The Why

If you've never done low level game programming (at least low enough that you're writing your own update loop) it may not be obvious why this would be important. There are various things in the game loop that are sensitive to variation in frame time, for me it came into play when I started adding audio to my game. When you do low level audio you get a sound buffer from the audio device (the speakers, headphones, etc.) and you write your audio stream directly to that buffer. Generally (at least on Windows) once you've written to this buffer you can't overwrite what you've written because the audio device could potentially be reading from that part of the buffer. This can be thought of as the audio device queuing up the audio you give it: It plays the audio in the order you send it and one audio stream doesn't play until the one before it is done.

This means you need to be careful how much audio you send to the buffer at a time. If you send too little sound and the audio card plays it all before you can send more then you get small bits of silence mixed in with your audio which makes it stutter and crackle. On the other hand if you try to avoid this problem by sending a big chunk of audio, big enough to guarantee it won't all have been played by the next frame, you introduce audio lag into your game.

Audio latency example.

For example, say you send 30ms of your game's audio at a time in an attempt to guarantee that you never fully empty the audio buffer. The first frame you send out 30ms of your game's background music because nothing else is happening. Then 16ms later (on the next frame) an explosion goes off, so you send 16ms of the explosion's audio mixed with the next 16ms of background music. Unfortunately there's still 14ms of audio queued up in the buffer and there's no way to preempt the existing audio. That means even though you started writing the explosion's audio as soon as it happened the audio device won't play it until the remaining 14ms of background music has been played. This means that you have introduced 14ms of audio lag into your game.

In practice 14ms is't a lot and likely wouldn't be noticed by most players, but what if your game were running at 30fps instead of 60? In that case your game would have an expected frame time of 33ms, so you'd need to write closer to 60ms of audio at a time, and you'd wind up with about 30ms of audio lag. 30ms of audio lag would certainly be noticeable to players, especially if tight reaction to audio queues is core to your game. Even at 60fps there are things external to your game that can exacerbate audio lag, such as the player's audio system being high-latency. In these cases the only way guarantee minimal audio latency is to minimize the latency within the game itself, and you do so by fixing your frame time so you know exactly how much time will pass until you next write audio to the buffer.

The Problem

Initially my game's update loop looked something like this:

loop {
    let start_time = time::now();
    
    self.update_game_state();
    self.draw();
    self.write_audio();
    if close {
        break;
    }

    let elapsed_time = time::since(start_time);
    let difference = TARGET_FRAME_TIME_SECONDS - elapsed_time;
    if difference > 0.0 {
        let sleep_ms = difference * 1000.0;
        thread::sleep_ms(sleep_ms);
    } else {
        println!("Failed to meet target frame time: {}ms", elapsed_time);
    }
}

In my naive loop my variance was coming entirely from this line:

thread::sleep_ms(sleep_ms);

A little background first. Every frame your game needs to do some amount of work, usually including reading input, updating the games state, rending the game, and outputting audio. In my experience the amount of time this works takes can vary wildly from frame to frame but (if I'm doing my job right) it's always less time than my target frame time, leaving me with some chunk of extra time left. One option would be to simply ignore this extra time and immediately go on to the next frame, effectively running the game at its maximum possible framerate. The problem with this is that it makes the framerate unpredictable and I've gone over why that's bad.

The other option is to eat the remaining time until the target time. The correct way to do this is to sleep the current thread, thereby relinquishing the remaining time back to the OS. The reason why you'd want to sleep the thread (instead of, say, checking the time in a tight loop until you've consumed it all) has to do with the way the OS divvies out processor time to different programs on your machine. Suffice to say that if you always run your game loop as fast as you can and never sleep the thread you'll drive the CPU usage up to 100% and can overheat your user's machine. There are other factors that come into play including how many cores the user's machine has and how the OS itself is configured (in some cases it simply may not let you monopolize the system), but it's best practice (and generally a nice thing to do) to not monopolize the user's system.

The problem is that sleeping the thread is an imprecise affair. The OS uses a scheduler to periodically interrupt running tasks, pause them, and give other tasks a chance to run (this is what allows you to run multiple programs at once even on a single core processor). A thread that has been put to sleep cannot wake up until the scheduler interrupts some other task, and the scheduler generally interrupts tasks at several millisecond intervals. So if the scheduler is running at 5ms granularity and you ask to sleep for 2ms, then you could be stuck sleeping for up to 5ms. This is a big problem for achieving a consistent frame time.

The Solution

On Windows (though I suspect this is the case for other platforms) you can request a finer scheduler granularity from the OS. You do this with the timeBeginPeriod() and timeEndPeriod() functions. With this you can request a granularity as low as 1ms (maybe lower on some systems, though I can't say for sure). Doing this doesn't completely fix the timing problems but it gets us closer and ensures that we know in advance what our minimum sleep time.

The first step in getting to our final result is the first step I always take when faced with a problem I can't solve: Scrap everything and start from scratch. Let's start with the most naive CPU-melting version:

loop {
	let start_time = time::now();

	// Regular game update stuff goes here.

	// Use remaining time.
	loop {
		let elapsed_time = time::since(start_time);
		if elapsed_time > TARGET_FRAME_TIME {
			break;
		}
	}
}

This does indeed give a perfect 16.66ms frame time, but it also eats an insane amount of processing power. On my machine the task manager shows it taking up 15% of the CPU power. Considering that my machine has 8 cores, this means that it's using a whole core and then some. CPU-melting indeed.

The next step would be to try to sleep 1ms at a time until we've reached our frame rate. I've already requested 1ms granularity from Windows so it should be able to give us that. In the case that we don't have a full millisecond left we can just loop as normal since doing so for less than a millisecond won't cause problems the way the CPU-melting version did.

loop {
	let start_time = time::now();

	// Regular game update stuff goes here.

	// Sleep the thread to kill time.
	let mut elapsed_time = time::since(start_time);
	let mut remaining_ms = (TARGET_FRAME_TIME - elapsed_time) * 1000.0;
	while remaining_ms > 1.0 {
		thread::sleep_ms(1);
		
		elapsed_time = time::since(start_time);
		remaining_ms = (TARGET_FRAME_TIME - elapsed_time) * 1000.0;
	}

	// Not enough time to sleep the thread,
	// just spin until we reach the target time.
	while remaining_ms > 0.0 {
		elapsed_time = time::since(start_time);
		remaining_ms = (TARGET_FRAME_TIME - elapsed_time) * 1000.0;
	}
}

This still gives me a consistent 16.66ms but eats way less CPU time. The last improvement would be to sleep for the actual time remaining:

loop {
	let start_time = time::now();

	// Regular game update stuff goes here.

	// Sleep the thread to kill time.
	let mut elapsed_time = time::since(start_time);
	let mut remaining_ms = (TARGET_FRAME_TIME - elapsed_time) * 1000.0;
	while remaining_ms > 1.0 {
		thread::sleep_ms(remaining_ms);
		
		elapsed_time = time::since(start_time);
		remaining_ms = (TARGET_FRAME_TIME - elapsed_time) * 1000.0;
	}

	// Not enough time to sleep the thread,
	// just spin until we reach the target time.
	while remaining_ms > 0.0 {
		elapsed_time = time::since(start_time);
		remaining_ms = (TARGET_FRAME_TIME - elapsed_time) * 1000.0;
	}
}

With this I'm getting my desired frame rate but CPU usage is down to 6%. It's worth pointing out that I'm still sleeping in a loop even though I should be sleeping for the maximum possible time. This is because not all operating systems guarantee that you'll sleep for the requested amount of time, and in some cases the thread can immediately resume. To gracefully handle these cases (and minimize the amount of time just spinning away time) we keep trying to sleep until there's less than 1ms left.

Comparing the end result to what I started with it looks like what I was missing in my naive implementation was to account for the fact that the system can only handle sleeping at 1ms granularity. This difference is subtle but for me it made the difference between jittery and clean audio.

Update 5/26/2015: I changed the last two examples to split the calls to thread::sleep_ms() into a separate loop for the sake of clarity.

David LeGare

David is a game developer and systems programmer. He likes cats, he tolerates dogs, and he often remembers to dress himself before leaving the house.

Read More