Caesar
Caesar

Reputation: 9841

Controlling frame rate

I'm creating a bunch of threads that need to do work in a frame cycle. I would like to control how many frames are done in a second. I simplified the code I have into this so I can show you what I have wrote

// setup the frame timer
std::chrono::time_point<std::chrono::system_clock> start = std::chrono::system_clock::now();
std::chrono::time_point<std::chrono::system_clock> end = std::chrono::system_clock::now();

while(running == true)
{
    // update timer
    start = std::chrono::system_clock::now();
    std::chrono::duration<double> elapsed_seconds = start - end;
    double frameTime = elapsed_seconds.count();

    this->Work(frameTime);

    // update timer
    std::chrono::time_point<std::chrono::system_clock> afterWork = std::chrono::system_clock::now();
    std::chrono::duration<double> elapsedWorkTime = afterWork - end ;

    const double minWorkTime = 1000 / this->timer.NumberOfFramePerSeconds;
    if(elapsedWorkTime.count() < minWorkTime)
    {
        double timeToSleep = minWorkTime - elapsedWorkTime.count();
        std::this_thread::sleep_for(std::chrono::milliseconds((int)timeToSleep));
    }

    // update fps
    end = start;
    timer.FrameCount += 1;
}

Not all threads have an equal amount of work, some have more than others and so without the sleep, I would get results that are around this Thread 1 : 150fps, Thread 2: 5000fps, Thread 3: 5000fps, Thread 4: 5000fps

What I want is to be able to set the frames per second to 60, and so using the code above I would set the this->timer.NumberOfFramePerSeconds to 60. My problem is that when I do that I end up with a result like this Thread 1 : 30fps, Thread 2: 60fps, Thread 3: 60fps, Thread 4: 60fps

This suggests that there is something wrong with my code as thread 1 will happily do over 150 frames when I comment out the sleep, but when the sleep is there it will work below the 60 I'm trying to achieve.

Is my code/algorithm correct?

Upvotes: 4

Views: 2705

Answers (3)

Howard Hinnant
Howard Hinnant

Reputation: 218700

It looks to me that you likely have a units conversion error. This is typically caused by handling units of time outside of the chrono type system, and introducing explicit conversion factors. For example:

double frameTime = elapsed_seconds.count();
this->Work(frameTime);

Can Work be modified to take duration<double> instead of double? This will help ensure that double is not reinterpreted as something other than seconds inside of Work. If for some reason it can not, you can at least reduce your exposure to error with:

this->Work(elapsed_seconds.count());

The following looks very suspicious:

std::chrono::duration<double> elapsedWorkTime = afterWork - end ;

const double minWorkTime = 1000 / this->timer.NumberOfFramePerSeconds;
if(elapsedWorkTime.count() < minWorkTime)

elapsedWorkTime clearly has units of seconds. minWorkTime appears to have units of milliseconds. The if throws away all units information. This should look like:

std::chrono::duration<double> minWorkTime(1./this->timer.NumberOfFramePerSeconds);
if(elapsedWorkTime < minWorkTime)

Or if you really want minWorkTime to represent milliseconds (which is not necessary):

std::chrono::duration<double, std::milli> minWorkTime(1000./this->timer.NumberOfFramePerSeconds);
if(elapsedWorkTime < minWorkTime)

I recommend the former because the introduction of 1000 is just another chance for an error to creep in.

    double timeToSleep = minWorkTime - elapsedWorkTime.count();
    std::this_thread::sleep_for(std::chrono::milliseconds((int)timeToSleep));

Here again you are unnecessarily leaving the safety net of chrono and doing things manually. This should instead simply be:

    std::this_thread::sleep_for(minWorkTime - elapsedWorkTime);

Or if you want to be more verbose:

    auto timeToSleep = minWorkTime - elapsedWorkTime;
    std::this_thread::sleep_for(timeToSleep);

The use of .count() should be rare. It is currently necessary to use it when printing out durations. It can also be necessary to deal with legacy code which can not be made to work with chrono. If you find yourself using .count() more than that, step back and try to remove it. chrono is there to help prevent unit conversion errors, and when you exit the system via .count(), you subvert chrono's reason for existing.

Update

VS2013 has a bug such that it won't sleep on double-based durations. So as a workaround...

std::this_thread::sleep_for(std::chrono::duration_cast
                <std::chrono::milliseconds>(minWorkTime - elapsedWorkTime));

This is ugly, but still leaves all the conversion factors to chrono.

Thanks to Caesar for testing this workaround out for me.

Upvotes: 4

Adrian McCarthy
Adrian McCarthy

Reputation: 47954

Sleeping usually isn't very precise because the resolution of the clock may be low.

For example, on Windows, a typical timer resolution is something like 15.6 ms. If you ask to sleep for just 1 ms, it will sleep until the next 15.6 ms interval that's at least 1 ms in the future. On average, that's 6.8 ms longer than you intended. And once the time does elapse, you've no guarantee that the processor will schedule your thread immediately.

So I suspect that your thread needs to sleep for a short time, but it sleeps significantly longer and misses the next frame. (The fact that the other threads are running at precisely 60 fps suggests that you also have something other rate limiter, like a vertical sync.)

One possible, though inefficient, solution is to do a busy-wait instead of a sleep for any duration that's shorter than the clock resolution. For example, if you have to delay for 3.2*clock_resolution, then you can sleep for 3*clock_resolutions and then spin in a loop until the actual time comes.

Another solution would be to have the threads wait on a synchronization primitive that signals at the desired rate. Depending on the types of primitives you have available, this can be tricky when you're trying to sync up multiple threads.

Upvotes: 1

ssube
ssube

Reputation: 48247

The simple answer is you don't. See also this SO question.

Instead of controlling framerate, which is error prone and doesn't handle slow threads very well, you should set up your code to handle the current framerate if at all possible.

Let the threads do as much work as they reasonably can. If they have to pause for some reason, use a message queue and sleep when it's empty.

Upvotes: 2

Related Questions