Frame Rate and VSync
Last Updated: Oct 19th, 2024
Odds are if you tried to any motion with SDL, everything ran incredibly fast. Here we're going to use a few tools to manage frame rate./* Constants */ //Screen dimension constants constexpr int kScreenWidth{ 640 }; constexpr int kScreenHeight{ 480 }; constexpr int kScreenFps{ 60 };
We're going to define a constant to define the frames per second we want to cap the frame rate to.
//Create window with renderer if( !SDL_CreateWindowAndRenderer( "SDL3 Tutorial: Frame Rate and VSync", kScreenWidth, kScreenHeight, 0, &gWindow, &gRenderer ) ) { SDL_Log( "Window could not be created! SDL error: %s\n", SDL_GetError() ); success = false; } else { //Enable VSync if( !SDL_SetRenderVSync( gRenderer, 1 ) ) { SDL_Log( "Could not enable VSync! SDL error: %s\n", SDL_GetError() ); success = false; } //Initialize PNG loading int imgFlags = IMG_INIT_PNG; if( !( IMG_Init( imgFlags ) & imgFlags ) ) { SDL_Log( "SDL_image could not initialize! SDL_image error: %s\n", SDL_GetError() ); success = false; } //Initialize font loading if( !TTF_Init() ) { SDL_Log( "SDL_ttf could not initialize! SDL_ttf error: %s\n", SDL_GetError() ); success = false; } }
After we create our window/renderer, we're going to enable VSync with SDL_SetRenderVSync. VSync means the screen will update when the monitor updates. Monitors update from top to bottom AKA vertically, this is why it's
called vertical sync. Here we're doing basic enabling of VSync but there are more options for VSync if you look in the SDL documentation.
//The quit flag bool quit{ false }; //The event data SDL_Event e; SDL_zero( e ); //VSync toggle bool vsyncEnabled{ true }; //FPS cap toggle bool fpsCapEnabled{ false }; //Timer to calculate FPS LTimer fpsTimer; //Timer to cap frame rate LTimer capTimer; //Frame counter Uint64 renderedFrames = 0; //Time spend rendering Uint64 renderingNS = 0; //Reset FPS calculation flag bool resetFps = true;
Before entering the main loop we have some variables we want to define.
First we have some toggles to enable/disable vsync/FPS capping. Then we have timers to calculate FPS and cap the frame rate. Next we have variables to keep track of how many frames we rendered and how long it took to render them. Lastly, we have flag we use to reset FPS calculation which we want to use when we change how FPS is regulated.
First we have some toggles to enable/disable vsync/FPS capping. Then we have timers to calculate FPS and cap the frame rate. Next we have variables to keep track of how many frames we rendered and how long it took to render them. Lastly, we have flag we use to reset FPS calculation which we want to use when we change how FPS is regulated.
//If the FPS calculation must be reset if( resetFps ) { //Reset FPS variables fpsTimer.start(); renderedFrames = 0; renderingNS = 0; resetFps = false; } //Start frame time capTimer.start();
Going into the main loop, we first check if we need to reset the FPS tracking variables. Then we start/restart the timer used to cap the frame rate.
//Get event data while( SDL_PollEvent( &e ) ) { //If event is quit type if( e.type == SDL_EVENT_QUIT ) { //End the main loop quit = true; } //Reset start time on return keypress else if( e.type == SDL_EVENT_KEY_DOWN ) { //VSync toggle if( e.key.key == SDLK_RETURN ) { vsyncEnabled = !vsyncEnabled; SDL_SetRenderVSync( gRenderer, ( vsyncEnabled ) ? 1 : SDL_RENDERER_VSYNC_DISABLED ); resetFps = true; } //FPS cap toggle else if( e.key.key == SDLK_SPACE ) { fpsCapEnabled = !fpsCapEnabled; resetFps = true; } } }
For this demo, we'll toggle VSync when enter is pressed and FPS capping with the space bar. When we update the toggles, we want to reset the FPS calculation variables.
If you're wondering what's up with the funky syntax in the call to
Some people aren't fans of the ternary operator because they say it makes things messy (I worked at companies that banned it as part of their coding standards), but just as long as you don't use it to obessively get everything in one line you should be fine.
If you're wondering what's up with the funky syntax in the call to
SDL_SetRenderVSync
, that's the ternary operator. The way it works is that it evaluates the condition and returns a value like so:( <condition> ) ? <value returned if condition is true> : <value returned if condition is false>
Some people aren't fans of the ternary operator because they say it makes things messy (I worked at companies that banned it as part of their coding standards), but just as long as you don't use it to obessively get everything in one line you should be fine.
//Update text if( renderedFrames != 0 ) { timeText.str(""); timeText << "Frames per second " << ( vsyncEnabled ? "(VSync) " : "" ) << ( fpsCapEnabled ? "(Cap) " : "" ) << static_cast<double>( renderedFrames ) / ( static_cast<double>( renderingNS ) / 1000000000.0 ); SDL_Color textColor = { 0x00, 0x00, 0x00, 0xFF }; gFpsTexture.loadFromRenderedText( timeText.str().c_str(), textColor ); }
After handling inputs we check if at least one frame rendered because we need at least one frame rendered in order to calculate frames per second.
If so, we assemble a string letting the user know if VSync/FPS capping is enabled (again using the ternary operator) and then we calculate the average frames per second. The way you calculate average frames per second is simply
If so, we assemble a string letting the user know if VSync/FPS capping is enabled (again using the ternary operator) and then we calculate the average frames per second. The way you calculate average frames per second is simply
number of frames rendered/time spent rendering in seconds
. Here we're casting the variables to doubles to avoid any funkyness from dividing integers and dividing the rendering nanoseconds by a billion to convert it to seconds (because remember a nanoseconds is a
billionth of a second).
//Fill the background SDL_SetRenderDrawColor( gRenderer, 0xFF, 0xFF, 0xFF, 0xFF ); SDL_RenderClear( gRenderer ); //Draw text gFpsTexture.render( ( kScreenWidth - gFpsTexture.getWidth() ) / 2.f, ( kScreenHeight - gFpsTexture.getHeight() ) / 2.f ); //Update screen SDL_RenderPresent( gRenderer ); //Update FPS variables renderingNS = fpsTimer.getTicksNS(); renderedFrames++; //Get time to render frame Uint64 frameNs = capTimer.getTicksNS(); //If time remaining in frame constexpr Uint64 nsPerFrame = 1000000000 / kScreenFps; if( fpsCapEnabled && frameNs < nsPerFrame ) { //Sleep remaining frame time SDL_DelayNS( nsPerFrame - frameNs ); }
After we finish rendering we store the time spend rendering immediately after updating the screen and increment the frame counter. The reason we store the time is because nanoseconds is a very precise way to measure time and it's possible to call
Then we get the time it took to render this specific frame (as opposed to the total time spent rendering from the FPS timer). Then we calculate the time to render a frame, which at 60 FPS should be about 16666666.6667 nanoseconds. If we want to cap the frame rate at 60 FPS, we want to make sure we spend 16666666.6667 nanoseconds each frame. If the frame rendering took (for example) 6666666.6667, we want it to sleep for 10000000 nanoseconds, which we do with SDL_DelayNS.
Now when you run this application, odds are you're going to get just under 60 FPS in your calculation. That's expected due to rounding errors.
getTicksNS
twice in a
row and get a different value. So we store the value immediately after we're done rendering the frame to get the most accurate results.Then we get the time it took to render this specific frame (as opposed to the total time spent rendering from the FPS timer). Then we calculate the time to render a frame, which at 60 FPS should be about 16666666.6667 nanoseconds. If we want to cap the frame rate at 60 FPS, we want to make sure we spend 16666666.6667 nanoseconds each frame. If the frame rendering took (for example) 6666666.6667, we want it to sleep for 10000000 nanoseconds, which we do with SDL_DelayNS.
Now when you run this application, odds are you're going to get just under 60 FPS in your calculation. That's expected due to rounding errors.
SDL_DelayNS
is not going to sleep 0.6667 nanoseconds and I doubt any user will notice 2/3rds of a nanosecond latency.
Addendum: 8K 500FPS with HDR and ray tracing
Part of teaching new game developers how to code games is getting it into their heads that the junk GPU vendors told them to sell them an expensive video cards is not what's most important when learning game/graphics programming. When you get into graphics programming, you learn a lot
of the stuff you get told in latest nVidia/AMD trailers is not as high priority as the game/graphics fundamentals.
This means don't bother trying to max the specs of your Nasty Tetris clone. Nobody who is going to look at your portfolio is going to care if it runs at 480p/30FPS or 8k/500FPS unless you really screwed up your performance by doing some overly complicated video effect badly which won't happen if you don't over engineer your game.
This means don't bother trying to max the specs of your Nasty Tetris clone. Nobody who is going to look at your portfolio is going to care if it runs at 480p/30FPS or 8k/500FPS unless you really screwed up your performance by doing some overly complicated video effect badly which won't happen if you don't over engineer your game.