本文主要是介绍【OpenGL经验谈03】关于缓冲区对象流,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
目录
- 说明
- 一、问题
- 二、显式多重缓冲
- 三、缓冲区重新指定
- 四、缓冲区更新
- 五、持久映射流
- 5.1、持续可见性
- 六、流媒体优化
说明
缓冲区对象流是在使用这些缓冲区时用新数据频繁更新缓冲区对象的过程。流媒体的工作原理如下。您对缓冲区对象进行修改,然后执行从缓冲区读取的 OpenGL 操作。然后,在调用该 OpenGL 操作后,您可以使用新数据修改缓冲区对象。接下来,您执行另一个 OpenGL 操作以从缓冲区中读取数据。
流式传输是一个修改/使用周期。在一个修改/使用周期与另一个修改/使用周期之间可能存在交换缓冲区(或等效的帧改变过程),但不是必须的。
一、问题
OpenGL 提供了使该过程正常运行的所有保证,但使其快速运行才是真正的问题。流式传输中最大的危险,也是导致最多问题的危险,是隐式同步。
OpenGL 规范允许延迟绘图命令的执行。这允许您绘制很多东西,然后让 OpenGL 自行处理。因此,在调用使用缓冲区对象的任何操作之后,完全有可能开始尝试将新数据上传到该缓冲区。如果发生这种情况,OpenGL 规范要求线程停止,直到所有可能受缓冲区对象更新影响的绘图命令完成。
这种隐式同步是流式传输顶点数据时的主要敌人。
有多种策略可以解决这个问题。某些实现对于某些实现比其他实现效果更好。每一种都有其优点和缺点。
当使用非不可变缓冲区时,您应该确保STREAM在缓冲区的提示中。
二、显式多重缓冲
这个解决方案相当简单。您只需创建两个或多个相同长度的缓冲区对象即可。当您使用一个缓冲区对象时,您可以修改另一个缓冲区对象。根据您的实现可以提供多少并行性,您可能需要两个以上的缓冲区才能完成这项工作。
该解决方案的主要缺点是它需要使用许多不同的缓冲区对象(单独的缓冲区句柄)。因此,您需要更改每帧用于 GPU 操作的缓冲区。
三、缓冲区重新指定
此解决方案是在开始修改缓冲区对象之前重新分配它。这被称为缓冲区“孤立”。有两种方法可以做到这一点。
第一种方法是使用NULL指针调用glBufferData,其大小和使用提示与之前完全相同。这允许实现简单地在后台为该缓冲区对象重新分配存储。由于分配存储(可能)比隐式同步更快,因此与同步相比,您可以获得显着的性能优势。由于您传递了 NULL,如果一开始就不需要同步,则可以将其简化为无操作。之前发送的 OpenGL 命令仍将使用旧存储。如果您继续一遍又一遍地使用相同大小,则 GL 驱动程序可能根本不会执行任何分配,而只是从未使用的缓冲区队列中拉出旧的空闲块并使用它(尽管当然这不能保证),所以它可能非常有效。
将glMapBufferRange与GL_MAP_INVALIDATE_BUFFER_BIT一起使用时,您可以执行相同的操作。您还可以使用glInvalidateBufferData(如果可用)。
所有这些都使 GL 实现能够自由地孤立以前的存储并分配新的存储。这就是为什么这被称为“孤儿”。
每当您看到其中任何一个时,请将其视为 OpenGL 的指令:1) 分离旧的存储块,2) 为您提供一个新的存储块来使用,所有这些都在同一个缓冲区句柄后面。旧的存储块将被 OpenGL 放入空闲列表中,并在队列中没有可能引用它的绘制命令时重新使用(例如,一旦所有排队的 GL 命令都已完成执行)。
显然,这些方法将缓冲存储与客户端可访问的工作空间分离,因此仅当不再需要从 GL 客户端读取或更新此特定存储块时,它们才实用。除非您计划将缓冲区更新与此技术结合使用,否则最好是在整个缓冲区而不是缓冲区的一部分上进行更新,并且每次都覆盖该缓冲区中的所有数据。
此方法的一个问题是它依赖于实现。仅仅因为一个实现可以自由地做某事并不意味着它会这样做。
四、缓冲区更新
缓冲区更新是一种流式传输形式,您需要非常小心。它通常与缓冲区重新指定结合使用,以提高提交性能。
为了实现缓冲区更新,我们使用GL_MAP_UNSYNCHRONIZED_BIT调用glMapBufferRange。这告诉 OpenGL 不要进行任何隐式同步。当你看到这个时,想一想“OpenGL,请给我一个‘快速’的缓冲区。如果你给我这个缓冲区对象与上次相同的缓冲区,那就没问题了。我保证不会修改这个缓冲区的任何可能被修改的部分。正在被我已经提交的 GL 命令使用。请相信我。”
虽然没有同步,但这并不意味着同步不重要。事实上,如果您修改 GPU 上已排队的 GL 命令(例如绘图命令)将从中读取的部分缓冲区,您将得到未定义的结果。不要那样做。
使用缓冲区更新的基本用例是,您可以使用未同步的映射逐步填充缓冲区对象、写入、取消映射、使用该缓冲区子区域发出 GL 命令、冲洗/重复。只要您的写入永远不会重叠,那么您就是安全的,并且在填满该缓冲区之前不需要考虑“弄乱 GPU 的数据”。填满后,您可以执行以下两种操作之一来继续避免破坏 GPU 的缓冲区数据:1) orphan或 2) Synchronize。孤儿是首选方法,因为避免同步通常会产生更高的性能(因为同步通常涉及等待)。
对于orphan,只需使用缓冲区重新指定技术(glBufferData(NULL)、glMapBufferRange(GL_MAP_INVALIDATE_BUFFER_BIT)或glInvalidateBufferData)。然后,您将在缓冲区句柄下方获得一个新的存储块,可以在其上进行书写,其他 GL 命令无法引用该存储块,因此不需要同步。
或者,要同步,请使用同步对象。如果您在从缓冲区读取的所有命令后面放置一个栅栏,则可以在映射缓冲区之前检查该栅栏是否已完成。如果没有,那么您可以等待更新缓冲区,同时执行一些其他重要任务。如果没有其他任务要执行,您还可以使用栅栏强制同步。一旦栅栏完成,您就可以使用 GL_MAP_UNSYNCHRONIZED_BIT 自由映射缓冲区,以防实现不知道缓冲区可以更新。
有关一般缓冲流的更多详细信息,请参阅 此线程。请特别注意 Rob Barris 的帖子。
五、持久映射流
鉴于 OpenGL 4.4或ARB_buffer_storage的可用性,使用缓冲区的持久映射成为可能。
这里的想法是分配一个不可变的缓冲区,其大小是所需大小的 2-3 倍,当您从缓冲区的一个区域执行操作时,同时写入另一区域。先前的映射方案之间的区别在于您不经常映射和取消映射缓冲区。当您创建缓冲区时,您可以持久地映射它,并保持它的映射状态,直到删除缓冲区为止。
这需要将glBufferStorage与GL_MAP_WRITE和GL_PERSISTENT_BIT一起使用。它还需要在映射时使用具有相同位的 glMapBufferRange 。
一般算法如下。缓冲区在逻辑上分为 3 个部分:您正在写入的部分和当前可能正在使用的两个部分。
第一步是写入缓冲区的第 1 部分。一旦完成写入,您必须通过刷新来使该范围的数据对 OpenGL 可见(如果您没有连贯地映射)。然后,您需要采取任何措施来确保该数据对 OpenGL 可见。一旦数据可见,您就可以发出一些从缓冲区的该部分读取的渲染命令。发出从缓冲区读取的所有命令后,您将创建一个栅栏同步对象。
下一帧,您开始写入缓冲区第 2 部分。您执行上述所有操作,并创建一个新的栅栏同步对象。将每个缓冲区部分的同步对象分开。
您对下一帧的缓冲区第 3 部分执行相同的操作。
在第四帧上,您想再次开始使用第 1 部分。但是,您需要先检查第 1 部分的同步对象,看看它是否已完成,然后才能开始。仅当该部分的同步对象已完成时,您才能开始写入该部分。
5.1、持续可见性
写入持久映射缓冲区并不能保证 OpenGL 自动看到写入的数据。为了确保可见性,您必须执行以下三件事之一。
使用GL_COHERENT_BIT一致地映射缓冲区。这还需要使用GL_COHERENT_BIT分配缓冲区。一致映射的缓冲区始终确保后续操作的可见性(但这并不意味着您可以写入当前正在读取的内容。您仍然需要同步)。虽然这听起来可能很慢,但有一些证据表明性能成本可以忽略不计,至少在某些硬件上是这样。
使用GL_MAP_FLUSH_EXPLICIT_BIT映射缓冲区,并在缓冲区的写入部分上调用glFlushMappedBufferRange 。
六、流媒体优化
glMapBufferRange还有另一个您应该了解的标志: GL_MAP_INVALIDATE_RANGE_BIT。这与上面已经介绍过的GL_MAP_INVALIDATE_BUFFER_BIT不同。
根据 Rob Barris 的说法,MAP_INVALIDATE_RANGE_BIT与WRITE位(但不是READ位)相结合基本上向驱动程序表明它不需要包含任何有效的缓冲区数据,并且您承诺写入映射的整个范围。这可以让驱动程序为您提供一个指向尚未初始化的暂存内存的指针。例如,驱动程序分配了直写式未缓存内存。请参阅这篇文章了解更多详细信息。
这篇关于【OpenGL经验谈03】关于缓冲区对象流的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!