在 Windows 上使用 OpenGL 进行可靠的窗口同步?

Reliable windowed vsync with OpenGL on Windows?

提问人:dialer 提问时间:8/14/2017 最后编辑:dialer 更新时间:3/13/2021 访问量:4654

问:

总结

似乎在窗口模式下,与 OpenGL 的 vsync 在 Windows 上被破坏了。我尝试了不同的 API(SDL、glfw、SFML),结果都是一样的:虽然帧速率有限(根据我尝试过的多个 60 Hz 设置的 CPU 测量结果,帧率始终在 16-17 毫秒左右),而且 CPU 实际上大部分时间都处于休眠状态,但帧经常被跳过。根据机器和渲染以外的 CPU 使用率,这可能与有效地将帧速率降低一半一样糟糕。这个问题似乎与驱动程序无关。

如何在窗口模式下使用 OpenGL 在 Windows 上工作 vsync,或者这些属性的类似效果(如果我忘记了一些值得注意的事情,或者有什么不明智的事情,请发表评论):

  • CPU 大部分时间都可以休眠
  • 不撕裂
  • 无跳过帧(假设系统未过载)
  • CPU 知道帧何时实际显示

细节/一些研究

当我在谷歌上搜索或类似的查询时,我发现很多人都遇到了这个问题(或非常相似的问题),但似乎没有连贯的解决方案来解决实际问题(在游戏开发堆栈交换上也有许多没有得到充分回答的问题;还有许多不费吹灰之力的论坛帖子)。opengl vsync stutteropengl vsync frame drop

总结一下我的研究:似乎较新版本的 Windows 中使用的合成窗口管理器 (DWM) 强制了三重缓冲,并且干扰了 vsync。人们建议禁用 DWM、不使用 vsync 或全屏显示,所有这些都不能解决原始问题 (FOOTNOTE1)。我也没有找到详细的解释,为什么三重缓冲会导致 vsync 出现此问题,或者为什么在技术上无法解决问题。

但是:我还测试过,即使在非常弱的 PC 上,也不会在 Linux 上发生这种情况。因此,基于 OpenGL 的硬件加速在技术上必须是可行的(至少在一般情况下),才能在不跳过帧的情况下启用功能垂直同步。

此外,在 Windows 上使用 D3D 而不是 OpenGL(启用垂直同步)时,这不是问题。因此,在 Windows 上进行工作 vsync 在技术上一定是可行的(我已经尝试了新的、旧的和非常旧的驱动程序和不同的(旧的和新的)硬件,尽管我可用的所有硬件设置都是 Intel + NVidia,所以我不知道 AMD/ATI 会发生什么)。

最后,肯定有适用于 Windows 的软件,无论是游戏、多媒体应用程序、创意制作、3D 建模/渲染程序还是其他什么,它们都使用 OpenGL 并在窗口模式下正常工作,同时仍然准确渲染,无需忙于 CPU 等待,也不会掉帧。


我注意到,当有一个传统的渲染循环时,比如

while (true)
{
    poll_all_events_in_event_queue();
    process_things();
    render();
}

CPU 在该循环中必须执行的工作量会影响卡顿的行为。然而,这绝对不是 CPU 过载的问题,因为这个问题也发生在人们可以编写的最简单的程序之一中(见下文),并且发生在一个非常强大的系统上,它不做任何其他事情(该程序只不过是清除每帧上不同颜色的窗口, 然后显示它)。

我还注意到,它似乎永远不会比每隔一帧跳过一次更糟糕(即,在我的测试中,在 60 Hz 系统上,可见帧速率总是在 30 到 60 之间)。在运行在奇数帧和偶数帧上更改 2 种颜色之间的背景颜色的程序时,您可以观察到一些奈奎斯特采样定理违规,这让我相信某些东西没有正确同步(即 Windows 或其 OpenGL 实现中的软件错误)。同样,就CPU而言,帧率是坚如磐石的。此外,在我的测试中没有明显的影响。timeBeginPeriod


(FOOTNOTE1)但应该注意的是,由于 DWM,在窗口模式下不会发生撕裂(这是使用 vsync 的两个主要原因之一,另一个原因是使 CPU 尽可能长时间休眠而不会丢失任何帧)。因此,对于我来说,拥有一个在应用层实现 vsync 的解决方案是可以接受的。

但是,我认为唯一可能的方法是有一种方法可以显式(且准确地)等待页面翻转发生(可能会超时或取消),或者查询翻页时设置的非粘性标志(以一种不强制刷新整个异步渲染管道的方式, 例如),我也没有找到一种方法。glGetError


这里有一些代码,可以快速运行一个示例来演示这个问题(使用 SFML,我发现这是最不痛苦的工作)。

您应该会看到均匀的闪烁。如果你在不止一帧中看到相同的颜色(黑色或紫色),那就不好了。

(这会以显示器的刷新率闪烁屏幕,因此可能是癫痫警告):

// g++ TEST_TEST_TEST.cpp -lsfml-system -lsfml-window -lsfml-graphics -lGL

#include <SFML/System.hpp>
#include <SFML/Window.hpp>
#include <SFML/Graphics.hpp>
#include <SFML/OpenGL.hpp>
#include <iostream>

int main()
{
    // create the window
    sf::RenderWindow window(sf::VideoMode(800, 600), "OpenGL");
    window.setVerticalSyncEnabled(true);

    // activate the window
    window.setActive(true);

    int frame_counter = 0;

    sf::RectangleShape rect;
    rect.setSize(sf::Vector2f(10, 10));

    sf::Clock clock;

    while (true)
    {
        // handle events
        sf::Event event;
        while (window.pollEvent(event))
        {
            if (event.type == sf::Event::Closed)
            {
                return 0;
            }
        }

        ++frame_counter;

        if (frame_counter & 1)
        {
            glClearColor(0, 0, 0, 1);
        }
        else
        {
            glClearColor(60.0/255.0, 50.0/255.0, 75.0/255.0, 1);
        }

        // clear the buffers
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        // Enable this to display a column of rectangles on each frame
        // All colors (and positions) should pop up the same amount
        // This shows that apparently, 1 frame is skipped at most
#if 0
        int fc_mod = frame_counter % 8;
        int color_mod = fc_mod % 4;

        for (int i = 0; i < 30; ++i)
        {
            rect.setPosition(fc_mod * 20 + 10, i * 20 + 10);
            rect.setFillColor(
                sf::Color(
                (color_mod == 0 || color_mod == 3) ? 255 : 0,
                    (color_mod == 0 || color_mod == 2) ? 255 : 0,
                    (color_mod == 1) ? 155 : 0,
                    255
                )
            );
            window.draw(rect);
        }
#endif

        int elapsed_ms = clock.restart().asMilliseconds();
        // NOTE: These numbers are only valid for 60 Hz displays
        if (elapsed_ms > 17 || elapsed_ms < 15)
        {
            // Ideally you should NEVER see this message, but it does tend to stutter a bit for a second or so upon program startup - doesn't matter as long as it stops eventually
            std::cout << elapsed_ms << std::endl;
        }

        // end the current frame (internally swaps the front and back buffers)
        window.display();
    }

    return 0;
}

系统信息:

已在以下系统上验证此问题:

  • Windows 10 x64 i7-4790K + GeForce 970(已验证此处的 Linux 上不会出现问题)(单个 60 Hz 显示器)
  • Windows 7 x64 i5-2320 + GeForce 560(单台 60 Hz 显示器)
  • Windows 10 x64 Intel Core2 Duo T6400 + GeForce 9600M GT(已验证此处的 Linux 上不会出现问题)(单个 60 Hz 笔记本电脑显示器)
  • 另外 2 个人分别使用 Windows 10 x64 和 7 x64,都是“强大的游戏装备”,如有必要,可以请求规格

更新20170815

我做过一些额外的测试:

我尝试添加 s(通过 SFML 库,它基本上只是从 Windows API 调用,同时确保它是最小的)。explicit sleepSleeptimeBeginPeriod

使用我的 60 Hz 设置,理想情况下,A 帧应该是 16 2/3 Hz。根据测量结果,大多数时候,我的系统在这些睡眠中都非常准确。QueryPerformanceCounter

添加 17 毫秒的睡眠会导致我的渲染速度慢于刷新率。当我这样做时,有些帧会显示两次(这是预期的),但永远不会丢弃任何帧。对于更长的睡眠时间也是如此。

添加 16 毫秒的休眠有时会导致帧显示两次,有时会导致帧丢失。在我看来,这是合理的,考虑到 17 毫秒时的结果或多或少的随机组合,以及完全没有睡眠的结果。

添加 15 毫秒的睡眠与完全没有睡眠的行为非常相似。一小会儿没问题,然后大约每 2 帧就会掉落一次。对于从 1 毫秒到 15 毫秒的所有值也是如此。

这强化了我的理论,即问题可能只不过是 OpenGL 实现或操作系统中的 vsync 逻辑中的一些普通的旧并发错误。

我还在 Linux 上做了更多测试。我之前并没有真正研究过它 - 我只是验证那里不存在掉帧问题,事实上,CPU 大部分时间都处于睡眠状态。我意识到,根据几个因素,我可以在我的测试机器上持续发生撕裂,尽管有垂直同步。到目前为止,我不知道这个问题是否与原始问题有关,或者它是否完全不同。

似乎更好的方法是一些粗糙的解决方法和技巧,并完全放弃 vsync 并在应用程序中实现所有内容(因为显然在 2017 年,我们无法使用 OpenGL 获得最基本的帧渲染)。


更新20170816

我试图对一堆开源 3D 引擎进行“逆向工程”(特别是挂断了 obbg (https://github.com/nothings/obbg))。

首先,我检查了那里没有出现问题。帧速率非常流畅。然后,我用彩色矩形添加了我旧的闪烁紫色/黑色,发现口吃确实很小。

我开始扯掉程序的胆量,直到我最终得到一个像我这样的简单程序。我发现 obbg 的渲染循环中有一些代码,当删除时会导致严重的卡顿(即渲染 obbg 游戏世界的主要部分)。此外,初始化中有一些代码在删除时也会导致卡顿(即启用多重采样)。经过几个小时的摆弄,OpenGL似乎需要一定的工作量才能正常运行,但我还没有弄清楚到底需要做什么。也许渲染一百万个随机三角形或其他东西就可以了。

我还放心,我所有现有的测试今天的表现都略有不同。与前几天相比,我今天的掉帧似乎总体上更少,但分布更随机。

我还创建了一个更好的演示项目,它更直接地使用 OpenGL,并且由于 obbg 使用了 SDL,我也切换到了它(尽管我简要地查看了库实现,如果有差异会让我感到惊讶,但无论如何,这整个考验都是一个惊喜)。我想从基于 obbg 的端和空白项目端接近“工作”状态,这样我就可以真正确定问题出在哪里。我只是将所有必需的 SDL 二进制文件放入项目中;如果只要你有 Visual Studio 2017,就不应该有额外的依赖项,它应该立即生成。有许多 s 控制正在测试的内容。#if

https://github.com/bplu4t2f/sdl_test

在创建这个东西的过程中,我还仔细研究了 SDL 的 D3D 实现是如何表现的。我以前测试过这个,但可能还不够广泛。仍然没有重复的帧,也没有丢帧,这很好,但在这个测试程序中,我实现了一个更准确的时钟。

令我惊讶的是,我意识到,当使用 D3D 而不是 OpenGL 时,许多(但不是大多数)循环迭代需要 17.0 到 17.2 毫秒之间(我在以前的测试程序中不会发现这一点)。OpenGL 不会发生这种情况。OpenGL 渲染循环始终在 15.0 范围内。17.0. 如果确实有时候需要稍微长一点的等待时间(无论出于何种原因),那么 OpenGL 似乎错过了这一点。这可能是整个事情的根本原因吗?

又是盯着闪烁的电脑屏幕的一天。我不得不说,我真的没想到会花那么多时间渲染一个闪烁的背景,我不是特别喜欢这样。

C++ Windows OpenGL 渲染

评论

1赞 Nicol Bolas 8/14/2017
"CPU 知道帧何时实际显示“ OpenGL 没有这样做的机制。“最后,肯定有适用于 Windows 的软件,无论是游戏、多媒体应用程序、创意制作、3D 建模/渲染程序还是其他什么,它们都使用 OpenGL 并在窗口模式下正常工作,同时仍然准确渲染,无需忙于 CPU 等待,也不会掉帧。”内容创建应用程序通常不关心掉帧或撕裂。
1赞 Izaan 4/4/2018
您是否考虑过使用 DwmFlush 函数?我的理解是,这将阻止您的应用程序,直到 DWM 显示当前的复合帧。我的测试表明,这种技术并不完美,但它使我的渲染器更加流畅。
3赞 dialer 8/14/2018
经过更多调查,我发现了 vsynctester.com/firefoxisbroken.htmlbugs.chromium.org/p/chromium/issues/detail?id=467617。显然,这个人非常坚持告诉 chrome 和 firefox 开发人员如何正确地进行垂直同步。核心是功能。我在谷歌上几乎没有找到任何文档,也几乎没有任何结果,但它似乎是用于内核模式驱动程序开发(!!)。我拼接了一个头文件(因为没有WinDDK),它似乎完全缓解了这个问题。需要更多的测试。DwmFlushD3DKMTWaitForVerticalBlankEvent
1赞 dialer 11/8/2019
@TimKane 简而言之,我遇到了更多(不同的)问题(类似于此),并最终完全放弃了 OpenGL。但效果不佳。 是我测试中最可靠的函数,每次调用后立即调用D3DKMTWaitForVerticalBlankEventDwmFlushSwapBuffers
1赞 Redee 3/13/2021
伟大的研究!!!谢谢!这非常有趣,几天前我在测试我的游戏时,在带有 SFML + V-Sync 的旧笔记本电脑上遇到了同样的问题。控制台中的 SFML 没有错误,但 V-Sync 已禁用...

答: 暂无答案