Skip to main content
GameDev.net gamedev.net
🔒 Locked

60fps framelock done properly - without duplicated or skipped frames

Started by d h k Dec 6, 2019 at 12:27 PM 11 replies 23.5k views
Original Post
d h k
d h k

I am trying to lock my framerate at 60 for a game prototype I am working on* but it seems to be quite difficult to ensure that I always push out a new frame for every monitor vertical retrace, without duplicated or skipped frames.

This is my current approach, written in C++ using std::chrono:

constexpr auto frameTime = chrono::duration<int64_t, ratio<1, 60>>(1);
auto nextFrame = chrono::steady_clock::now() + frameTime; // schedule beginning of next frame
while (true)
{
  // process frame here

  this_thread::sleep_until(nextFrame); // wait until the next frame should start
  nextFrame += frameTime;
}

This schedules the time_point when the next frame should start processing, processes the current frame and then sleeps until the next frame is supposed to start. I have measured it and know that my frames do not take too long to process. I have also confirmed that steady_clock and high_resolution_clock are the same on my systems so it's not a clock precision problem.

I am moving a sprite 1px to the left/right every frame and it is immediately obvious when frames are duplicated or skipped. It works fine for a couple of seconds and then breaks down with multiple duplications and skips. That goes on for about a second, then it works well for a couple of seconds again. How long it works fine in between 'break downs' appears to depend on the machine.

There has to be some way to achieve a better stable framelock, right? What am I missing here?


* It's a 2D game with handdrawn animations so I think locking the framerate is reasonable.

a light breeze
a light breeze

A timer-based solution is never going to be exactly synchronized with the screen. sleep_until will never wake up at exactly the right moment in time. But if you're doing this:

while (true) {
  render_frame();
  present_to_screen();
  this_thread::sleep_until(nextFrame);
  nextFrame += frameTime;
}


...then you could try doing this instead:

while (true) {
  render_frame();
  this_thread::sleep_until(nextFrame);
  present_to_screen();
  nextFrame += frameTime;
}

Also, 60Hz is a pretty crappy screen refresh rate.

frob
frob

I think you're going about this a little backwards. Instead of thinking "I want to render at 60 frames per second, slow down until I reach it", say "I want to render as fast as I can, block only as long as required."

Games should generally render as fast as possible, limited only by refresh rates if players don't want to experience tearing, or as fast as possible without limits if they do want to experience tearing.

First, recognize that 60Hz is a legacy thing, based on TV standards. While many monitors are still 60Hz, games should recognize the actual screens being used. 72 Hz, 75 Hz, 90 Hz (especially in VR) and 120 Hz (especially in competitions) are all common. Less common timings exist, as do high refresh monitors like 144 Hz and 240Hz competitive screens. While older standards like VGA, DVI, and HDMI prefer fixed rates, new systems like GSync allow variable rates displaying whenever data is ready. In general, try to use the actual hardware's speed.

In many competitive environments screen tearing to display information early is often considered an advantage, with players turning off the fancy visual features and setting the graphics down to zero just so they can get a four or five millisecond advantage over their opponents. In highly competitive environments, the choice between displaying a photorealistic beautiful vehicle slowly versus a Tesla Truck model quickly, speed wins over beauty every time.

Said differently:

DO NOT call sleep. Call a blocking operation like d3d's Present() call. The operation will block until the display is ready, however long the hardware actually needs.

If a player has high end competitive hardware, let their hardware reach amazing speeds. If a player is using their grandma's old machine, let the hardware decide the speed.

Sleep in general is tricky to get right and not what programmers first expect. The details depend on the version of Windows you are using, and on system settings, but generally Window's sleep calls say wait around for at least this much time, more or less. The same is true (with different internal details) on other systems like Linux, which still have scheduler timings. While there are Real Time OS (RTOS) like the old Windows CE that did accurately respect sleep timings down to milliseconds accuracy, on desktop Windows the accuracy is far less, typically 10ms-15ms although it can be adjusted. And those are generally estimates, the system is free to block for much longer than that, whatever works for the scheduler.

Note that this is true even on Linux and Posix systems, functions like usleep() and nanosleep() are still entirely at the mercy of the scheduler. By default functions like nanosleep() and pselect() are scheduled at 500 microseconds in the best case. Even when you bump the priority and modify the OS scheduler to give your process a high priority, then manually tune the kernel, Linux won't go below about 10 microseconds per sleep. When you specify you want to sleep for 1 nanosecond, the call is to wait at least 1 nanosecond, not to wait exactly one nanosecond.

Block on the hardware, not a timer.

Blocking until the hardware is ready can also take a long time. While usually it means waiting only a few milliseconds until the transfer completes during vsync, or if vsync is disabled waiting a few microseconds for the operation to complete, sometimes it will take longer because the system is busy doing other tasks. Sometimes those other tasks are extreme, not just blocking for another process, but really big like being put into sleep mode or hibernation. While the OS will send a message in the message pump that the hardware is sleeping or hibernating, any thread that is blocked sees it as a really long blocking operation.

And to further complicate matters, games should generally run their simulations at a different rate than their display, preferably running with a fixed step, should run networking at a rate based on data availability, and should run audio at a rate based on audio processing. Asynchronous processing and multiple threads/fibers/processes are your friend if you want performance.


d h k
d h k

Thanks for the answers so far. I should have provided a bit more info to justify my choice to lock the framerate at 60 for this specific game prototype, but decided to keep the original post as short as possible.

In a nutshell: I am fully aware of the benefits of high refresh rate monitors and framerate independent game loops. But this specific project is a 2D retro platformer using hand drawn sprite sheets and a very low screen resolution. So at a high refresh rate, I'd simply be drawing the same exact frame multiple times since animation frames are not interpolated and sprites can not move less than one on-screen pixel.

I understand that sleep() functions are imperfect but try to mitigate that fact in my code by calculating the next time_point based on the old one, instead of using chrono::steady_clock::now() every frame. This should eliminate the clock drift that over-long sleeps create. And my goal is to only present a new frame in time for every 60Hz monitor retrace, so I have a duration of time to do it and it doesn't need to happen at one exact time_point so I don't think clock precision is the killer here.

I tried to present to the screen after the wait - but that unfortunately does not appear to help the issue I described.

Any further ideas? Or is it simply not possible to output a fresh frame just in time for every 60Hz retrace of my monitor?

aganm
aganm

Why not just use vsync? Turning vsync on in your windowing API takes care of locking the rendering loop at 60fps on a 60hz screen. No need to do anything else.

d h k
d h k

Vsync would be a perfect solution for my problem but it's more of a user setting, right? It can be overwritten and force disabled if the user wants to I believe? And I would have to somehow guarantee that it always limits the FPS to 60 and never anything above.

aganm
aganm

Vsync locks the framerate at the monitor's refresh rate, if 60hz 60fps, 144hz, 144fps.

aganm
aganm

Vsync locks the framerate at the monitor's refresh rate, if 60hz 60fps, 144hz, 144fps.

aganm
aganm

Vsync locks the framerate at the monitor's refresh rate, if 60hz 60fps, 144hz, 144fps. It can be enabled/disabled by the user, you just have to make an option for it in the game.

LandonJerre
LandonJerre
d h k said:

Thanks for the answers so far. I should have provided a bit more info to justify my choice to lock the framerate at 60 for this specific game prototype, but decided to keep the original post as short as possible.

In a nutshell: I am fully aware of the benefits of high refresh rate monitors and framerate independent game loops. But this specific project is a 2D retro platformer using hand drawn sprite sheets and a very low screen resolution. So at a high refresh rate, I'd simply be drawing the same exact frame multiple times since animation frames are not interpolated and sprites can not move less than one on-screen pixel.

I understand that sleep() functions are imperfect but try to mitigate that fact in my code by calculating the next time_point based on the old one, instead of using chrono::steady_clock::now() every frame. This should eliminate the clock drift that over-long sleeps create. And my goal is to only present a new frame in time for every 60Hz monitor retrace, so I have a duration of time to do it and it doesn't need to happen at one exact time_point so I don't think clock precision is the killer here.

I tried to present to the screen after the wait - but that unfortunately does not appear to help the issue I described.

Any further ideas? Or is it simply not possible to output a fresh frame just in time for every 60Hz retrace of my monitor?

If you really want to do it this way, you could technically do busy waiting instead of sleeping, that would probably get you closer to what you want. On the other hand it will spin the core your main thread is on at 100%, so generally I wouldn't recommend doing it, and in theory you could still miss frames.

d h k said:
Or is it simply not possible to output a fresh frame just in time for every 60Hz retrace of my monitor?

The problem is that you are having two clocks, one of which you base your measurements on, and the other is the one your monitor refreshes on. There is no guarantee that the two clocks stay in exact lockstep sync, plus there is no guarante that your monitor's 60Hz refresh rate is actually physically 60Hz. It can be slightly off in either ways and can even fluctuate in minuscue amounts. (Borderline related stuff: Remember the time when last year european clocks run 6 minutes late, because Serbia and Kosovo got into a power dispute that affected the frequency of the entire european grid slightly, causing the clocks to run slower ever so slightly.)

The proper solution is what frob already mentioned, decoupling your game logic/updates from your rendering code, letting rendering run as fast as it can (either limited by vsync and a set refresh rate, or just letting it loose altogether), and running your game logic at a set fixed rate of your choosing.


VoxycDev
VoxycDev

This comes up periodically here and the term used for it is fixed timestep. Below is the solution I use. It's a modified version of what's been offered here and it does work for me on Windows. This runs in your main loop before the draw routine:

int numLoops = 0;

long msecInterval = 1000 / targetFps;
		
if (updatedTime == 0)
    updatedTime = PLAT_GetTime() - msecInterval;
			
unsigned long curTime = PLAT_GetTime();
		
while ((updatedTime < curTime) && (curTime - updatedTime) > msecInterval)
{
	fixedTick();
	
	updatedTime += msecInterval;
				
	numLoops++;
    
    if (numLoops >= 100)
		updatedTime = PLAT_GetTime();
}

targetFps is the desired frame rate (30, 60, 90, etc)

fixedTick() is the timed update function where game updates.

PLAT_GetTime() is below:

#include <windows.h>

long PLAT_GetTime()
{
	SYSTEMTIME time;
	GetSystemTime(&time);
	LONG time_ms = (time.wMinute * 1000 * 60) + (time.wSecond * 1000) + time.wMilliseconds;
	
	return time_ms;
}


The numLoops > 100 is a safeguard in case we get stuck in the loop. I don't know why but it does happen in my case sometimes.

Also, I have a suspicion that the above only works if we can render frames faster than we need to update them. In other words, I suspect it freezes when rendering gets too slow. Please feel free to offer improvements. I struggled with this for a while and while not perfect, at least it works, unlike the out-of-the box code I found here.

frob
frob

Physics and simulations should run at a fixed time step. There are plenty of bugs you can search for spanning decades where players experience different things when those run at different steps. They can run faster and reach unplayable speeds as new machines come out, or allow exploits like certain processor speeds passing through objects or landing on walls.

Rendering should happen as fast as possible, running independently of the world's simulation. Use a blocking operation like D3D's Present() to wait until graphics are rendered.

If you're looking for pseudocode:

Loop 
  while( simulation time < now ) 
    advance simulation by one step // Notice this may run zero or more times 
  render to back buffer 
  present the back buffer, which blocks until the screen is ready
End loop

You don't need to code any special delays in your code for varying frame rates, nor do you need to do any special code to account for missed frames. The blocking page flip function like Present() handles that work for you. If the monitor can render at 50Hz, 60Hz, 72Hz, 90Hz, 144Hz, or anything else, you need to do nothing different at all other than call the blocking display function.




a light breeze
a light breeze

I would actually try using vsync if the monitor is 60Hz or a multiple thereof, and a timer-based approach if it isn't. You can ask the OS about the monitor refresh rate, or you can measure it if you don't trust the OS. If the monitor is at 75Hz, you're going to get irregular frame duplication no matter what you do, and if the monitor has a variable refresh rate, then using a timer is how you fix the refresh rate to 60Hz.

d h k
d h k

Thanks for the suggestions and ideas, everyone. That medium.com article really hit the nail on the head!

I have just converted my game loop to busy wait, like this:

constexpr auto frameTime = chrono::duration<int64_t, ratio<1, 60>>(1);
auto nextFrame = chrono::steady_clock::now() + frameTime;
while (true)
{
  if (std::chrono::steady_clock::now() < nextFrame)
  {
    continue;
  }

  // process frame here

  nextFrame += frameTime;
}

Now I only get a very rare single stutter, pretty much what is described in the article. I will modify my solution and try to get rid of that as well. Maybe I could also sleep for, say, half the frame time, knowing that processing my frames is going to be very fast...

frob
frob
d h k said:
Maybe I could also sleep for, say, half the frame time

That puts you right back at the mercy of the OS task scheduler, which was part of the problem to begin with. The default scheduler in Windows is about a full frame in duration. Any sleep at all, even those based on higher resolution timers, will still suffer from the task scheduler's resolution.

Blocking for hardware is the best reliable way to do it, and even that is filled with issues and gotchas.

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.