4

Recently I've been working on some SDL wrappers for my own use and I've come with a problem I've been having since I first started working with the SDL library.

See, like many others I've been using a timer akin to this one http://lazyfoo.net/SDL_tutorials/lesson14/index.php to regulate my framerate and the movement is never smooth. It doesn't seem like a double buffering or vsync problem, rather the movement is smooth but skips and stutters periodically (actually it has a certain pulse and rhythm to it). Whenever framerate regulation is turn off the pulse dissapears - and of course, everything becomes unusable) so I am mostly sure it has to do with it.

I've put together a little application consisting only of the timer and a red square moving around it. I'll put the code at the end of this post. The code is garbage and does not use any kind of sprite or anything: just know that it stutters the same no matter how much or little stress I put into it (say, add more moving things or just update part of the screen). I've tried with different framerate settings and the pulse always appears (just with a different "tempo"). Needless to say I've tried this on several machines (linux machines, all of them).

Anyway, I've been reading and seen other people having this problem, but no real answer. I, for one, have about the delta timing tecniques and will gladly try it but my issue here is with the timer itself on this very simple application. Why does it stutter?. Is it a problem with the SDL_GetTicks precision and accuracy?. What can I do to improve it?.

So, here's the code for those who want to try it:

#include <SDL/SDL.h>

class fps_control
{
    private:

    bool apply;
    Uint32 ticks_frame_count;
    Uint32 ticks_frame_end;
    Uint32 ticks_frame_begin;
    Uint32 diff;
    unsigned int frame_count;   //Visible to the outside
    unsigned int frame_count_inner; //Keeps count until a second has passed, then overwrites former to give the elapsed frames in a second.
    unsigned int frame_length;
    unsigned int desired_framerate;

    private:

    void calculate_frame_length()
    {
        this->frame_length=1000 / this->desired_framerate;  
    }

    public:

    fps_control(unsigned int p_f=30):desired_framerate(p_f), apply(true), ticks_frame_count(0), ticks_frame_end(0), ticks_frame_begin(0), diff(0), frame_count(0), frame_count_inner(0), frame_length(0)
    {
        this->calculate_frame_length();
    }

    unsigned int get_frame_count() const {return this->frame_count;}
    unsigned int get_desired_framerate() const {return this->desired_framerate;}
    void framerate_increase(){this->set_framerate(this->desired_framerate+1);}
    void framerate_decrease(){this->set_framerate(this->desired_framerate+1);}
    void set_framerate(unsigned int p_param)
{
this->desired_framerate=p_param;
this->calculate_frame_length();
}
    void toggle_apply() {this->apply=!this->apply;}

    void init()
    {
        //Call this once before starting, to set the beginning frame count to the initial values.

        this->ticks_frame_count=SDL_GetTicks();
        this->ticks_frame_begin=this->ticks_frame_count;
        this->frame_count_inner=0;
        this->frame_count=0;
    }

    void turn()
    {
        //Call this when all drawing and logic is done.

        if(!this->apply) return;    //Only apply when asked for.

        //Ask for time before drawing and logic to calculate the difference. Add a frame.

        this->ticks_frame_end=SDL_GetTicks();
        this->frame_count_inner++;

        //Whenever a second has passed, update the visible frame count.
        if( (this->ticks_frame_end - this->ticks_frame_count) > 1000)
        {
            this->frame_count=this->frame_count_inner;
            this->frame_count_inner=0;
            this->ticks_frame_count=SDL_GetTicks();
        }

        //Calculate difference and apply delay when needed.

        this->diff=this->ticks_frame_end - this->ticks_frame_begin;

        if(this->diff < this->frame_length) SDL_Delay(this->frame_length-this->diff);


        //Get the beginning time and start again.
        this->ticks_frame_begin=SDL_GetTicks();
    }
};

class Box
{
    private:

    SDL_Rect position;
    int movement_x;
    int movement_y;

    public:

    Box():movement_x(4), movement_y(4)
    {
        this->position.x=100;
        this->position.y=100;
        this->position.w=30;
        this->position.h=30;
    }

    SDL_Rect get_position() {return this->position;}

    void move_around()
    {
        //Won't touch the edges, but doesn't really matter.

        if(this->position.x<=0 || this->position.x>=800-this->position.w) this->movement_x=-this->movement_x;
        if(this->position.y<=0 || this->position.y>=600-this->position.h) this->movement_y=-this->movement_y;

        this->position.x+=this->movement_x;
        this->position.y+=this->movement_y;
    }
};

bool init_sdl(){return SDL_Init( SDL_INIT_VIDEO ) >= 0;}
void quit_sdl(){SDL_Quit();}
void fill_screen(SDL_Surface * screen)
{
    SDL_FillRect(screen, NULL, SDL_MapRGB(screen->format,0,0,0));
}

void update_screen(SDL_Surface * screen, Box& box)
{
    SDL_Rect b=box.get_position();
    SDL_FillRect(screen, &b, SDL_MapRGB(screen->format, 200, 20, 20));
    SDL_Flip(screen);
}

int get_input()
{
    SDL_Event event;

    while(SDL_PollEvent(&event))
    {   
        if(event.type==SDL_QUIT) return 1;
        else if(event.type==SDL_KEYDOWN)
        {
            switch(event.key.keysym.sym)
            {
                case SDLK_ESCAPE: return 1; break;
                case SDLK_SPACE: return 2; break;
                case SDLK_UP: return 3; break;
                case SDLK_DOWN: return 4; break;
                default: break;
            }
        }
    }

    return 0;
}

int main(int argc, char **argv)
{
    if(!init_sdl())
    {
        return 1;
    }
    else
    {
        //Init things...

//      SDL_Surface * screen=SDL_SetVideoMode(800, 600, 16, SDL_DOUBLEBUF | SDL_HWSURFACE); /*SDL_SWSURFACE | SDL_ANYFORMAT);*/
        SDL_Surface * screen=SDL_SetVideoMode(800, 600, 16, SDL_SWSURFACE | SDL_ANYFORMAT);
        Box box=Box();
        fps_control fps=fps_control(60);    //Framerate is set to 60.
        bool run=true;
        int input=0;

        SDL_FillRect(screen, NULL, SDL_MapRGB(screen->format,255,0,0));

        //Main loop

        fps.init();

        while(run)
        {
            input=get_input();

            switch(input)
            {
                case 1: run=false; break;   
                case 2: fps.toggle_apply(); break;
                case 3: fps.framerate_increase(); break;
                case 4: fps.framerate_decrease(); break;
                default: break;
            }

            box.move_around();
            fill_screen(screen);
            update_screen(screen, box);
            fps.turn();
        }

        quit_sdl();
    }
}

It is self contained (and again, pure garbage) so just link it with the SDL and try it... Do you see any stuttering pulse here?.

I'll try and apply the delta timing to see if the problem goes away. I just want to know why this happens in such simple applications. Thanks a lot.

Edit: One last thing, I know I probably shouldn't go with SDL_Delay at all (something about the system sleeping for at least the asked value) but I am really puzzled about this behaviour on seemingly powerful machines.

Edit: Corrected a bit on "set_framerate", thanks to Yno for the comments on it.

Edit 2: Since I changed all code to work with a delta time value instead of a set framerate everything has been going much better. I still get some periodic slowdowns (this time each forty something seconds) but I can blame those on the system since the framerate and slowdowns vary wildly depending on the Linux Desktop I am using (say Gnome, Unity, Gnome2d...).

The Marlboro Man
  • 971
  • 7
  • 22

2 Answers2

4

Your class fps_control simply doesn't do the job, it is completely buggy. The framerate_decrease() function is wrong:

void framerate_decrease(){this->set_framerate(this->desired_framerate+1);}

It should be -1 here. calculate_frame_length() should be called everytime your desired FPS is changed, that is, into set_framerate():

void set_framerate (unsigned int p_param) {
    desired_framerate = p_param;
    calculate_frame_length ();
}

Also be careful with your division in calculcate_frame_length(), you might divide by zero:

void framerate_decrease() {
    if (desired_framerate > 1)
        set_framerate (desired_framerate - 1);
}

Apart from these issues, your class looks uselessly complicated. If you want to restrict your framerate, you have to understand how it is computed. The usual way to go about it is to compute the amount of time required by your frames using SDL_GetTicks():

int t;
while (run) {
    t = SDL_GetTicks ();
    // ...
    t = SDL_GetTicks () - t;
}

At the end of the loop, t is the number of ms your frame took to complete. 1000 / t is thus your frames per second. If your framerate is too high (that is, t is too small), you have to fill the gap between your current frame time and the desired frame time with a call to SDL_Delay(). Say FPS is your FPS limit, the time each frame should take is 1000 / FPS.

int t;
while (run) {
    t = SDL_GetTicks ();
    // ...
    t = SDL_GetTicks () - t;

    // if the framerate is too high
    if (t < 1000 / FPS) {
        // compute the difference to have a total frame time of 1000 / FPS
        SDL_Delay ((1000 / FPS) - t);
    }
}

Hope this helps.

Yno
  • 786
  • 4
  • 9
  • Thanks for the answer... Actually, framerate decrease is as wrong as it gets (some typo I have there) but calculate_frame_length() is right there on the constructor, right?. The code is actually a translation from the spanish original so it may have some inconsistencies. As for the rest, I'd say it's all there, just in other forms, right?. [to be continued, broken "enter key"] – The Marlboro Man Aug 31 '12 at 09:03
  • Still, even if some things may be buggy or strange (like how the first t (ticks_frame_begin is calculated at the end) I don't think I should be expecting this pulse... Unless something is very wrong with SDL_Delay. Of course, everything helps, thanks a lot :). – The Marlboro Man Aug 31 '12 at 09:07
  • `calculate_frame_length()` should be called everytime the desired framerate is changed, that is in `set_framerate()`. I'll edit my post. – Yno Aug 31 '12 at 09:22
  • No prob, I'll also edit my own since all those increments and decrements aren't really in the real code and seems to be confusing. Have you seen the pulsing skips I mention?. On a side note I tried delta time techniques and saw no stuttering at all... – The Marlboro Man Aug 31 '12 at 09:24
  • I'm not experiencing anything weird with your code once fixed. – Yno Aug 31 '12 at 09:31
  • Copied, pasted and recompiled... Same stuttering, same pulse. I keep on blaming SDL_Delay. Maybe it is something about these linux machines?. – The Marlboro Man Aug 31 '12 at 09:36
  • I'm running on Linux myself. Except for the vsync which introduces regular skips, there is nothing wrong for me. – Yno Aug 31 '12 at 09:47
  • Interesting and strange. I've been seeing this since I started with SDL on like... Six different computers. I would love to record a video of it but I don't think it will be noticeable on it all (it seems to pulse like twice per second). Still, thanks a real lot for your time, I think I will take the delta time approach and cross my fingers :). – The Marlboro Man Aug 31 '12 at 10:04
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/16085/discussion-between-the-marlboro-man-and-yno) – The Marlboro Man Aug 31 '12 at 10:54
3

I don't know what's up with your SDL code, but if it's of any use I've just written a GLFW-using FpsManager class that limits to a top-end value and provides time deltas to keep movement consistent regardless of framerate. The principles will be identical.

It's not the best code ever, but it's very clearly documented and fully smooth between 10fps and 1000fps when changing the target FPS +/- with a keystroke.

If it's any use to you, you can find it at: FpsManager - A C++ helper class for framerate independent movement

r3dux
  • 33
  • 5