在Windows上使用OpenGL实现可靠的窗口式vsync?

dia*_*ler 6 c++ windows opengl rendering

摘要

似乎在Windows窗口模式下,与OpenGL的vsync中断了。我尝试了不同的API(SDL,glfw,SFML),但都得到了相同的结果:虽然帧频受到限制(根据我尝试过的多个60 Hz设置的CPU测量,始终保持16-17 ms左右),并且实际上,CPU大多数时候都处于睡眠状态,经常跳过帧。视机器和CPU使用率(而非渲染)而定,这可能与将帧速率有效降低一半一样糟糕。此问题似乎与驱动程序无关。

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

  • CPU大部分时间可以休眠
  • 不流泪
  • 没有跳过的帧(假设系统未过载)
  • CPU知道何时实际显示帧

细节/一些研究

当我用谷歌搜索opengl vsync stutteropengl vsync frame drop类似的查询时,我发现很多人都遇到了这个问题(或一个非常类似的问题),但是似乎并没有解决实际问题的连贯解决方案(在gamedev stackexchange上也有很多未充分回答的问题;也许多省力的论坛帖子)。

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

但是:我还测试了即使在非常弱的PC上,在Linux上也不会发生这种情况。因此,在技术上(至少通常),对于基于OpenGL的硬件加速,必须启用功能vsync而不跳过帧。

另外,在Windows(启用vsync)上使用D3D而不是OpenGL时,这也不是问题。因此,从技术上讲,必须能够在Windows上运行vsync(我尝试了新的,旧的和非常旧的驱动程序以及不同的(旧的和新的)硬件,尽管我可用的所有硬件设置都是Intel + NVidia,所以我不这样做。不知道AMD / ATI会发生什么。

最后,肯定有用于Windows的软件,包括游戏,多媒体应用程序,创意作品,3D建模/渲染程序等,它们使用OpenGL并在窗口模式下正常工作,同时仍可以准确地渲染,而无需在CPU上等待,并且没有掉帧。


我注意到,当使用传统的渲染循环

while (true)
{
    poll_all_events_in_event_queue();
    process_things();
    render();
}
Run Code Online (Sandbox Code Playgroud)

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;
}
Run Code Online (Sandbox Code Playgroud)

系统信息:

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

  • 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

我已经完成了一些其他测试:

我尝试添加explicit sleeps(通过SFML库,该库基本上只是Sleep从Windows API进行调用,同时确保它timeBeginPeriod最少)。

使用我的60 Hz设置时,理想情况下,一个帧应为16 2/3 Hz。根据QueryPerformanceCounter测量,我的系统在大多数情况下对于这些睡眠都是非常准确的。

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

添加16 ms的睡眠有时会导致一帧显示两次,有时会导致丢帧。在我看来,考虑到在17 ms时或多或少的结果与根本不睡觉的结果的随机组合,这是合理的。

添加15 ms的睡眠与完全不睡眠非常相似。片刻好,然后大约每丢掉第二帧。从1 ms到15 ms的所有值都适用。

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

我还在Linux上进行了更多测试。我之前并没有真正研究过-我只是验证了那里不存在丢帧问题,并且实际上CPU大部分时间都在休眠。我意识到,尽管有vsync的问题,我还是可以根据一些因素,在测试机上一致地进行撕裂。到目前为止,我还不知道该问题是否与原始问题有关,或者是否完全不同。

似乎更好的方法似乎是一些过时的变通方法和黑客,并完全放弃vsync并在应用程序中实施所有操作(因为显然在2017年,我们无法使用OpenGL获得最基本的帧渲染)。


更新20170816

我试图对一组开源3D引擎进行“逆向工程”(尤其是挂在obbg(https://github.com/nothings/obbg)上)。

首先,我检查了问题是否在那里没有发生。帧速率黄油平滑。然后,我用彩色的矩形添加了很好的旧的闪烁的紫色/黑色,并发现结巴确实很小。

我开始精疲力尽该程序的勇气,直到最终得到像我这样的简单程序。我发现obbg的渲染循环中有一些代码,这些代码被删除后会导致结结(即渲染游戏世界中obbg的主要部分)。另外,初始化中有一些代码在删除时也会引起结结(即,启用多重采样)。经过几个小时的摆弄后,OpenGL似乎需要一定的工作量才能正常运行,但是我还没有找到确切需要做的事情。也许渲染100万个随机三角形或其他方法。

我还感到欣慰的是,我现有的所有测试今天的表现都略有不同。看来我今天总体上比前几天更少,但随机分布的帧丢失更多。

我还创建了一个更好的演示项目,该项目更直接地使用OpenGL,并且由于obbg使用了SDL,因此我也切换到了该示例项目(尽管我简短地查看了库的实现,如果有所不同,这会让我感到惊讶,但是整个过程还是如此无论如何还是一个惊喜)。我想从基于obbg的一侧和空白项目的一侧都进入“工作”状态,因此我可以确定问题出在哪里。我只是将所有必需的SDL二进制文件放入了项目中;如果您拥有Visual Studio 2017,则应该没有其他依赖关系,并且应该立即进行构建。有许多#if控制所测试内容的。

https://github.com/bplu4t2f/sdl_test

在创建该对象的过程中,我还另外查看了SDL的D3D实现的行为。我之前已经测试过,但可能还不够广泛。仍然没有重复的帧,也没有帧丢失,这很好,但是在此测试程序中,我实现了更准确的时钟。

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

又一天,盯着闪烁的电脑屏幕。我不得不说,我真的没想到要花这么多的时间来渲染闪烁的背景,而我对此并不特别喜欢。