Fixed time step, capped framerate

Started by
9 comments, last by eczkchtl 16 years, 4 months ago
We're using a fixed time step in our game, doing game logic at 60Hz while rendering when we can afford it. It works very well when using vsync with a 60Hz update rate but without vsync we will have to wait for some time each frame on fast computers with high screen refresh rates. Strangely this makes the game very choppy. People with slow computers and 60hz refresh rate will play a very smooth game while people with fast computers will be experiencing choppy movements. We're using timers from SDL, which means QueryPerformace timer on windows, so we can only measure milliseconds. We don't have any interpolation when rendering frames so there is no use in rendering twice without a game logic update. The implementation is simply like this (considering only fast computers):

frameTime = 1000 / 60 (milliseconds)
frameTimeDiff = 0

loop {
timer.start()
gameTick()
frameTimeDiff += timer.getMilliseconds()

timer.start()
renderTick()
frameTimeDiff += timer.getMilliseconds()

// Busy waiting here
if frameTimeDiff < frameTime {
   timer.start()
   while(timer.getMilliseconds() < frameTime - frameTimeDiff) { /* waiting */ }
   frameTimeDiff += timer.getMilliseconds()
}

frameTimeDiff -= frameTime
}
We've tried some different implementations but this the most simple one worked best but still gives very noticable choppy movement. I figured this would have the same effect as vsync, waiting out the remaining time on each frame, but unfortunately it doesn't. Are there better ways to cap the frame rate? [Edited by - eczkchtl on December 2, 2007 10:39:00 AM]
Advertisement
Regular Windows timers such as GetTickCount() don't have a very high resolution. In GetTickCount()'s case, it's about 10-15ms on XP/Vista, IIRC.

15 milliseconds is a lot. At 60fps, 1 frame is 16.6ms. This may be the cause of your choppiness. I suggest moving to a more accurate timer, such as QueryPerformanceCounter(). More information on how to use these high-resolution timers are available at MSDN.
NextWar: The Quest for Earth available now for Windows Phone 7.
Quote:Original post by eczkchtl
We're using timers from SDL, which means QueryPerformace timer on windows, so we can only measure milliseconds.

Herein lies your problem. While the SDL docs state that they use QueryPerformanceCounter() where supported, in fact the current release has this entire code-path #ifdef'd out, so the are using plain old GetTickCount(), with its ~15 millisecond accuracy.

As I see it, you have 3 realistic solutions here, some easier than others:
1) use vsync, and set your logic to use the v-sync rate for calculations.
2) dump fixed frame rate, and re-design your logic to a standard variable step system.
3) implement your own timer, avoiding all the classic pitfalls of timing on Windows (timer jumps, missing support, etc.)

I would tend to push you towards (3), as it is the least destructive of your existing codebase, and should provide a more accurate simulation as well.

Tristam MacDonald. Ex-BigTech Software Engineer. Future farmer. [https://trist.am]

Quote:Original post by swiftcoder
While the SDL docs state that they use QueryPerformanceCounter() where supported, in fact the current release has this entire code-path #ifdef'd out, so the are using plain old GetTickCount(), with its ~15 millisecond accuracy.


Yes, it help a lot when I replaced SDL's timer with QueryPerformanceCounter. Still there is the problem of choosing the best times to do the waiting. Optimally we would like the render to be completed just in time for a vblank. There must be some neat algorithm for this (at least something that gives better results than the trivial solution), I'll see if I can come up with something.
QPC is actually nanosecond accuracy according to the documentation not millisecond :)

"Those who would give up essential liberty to purchase a little temporary safety deserve neither liberty nor safety." --Benjamin Franklin

Quote:Original post by Mike2343
QPC is actually nanosecond accuracy according to the documentation not millisecond :)


Yep, but SDL only returns in integer milliseconds so the rest is lost.

As for a higher screen update frequency than game logic frequency we will have to interpolate frames and also we need the rendering to be fast enough to catch the vblanks. For example 60Hz game logic with 75Hz update frequency will give us 4 vblanks on 3 updates meaning we have to interpolate one frame on half a time step. If we do not interpolate we will render the same frame twice giving a single bigger time gap then rendering each frame at 60hz resulting in a jerky movement every fourth frame.

OK, so the simple way to fix this is to force 60Hz update rate on the screen (or any multiple of 60). Render interpolation will be more flexible/portable but higher display frequencies will require faster computers (missing one vblank with a frequency < 120Hz will result in a jerk).

[Edited by - eczkchtl on December 4, 2007 9:16:14 AM]
Quote:Original post by eczkchtl
Quote:Original post by Mike2343
QPC is actually nanosecond accuracy according to the documentation not millisecond :)


Yep, but SDL only returns in integer milliseconds so the rest is lost.

Since the unix gettimeofday() also gives nanoseconds (where supported by hardware), I tend to implement my own timer that tracks nanoseconds. It can be a problem when I hit a Windows machine with no performance counter, but variable frame rate tends to smooth that out.

Quote:As for a higher screen update frequency than game logic frequency we will have to interpolate frames and also we need the rendering to be fast enough to catch the vblanks. For example 60Hz game logic with 75Hz update frequency will give us 4 vblanks on 3 updates meaning we have to interpolate one frame on half a time step. If we do not interpolate we will render the same frame twice giving a single bigger time gap then rendering each frame at 60hz resulting in a jerky movement every fourth frame.

OK, so the simple way to fix this is to force 60Hz update rate on the screen (or any multiple of 60). Render interpolation will be more flexible/portable but higher display frequencies will require faster computers (missing one vblank with a frequency < 120Hz will result in a jerk).

The elegant way to solve this problem, though not the easy way, is to multi-thread: Vsync the presentation thread, and run the logic thread at whatever fixed rate you want. If the simulation is running at 60Hz, you don't need any extra interpolation (unless your objects are going incredibly fast), because you have moved out of the range of normal perception. If you are still worried about temporal aliasing, try a very minimal motion blur.

Tristam MacDonald. Ex-BigTech Software Engineer. Future farmer. [https://trist.am]

Quote:Original post by swiftcoder
The elegant way to solve this problem, though not the easy way, is to multi-thread: Vsync the presentation thread, and run the logic thread at whatever fixed rate you want. If the simulation is running at 60Hz, you don't need any extra interpolation (unless your objects are going incredibly fast), because you have moved out of the range of normal perception. If you are still worried about temporal aliasing, try a very minimal motion blur.


Actually multi-threading doesn't solve anything here. Think about it. Say I have infinite fast updates and rendering, but that doesn't help since I still have to wait for a vblank. With the 3 updates per 4 vblanks two of the vblanks must produce the same image if we're not using interpolation. The time lost while producing the same image twice is more than the time between two consecutive 60hz vblanks which then gives us a jerk. Two images may only be the same if we have 120Hz or more.
I use time based animation, and have a timer class which looks like this:

* *  fps_counter.h *  worms * *  Created by Gavin Burton on 27/11/2007. *  Copyright 2007 __MyCompanyName__. All rights reserved. * */class fps_counter;#ifndef class_fps_counter_h#define class_fps_counter_h 1#include <SDL/SDL.h>class fps_counter{private:        unsigned int app_ticks;//total ticks so far in this app        unsigned int last_ticks;// time in ms since last call to tick();        int frame_cap; //limit FPS to this         bool cap_frames; //do we need to?public:        fps_counter();        void set_cap(int fps);        void cap_on();        void cap_off();        float get_ms(); //time in ms since last call to tick();        float get_fps(); //approximate framerate assuming app is taking get_ms() milliseconds per frame. (less accurate)        void tick();        //if cap is on this will delay until frame_cap ms have passed since last call to tick()};#endif


I called it an fps counter here but I can't remember why. How I use it is simple; I define one for each place where time based animation is needed, and i call tick() to update the timer. Then I can read either the framerate or the number of milliseconds since the last tick.

Suppose your object is moving at 1 m/s. If it has been 12 ms since the last time your timer ticked, then the distance your object must move is 1/1000 * 12 units.

I have a timer in the main function which is told to cap the framerate to 60 fps. If when you call tick() is going faster than that, tick() yields until the framerate will be 60, then returns, thus ensuring I don't waste cpu time drawing things faster than the refresh rate of my monitor. I can remove this cap for testing purposes. If the framerate is below 60 or the cap is off, it returns immediately, enabling the game to run as fast as it can.

Take for example my animation class, which scans through frames at a certain number of frames per second. This framerate may be slower than the frame rate of the game. SO i do this:

void animation::update(){        //get the fraction of a second since last execution        //second_fraction        c.tick();        float f = c.get_fps();        float second_fraction = animation_framerate / f;        //got fraction of a second        interpolation += second_fraction;        if(interpolation > 1){                current_frame++;                interpolation -= (int)interpolation;        };};


On the class constructor I disable the frame cap ability of the local timer, so its sole purpose is to calculate how many ms have passed.

The animation then knows which frame must be drawn when I call the draw function. I've tested all this briefly by having worms - style character moving left and right and shooting at each other at different frame-rates.

I'll post all of my timer code if you wish, but I'll have to clean it up first. You coudl use it by allowing each system to have a seperate timer, with frame capping turned off for all but the one in the main loop. Tick each timer, acumilate the number of ms each time, and when it gets to > 1000 / desired_framerate your update that system.

I just wanted to see if he would actually do it. Also, this test will rule out any problems with system services.
If you interpolate between frames, you can also cut the simulation cycles by half or even more since most games don't really need a 60 Hz simulation rate (although usually this sort of interpolation only makes sense with 3d rendering; in a 2d game it is easier for rendering artifacts to stand out when this is done).

Displaying the same image for 2 frames occasionally isn't really that bad. Film is usually shot at 24 frames per second, while NTSC televisions display approximately 30 frames per second. 4 frames need to be stretched into 5, although NTSC uses interleaved fields (each field is either odd or even numbered lines). Usually two adjacent frames of film have one of their fields shown twice, creating one frame of video where the even lines are from one film frame and the odd lines are from the next, or something like that. While a monitor probably won't be interleaved, going from 60 to 75 won't be that noticable.

This topic is closed to new replies.

Advertisement