如何同步多线程的OpenGL缓冲区访问?

9
我有包含地形块网格的顶点缓冲区。每当玩家编辑地形时,相应块的网格必须重新生成并上传到顶点缓冲区。由于重新生成网格需要一些时间,我在异步工作线程中执行此操作。
问题在于主线程在同一时刻绘制缓冲区,而工作线程上传新数据。这意味着,在玩家编辑地形后,一个损坏的块会渲染一帧。它只会突然出现一次,之后就会正确地绘制缓冲区。
这对我来说有点合理,我们当然不应该同时写入和读取同一数据。因此,我创建了一个新的缓冲区,填充它并交换它们,而不是更新旧缓冲区。交换只是更改存储在地形块结构中的缓冲区ID,因此应该是原子的。但是,这并没有帮助到我。
由于OpenGL命令被发送到GPU上的队列中,它们不必在CPU上的应用程序继续执行时执行。因此,在新缓冲区实际准备好之前,我可能已经交换了缓冲区。
我还尝试了另一种切换缓冲区的替代方法,使用互斥锁来访问缓冲区。主线程在绘制时锁定互斥锁,而工作线程在上传新缓冲区数据时锁定它。然而,这也没有帮助,可能是由于OpenGL的异步性质。主线程实际上没有绘制,只是将绘制命令发送到GPU。另一方面,当真正只有一个命令队列时,上传缓冲区和绘制它们永远不会同时发生,对吗?
我该如何同步我的两个线程对顶点缓冲区的访问,以防止未定义的缓冲区被绘制一帧?

如果我正确理解了您的情况,我必须说...它不应该那样工作。缓冲区更新会隐式地进行同步,每次调用类似于glBufferSubData(...)的函数实际上是原子性的。如果实现正确,您不必担心部分写入会显示在其他线程中。话虽如此,在尝试以这种方式执行时,存在大量的开销-您是否考虑过循环缓冲区,您的工作线程写入其中(例如后缓冲区),而渲染线程从另一个缓冲区读取(例如前缓冲区)? - Andon M. Coleman
让我澄清一下出现的问题。是的,我正在使用两个上下文,每个线程一个。当我在问题中提到“缓冲区”时,我总是指顶点缓冲区,而不是绘制缓冲区。只有主线程在绘制。工作线程使用glBufferData来更改数据。 - danijar
@derhass:这是一个很好的观点,我关注的是倒数第二段。由于某种原因,这让我觉得他正在通过线程之间传递相同的上下文句柄来创建互斥锁。 - Andon M. Coleman
1
@danijar:glBufferData(...)不会更改数据,它只是分配一个全新的数据存储并填充数据。你所做的是通过调用它来进行缓冲区孤立。任何在管道中使用旧数据存储的挂起命令都不会受到影响。 - Andon M. Coleman
1
当涉及到两个独立的上下文时,是的。它们有独立的命令队列、状态机等。实际上,所有上下文共享所做的只是允许您在多个上下文中引用相同的对象。如果每个命令都从单个上下文发出,您将获得相同的同步保证。 - Andon M. Coleman
显示剩余6条评论
1个回答

6
在您的绘制线程中使用缓冲区之前,必须确保缓冲区更新已经完成。最简单的解决方法是在发出所有更新GL命令后,在更新线程中调用glFinish,并且只有在该函数返回后才通知绘制线程。
为了更精细地控制同步,我建议您查看“fence sync”对象(如GL_ARB_sync扩展中所述)。您可以在发出更新命令后发出“fence sync”,并将同步对象句柄与缓冲区句柄一起存储,以便绘制线程可以检查更新是否实际完成(或等待它)。请注意,同步对象有点特殊,因为它们是唯一一个不与GL上下文关联的对象,因此可以在多上下文设置中使用。

@danijar:是的,完全正确。 - derhass
@danijar:使用循环缓冲区,您将始终拥有一个完整的“前”缓冲区可供显示,而后台线程则在“后”缓冲区上工作。当后台线程完成后,您可以交换前后缓冲区。更新可能需要额外的一帧才能出现,但至少您不再会遇到部分更新出现的问题了。 - Andon M. Coleman
@AndonM.Coleman,我能否在工作线程上创建和加载新缓冲区后,在交换ID之前使用glFinish()?当每个上下文都有自己的管道时,这不应该在主线程的管道中创建一个空泡。 - danijar
4
你可以采用我在回答中概述的ARB_sync方法。你的工作线程将更新的缓冲区排队,绘制线程检查队列的头部是否已经完成更新。如果是,则可以使用它。如果没有完成,它可以继续使用旧缓冲区进行渲染,并在下一帧再次进行检查。 - derhass
我会先尝试让简单的方法起作用,如果需要再进行优化。非常感谢。 - danijar
显示剩余4条评论

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