如何在C++循环中限制帧率?

29

我正在尝试使用C++,结合chrono和thread来限制每秒循环帧数进行交叉检查。

以下是我的代码:

std::chrono::system_clock::time_point now = std::chrono::system_clock::now();
std::chrono::system_clock::time_point lastFrame = std::chrono::system_clock::now();

while (true)
{
    // Maintain designated frequency of 5 Hz (200 ms per frame)
    now = std::chrono::system_clock::now();
    std::chrono::duration<double, std::milli> delta = now - lastFrame;
    lastFrame = now;

    if (delta.count() < 200.0)
    {
        std::chrono::duration<double, std::milli> delta_ms(200.0 - delta.count());
        auto delta_ms_duration = std::chrono::duration_cast<std::chrono::milliseconds>(delta_ms);
        std::this_thread::sleep_for(std::chrono::milliseconds(delta_ms_duration.count()));
    }

    printf("Time: %f \n", delta.count());

    // Perform intersection test

}
我遇到的问题是delta每隔一段时间输出的结果都非常小,而不是我想要的每帧大约200毫秒。

我遇到的问题是delta每隔一段时间输出的结果都非常小,而不是我想要的每帧大约200毫秒。

Time: 199.253200
Time: 2.067700
Time: 199.420400
Time: 2.408100
Time: 199.494200
Time: 2.306200
Time: 199.586800
Time: 2.253400
Time: 199.864000
Time: 2.156500
Time: 199.293800
Time: 2.075500
Time: 201.787500
Time: 4.426600
Time: 197.304100
Time: 4.530500
Time: 198.457200
Time: 3.482000
Time: 198.365300
Time: 3.415400
Time: 198.467400
Time: 3.595000
Time: 199.730100
Time: 3.373400

你觉得这种情况可能是由什么原因引起的?


4
你的时间大约在200毫秒左右,所以问题在哪里?你不能指望得到准确的计时... 有时你会比200稍微短一些,有时你会略微超过,接受这种情况 - 你不可能每次都恰好达到200。 - Jesper Juhl
2
我不追求精确的时间,但我不明白为什么有些循环迭代需要1-4毫秒,而其他的则需要大约200毫秒。 - Irongrave
2
有时候您的 deltacount 小于 200,但您仍然输出它。 - strangeqargo
1
操作系统可能会在任何时候安排您的程序 - 这是一个原因。您可能在某个地方做了一些愚蠢的事情,比如睡眠一段时间。 - Jesper Juhl
1
假设由于所有睡眠时间都比预期的短,您首先睡了198ms,因此增量约为198ms,因此再睡约2ms,因此您的增量约为2ms。 - Jarod42
显示剩余4条评论
4个回答

26

如果你思考一下你的代码是如何工作的,你会发现它会按照你编写的方式精确地运行。Delta振荡是由于代码中的逻辑错误造成的。

具体来说:

  • 我们从 delta == 0 开始。
  • 因为 delta 小于 200,你的代码会睡眠 200 - delta(0) == 200 毫秒。
  • 现在,delta 本身变得接近于 200(因为你还测量了实际工作和睡眠时间),而你会睡眠 200 - delta(200) == 0 毫秒。
  • 之后这个循环就一直重复。

要解决这个问题,你需要不去测量睡眠时间。

方法如下:

#include <iostream>
#include <cstdio>
#include <chrono>
#include <thread>

std::chrono::system_clock::time_point a = std::chrono::system_clock::now();
std::chrono::system_clock::time_point b = std::chrono::system_clock::now();

int main()
{
    while (true)
    {
        // Maintain designated frequency of 5 Hz (200 ms per frame)
        a = std::chrono::system_clock::now();
        std::chrono::duration<double, std::milli> work_time = a - b;

        if (work_time.count() < 200.0)
        {
            std::chrono::duration<double, std::milli> delta_ms(200.0 - work_time.count());
            auto delta_ms_duration = std::chrono::duration_cast<std::chrono::milliseconds>(delta_ms);
            std::this_thread::sleep_for(std::chrono::milliseconds(delta_ms_duration.count()));
        }

        b = std::chrono::system_clock::now();
        std::chrono::duration<double, std::milli> sleep_time = b - a;

        // Your code here

        printf("Time: %f \n", (work_time + sleep_time).count());
    }
}
这段代码给我一系列稳定的差异:
Time: 199.057206 
Time: 199.053581 
Time: 199.064718 
Time: 199.053515 
Time: 199.053307 
Time: 199.053415 
Time: 199.053164 
Time: 199.053511 
Time: 199.053280 
Time: 199.053283    

非常好的解释,我现在知道我哪里错了。谢谢。 - Irongrave
sleep_for()函数内部的对话不是必需的,证明如下:static_assert(std::is_same<decltype(std::chrono::milliseconds()),decltype(delta_ms_duration)>(),"");没有失败, - Hassen Dhia
但是这段代码在每个循环中调用了系统时钟两次,效率不高。 - rsalmei

17

这很像 Galik 的回答,但它保留了 OP 问题的语法,并且没有转到 C API。此外,它创建了一个自定义的帧时长单位,我认为这对可读性很重要:

#include <chrono>
#include <cstdint>
#include <iostream>
#include <thread>

int
main()
{
    using namespace std;
    using namespace std::chrono;

    using frames = duration<int64_t, ratio<1, 5>>;  // 5Hz
    auto nextFrame = system_clock::now();
    auto lastFrame = nextFrame - frames{1};;

    while (true)
    {
        // Perform intersection test

        this_thread::sleep_until(nextFrame);
        cout << "Time: "  // just for monitoring purposes
             << duration_cast<milliseconds>(system_clock::now() - lastFrame).count()
             << "ms\n";
        lastFrame = nextFrame;
        nextFrame += frames{1};
    }
}

这是我的输出结果:

Time: 200ms
Time: 205ms
Time: 205ms
Time: 203ms
Time: 205ms
Time: 205ms
Time: 200ms
Time: 200ms
Time: 200ms
...

需要注意的关键点:

  • 一种简洁的记录5Hz的方式:using frames = duration<int64_t, ratio<1, 5>>;
  • 使用sleep_until而不是sleep_for,它会处理无法确定实际工作完成所需时间的情况。
  • 除了I/O外没有使用.count()这里有一个库可以摆脱这个问题
  • 没有手动转换单位(例如/ 1000)。
  • 没有浮点单位,当然这并没有什么问题。
  • 最小化需要指定或依赖于显式单位的需求。

通过添加持续时间I/O库,以上代码将更改如下:

#include "chrono_io.h"
#include <chrono>
#include <cstdint>
#include <iostream>
#include <thread>

int
main()
{
    using namespace date;
    using namespace std;
    using namespace std::chrono;

    using frames = duration<int64_t, ratio<1, 5>>;  // 5Hz
    auto nextFrame = system_clock::now();
    auto lastFrame = nextFrame - frames{1};;

    while (true)
    {
        // Perform intersection test

        this_thread::sleep_until(nextFrame);
        // just for monitoring purposes
        cout << "Time: " << system_clock::now() - lastFrame << '\n';
        lastFrame = nextFrame;
        nextFrame += frames{1};
    }
}

输出结果会因平台而异(取决于 system_clock 的“本地持续时间”)。在我的平台上,它看起来像这样:

The output would differ depending upon platform (depending upon the "native duration" of system_clock). On my platform it looks like this:
Time: 200042µs
Time: 205105µs
Time: 205107µs
Time: 200044µs
Time: 205105µs
Time: 200120µs
Time: 204307µs
Time: 205136µs
Time: 201978µs
...

当我将比率更改为30或60 Hz时,我会遇到编译错误(在VS 2017和GCC 7中都是如此)- error C2679:binary '+=':没有找到接受右操作数类型为'frames'的运算符(或者没有可接受的转换) - 有任何想法吗? - onqtam
1
是的,这是编译器警告您在将1/30秒(例如)添加到system_clock::time_point时存在截断误差,因为system_clock::time_point无法准确表示1/30秒。 但这很容易解决!只需在system_clock::now()中添加frames{0}并让auto发挥其作用:auto nextFrame = system_clock::now() + frames {0};。现在,您不再使用system_clock::time_point,而是使用一个可以精确表示帧和system_clock::time_point精度总和的时间点。 - Howard Hinnant

15

我通常会做这样的事情:

#include <chrono>
#include <ctime>
#include <iostream>
#include <thread>

int main()
{
    using clock = std::chrono::steady_clock;

    auto next_frame = clock::now();

    while(true)
    {
        next_frame += std::chrono::milliseconds(1000 / 5); // 5Hz

        // do stuff
        std::cout << std::time(0) << '\n'; // 5 for each second

        // wait for end of frame
        std::this_thread::sleep_until(next_frame);
    }
}

输出:(每个秒值五个)

1470173964
1470173964
1470173964
1470173964
1470173964
1470173965
1470173965
1470173965
1470173965
1470173965
1470173966
1470173966
1470173966
1470173966
1470173966

4
这是我的最爱,除此之外,我会定义一个自定义的frame单位:using frames = std::chrono::duration<std::int64_t, std::ratio<1, 5>>;,然后执行next_from += frames{1};。这样可以避免代码中出现手动转换。 - Howard Hinnant
1
解决方案缺失 #include <thread> - OneArb
我可以在不包含 #include <ctime> 的情况下运行该解决方案。 - OneArb
1
@OneArb 是的,但是C++标准并不保证这一点。因此需要考虑可移植性。 - Galik

4
交替的Delta时间源于逻辑问题:您正在基于前一帧的持续时间(就帧持续时间的计算方式而言)向一帧添加延迟。这意味着在长帧(约200毫秒)之后,您不会应用延迟并获得短帧(几毫秒),然后触发下一帧的延迟,从而产生长帧,如此循环。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接