【问题标题】:OpenGL VBO within multiple threads多线程内的 OpenGL VBO
【发布时间】:2012-01-18 15:44:38
【问题描述】:

我正在用 C++/OpenGL 开发一个程序,它可以绘制整个世界的地形。我有一个存储为瓷砖的海拔高度数据库。每次我启动程序时,都会加载一个图块。然后当人移动时,应该加载另一个图块,这不会每帧都发生,可能每 5 分钟一次。

我将初始图块加载到视频卡的内存中:

glBindBufferARB(GL_ARRAY_BUFFER_ARB, VertexBuffer[idx]);
glBufferDataARB(GL_ARRAY_BUFFER_ARB, VBOsz * 3 * sizeof(float), tile_data, GL_STATIC_DRAW_ARB);

...有法线、颜色和索引缓冲区

然后我画它们:

glBindBufferARB(GL_ARRAY_BUFFER_ARB, VertexBuffer[idx]);
glBufferDataARB(GL_ARRAY_BUFFER_ARB, VBOsz * 3 * sizeof(float), tile_data, GL_STATIC_DRAW_ARB);


glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_COLOR_ARRAY);
glEnableClientState(GL_NORMAL_ARRAY);

glBindBufferARB(GL_ARRAY_BUFFER_ARB, VertexBuffer[idx]);
glVertexPointer(3, GL_FLOAT, 0, BUFFER_OFFSET(0));

...

glBindBufferARB(GL_ELEMENT_ARRAY_BUFFER_ARB, IndexBuffer[idx]);
glDrawElements(GL_TRIANGLES, IndexBuffersz, GL_UNSIGNED_INT, BUFFER_OFFSET(0));

glDisableClientState(GL_NORMAL_ARRAY);
glDisableClientState(GL_COLOR_ARRAY);
glDisableClientState(GL_VERTEX_ARRAY);

由于我希望程序尽可能的流畅,我无法计算同一帧的顶点+颜色+法线+其他纹理,因为创建一个平铺大约需要20秒。

所以我决定创建一个加载器线程,它会检查何时需要加载新图块,然后加载它。当一切都完成后,它应该只交换 VBO(因此是 [idx]。

所以对于加载器线程,我知道我需要第二个 OpenGL 上下文,我创建了一个并在它们之间共享列表。这个想法是可行的,但是在加载器线程中,当我发送新的 VBO 数据时,我需要这个函数:wglMakeCurrent

只有当所有数据都加载完毕后,我才能为渲染线程(主程序的线程)设置上下文。这会导致在这段时间内没有绘制任何内容,从而使程序无用。

您对解决方案有任何想法吗?我需要改变概念吗?

我使用的是 OpenGL 2.1。升级到 OpenGL 3 会解决问题吗?

【问题讨论】:

  • 请停止使用缓冲区对象的 ARB 扩展形式。近十年来,它们一直是核心功能;没有任何借口了。

标签: multithreading opengl vbo


【解决方案1】:

这真的没那么复杂。

您只需创建两个缓冲区对象:一个用于当前渲染,另一个用于将来。当未使用的缓冲区充满数据时,渲染线程会从该缓冲区切换到渲染。先前使用的缓冲区变成未使用的缓冲区。

您可以通过以下两种方式之一上传数据。一种方法是让您的数据创建线程创建一个数据数组,渲染线程将使用glBufferData 将其上传到缓冲区对象。显然,这需要一些同步,但您需要的是同步代码:最终必须在数据准备好时通知您的渲染线程,以便它可以使用新数据进行渲染。

另一种方法是让您的渲染线程被告知需要开始生成数据。此时,它将映射未使用的缓冲区对象并将映射的指针传递给数据创建线程。该线程将直接在该映射指针中生成数据。完成后,它会通知渲染线程,渲染线程将取消映射缓冲区,然后使用数据进行渲染。

这两种方法都不需要多个上下文或通过 OpenGL 代码进行线程化。

请注意,不要使缓冲区越来越大,才能获得最佳性能。选择一个尺寸并坚持下去;您不想使用 glBufferData 调整缓冲区大小。

【讨论】:

  • 我尝试了这两种方法,但 glBufferData 至少需要 200 毫秒,映射/取消映射也需要 100 毫秒。在我的应用程序中,我不能拥有它,因为它会结结巴巴。缓冲区大小不会改变。
  • @Tibi:当您的数据实际上不是静态时,您是否在使用STATIC_DRAW 对OpenGL 实现撒谎?
  • 我不修改 VBO 数据,我使用的是双缓冲。所以我猜 STATIC_DRAW 没问题,对吧?
  • @Tibi:没有。STATIC_DRAW 表示您上传数据一次。上传后,缓冲区是固定不变的。再次调用glBufferData,就违反了这个约束。
  • 让我解释一下,我们有最初使用的缓冲区 0 和缓冲区 1。当代码决定需要加载其他数据时,它会将其加载到缓冲区 1 中,然后渲染从那个缓冲区。缓冲区 0 可以删除,下次加载线程计算重绘时,它会重新创建缓冲区。
【解决方案2】:

我已经有一个answer 用于此类任务。

简而言之,您创建了两个共享上下文。然后,正如 Damon 所建议的,使上下文在它们自己的线程上处于当前状态,仅在线程执行开始时一次。这两个上下文将在不同线程(一个线程,一个上下文)上同时处于当前状态。

那么,辅助线程不会用于渲染,而是用于加载资源(我的意思是,实际上是加载地形数据、纹理……并在每个数据上创建对应的OpenGL对象,例如纹理和缓冲区对象) .这发生在主要上下文正在绘制时。

本质上,您无需担心在应用程序周围带来指针并阻塞渲染线程以上传数据;但以创建另一个上下文为代价。让驱动程序同步上下文状态:如果它可以顺利执行该操作,您的应用程序将受益;至少,你的代码会更干净。

我对这个主题的其他贡献:

【讨论】:

  • 谢谢,这正是我设置它的方式:一个完成工作的辅助线程,共享上下文。对于某些应用程序,阻止渲染不是一种选择。
【解决方案3】:

您只需要在每个线程中调用wglMakeCurrent 一次。这工作可靠,这就是我正在做的事情(尽管使用 OpenGL 3.3)。这标志着一个上下文属于一个线程。它会一直保持这种状态,直到您以不同的方式告诉 OpenGL,所以在开始时执行一次并忘记(事实上,如果您使用它们在各自的线程中创建上下文,则根本不需要调用它,但无论如何都这样做只是为了100% 安全,而且我更喜欢在启动之前创建所有上下文,它不会那么混乱......)。

您不必担心函数指针,顺便说一句,只需使用您在渲染线程中使用的同一个指针(假设您已在此处正确初始化它)。
从技术上讲,使用来自另一个上下文的函数指针是无效的。然而,WGL 友好地保证(隐藏在小字体中!)对于具有相同像素格式的所有上下文,函数指针都是相同的。因此,您可以开始了。

使用单个上下文的替代方法是在渲染线程中 glMapBuffer 并将指针传递给工作线程。然后,在完成后(例如,发出信号量)glUnmapBuffer,再次在渲染线程中。
有些人更喜欢这个,因为它不涉及上下文杂耍,并且可能在一些旧的有问题的驱动程序上效果更好。我不喜欢它,因为需要额外的同步。口味问题,效果一样。

【讨论】:

  • 感谢您的帮助。添加 wglMakeCurrent 作为渲染线程的开头使其工作。然而,在我第一次尝试时,我的电脑死机了。我不确定确切的行,但它发生在加载程序线程中调用 wglMakeCurrent 时,或者紧随其后:glBindBuffer 和 glBufferData。现在它似乎工作正常。是不是因为编码不好而受到指责?
  • @Tibi:听起来更像是线程故障。您采取了哪些措施来确保您不会同时在两个线程上使用同一个对象?
  • @Nicol:在加载器线程中我有一个变量,只有在它完成工作后,我才将变量设置为 true。然后渲染线程执行缓冲区的实际“翻转”。加载数据后,加载线程不做任何 OpenGL 工作。
  • @Tibi:如果该变量不是正确的互斥锁或原子值或其他东西,那么这将失败。在开始尝试线程化图形工作之前,您需要完全了解线程化。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2023-04-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多