4

Can anyone tell what the problem with following example is?

It produces 65 instead of 300 frames per second.

#define WIN32_LEAN_AND_MEAN

#include <Windows.h>

#include <Thread>
#include <Chrono>
#include <String>

int main(int argc, const char* argv[]) {

    using namespace std::chrono_literals;

    constexpr unsigned short FPS_Limit = 300;

    std::chrono::duration<double, std::ratio<1, FPS_Limit>> FrameDelay = std::chrono::duration<double, std::ratio<1, FPS_Limit>>(1.0f);

    unsigned int FPS = 0;

    std::chrono::steady_clock SecondTimer;
    std::chrono::steady_clock ProcessTimer;

    std::chrono::steady_clock::time_point TpS = SecondTimer.now();
    std::chrono::steady_clock::time_point TpP = ProcessTimer.now();

    while (true) {

        // ...

        // Count FPS

        FPS++;

        if ((TpS + (SecondTimer.now() - TpS)) > (TpS + 1s)) {

            OutputDebugString(std::to_string(FPS).c_str()); OutputDebugString("\n");

            FPS = 0;

            TpS = SecondTimer.now();
        }

        // Sleep

        std::this_thread::sleep_for(FrameDelay - (ProcessTimer.now() - TpP));    // FrameDelay minus time needed to execute other things

        TpP = ProcessTimer.now();
    }

    return 0;
}

I guess it has something to do with std::chrono::duration<double, std::ratio<1, FPS_Limit>>, but when it is multiplied by FPS_Limit the correct 1 frames per second are produced.

Note that the limit of 300 frames per second is just an example. It can be replaced by any other number and the program would still sleep for way too long.

Rigid
  • 145
  • 1
  • 4
  • I don't know the implementation of `sleep_for`, but if it's based on a OS timer there will be a limit on the resolution. I think Windows uses a timer rate specified in the dark ages. – Mark Ransom Oct 17 '19 at 16:18
  • @MarkRansom Only heathens fail to set high resolution timer. Windows supports up to 1 ms resolution timers through multimedia API. Not sure if it can go higher than that. Default is still that 15.6 ms value. – Tanveer Badar Oct 17 '19 at 16:20
  • 1
    A sleep is typically AT LEAST as long as requested and rounded up to the next tick. 65 is right in line with the 1/64th of a second default windows tick. A few months back I needed a reliable 50 ms period and I wound up having to use win32 API calls. the port of chrono in my mingw distribution just wasn't "good" enough. – user4581301 Oct 17 '19 at 16:30
  • A sleep is usually only required to sleep for *at least* the time specified. That is, it may always sleep longer. – Jesper Juhl Oct 17 '19 at 16:34
  • On Windows, thread::sleep_for() calls Sleep(). Whose resolution is determined by the clock interrupt rate, the mechanism that is used to jerk the processor back from its halt state. Default rate is 64 ticks per second. Increasing the rate [is possible](https://learn.microsoft.com/en-us/windows/win32/api/timeapi/nf-timeapi-timebeginperiod). – Hans Passant Oct 17 '19 at 16:37

1 Answers1

6

In short, the problem is that you use std::this_thread::sleep_for at all. Or, any kind of "sleep" for that matter. Sleeping to limit the frame rate is just utterly wrong.

The purpose of sleep functionality is, well... I don't know to be honest. There are very few good uses for it at all, and in practically every situation, a different mechanism is better.

What std::this_thread::sleep_for does (give or take a few lines of sanity tests and error checking) is, it calls the Win32 Sleep function (or, on a different OS, a different, similar function such as nanosleep).

Now, what does Sleep do? It makes a note somewhere in the operating system's little red book that your thread needs to be made ready again at some future time, and then renders your thread not-ready. Being not-ready means simply that your thread is not on the list of candidates to be scheduled for getting CPU time.

Sometimes, eventually, a hardware timer will fire an interrupt. That can be a periodic timer (pre Windows 8) with an embarrassingly bad default resolution, or programmable one-shot interrupt, whatever. You can even adjust that timer's resolution, but doing so is a global thing which greatly increases the number of context switches. Plus, it doesn't solve the actual problem. When the OS handles the interrupt, it looks in its book to see which threads need to be made ready, and it does that.

That, however, is not the same as running your thread. It is merely a candidate for being run again (maybe, some time).

So, there's timer granularity, inaccuracy in your measurement, plus scheduling... which altogether is very, very unsuitable for short, periodic intervals. Also, different Windows versions are known to round differently to the scheduler's granularity.

Solution: Do not sleep. Enable vertical sync, or leave it to the user to enable it.

Damon
  • 67,688
  • 20
  • 135
  • 185
  • Do you have any suggestions other than a simple `while (getTime() < desiredTime) {}`? I suppose you could pump messages in there, or grab something off a job queue. – Mark Storer Oct 17 '19 at 16:52
  • 1
    @MarkStorer: When you need to wait until a particular point in time, a timer is the correct solution. Your suggestion would busy spin which, although more reliable than sleeping, will burn an awful lot of CPU (producing heat, wasting energy, and stealing CPU from tasks that could do something useful). When "FPS" is involved such as in the Q, vertical sync, which is a timer of sorts, is the only valid timer because otherwise you inevitably get timers running out of sync over time (which, too, is a problem). Any _two_ timers will never stay in sync forever. – Damon Oct 17 '19 at 17:02
  • 3
    A good implementation of `std::this_thread::sleep_until` _should_ indeed use a timer, but unluckily that's not guaranteed to be the case. In case you wonder why a timer would be better, there's several reasons. The maybe most compelling one being that even on a non-RT operating system such as Windows (even 15 year old versions!), timers are much more accurately working, since your thread gets its priority boosted for two slices the moment the timer fires. Which is not a hard guarantee that it will run ASAP, but "as good as". – Damon Oct 17 '19 at 17:07
  • It is worth noting that some general-purpose, best-effort kernels are moving towards something similar to hard real-time. Linux, for example, has the real-time patchset since many years, AFAIK some of it is still outside the mainline kernel, but kept in sync. Also, mainline Linux got the SCHED_DEADLINE realtime scheduler a few years back, a huge step forward. – Erik Alapää Oct 18 '19 at 07:27