Search code examples
c++sdlgame-developmentsdl-2

Achieving stable 60fps with SDL2


I was struggling to find information on what is the industry standard on how to achieve stable 60 fps in a game that uses SDL2 and after reading a bunch of articles, I iterated through different ideas until I was able to measure 60 (+- 0.1) fps in my game.

Below is my current approach and I am looking for validation that it is not completely wrong / correction about how actual games (2D) do it.

const float TARGET_FPS = 60.04f;
const float SCREEN_TICKS_PER_FRAME = 1000.0f / static_cast<float>(TARGET_FPS);
...
class Timer{
public:
    Uint64 started_ticks = 0;

    Timer()
            :started_ticks{SDL_GetPerformanceCounter()}
    {}

    void restart() {
        started_ticks = SDL_GetPerformanceCounter();
    }

    float get_time() {
        return (static_cast<float>(SDL_GetPerformanceCounter() - started_ticks) / static_cast<float>(SDL_GetPerformanceFrequency()) * 1000.0f);
    }
};

...

float extra_time{0};
Timer fps_cap_timer{};

while(game_loop_condition){
    fps_cap_timer.restart();
    ...
    render();
    while((fps_cap_timer.get_time() + extra_time) < SCREEN_TICKS_PER_FRAME) {
            SDL_Delay(1); // I am aware of the issues with delay on different platforms / OS scheduling
        }
    if (fps_cap_timer.get_time() < (SCREEN_TICKS_PER_FRAME)) {
            extra_time -= SCREEN_TICKS_PER_FRAME - fps_cap_timer.get_time();
        }
        else {
            extra_time += fps_cap_timer.get_time() - SCREEN_TICKS_PER_FRAME;
        }
}

With this approach, there is always a little extra time that fluctuates between 0 - 1ms, which leads to FPS slightly <60, therefore I adjusted the target FPS to 60.04 and I am getting 60±0.1 FPS.

So.. is this actually viable or did I screw something up and should do more reading on the topic?


Solution

  • Note: Relying on FPS is not usually how it's done. Instead, measure the time since the last frame and use that as a multiplier for how far your game objects have moved.

    Having a rather precise FPS may be wanted in some situations though, but since you restart the timer in every iteration of the loop, it's going to drift. I suggest just adding a fixed duration to the timer and wait until that point in time occurs. Using only standard library classes and functions, it could look like this:

    #include <chrono>
    #include <cstdint>
    #include <iostream>
    #include <thread>
    #include <type_traits>
    
    template<std::intmax_t FPS>
    class Timer {
    public:
        // initialize Timer with the current time point:
        Timer() : tp{std::chrono::steady_clock::now()} {}
    
        // a fixed duration with a length of 1/FPS seconds
        static constexpr std::chrono::duration<double, std::ratio<1, FPS>>
            time_between_frames{1};
    
        void sleep() {
            // add to the stored time point
            tp += time_between_frames;
    
            // and sleep almost until the new time point
            std::this_thread::sleep_until(tp - std::chrono::microseconds(100));
    
            // less than 100 microseconds busy wait
            while(std::chrono::steady_clock::now() < tp) {}
        }
    
    private:
        // the time point we'll add to in every loop
        std::chrono::time_point<std::chrono::steady_clock,
                                std::remove_const_t<decltype(time_between_frames)>> tp;
    };
    

    The busy waiting is there to make the FPS as accurate as possible, but it eats CPU so you want to keep it as short as possible. 100µs may be too much so you can make it shorter or even remove it if you find the accuracy good enough without it.

    Then your game loop would be:

    int main() {
        // setup ...
    
        Timer<60> fps_cap_timer; // 60 FPS
        while(game_loop_condition) {
            render();
            fps_cap_timer.sleep(); // let it sleep any time remaining
        }
    }