提问人:dialer 提问时间:8/14/2017 最后编辑:dialer 更新时间:3/13/2021 访问量:4654
在 Windows 上使用 OpenGL 进行可靠的窗口同步?
Reliable windowed vsync with OpenGL on Windows?
问:
总结
似乎在窗口模式下,与 OpenGL 的 vsync 在 Windows 上被破坏了。我尝试了不同的 API(SDL、glfw、SFML),结果都是一样的:虽然帧速率有限(根据我尝试过的多个 60 Hz 设置的 CPU 测量结果,帧率始终在 16-17 毫秒左右),而且 CPU 实际上大部分时间都处于休眠状态,但帧经常被跳过。根据机器和渲染以外的 CPU 使用率,这可能与有效地将帧速率降低一半一样糟糕。这个问题似乎与驱动程序无关。
如何在窗口模式下使用 OpenGL 在 Windows 上工作 vsync,或者这些属性的类似效果(如果我忘记了一些值得注意的事情,或者有什么不明智的事情,请发表评论):
- CPU 大部分时间都可以休眠
- 不撕裂
- 无跳过帧(假设系统未过载)
- CPU 知道帧何时实际显示
细节/一些研究
当我在谷歌上搜索或类似的查询时,我发现很多人都遇到了这个问题(或非常相似的问题),但似乎没有连贯的解决方案来解决实际问题(在游戏开发堆栈交换上也有许多没有得到充分回答的问题;还有许多不费吹灰之力的论坛帖子)。opengl vsync stutter
opengl 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 sleep
Sleep
timeBeginPeriod
使用我的 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 似乎错过了这一点。这可能是整个事情的根本原因吗?
又是盯着闪烁的电脑屏幕的一天。我不得不说,我真的没想到会花那么多时间渲染一个闪烁的背景,我不是特别喜欢这样。
答: 暂无答案
评论
DwmFlush
D3DKMTWaitForVerticalBlankEvent
D3DKMTWaitForVerticalBlankEvent
DwmFlush
SwapBuffers