我如何在我的OpenGL应用程序中实现准确(但可变)的FPS限制/上限?

How can I implement an accurate (but variable) FPS limit/cap in my OpenGL application?

本文关键字:FPS 上限 限制 我的 OpenGL 应用程序 实现      更新时间:2023-10-16

我目前正在制作一个OpenGL应用程序,向用户显示一些3D球体,它们可以旋转,移动等。也就是说,这里没有太多的复杂性,所以应用程序以相当高的帧率(~500 FPS)运行。

显然,这是过度的-即使120也足够了,但我的问题是,在全状态下运行应用程序会消耗我的CPU,导致多余的热量,功耗等。我想做的是能够让用户设置FPS上限,这样CPU就不会在不必要的时候被过度使用。

我正在使用freeglut和c++,并且已经设置了动画/事件处理来使用计时器(使用glutTimerFunc)。然而,glutTimerFunc只允许设置一个整数毫秒数——所以如果我想要120 FPS,我能得到的最接近的是(int)1000/120 = 8 ms分辨率,它相当于125 FPS(我知道这是一个可以忽略不计的数量,但我仍然想要设置一个FPS限制,如果我知道系统可以更快地渲染,我就能得到那个FPS)。

此外,使用glutTimerFunc来限制FPS从来没有一致的工作。假设我将应用程序的帧率设定为100 FPS,它通常不会高于90-95 FPS。再一次,我试图找出渲染/计算之间的时间差,但它总是超过限制5-10 FPS(可能是计时器分辨率)。

我想最好的比较是游戏(如《半条命2》)——你设定了FPS上限,它总是达到那个数值。我知道我可以在渲染每一帧之前和之后测量时间增量,然后循环直到我需要绘制下一帧,但这并不能解决我100%的CPU使用问题,也不能解决时间分辨率问题。

是否有任何方法可以在我的应用程序中实现有效的,跨平台的,可变帧速率限制/上限?或者,换句话说,是否存在实现高分辨率计时器和睡眠函数的跨平台(和开源)库?

编辑:我宁愿找到一个解决方案,不依赖于终端用户启用垂直同步,因为我要让他们指定FPS上限。

编辑#2:对于所有推荐SDL的人(其中我最终将我的应用程序移植到SDL),使用glutTimerFunc函数触发抽签或使用SDL_Delay在抽签之间等待有任何区别吗?每种方法的文档都提到了相同的注意事项,但我不确定哪种方法效率更高还是更低。

编辑#3:基本上,我想弄清楚是否有一种(简单的方法)在我的应用程序中实现准确的FPS限制器(再次,像半条命2)。如果这是不可能的,我很可能会切换到SDL(对我来说使用延迟函数比使用glutTimerFuncx毫秒回调渲染函数更有意义)。

建议您使用SDL。我个人用它来管理我的计时器。此外,它可以限制你的fps到你的屏幕刷新率(V-Sync)与SDL 1.3。这使您能够在限制CPU使用的同时拥有最佳的屏幕性能(即使您有更多的帧,它们也无法显示,因为您的屏幕刷新速度不够快)。

函数

SDL_GL_SetSwapInterval (1),

如果你想要一些使用SDL的定时器代码,你可以看到:

my timer class

祝你好运

我认为实现这一点的一个好方法是,无论你使用什么图像库,在游戏循环中都要有一个单一的时钟测量,以考虑到每一个滴答(毫秒)。这样平均帧数就会像《半条命2》一样达到极限。希望下面的代码片段能够解释我所说的内容:

//FPS limit
unsigned int FPS = 120;
//double holding clocktime on last measurement
double clock = 0;
while (cont) {
    //double holding difference between clocktimes
    double deltaticks;
    //double holding the clocktime in this new frame
    double newclock;
    //do stuff, update stuff, render stuff...
    //measure clocktime of this frame
    //this function can be replaced by any function returning the time in ms
    //for example clock() from <time.h>
    newclock = SDL_GetTicks();
    //calculate clockticks missing until the next loop should be
    //done to achieve an avg framerate of FPS 
    // 1000 / 120 makes 8.333... ticks per frame
    deltaticks = 1000 / FPS - (newclock - clock);
    /* if there is an integral number of ticks missing then wait the
    remaining time
    SDL_Delay takes an integer of ms to delay the program like most delay
    functions do and can be replaced by any delay function */
    if (floor(deltaticks) > 0)
        SDL_Delay(deltaticks);
    //the clock measurement is now shifted forward in time by the amount
    //SDL_Delay waited and the fractional part that was not considered yet
    //aka deltaticks
    the fractional part is considered in the next frame
    if (deltaticks < -30) {
        /*dont try to compensate more than 30ms(a few frames) behind the
        framerate
        //when the limit is higher than the possible avg fps deltaticks
        would keep sinking without this 30ms limitation
        this ensures the fps even if the real possible fps is
        macroscopically inconsitent.*/
        clock = newclock - 30;
    } else {
        clock = newclock + deltaticks;
    }
    /* deltaticks can be negative when a frame took longer than it should
    have or the measured time the frame took was zero
    the next frame then won't be delayed by so long to compensate for the
    previous frame taking longer. */

    //do some more stuff, swap buffers for example:
    SDL_RendererPresent(renderer); //this is SDLs swap buffers function
}

我希望这个使用SDL的例子能有所帮助。重要的是每帧只测量一次时间,这样每帧都被考虑在内。我建议在一个函数中模块化这个计时,这也使您的代码更清晰。这段代码没有注释,以防它们在上一个代码中惹恼了您:

unsigned int FPS = 120;
void renderPresent(SDL_Renderer * renderer) {
    static double clock = 0;
    double deltaticks;
    double newclock = SDL_GetTicks();
    deltaticks = 1000.0 / FPS - (newclock - clock);
    if (floor(deltaticks) > 0)
        SDL_Delay(deltaticks);
    if (deltaticks < -30) {
        clock = newclock - 30;
    } else {
        clock = newclock + deltaticks;
    }
    SDL_RenderPresent(renderer);
}

现在您可以在主循环中调用这个函数,而不是在swapBuffer函数(SDL中的SDL_RenderPresent(renderer))中调用。在SDL中,您必须确保关闭SDL_RENDERER_PRESENTVSYNC标志。该函数依赖于全局变量FPS,但您可以考虑其他存储方法。我只是把整个东西放在我的库的命名空间中。


由于deltaticks的30ms限制,如果多个帧的循环时间没有大的差异,那么这种封顶帧率的方法可以准确地提供所需的平均帧率。deltaticks限制是必需的。当FPS限制高于实际帧率时,deltaticks将无限下降。此外,当帧率再次超过FPS限制时,代码将尝试通过立即渲染每帧来补偿损失的时间,从而导致巨大的帧率,直到deltaticks上升回零。你可以根据你的需要修改30ms,这只是我的估计。我用Fraps做了几个基准测试。它可以在任何可以想象的帧率下工作,并且根据我的测试提供了漂亮的结果。


我必须承认这是我昨天才写的,所以不可能有什么bug。我知道这个问题是5年前问的,但给出的答案并不能说明我。也请随意编辑这篇文章,因为这是我的第一个帖子,可能有缺陷。

编辑:我注意到SDL_Delay在某些系统上非常非常不准确。我听说它在android上延迟太久了。这意味着我的代码可能无法移植到您想要的所有系统上。

最简单的解决方法是启用垂直同步。我在大多数游戏中都是这么做的,以防止我的笔记本电脑太热。只要您确保渲染路径的速度没有连接到其他逻辑,这应该没问题。

有一个函数glutGet(GLUT_ELAPSED_TIME),它以毫秒为单位返回自启动以来的时间,但这可能仍然不够快。

一个简单的方法是创建自己的计时器方法,它在windows上使用HighPerformanceQueryTimer,在POSIX系统上使用getTimeOfDay。

或者,您也可以使用SDL或SFML中的计时器函数,它们的作用与上述基本相同。

您不应该尝试手动限制呈现速率,而应该与显示垂直刷新同步。这是通过在图形驱动程序设置中启用V同步来完成的。除了防止(您的)程序以高速率渲染外,它还通过避免撕裂来提高图像质量。

交换间隔扩展允许您的应用程序微调V同步行为。但在大多数情况下,只是在驱动程序中启用V同步,并让缓冲区交换块,直到同步足够。

我建议使用sub-ms精度系统计时器(QueryPerformanceCounter, gettimeofday)来获取定时数据。这些还可以帮助您在优化的版本构建中分析性能。

一些背景资料:

  • SDL_Delay与Sleep/Sleep/ussleep/nanosleep几乎相同,但它作为参数
  • 限制为毫秒。
  • sleep通过依赖系统线程调度程序来继续你的代码。
  • 根据您的操作系统和硬件,调度器可能具有低于1000hz的刻度频率,这导致调用sleep时的时间间隔比您指定的时间长,因此您无法保证获得所需的睡眠时间。
  • 你可以尝试改变调度器的频率。在Windows上,你可以通过调用timeBeginPeriod()来实现。对于linux系统,请检查此答案。
  • 即使你的操作系统支持1000hz的调度器频率,你的硬件也可能不支持,但大多数现代硬件都支持。
  • 即使你的调度程序的频率是1000hz,如果系统忙于高优先级的进程,睡眠可能需要更长的时间,但如果你的系统不是在超高负载下,这种情况不应该发生。

总而言之,您可能在一些无痒的linux内核上休眠微秒,但如果您对跨平台解决方案感兴趣,您应该尝试将调度器频率调到1000hz,以确保在大多数情况下休眠是准确的。

解决120FPS的舍入问题:

1000/120      = 8,333ms
(int)1000/120 = 8ms

你要么忙等待333微秒,然后睡8ms。这花费了一些CPU时间,但是非常准确。或者你按照Neop的方法,有时睡8毫秒,有时睡9毫秒,平均睡8333毫秒。这是更有效的方式,但不太准确。