Descriptor
Descriptor 概念
Descriptor(描述符)在现代图形API中扮演着至关重要的角色。它是一个抽象的概念,用于表示和管理一个着色器资源,如缓冲区(Buffers)、纹理(Textures)、采样器(Samplers)等。
每个Descriptor包含了指向实际资源的引用,以及如何使用这些资源的信息。可以认为Descriptor为一个句柄(Handle)或者一个指向这些资源的指针。这种设计允许GPU高效地处理资源,因为GPU可以直接通过Descriptor来定位和使用这些资源。
Descriptor Set(描述符集)是一组Descriptor的集合,它们在渲染命令的记录过程中被绑定。这样,当执行Draw Call(绘制调用)时,GPU就可以直接使用这些已经绑定的资源。
Descriptor Set的布局(Descriptor Set Layout)定义了哪些类型(Buffers、Textures、Samplers)的Descriptor可以包含在其中,以及它们的排列顺序。这种布局的设置对于着色器的编写和资源的组织至关重要。一个Descriptor Set只能遵循一个Descriptor Set Layout,可以有多个Descriptor Set遵循同一个Descriptor Set Layout。
Descriptor 设计原因
需要Descriptor的主要原因是为了解决现代图形渲染中的性能瓶颈和提高硬件的灵活性。在复杂的渲染场景中,频繁的资源绑定和渲染状态设置会导致显著的性能损耗。为了优化这些性能问题,现代图形API如Vulkan引入了Descriptor的概念,以提供一种更高效的方式来管理和使用着色器资源。
使用Descriptor的设计动机包括:
-
性能优化:通过减少在渲染循环中对资源的绑定次数,可以显著降低CPU到GPU的通信开销,从而提高渲染效率。
-
资源管理:Descriptor允许开发者将相关的资源组织在一起,形成Descriptor Set。这样可以预先定义好资源的布局,使得GPU能够更快速地定位和使用这些资源。
-
灵活性:Descriptor Set提供了更多的自由度,允许开发者根据需要组合不同的资源。这种灵活性使得开发者可以更好地适应不同的渲染需求和硬件特性。
-
状态预定义:通过预先定义Pipeline Layout和Descriptor Set,开发者可以告诉驱动程序哪些资源是必要的,从而减少运行时的状态检查和更新。
-
兼容性:Descriptor Set允许部分兼容的Pipeline Layout共存,这意味着只要使用的Descriptor Set相同,即使在切换不同的渲染管线时,也可以重用已绑定的资源,减少了资源绑定的次数。
-
减少CPU开销:通过减少运行时的资源绑定和状态检查,可以显著降低图形驱动程序的CPU开销,使得CPU资源可以更有效地用于其他任务。
总的来说,Descriptor的设计是为了提供一个更加高效、灵活且性能优化的资源管理机制,以适应现代图形渲染的复杂性和高性能需求。通过这种方式,开发者可以更好地控制渲染过程中的资源使用,从而实现更高质量的渲染效果。
Descriptor Pool
Descriptor Pool 的设计原因
在Vulkan中,创建 Descriptor Pool (描述符池)是为了管理 Descriptor Set 的分配和释放。Descriptor Pool 作为一个资源池,允许开发者预先分配一定数量的 Descriptor Set,这样可以提高性能:
-
性能优化:
- 减少内存分配开销:频繁地创建和销毁Descriptor Set会导致大量的内存分配和释放操作,这在某些情况下可能会导致性能瓶颈。Descriptor Pool允许预先分配一定数量的Descriptor Set,从而减少了运行时的内存分配频率。
- 提高内存使用效率:Descriptor Pool可以更有效地利用GPU内存,因为它允许多个Descriptor Set共享相同的内存块,而不是为每个Descriptor Set分配独立的内存。
-
资源管理:
- 简化Descriptor Set的创建:通过Descriptor Pool,开发者可以一次性创建多个Descriptor Set,而不需要为每个Descriptor Set单独调用创建函数。这简化了资源管理流程。
- 提高资源重用性:Descriptor Pool使得Descriptor Set可以在不同的渲染管线或渲染批次之间重用,提高了资源的利用率。
-
外部同步:
- 避免竞态条件:由于Descriptor Pool是外部同步的,它确保了在多线程环境中,对Descriptor Set的分配和释放操作不会发生冲突,从而避免了潜在的竞态条件。
-
灵活性:
- 动态调整资源需求:虽然Descriptor Pool在创建时需要指定最大Descriptor Set数量和每种类型的Descriptor数量,但开发者可以根据实际需求动态调整这些参数,以适应不同的渲染场景。
-
减少CPU到GPU的通信:
- 减少状态更新:通过Descriptor Pool,可以减少CPU到GPU的状态更新次数,因为Descriptor Set的创建和销毁操作在GPU上是相对昂贵的。通过预先分配和管理Descriptor Set,可以减少这种通信。
-
提高渲染效率:
- 批量处理:在渲染循环中,可以批量分配和释放Descriptor Set,这样可以减少每次渲染时的开销,提高渲染效率。
总的来说,Descriptor Pool是Vulkan中用于优化性能和提高资源管理效率的重要机制。它通过减少内存分配的频率、提高内存使用效率、简化资源管理流程以及减少CPU到GPU的通信,帮助开发者实现更高效、更稳定的图形渲染。
Descriptor Pool 的具体使用
- 首先定义 vector<VkDescriptorPoolSize> :指定多种 Descriptor 的类型(如VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER、VK_DESCRIPTOR_TYPE_SAMPLER等)和数量(uint32_t descriptorCount);
- 填充VkDescriptorPoolCreateInfo并创建Descriptor Pool:指定vector<VkDescriptorPoolSize>的数组大小及数据、能够创建的Descriptor Set的最大数量maxSets;
指定
VkDescriptorPoolSize
和 maxSets 的原因:
-
资源类型管理:
VkDescriptorPoolSize
允许开发者指定池中每种类型的 Descriptor(如 Uniform Buffers、Sampled Images、Storage Buffers 等)的数量。maxSets
提供了一种控制资源池大小的方法。开发者可以根据应用程序的需求和预期的渲染负载来设置这个值,以确保Descriptor Pool能够满足渲染需求,同时避免资源浪费。这使得开发者可以根据应用程序的需求,为不同类型的资源分配合适的空间来满足渲染需求,同时避免资源浪费。 -
资源预分配:通过
VkDescriptorPoolSize
,开发者可以预先为特定的 Descriptor 类型分配资源。maxSets
允许开发者预先为Descriptor Pool分配一定数量的Descriptor Set,这样做可以减少运行时创建和销毁 Descriptor Set 的频率,从而减少 CPU 和 GPU 之间的同步开销。 -
灵活性:
VkDescriptorPoolSize
提供了灵活性,允许开发者根据渲染场景的不同,为不同的 Descriptor 类型设置不同的数量。这有助于优化资源使用,避免某些类型的 Descriptor 过剩或不足。 -
避免资源竞争:在多线程或多渲染队列的环境中,
VkDescriptorPoolSize
有助于避免资源竞争。通过确保池中有足够的 Descriptor,可以减少不同渲染任务之间因资源不足而产生的冲突。 - 避免内存碎片化:通过预先分配一定数量的Descriptor Set,可以减少内存碎片化的风险。内存碎片化可能会导致内存使用效率低下,而预先分配可以确保Descriptor Set在内存中连续存储。
- 减少API调用:在渲染循环中,如果需要大量的Descriptor Set,通过设置合适的
maxSets
值,可以减少对vkAllocateDescriptorSets
和vkFreeDescriptorSets
函数的调用次数,从而减少API调用的开销。 - 适应不同的渲染场景:不同的渲染场景可能需要不同数量的Descriptor Set。通过调整
maxSets
的值,开发者可以为特定的渲染场景优化Descriptor Pool的配置。
DescriptorSet Layout
DescriptorSet Layout 的设计原因
DescriptorSet Layout(描述符集布局)主要与图形API的组织、性能优化、资源管理和灵活性有关:
-
组织和管理资源:DescriptorSet Layout提供了一种结构化的方式来组织和管理着色器资源(如缓冲区、纹理、采样器等),以及它们如何在内存中布局。定义了哪些类型的资源可以被绑定到一个Descriptor Set中,以及这些资源的顺序和数量。这为着色器提供了一个清晰的资源绑定蓝图。
-
提高渲染性能:通过Descriptor Set Layout,可以预先定义资源的绑定点,这有助于GPU在渲染时快速定位和访问这些资源。这种预定义的布局减少了运行时的开销,提高了渲染效率。
-
减少状态切换开销:在渲染过程中,频繁地切换着色器资源会导致性能下降。DescriptorSet Layout允许开发者将相关的资源绑定到同一个Descriptor Set中,从而减少了状态切换的次数和开销。
-
灵活性和可重用性:DescriptorSet Layout允许开发者创建多个具有相同布局的Descriptor Set,确保了不同着色器之间可以共享相同的Descriptor Set,这使得资源可以在不同的渲染管线和场景中重用。这种设计提高了代码的可维护性和可重用性
-
兼容性和一致性:DescriptorSet Layout确保了不同着色器之间的兼容性。只要它们使用相同布局的DescriptorSet,就可以共享资源,而不需要关心资源的具体内容。这有助于保持代码的一致性,简化了着色器的编写和维护。
-
优化资源访问模式:通过合理设计DescriptorSet Layout,可以优化GPU对资源的访问模式。例如,将经常一起使用的资源放在同一个DescriptorSet中,可以减少GPU在内存中的跳转,提高缓存利用率。
-
减少CPU到GPU的通信:DescriptorSet Layout允许开发者预先定义资源的布局,这样在运行时,CPU只需要一次性地将这些信息传递给GPU,而不是在每次渲染时都进行通信。这减少了CPU到GPU的通信开销。
-
支持动态渲染:在某些情况下,开发者可能需要在运行时动态地改变着色器资源。Descriptor Set Layout提供了一种机制,允许在不改变着色器代码的情况下,动态地更新和绑定资源。
总之,Descriptor Set Layout是为了提供一个高效、灵活且一致的方式来管理和使用着色器资源。它有助于提高渲染性能,简化资源管理,同时保持代码的可维护性和可扩展性。
DescriptorSet Layout 的具体使用
- 首先定义 vector<VkDescriptorSetLayoutBinding>:指定多种 Descriptor 的类型(如VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER、VK_DESCRIPTOR_TYPE_SAMPLER等)和数量(uint32_t descriptorCount)、使用这个描述符资源的着色器类型(VkShaderStageFlags stageFlags:顶点、几何、片段着色器等)以及着色器中的绑定点(uint32_t binding:0、1、2等);
- 填充VkDescriptorSetLayoutCreateInfo并创建DescriptorSet Layout:指定vector<VkDescriptorPoolSize>的数组大小及数据;
指定Descriptor Bindings(描述符绑定)的原因是为了精确控制着色器资源的组织和访问方式:
-
资源定位:Descriptor Bindings允许开发者为每个资源指定一个唯一的绑定点(binding index),这样着色器就可以通过这个索引来访问特定的资源。这有助于在着色器代码中明确地引用资源,而不需要硬编码资源的位置。
-
资源管理:通过指定Bindings,开发者可以更好地管理和组织着色器资源。这使得在不同的渲染管线和场景中重用资源变得更加容易,同时也简化了资源的创建和更新过程。
vkAllocateDescriptorSets 【分配】
分配DescriptorSet需要指定前面创建的 DescriptorSet Layout 和 DescriptorPool。原因是为了确保正确地从池中分配出符合特定布局要求的 Descriptor Set。
VkDescriptorSetAllocateInfo 结构体,包含了分配DescriptorSet所需的信息:
descriptorPool
:指定了要从中分配Descriptor Set的Descriptor Pool。descriptorSetCount
:指定了需要创建的Descriptor Set的数量。pSetLayouts
:一个指针数组,包含了要分配的DescriptorSet的布局。在当前的Vulkan API版本中,这个数组只能包含一个元素,即只能指定一个布局。
VkDescriptorSetAllocateInfo
结构体在Vulkan API中设计为接收单个 VkDescriptorSetLayout
和单个 VkDescriptorPool。
在实际应用中,通常会为每个渲染管线创建一个Descriptor Pool,并为该管线的所有DescriptorSet使用相同的布局。这样可以确保每个DescriptorSet都包含着色器所需的正确类型的资源。
如果你需要为多个渲染管线或不同的渲染场景分配 DescriptorSet,你可以创建多个具有不同布局的Descriptor Pool,每个池对应一种特定的布局。然后,你可以为每个场景或管线分配一个或多个DescriptorSet,每个DescriptorSet都遵循其对应的布局。
如果你需要在多个管线之间共享资源,你可以创建一个包含所需资源的通用布局,并为所有管线使用这个布局。
Pipeline Layout
Pipeline Layout 的设计原因
Pipeline Layout
定义了渲染管线(Pipeline)可以访问的资源集合,包括 DescriptorSet 和 Push Constants。PipelineLayout
确保了渲染管线能够正确地与着色器资源进行交互。以下是 PipelineLayout
的主要作用和如何与渲染管线关联的解释:
-
资源定义:
PipelineLayout
组合零或多个DescriptorSetLayout
和PushConstantRange
来定义渲染管线可以访问的资源。每个DescriptorSetLayout
描述了一组 DescriptorSet,而PushConstantRange
描述了着色器可以直接访问的常量数据。 -
渲染管线创建:在创建渲染管线时,必须指定一个
PipelineLayout
。这个PipelineLayout
定义了着色器在运行时可以访问的资源。 -
着色器访问:在着色器代码中,开发者需要根据
PipelineLayout
中定义的资源来编写代码。着色器只能访问在PipelineLayout
中声明的资源,这确保了着色器与渲染管线的兼容性。 -
DescriptorSet 绑定:在渲染命令中,通过
vkCmdBindDescriptorSets
函数将 DescriptorSet 绑定到当前的渲染管线。这个绑定操作是基于PipelineLayout
中定义的 DescriptorSet Layout 的。渲染管线会根据这个布局来查找和使用 DescriptorSet 中的资源。 -
PipelineLayout 的唯一性:每个渲染管线都需要一个唯一的
PipelineLayout
。如果多个渲染管线需要访问相同的资源集合,它们可以共享同一个PipelineLayout
。这有助于减少资源的重复定义,并提高资源管理的效率。 -
资源的动态更新:虽然
PipelineLayout
在创建时定义了资源的静态结构,但开发者仍然可以通过vkUpdateDescriptorSets
函数动态地更新 DescriptorSet 中的资源。这样,即使渲染管线的布局保持不变,资源内容也可以在运行时进行调整。
总之,PipelineLayout
是 Vulkan 中连接渲染管线和着色器资源的桥梁。
Pipeline Layout 的具体使用
- 首先创建 VkDescriptorSetLayout (前面的步骤已完成,一般只创建一个)和 VkPushConstantRange 的实例对象:
Push Constants 的布局是通过
VkPushConstantRange
结构体在PipelineLayout
中指定的。以下是VkPushConstantRange
的主要成员:
- stageFlags:指定哪些着色器阶段可以访问这些 Push Constants。
- offset:指定 Push Constants 在 Command Buffer 中的起始偏移量。
- size:指定 Push Constants 的大小,不能超过规范要求的 128 字节限制。
2. 填充VkPipelineLayoutCreateInfo并创建Pipeline Layout:
指定VkDescriptorSetLayout 和 VkPushConstantRange的数量及对象;
在渲染过程中,你可以使用 vkCmdPushConstants
函数向 Command Buffer 中的 Push Constants 区域写入数据(write)。这样,着色器就可以在执行时读取这些常量值。
vkUpdateDescriptorSets 【更新】
vkUpdateDescriptorSets
用于更新一个或多个 DescriptorSet 的内容。这个函数允许你将实际的资源(如缓冲区、图像、采样器等)绑定到之前分配的 DescriptorSet 的特定绑定点上。
- 首先定义 vector<VkWriteDescriptorSet>:描述如何将资源(如缓冲区、图像、采样器等)写入到 Descriptor Set 的特定绑定点(Binding);
- dstSet:指定要更新的 DescriptorSet;
- descriptorType:指定要写入的描述符的类型,如
VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER
、VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER
等。 - dstBinding:指定资源将被写入的绑定点索引。对应于 DescriptorSet Layout 中定义的绑定点索引。
- pImageInfo、pBufferInfo、pTexelBufferView:根据
descriptorType
,这些指针指向包含资源信息的结构体数组。例如,对于图像描述符,会使用pImageInfo
;对于缓冲区描述符,会使用pBufferInfo
。 - descriptorCount:指定要写入的描述符的数量。对于非数组绑定点,这通常是 1。
- dstArrayElement:如果绑定点是一个数组,这个成员指定了数组中要更新的元素的索引。对于非数组绑定点,通常设置为 0。
2. 调用vkUpdateDescriptorSets 所用参数:vector<VkWriteDescriptorSet>的数组大小及数据;
使用 VkWriteDescriptorSet
结构体时,开发者需要为每个要更新的绑定点创建一个实例,并在 vkUpdateDescriptorSets
函数中传递这些实例的数组。这样,Vulkan 就可以根据这些描述符集更新对应的 Descriptor Set,使得着色器能够访问到最新的资源数据。
vkCmdBindDescriptorSets 【绑定】
首先需要指定命令缓冲及管线类型,即:
commandBuffer
:当前的 Command Buffer,它包含了渲染命令。pipelineBindPoint
:指定绑定点,可以是VK_PIPELINE_BIND_POINT_GRAPHICS
(图形管线)或VK_PIPELINE_BIND_POINT_COMPUTE
(计算管线)。
其次是管线中着色器要用到的资源如何布局:
layout
:当前渲染管线的 Pipeline Layout,它定义了 DescriptorSet 的布局。
在渲染命令缓冲区中,vkCmdBindDescriptorSets
函数主要用于指定哪些 Descriptor Set 应该被绑定到当前的渲染管线。有三个相关参数:
firstSet
:要绑定的 Descriptor Set 的起始索引。descriptorSetCount
:要绑定的 Descriptor Set 的数量。pDescriptorSets
:指向 Descriptor Set 句柄数组的指针。
如果使用了动态偏移量(例如,为了更新 Uniform Buffer Object 的内容),那么允许在运行时调整缓冲区的特定部分,而不需要重新创建整个缓冲区。
dynamicOffsetCount
:动态偏移量的数量,用于更新缓冲区的动态偏移量。pDynamicOffsets
:指向动态偏移量数组的指针,用于更新缓冲区的动态偏移量。
总结
Vulkan的描述符集设置是一个层次化的结构,它允许开发者定义和管理着色器资源。以下是其关键组成部分和它们的功能:
-
Descriptor Set Layout(描述符集布局):
- 由Descriptor Bindings组成,定义了Descriptor Set中资源的类型、数量和顺序。
- 控制每个Descriptor的排列方式,确保着色器可以正确地访问和操作资源。
-
Descriptor Binding(描述符绑定):
- 将Descriptor与着色器中的资源(如缓冲区、图像、采样器等)绑定。
- 提供了着色器对资源操作的接口,使得着色器能够在渲染过程中使用这些资源。
-
Descriptor Set(描述符集):
- 需要指定一个Descriptor Set Layout,这是创建Descriptor Set的基础之一。
- 通过Descriptor Pool分配出来,Descriptor Pool负责管理Descriptor Set的内存分配。
-
资源绑定和更新:
- 使用
VkWriteDescriptorSet
或VkCopyDescriptorSet
来将实际的资源数据绑定到Descriptor Set的特定绑定点,或者更新已有的资源数据。 - 这些操作允许开发者在运行时动态地改变着色器使用的资源。
- 使用
-
Pipeline Layout(管线布局):
- 结合了Descriptor Set Layout和Push Constants(如果使用)来创建。
- 定义了渲染管线可以访问的所有资源,包括Descriptor Set和Push Constants。
- 在创建渲染管线时指定,确保着色器能够访问正确的资源。
-
Descriptor Set的绑定:
- 在渲染命令缓冲区中,使用
vkCmdBindDescriptorSets
来绑定Descriptor Set到当前的渲染管线。 - 这个操作告诉GPU在执行渲染管线时应该使用哪些资源。
- 在渲染命令缓冲区中,使用
通过描述符集,Vulkan提供了一种灵活且高效的方式来管理着色器资源。开发者可以根据渲染需求创建和管理Descriptor Set,更新资源数据,并在渲染时将它们绑定到渲染管线。这种模型有助于提高渲染性能,同时提供了对资源管理的细粒度控制。
着色器访问图像数据的流程【补充】
下面步骤是从创建图像资源到在渲染管线中使用这些资源的完整过程。每个步骤都是确保着色器能够正确读取和处理图像数据的关键。
-
创建和管理图像资源:
- 使用
VkImage
创建图像资源。 - 通过
vkBindImageMemory
将VkImage
绑定到VkDeviceMemory
,以分配和管理内存。
- 使用
-
创建图像视图:
- 使用
vkCreateImageView
创建VkImageView
,这为图像提供了一个视图,允许着色器以特定的格式(如颜色、深度等)访问图像数据。
- 使用
-
准备着色器资源:
- 使用
VkWriteDescriptorSet
结构体来描述如何将图像视图和采样器绑定到 Descriptor Set。 - 调用
vkUpdateDescriptorSets
函数来实际更新 Descriptor Set,将图像视图和采样器的引用写入其中。
- 使用
-
绑定Descriptor Set到渲染管线:
- 使用
vkCmdBindDescriptorSets
函数在 Command Buffer 中绑定 Descriptor Set。 - 这使得着色器能够在渲染过程中访问之前绑定的图像资源。
- 使用
-
执行渲染命令:
- 在
VkCommandBuffer
中执行渲染命令,包括绘制调用(Draw Calls)。 - 着色器通过
texture()
函数访问绑定的图像数据,使用采样器(sampler)来获取图像的特定像素(RGB)。
- 在
-
渲染输出:
- 渲染管线处理着色器的输出,最终生成图像。
总的来说:
VkDescriptorPool 分配描述符类型及个数、描述符集数量;
VkDescriptorSetLayout 指定描述符类型绑定的着色器阶段和绑定点;
VkWriteDescriptorSet 将描述符类型、绑定点及描述符内存信息(VkDescriptorBufferInfo、VkDescriptorImageInfo)写入由上面两个分配的描述符集。
全篇完,辛苦了!!