到目前为止,我们一直在使用推送常量将数据从 CPU 上传到 GPU。虽然推送常量很有用,但它们有很多限制。例如,你不能上传数组,你不能指向缓冲区,也不能将纹理与它们一起使用。要做到这一点,你需要使用描述符集,这是连接 CPU 数据到 GPU 的主要方式。
与其他图形 API 中使用的所有其他类似替代方案相比,描述符集的使用可能非常复杂。因此,我们将从非常简单的开始,仅将它们用于缓冲区,并在本章继续进行更多操作。描述符集的纹理方面将在第 5 章介绍。
心理模型
将单个描述符视为指向资源的句柄或指针。该资源是缓冲区或图像,并且还包含其他信息,例如缓冲区的大小,或者如果是图像,则包含采样器类型。 VkDescriptorSet
是绑定在一起的这些指针的集合。 Vulkan 不允许你在着色器中单独绑定资源。它们必须分组在集合中。如果你仍然坚持能够单独绑定它们,那么你将需要为每个资源设置一个描述符集。这是非常低效的,并且在许多硬件中都无法工作。如果你查看 https://vulkan.gpuinfo.org/displaydevicelimit.php?name=maxBoundDescriptorSets&platform=windows ,你将看到在 PC 上,某些设备仅允许最多 4 个描述符集绑定到给定的管线。因此,如果我们希望引擎在 Intel 集成 GPU 上运行,那么在我们的管线中实际上只能使用最多 4 个描述符集。处理 4 个描述符的限制的常见且高性能的方法是按绑定频率对它们进行分组。
描述符集编号 0 将用于引擎全局资源,并在每帧绑定一次。描述符集编号 1 将用于每个通道的资源,并在每个通道绑定一次。描述符集编号 2 将用于材质资源,编号 3 将用于每个对象资源。这样,内部渲染循环将仅绑定描述符集 2 和 3,并且性能将很高。
描述符分配
描述符集必须由引擎直接从 VkDescriptorPool
分配。描述符集分配通常在 GPU VRAM 的一部分中分配。分配描述符集后,你需要写入它以使其指向你的缓冲区/纹理。一旦你绑定了描述符集并在 vkCmdDraw()
函数中使用它,除非你指定 VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT
标志,否则你将无法再修改它。分配描述符池时,你必须告诉驱动程序你需要多少个描述符集以及你将使用多少个资源。常见的做法是默认使用一些较大的数字,例如 1000 个描述符,当描述符池空间不足时,分配新描述符将返回错误。然后你可以直接创建一个新池来容纳更多描述符。
如果你通过不设置 VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT
标志来显式禁止释放单个集合,则分配描述符集可能非常便宜。通过使用该标志,你是在告诉驱动程序你希望描述符能够单独释放。如果你是每帧分配描述符集,则不应使用该标志,然后你重置整个池而不是单个描述符集。对于你的全局描述符集,可以分配一次,然后从帧到帧重复使用它们。这是我们将在本教程中执行的操作,因为它也以更简单的代码结尾。
生产引擎中常用的技术是每帧拥有一组描述符池。一旦描述符分配失败,你将创建一个新池并将其添加到列表中。当帧被提交并且你已等待其栅栏时,你将重置所有这些描述符池。
写入描述符。
新分配的描述符集只是一小块 GPU 内存,你需要使其指向你的缓冲区。为此,你可以使用 vkUpdateDescriptorSets()
,它为描述符集指向的每个资源采用 VkWriteDescriptorSet
数组。如果你使用的是“绑定后更新”标志,则可以使用描述符集,并在命令缓冲区中绑定它们,并在提交命令缓冲区之前立即更新它。这主要是一个小众用例,并不常用。除非你使用该标志,否则你只能在首次绑定之前更新描述符集,在这种情况下,你只能在将命令缓冲区提交到队列之前更新它。当描述符集正在使用时,它是不可变的,尝试更新它将导致错误。验证层会捕获到这一点。为了能够再次更新描述符集,你需要等到命令执行完成。
绑定描述符
描述符集绑定到 Vulkan 管线上的特定“插槽”中。创建管线时,你必须为可以绑定到该管线的每个描述符集指定布局。这通常是自动完成的,从对着色器的反射生成。我们将手动完成它以展示它是如何完成的。一旦你在命令缓冲区中绑定了管线,该管线就具有用于不同描述符集的插槽,然后你可以将一个集合绑定到每个插槽中。如果描述符集与插槽不匹配,则会发生错误。如果你将描述符集绑定到插槽 0,然后通过绑定另一个管线来切换管线,则描述符集将保持绑定状态,如果新管线上的插槽相同。如果插槽不完全相同,则插槽将“未绑定”,你需要再次绑定它。例如,假设我们有 2 个管线,其中一个管线具有描述符集 0(绑定到缓冲区)和描述符集 1(绑定到 4 个图像)。然后,另一个管线具有描述符集 0(绑定到缓冲区,与另一个管线中的同一插槽相同),但在描述符集 1 中,它具有绑定到 3 个图像而不是 4 个图像的描述符集。如果你绑定第二个管线,则描述符集 0 将保持绑定状态,但描述符集 1 将未绑定,因为它不再匹配。这就是我们为描述符插槽分配频率以最大程度地减少绑定的原因。
描述符集布局。
VkDescriptorSetLayout
在管线和分配描述符时都使用,它是描述符的形状。例如,可能的布局将是绑定 2 个缓冲区和 1 个图像的布局。在创建管线或分配描述符集本身时,你都必须使用布局。在本教程中,我们将为所有内容重用布局对象,但这并非强制性的。即使在两个不同的位置创建,描述符集布局也可以兼容,只要它们相同即可。
统一缓冲区
描述符集指向缓冲区,但我们没有对此进行解释。现在我们正在创建保存顶点数据的 GPU 缓冲区,但你也可以创建保存任意数据的缓冲区以供你在着色器中使用。对于这种类型的数据,统一缓冲区是常见的选择。它们尺寸较小(最多几千字节),但读取速度非常快,因此非常适合着色器参数。通过创建统一缓冲区并从 CPU 写入它,你可以以比推送常量更有效的方式将数据发送到 GPU。我们将使用它来获取相机信息。可以有多个描述符集指向一个统一缓冲区,也可以有一个大的统一缓冲区,然后每个描述符集都指向缓冲区的一部分。着色器不会知道其中的区别。
下一步:设置描述符集