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

概要

在窗口模式下,似乎在OpenGL上的vsync被打破了。 我已经尝试了不同的API(SDL,glfw,SFML),所有的结果都一样:虽然帧率有限( 根据我试过的多个60Hz设置的CPU测量 ,一直在16-17毫秒左右) CPU实际上大部分时间都在睡觉,帧经常被跳过。 根据机器和渲染以外的CPU使用情况,这可能与将帧速率有效降低一半相当。 这个问题似乎不是驱动程序相关的。

如何在窗口中使用OpenGL在窗口模式下工作vsync,或者使用这些属性的类似效果(如果我忘记了某些值得注意的东西,或者某些东西不明智,请评论):

  • CPU大部分时间都可以睡觉
  • 不撕裂
  • 没有跳帧(假设系统没有过载)
  • CPU知道帧何时实际显示

细节/一些研究

当我googled的opengl vsync stutteropengl vsync frame drop或类似的查询,我发现很多人都有这个问题(或非常相似的一个),但似乎没有连贯的解决scheme的实际问题(许多回答不充分的问题gamedev stackexchange;也是很多低效的论坛post)。

总结我的研究:似乎在较新版本的Windows中使用的合成窗口pipe理器(DWM)强制三重缓冲,并干扰vsync。 人们build议禁用DWM,不使用vsync或全屏,所有这些都不是对原始问题(FOOTNOTE1)的解决scheme。 我也没有find一个详细的解释,为什么三重缓冲导致这个问题vsync,或为什么它在技术上是不可能解决这个问题。

但是,我也testing过,这在Linux上不会发生,即使在非常微弱的PC上也是如此。 因此,基于OpenGL的硬件加速必须在技术上可行(至less在一般情况下)才能在不跳过帧的情况下启用function性vsync。

另外,在Windows上使用D3D而不是OpenGL时,这不是问题(启用了vsync)。 因此,在Windows上运行vsync必须在技术上是可行的(尽pipe所有硬件设置都是英特尔+ NVidia,但我尝试过使用新旧,旧的驱动程序和不同的(旧的和新的)硬件,不知道AMD / ATI会发生什么)。

最后,肯定必须有用于Windows的软件,无论是游戏,多媒体应用程序,创意制作,3Dbuild模/渲染程序,还是其他任何使用OpenGL,并在窗口模式下正常工作,同时仍然准确渲染而不需要CPU等待的软件,而且没有丢帧。


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

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

CPU在该循环中所做的工作量会影响到口吃的行为。 然而,这绝对不是CPU超载的问题,因为问题也发生在一个可以编写的最简单的程序之一(见下文)以及一个非常强大的系统上,除了在每一帧上用不同的颜色清除窗口,然后显示它)。

我也注意到,它似乎没有跳过每一个其他帧(比如,在我的testing中,60Hz系统中的可见帧率总是介于30到60之间)。 当运行改变奇数帧和偶数帧上的2种颜色之间的背景颜色的程序时,你可以观察到奈奎斯特采样定理违反的某种程度,这使我相信某些东西不能正确同步(即Windows或其OpenGL实现中的软件错误)。 再一次, 就CPU而言 ,帧速率是坚如磐石的。 另外, timeBeginPeriod在我的testing中没有明显的影响。


(FOOTNOTE1)需要注意的是,由于DWM的原因,在窗口模式下不会出现撕裂现象(这是使用vsync的两个主要原因之一,另一个原因是让CPU尽可能长时间地hibernate)没有错过一个框架)。 所以我可以接受一个在应用层中实现vsync的解决scheme。

然而,我认为有可能的唯一方法是有一种方法可以显式地(准确地)等待页面翻转发生(有可能超时或取消),或者查询一个非粘性标志,当页面翻转(以不强制刷新整个asynchronous渲染pipe道的方式,例如glGetError ),我还没有find任何方法。


这里有一些代码来获得一个快速的示例运行,演示了这个问题(使用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; } 

系统信息:

在这些系统上validation了这个问题:

  • Windows 10 x64 i7-4790K + GeForce 970(validation问题不会发生在Linux上)(单个60 Hz显示器)
  • Windows 7 x64 i5-2320 + GeForce 560(单个60 Hz显示器)
  • Windows 10 x64英特尔酷睿双核T6400 + GeForce 9600M GT(在Linux上validation问题不会发生在这里)(单个60赫兹笔记本电脑显示屏)
  • 而另外两个分别使用Windows 10 x64和7 x64的人,都是“结实的游戏平台”,如果有必要,可以要求提供规范

更新20170815

我做了一些额外的testing:

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

用我的60赫兹设置,理想情况下帧应该是16 2/3赫兹。 根据QueryPerformanceCounter测量结果,我的系统在大多数情况下对于这些睡眠非常准确。

添加一个17毫秒的睡眠使我呈现比刷新率慢。 当我这样做时,一些帧会显示两次(这是预期的),但没有帧被丢弃。 对于更长时间的睡眠也是如此。

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

增加一个15毫秒的睡眠行为非常类似于根本没有睡眠。 很短的时间就好了,然后每个第二帧都被丢弃。 对于1 ms到15 ms的所有值也是如此。

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

我也在Linux上做了更多的testing。 我之前并没有真正考虑过这个问题 – 我只是证实了这个问题在那里并不存在,事实上,大部分时间CPU都处于睡眠状态。 我意识到,取决于几个因素,尽pipevsync,我可以使我的testing机器一直发生撕裂。 到目前为止,我不知道这个问题是否与原来的问题有关,还是完全不同的问题。

看起来更好的方法将是一些粗糙的解决方法和黑客,完全抛弃vsync并在应用程序中实现一切(因为显然在2017年,我们无法使用OpenGL获得最基本的框架渲染)。


更新20170816

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

首先,我检查了那里没有发生问题。 帧频是黄油光滑的。 然后,我用紫色加上我那古老闪烁的紫色/黑色,看到口吃确实很less。

我开始删除程序的内容,直到我结束了像我这样简单的程序。 我发现在obbg的渲染循环中有一些代码,当被移除时,会导致严重的结尾(即渲染obbg游戏世界的主要部分)。 此外,初始化中还有一些代码在移除时也会造成口吃(即启用多重采样)。 经过几个小时的摆弄之后,似乎OpenGL需要一定的工作量才能正常工作,但是我还没有find究竟需要做什么。 也许渲染一百万个随机三angular形或其他东西会做。

我也重新调整了一下,现在的所有testing在今天的performance都略有不同。 现在看来,我的整体数据量比前几天less,但随机分布的数量更多。

我还创build了一个更直接使用OpenGL的更好的演示项目,而且由于obbg使用了SDL,所以我也转而使用了这个(尽pipe我简单地看了一下库的实现,如果有区别的话,无论如何是一个惊喜)。 我想从基于obbg的方面和空白的项目方面来看待“工作”状态,所以我可以确定问题是什么。 我只是将所有必需的SDL二进制文件放在项目中; 如果只要你有Visual Studio 2017,就不应该有额外的依赖关系,它应该马上build立。 有很多#if可以控制正在testing的内容。

https://github.com/bplu4t2f/sdl_test

在创build这件事情的过程中,我又看了一下SDL的D3D实现的行为。 我以前testing过,但可能不够广泛。 仍然没有重复的帧,也没有帧丢失,这是好的,但在这个testing程序中,我实现了一个更精确的时钟。

令我惊讶的是,我意识到,当使用D3D而不是OpenGL时,许多(但不是大多数)循环迭代需要17.0到17.2 ms之间(我不会用我以前的testing程序捕捉到)。 这在OpenGL中不会发生。 OpenGL渲染循环始终在15.0 … 17.0范围内。 如果确实有时需要为垂直空白(无论什么原因)需要稍微长一点的等待时间,那么OpenGL似乎就错过了。 这可能是整个事情的根源呢?

还有一天,我们正盯着闪烁的电脑屏幕。 我不得不说,我真的没有想过要花这么多时间来渲染一些闪烁的背景,我不是特别喜欢这个。