Graphics Interpolation with Framerate Limiting in Video Games

A problem that I've had ever since I made my first Pong clone in SDL, is a phenomenon known as microstutter. If you've tried your hand at programming games from scratch, you've probably ran into this problem. And you've probably read articles such as these. If you haven't, do it now and come back when you're done.

Now my problem was that none of those solutions worked for me! I still got that annoying stutter, and so I decided to debug and draw some pretty graphs.

I found out that the issue was due to the game rendering at 15000 fps. This was a problem because the clock couldn't update fast enough for me to calculate a fresh interpolation value for each draw, which caused sprites to be drawn at the same position multiple times. Of course, in an actual game with 1000+ sprites and effects the framerate wouldn't be this high, and the clock would have a sufficient update frequency.

The solution

I needed to have the draw method spend a minimum amount of time to finish, and this minimum time must be greater than the clock's update frequency. Why don't I just use the built-in solution for limiting framerate in SFML you ask? Well, I did. And it made everything stutter again. My uneducated guess is that SetFramerateLimit() sets the thread to sleep before drawing, inside the Window.Display() method. This would cause the interpolated render positions to not be in sync with the elapsed time between updates. But alas, some things are a mystery.

My solution was like this:

  1. Store the time before drawing
  2. Perform the drawing
  3. Find out how long it took to draw (currenttime - time_before_drawing)
  4. Let the thread sleep a remainder duration, i.e. if the minimum time is 8 ms (125 fps), the sleep duration would be 8 - frametime.

So far it appears to work and gives sexy-looking graphs.

To make it better you could have a launcher for your game, where the user could set the framerate to a specified limit (or no limit), and then do 1000 / framerate to get the minimum time each draw call must take.

Here is a working example (in some build of SFML 2) with source code:

smoothrendering.zip

I know, it's messy, but the code of interest is the gameloop in main.cpp and the interpolation method in renderentity.cpp

Note:

In order to get smooth results, your renderer needs to support drawing at floating point coordinates. A way to do this in SFML is to simply enable anti-aliasing.

3 comments:

  1. Thanks! That nasty "jittery movement" is haunting me and this is a great article - the first, one that provides solution.
    Unfortunately, it doesn't work on current build of SFML. I implement it in my project and then build your source, both without success - stutters remain, maybe become slightly less. It seems, now it's not depend on anti-aliasing mode(I saw no differences at all) and the best result was with v-sync turned on - movements were supersmooth without microstutters, but with freezes with bigger intervales.So I tried to fix this - set minimal time to 16 (limit to 60 fps), then was toying around with others numbers - nothing helped and now this thing just driving me crazy. So I thought maybe you can share some thoughts on this, it'd be very generous.

    ReplyDelete
    Replies
    1. Sure, this problem has left me stumped for so long that I would like to help a fellow programmer as much as I can. I've made some changes to my code, but I still use the same solution as shown in this article. However, I don't think the problem has gone away completely, as stuff still looks better with unlimited framerate.

      Anyhow, here are some things I've noticed that might help you solve the issue:
      - Running in debug mode decreases performance

      - Changing the window title or writing to the console every frame increases stutter

      - SFML's timer is inaccurate at very small time values (0 - 8ms). For example, you may do sleep(4ms) or sleep(0ms) and it will still sleep for 16ms. And on that same note:

      - SFML's timer is magical; depending on what day I run my application, it may or may not be accurate to those tiny values.

      As for drawing at floating point coordinates, try using .setSmooth(true) for the textures you use. Or check if AA is actually enabled using Window.getSettings().antialiasingLevel.

      Also, if you have vsync enabled with this approach, try not letting the thread sleep at all.

      If this does nothing to help you, maybe you'll find some insight in Love2D's source code. It's a framework written in C++ and SDL, and actually renders smoothly. https://bitbucket.org/rude/love/overview

      Good luck!

      Delete
  2. I'll try theese, thanks a lot.

    ReplyDelete