Lazy Foo' Productions

SDL Forums external link SDL Tutorials 🐰SDL3 Tutorials🥚 Articles OpenGL Tutorials OpenGL Forums external link
Follow BlueSky Follow Facebook Follow Twitter Follow Threads
Donate
News FAQs Contact Bugs

Frame Rate and VSync

Frame Rate and VSync screenshot

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.
                //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 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 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 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.