Wednesday, February 11, 2015

Rock solid frame rates with SDL2

For fast-paced action games you need to have a steady framerate. If the framerate is not steady, you are likely to see stutter. It's a periodic wobble, jerky movement, as if a frame was skipped. It's very annoying, and once you see it, you can't unsee it. I ran into this problem and for a moment there, I just blamed SDL, heh! In forums online, I found more people complaining about stutter with SDL. The accepted answer on StackOverflow for this problem was wrong. Game dev forums did turn up some good tips. Finally, I was able to piece it together. Apparently this topic is not well understood by many—and the ones who do get it right don't seem to get just what the problem is. It has indeed to do with frame rate, but it's not skipping. Something else is up.

Loop the loop
Games run in a loop. You update some monsters, do the rendering, and cap the frame rate by delaying until it's time to display the next frame. It's likely to be programmed something like this:

    float freq = SDL_GetPerformanceFrequency();

    now = SDL_GetPerformanceCounter();
    secs = (now - last_time) / freq;
    SDL_Delay(FRAMERATE - secs * 1000);

NB. You might also use SDL_GetTicks(). SDL  2 offers new performance counter functions. These have much greater precision than SDL_GetTicks().

We've subtracted the time that the game was busy, so to complete a frame we need to wait the remaining time. This is exactly where the bug is, odd as it may seem. To prove it, try timing SDL_Delay() calls in a loop:
    float freq = SDL_GetPerformanceFrequency();

    t0 = SDL_GetPerformanceCounter();
    t1 = SDL_GetPerformanceCounter();

    printf("time slept: %.2f msecs", (t1 - t0) / freq * 1000.0f);
    time slept: 31.51 msecs
    time slept: 31.31 msecs
    time slept: 34.06 msecs
    time slept: 32.15 msecs
    time slept: 34.54 msecs
    time slept: 33.07 msecs
But we asked to sleep only 30 milliseconds! How can this be?

The problem is not so much with SDL as with the operating system. This can be proved by replacing SDL_Delay() with usleep(), and the problem persists. Even if you try the insanely precise nanosleep() call, the problem persists. Sleeping is inaccurate—despite the fact that modern computers are very capable of doing high precision timing.

Some try fixing this by sleeping a little less, and then doing a busy loop to get as close to the sleep time as possible. This almost works, but it it isn't perfect and you will still see stutter. The non-deterministic nature of a multitasking operating system gets in our way.

Thou shall wait
So, the system is incapable of sleeping an exact amount of time. How about not sleeping at all.
The trick of sleeping until the next frame is probably a spin-off of the waiting for vsync trick. But sleeping isn't the same thing, and it doesn't work because it doesn't necessarily bring you in sync with anything. So instead of sleeping, we are going to wait for vsync.
    sdl_window = SDL_CreateWindow(title, x, y, w, h, SDL_WINDOW_OPENGL);
    gl_context = SDL_GL_CreateContext(sdl_window);
This is for use with OpenGL. It will enable the swap interval (wait for vsync) before swapping the render buffers. You can remove any SDL_Delay() calls, because during the swap interval the program already sleeps. If you are using the SDL render functions, it's done like this:
    renderer = SDL_CreateRenderer(win, -1,
Now SDL_RenderPresent() will wait for vsync before presenting the buffer.

Moving on
That's only half of the story. The problem of stutter is not solely caused by an inaccurate sleep timer. It stutters because movement is too rigid:
    new_pos = old_pos + speed;
This sort of code always advances the position, no matter whether time advanced evenly or not. So in the case where time did not advance perfectly in line with the desired rate (which is impossible, as we saw earlier), this code will produce stutter.

To solve this problem, we must write code that can handle variable timings. It works just like in physics class:
    float delta_time = (now - t0) * (float)freq;

    new_pos = old_pos + speed * delta_time;
With SDL_GetPerformanceCounter() and SDL_GetPerformanceFrequency() you can calculate the time step for this frame in seconds. Note that delta_time holds a small value, so crank up the speed. If your game world is defined in meters, you can simulate things rather faithfully with speed in meters per second.

At last
Lo and behold, our frame rate is now rock steady and scrolling is super smooth. It's locked down at 60 fps, with very low CPU usage, even on my five year old computer. Like with Columbus' egg, it's easy once you know how.

My advice for testing is to create a large checkerboard and scrolling that across the screen. If it stutters, you will see it. Also try disabling the vsync just for fun, and watch the fps counter go nuts.