多个OpenGL上下文、多个窗口、多线程和垂直同步

25
我正在使用OpenGL创建一个图形用户界面应用程序,其中可以有任意数量的窗口 - “多文档接口”风格。
如果只有一个窗口,则主循环可能如下所示:
1. 处理事件 2. 绘制() 3. 交换缓冲区(垂直同步会导致此操作阻塞,直到垂直监视器刷新)
然而,考虑当有3个窗口时的主循环:
1. 每个窗口处理事件 2. 每个窗口绘制() 3. 窗口1交换缓冲区(阻塞直到垂直同步) 4. (稍后)窗口2交换缓冲区(阻塞直到垂直同步) 5. (稍后)窗口3交换缓冲区(阻塞直到垂直同步)
糟糕……现在应用程序的渲染每帧发生的次数只有正常帧率的1/3。
解决方法:实用程序窗口
一种解决方法是只有一个窗口开启垂直同步,其余窗口关闭垂直同步。先调用vsync窗口的swapBuffers()并绘制它,然后绘制其余窗口并在每个窗口上调用swapBuffers()。
这种解决方法大部分时间看起来都很好,但并非没有问题:
  • 有一个窗口特殊是不优雅的
  • 竞态条件仍可能导致屏幕撕裂
  • 某些平台忽略垂直同步设置并强制其开启
  • 我读到切换绑定的OpenGL上下文是一项昂贵的操作,应该避免使用。

解决方法:每个窗口一个线程

由于每个线程只能绑定一个OpenGL上下文,因此也许答案是每个窗口一个线程。

然而,我仍希望GUI是单线程的,因此3个窗口情况下的主循环如下:

(对于每个窗口)

  1. 锁定全局互斥量
  2. 处理事件
  3. 绘制()
  4. 解锁全局互斥量
  5. 交换缓冲区()

这样行吗?这个其他问题表明它不会:

原来这些窗口正在互相“斗争”:看起来SwapBuffers调用是同步的,即使它们在不同的线程中。我正在测量每个窗口的帧间时间,对于两个窗口,这会降至30fps,三个窗口则为20fps等等。
为了调查这一说法,我创建了一个简单的测试程序(simple test program)。该程序创建N个窗口和N个线程,将每个窗口绑定到一个线程,请求每个窗口打开垂直同步,并报告帧速率。到目前为止,结果如下:
  • Linux,X11,4.4.0 NVIDIA 346.47(2015-04-13)
    • 无论打开多少个窗口,帧速率都为60fps。
  • OSX 10.9.5(2015-04-13)
    • 帧速率没有上限;SwapBuffers不会阻塞。

解决方法:只使用一个上下文,一个大的帧缓冲区

另一个我想到的想法是:只有一个OpenGL上下文和一个大的帧缓冲区,大小等于所有窗口的大小之和。
每一帧,每个窗口在绘制前调用glViewport来设置它们各自的帧缓冲矩形。
在所有绘制完成后,对唯一的OpenGL上下文进行swapBuffers()操作。
我即将调查这个解决方法是否可行。我有一些问题:
  • 拥有如此大的帧缓冲区是否可行?
  • 每帧多次调用glViewport是否可行?
  • 我使用的窗口库API是否允许我创建独立于窗口的OpenGL上下文?
  • 如果窗口大小都不同,帧缓冲区会浪费空间吗?
GLFW的维护者Camilla Berglund说:

glViewport的工作原理并非如此。缓冲区交换也不是这样工作的。每个窗口都有一个帧缓冲区,您无法使它们共享一个帧缓冲区。缓冲区交换是针对每个窗口的帧缓冲区进行的,并且上下文一次只能绑定到单个窗口。这是在操作系统级别上的限制,而不是GLFW的限制。

解决方法:仅使用一个上下文

这个问题 表明这个算法可能有效:

Activate OpenGL context on window 1  
Draw scene in to window 1

Activate OpenGL context on window 2  
Draw scene in to window 2

Activate OpenGL context on window 3  
Draw scene in to window 3

For all Windows
SwapBuffers

根据问题提出者的说法,启用V-Sync后,SwapBuffers将会同步到最慢的显示器上,而在更快的显示器上的窗口会变慢。看起来他们只在Microsoft Windows上进行了测试,而且不清楚这个解决方案是否适用于其他平台。此外,再次有许多来源告诉我,在draw()例程中使用makeContextCurrent()速度太慢。另外,这似乎也不符合EGL的规范。为了允许另一个线程调用eglSwapBuffers(),您必须使用eglMakeCurrent(NULL),这意味着您的eglSwapBuffers现在应该返回EGL_BAD_CONTEXT。所以,我的问题是:有没有最佳方法来解决带有垂直同步的多窗口应用程序的问题?这似乎是一个常见的问题,但我还没有找到令人满意的解决方案。
与此问题类似:如何同步多个OpenGL窗口到垂直同步?,但我想要一个跨平台的解决方案 - 或者至少为每个平台提供一个解决方案。
以及这个问题:如何在多个OpenGL画布和垂直同步中使用SwapBuffers()?,但实际上这个问题与Python无关。

所以您的意思是,根据您的实验,每个线程(和窗口)一个上下文效果良好? - dudu
2个回答

15

交换缓冲区(垂直同步会导致此操作阻塞,直到垂直监视器刷新)

不,它并不会阻塞。缓冲区交换调用可能立即返回并且不会阻塞。但它会插入一个同步点,以延迟修改后缓冲区命令的执行,直到缓冲区交换完成。OpenGL 命令队列长度是有限的。因此,一旦命令队列已满,其余的 OpenGL 调用将会阻塞程序,直到进一步的命令可以被推入队列中。

而且缓冲区交换不是一个 OpenGL 操作。它是一个图形/窗口系统级别的操作,独立于 OpenGL 上下文发生。只需看一下缓冲区交换函数:它们接受的唯一参数是可绘制对象的句柄(=窗口)。事实上,即使您在单个可绘制对象上有多个 OpenGL 上下文进行操作,您也只需要交换一次缓冲区,并且可以在可绘制对象上没有当前的 OpenGL 上下文的情况下执行此操作。

因此,通常的方法是:

' first do all the drawing operations
foreach w in windows:
    foreach ctx in w.contexts:
        ctx.make_current(w)
        do_opengl_stuff()
        glFlush()

' with all the drawing commands issued
' loop over all the windows and issue
' the buffer swaps.
foreach w in windows:
    w.swap_buffers()

由于缓冲区交换不会阻塞,您可以为所有窗口发出所有缓冲区交换,而不会受到V-Sync的延迟。但是,下一个针对发出用于交换的后备缓冲区的OpenGL绘图命令可能会停顿。

解决方法是使用一个FBO进行实际绘制,并将其与循环结合起来,在交换缓冲区循环之前对FBO进行传输到后备缓冲区:

' first do all the drawing operations
foreach w in windows:
    foreach ctx in w.contexts:
        ctx.make_current(w)
        glBindFramebuffer(GL_DRAW_BUFFER, ctx.master_fbo)
        do_opengl_stuff()
        glFlush()

' blit the FBOs' renderbuffers to the main back buffer
foreach w in windows:
    foreach ctx in w.contexts:
        ctx.make_current(w)
        glBindFramebuffer(GL_DRAW_BUFFER, 0)
        blit_renderbuffer_to_backbuffer(ctx.master_renderbuffer)
        glFlush()

' with all the drawing commands issued
' loop over all the windows and issue
' the buffer swaps.
foreach w in windows:
    w.swap_buffers()

非常感谢您提供这个非常有启发性的答案。相关问题:IRC上的人们一直告诉我ctx.make_current(w)非常慢,因此我应该为每个上下文使用一个线程。这真的是个问题吗? - andrewrk
我用这个来测试你的伪代码:https://github.com/andrewrk/opengl-multi-window-test在OSX和Windows上,结果如预期。在Linux上使用NVIDIA 346.47驱动程序时,我的得分是 60 / window_count fps。 - andrewrk
1
根据在IRC上为NVIDIA工作的某人所说,规范不要求swap_buffers()是异步的,而获得所需行为的最可靠方法是每个窗口有一个线程。 - andrewrk
1
@andrewrk:实际上,规范可以有两种解释。确切的措辞是:“客户端必须使用GLX范围之外的某些手段同步执行交换和渲染的线程,以确保每个新帧在可见之前完全呈现”,以及“随后的OpenGL命令可以立即发出,但在缓冲区交换完成之前不会被执行,通常在显示器垂直消隐期间完成”。除此之外,GLX规范没有其他说明。NVIDIA以有利于其实现的方式解释OpenGL规范。 - datenwolf
1
@Maypeur:请给我展示一下明确说明这一点的规范。相信我,当我发现规范对SwapBuffers的时间行为没有明确的陈述时,我也感到惊讶。我已经进行了上面的测量……不是用软件计时器,而是用示波器探测V-Sync信号并通过单个I/O指令在SwapBuffer代码中切换GPIO引脚(可以允许用户空间程序直接对特定地址进行直接I/O写入,请参见ioperm(2))。 - datenwolf
显示剩余4条评论

0

感谢@andrewrk的所有研究,我个人喜欢这样做:

创建第一个窗口和它的OpenGL上下文,使用双缓冲。 在此窗口上启用垂直同步(swapinterval 1)

创建其他窗口并附加第一个上下文与双缓冲。 在这些其他窗口上禁用垂直同步(swapinterval 0)

对于每一帧 对于反转每个窗口(最后一个启用垂直同步的窗口)。 wglMakeCurrent(hdc,commonContext);
绘制。 SwapBuffer

通过这种方式,我实现了垂直同步,并且所有窗口都基于相同的垂直同步。

但是我在没有Aero的情况下遇到了问题:撕裂...


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接