【问题标题】:How can I implement an accurate (but variable) FPS limit/cap in my OpenGL application?如何在我的 OpenGL 应用程序中实现准确(但可变)的 FPS 限制/上限?
【发布时间】:2011-10-06 03:42:10
【问题描述】:

我目前正在开发一个 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 使用率问题,也不能解决时间分辨率问题。

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

编辑:我希望找到一个不依赖最终用户启用 VSync 的解决方案,因为我将让他们指定 FPS 上限。

编辑 #2:致所有推荐 SDL 的人我确实最终将我的应用程序移植到 SDL),使用 glutTimerFunc 函数有什么区别触发平局,还是使用SDL_Delay 在平局之间等待?每个文档都提到了相同的警告,但我不确定其中一个是否比另一个更有效。

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

【问题讨论】:

标签: c++ opengl 3d cross-platform freeglut


【解决方案1】:

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

功能是

SDL_GL_SetSwapInterval(1);

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

my timer class

祝你好运:)

【讨论】:

  • SDL_GL_SetSwapInterval(1); 是否会导致 FPS 被限制为我的刷新率,即使我在显卡驱动程序中明确禁用它? (例如,在我的 NVIDIA 控制面板中,我可以将垂直同步设置为“强制关闭”)。
  • 回答您的其他问题之一,您无法以比毫秒更好的分辨率真正控制游戏中的时间。 CPU 中断系统不允许您这样做,因为可能其他应用程序也会占用 CPU 时间。毫秒就够了 :) 我不知道你的 VSync 强制关闭,我使用 ATI。
【解决方案2】:

我认为,无论您使用什么图形库,实现这一点的一个好方法是在游戏循环中进行一次时钟测量,以将每个滴答声 (ms) 考虑在内。这样一来,平均 fps 将与《半条命 2》中的限制完全相同。希望以下代码 sn-p 能解释我在说什么:

//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 示例对您有所帮助。重要的是每帧只测量一次时间,因此要考虑每一帧。 我建议在一个函数中模块化这个时间,这也使你的代码更清晰。这段代码没有 cmets 在他们只是在最后一个惹恼你的情况下:

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 的 30 毫秒限制,多个帧的循环时间没有太大差异,则这种限制帧速率的方法可准确提供所需的平均帧速率。 deltaticks 限制是必需的。当 FPS 限制高于实际帧率时,deltaticks 将无限下降。此外,当帧速率再次上升到 FPS 限制之上时,代码将尝试通过立即渲染每一帧来补偿损失的时间,从而导致巨大的帧速率,直到 deltaticks 回升至零。您可以修改 30ms 以满足您的需求,这只是我的估计。我用 Fraps 做了几个基准测试。它确实适用于所有可以想象的帧速率,并根据我的测试提供漂亮的结果。


我必须承认我昨天才编写了这个代码,因此不太可能出现某种错误。我知道这个问题是 5 年前提出的,但给出的答案并没有说明我的情况。也可以随意编辑这篇文章,因为这是我的第一篇文章,可能有缺陷。

编辑: 我注意到SDL_Delay 在某些系统上非常非常不准确。我听说过一个在 android 上延迟太多的案例。这意味着我的代码可能无法移植到您所需的所有系统。

【讨论】:

    【解决方案3】:

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

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

    一种简单的方法是制作自己的计时器方法,在 Windows 上使用 HighPerformanceQueryTimer,在 POSIX 系统上使用 getTimeOfDay。

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

    【讨论】:

    • 感谢您的回复,但我还应该提到我想要实现一个不需要用户启用 VSync 的解决方案。至于高性能计时器,在我告诉它绘制下一帧之前,我将如何让应用程序休眠? (我知道如何以毫秒的分辨率做到这一点,但希望有更好的东西)
    • @Breakthrough:永远无法保证睡眠操作的分辨率。要求睡更短(更安全)的间隔,并使用计数器准确找出您后验的睡眠时间。然后,如果还有时间就睡觉。
    • 个别应用程序仍然可以通过 swapinterval 设置使用 vsync,而无需用户在图形驱动程序中启用它。即使您想将帧率限制在较低的数量(例如视频播放应用程序),您仍然应该使用 vsync 来避免图像撕裂。
    【解决方案4】:

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

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

    【讨论】:

    • 我只需要指出 vsync 确实会增加一些延迟,尤其是因为显卡驱动程序所做的所有废话。例如,nvidia 默认情况下会在 vsync 开启时进行 3 个预渲染帧,并且 60fps 已经是 50ms。所以我对游戏开发者的建议是始终给用户 3 个选项:1)垂直同步 2)无限 fps 3)手动 fps 限制
    • @Timo: 认真的吗?我从来没有遇到过这样的问题。在我的所有系统和所有显卡及其驱动程序上,启用 V 同步使我的程序以显示刷新频率运行。所以请提供一些支持信息。
    • @Timo,你是对的,我现在记得那个问题。当我启用垂直同步时,屏幕上有一个noticeable lag between my input and the response,所以我从那以后就禁用了它。但是,在我这里的特殊情况下,可以接受一点延迟(这不是游戏)-尽管我希望完全按照您所说的那样为用户提供三者之间的选择。 @datenwolf,您知道我是否可以使用 freeglut 设置交换间隔吗?如果没有,那我可能不得不去 SDL...
    • @datenwolf 是的,我不知道我在阅读这篇文章和写那篇评论时一定是在癫痫发作。要删除它大声笑。
    • 我只记得刷新可能会低至 24Hz,例如movieplayer 将 24Hz 电影播放到支持 24Hz 的显示器上,因此一切都保持在原始帧率,无需转换。我也曾经玩过一个可以产生 300H 的系统(与快门眼镜立体镜一起使用)。
    【解决方案5】:

    我建议使用亚毫秒精度的系统计时器(QueryPerformanceCounter、gettimeofday)来获取计时数据。这些也可以帮助您在优化的发布版本中分析性能。

    【讨论】:

    • 回想起来,这就是我试图暗示的。我知道使用垂直同步,但我最终的目标实际上是需要使用亚毫秒级的高性能计时器。事实上,我喜欢让我的应用程序独立于垂直同步频率运行,所以其他人可能想考虑以下问题:C++ Cross-Platform High-Resolution Timer
    • Vsync 很好,但根据我的经验,很难确保它正常工作。似乎有很多方法可以覆盖您的程序可能用来设置 Vsync 的任何 API 设置,但它会被忽视。新硬件上还支持一些花哨的新东西,称为帧率目标或其他类似名称,基本上是垂直同步,当低于刷新率时不会影响帧率。它非常很好,因为它很节能并且不会降低性能。也就是说,使用高精度计时器作为回退来限制帧速率是个好主意。
    【解决方案6】:

    一些背景资料:

    • SDL_Delay 与 Sleep/sleep/usleep/nanosleep 几乎相同,但参数限制为毫秒
    • 睡眠依靠系统线程调度程序来继续您的代码。
    • 根据您的操作系统和硬件,调度程序的滴答频率可能低于 1000hz,这会导致时间跨度比您在调用 sleep 时指定的时间跨度更长,so you have no guarantee to get the desired sleep time
    • 您可以尝试更改调度程序的频率。在 Windows 上,您可以通过调用 timeBeginPeriod() 来完成。对于 linux 系统,请查看this answer
    • 即使您的操作系统支持 1000hz 的调度程序频率,您的硬件也可能不支持,但大多数现代硬件支持。
    • 即使您的调度程序的频率为 1000hz,如果系统正忙于处理更高优先级的进程,睡眠可能需要更长的时间,但如果您的系统不是处于超高负载下,则不应发生这种情况。

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

    解决120FPS的舍入问题:

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

    您要么执行busy wait 333 微秒,然后休眠8 毫秒。这会花费一些 CPU 时间,但非常准确。 或者你遵循 Neop 方法,有时睡 8 毫秒,有时睡 9 毫秒,平均为 8,333 毫秒。哪种方式更有效但不太准确。

    【讨论】:

      猜你喜欢
      • 2013-02-04
      • 1970-01-01
      • 2020-05-13
      • 2019-09-24
      • 1970-01-01
      • 2021-08-30
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多