本章旨在详细说明 VK_EXT_descriptor_heap 扩展的内存映射工作原理。本文的目的并非展示完整的实际示例或推荐用法而是帮助读者理解该 API 如何将数据映射到着色器以便后续能灵活运用该 API。什么是描述符“描述符descriptor” 是一种小型、不透明的数据结构用于描述着色器中资源变量的数据访问方式。该数据结构由驱动内部定义。在VK_EXT_descriptor_buffer/VK_EXT_descriptor_heap扩展出现之前其实现细节被抽象隐藏而现在应用程序可完全控制和管理这些 “描述符” 数据结构。例如在部分驱动中VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER类型的描述符仅占 16 字节这些字节可能编码了虚拟地址、步长及其他元数据供着色器读取统一缓冲区时使用。以下是描述符的二进制数据示例0x12345678 0xFFFF0001 0x10101010 0x11223344开发者无需关注描述符二进制数据的具体含义关键在于理解与 Vulkan 1.0 不同驱动现在会将这种不透明、可变大小的内部数据结构直接返回给应用程序。不再由驱动控制VkDescriptorSet对象而是由应用程序负责管理这些数据确保其位于 GPU 可读取的正确内存位置。单个资源的多个描述符描述符与VkBuffer等资源并非总是一对一的关系。例如一个 1024 字节的VkBuffer可通过设置不同的偏移量和范围划分为 3 个独立的描述符。上图中每个 “描述符数据块Descriptor Blob” 都是一个间接引用指向着色器将访问VkBuffer中的具体内存位置。在 Vulkan 1.0 中应用程序甚至不会感知到这些 “描述符数据块” 的存在而通过VK_EXT_descriptor_heap扩展应用程序会获取这些 “描述符数据块”并负责管理其内存位置确保着色器能正常访问。如何获取描述符二进制数据块统一缓冲区和存储缓冲区首先需创建VkBuffer在 Vulkan 1.0 中调用vkUpdateDescriptorSets驱动会自动处理描述符创建。在VK_EXT_descriptor_buffer扩展中先调用vkGetBufferDeviceAddress()获取VkBuffer的地址再将地址和范围传入vkGetDescriptorEXT()生成描述符。在VK_EXT_descriptor_heap扩展中流程类似但需将地址和范围传入vkWriteResourceDescriptorsEXT()。采样图像逻辑与缓冲区类似但VK_EXT_descriptor_heap扩展不再需要VkImageView对象而是直接将VkImageViewCreateInfo传入vkWriteResourceDescriptorsEXT()由该函数直接生成描述符。描述符堆VK_EXT_descriptor_heap扩展明确定义了两类描述符堆采样器堆samplers用于存储采样器描述符。资源堆other resources用于存储其他资源的描述符缓冲区、图像、加速结构等。描述符堆与VkDescriptorSet并非一对一关系理想情况下应包含命令缓冲区中使用的所有描述符。分配描述符堆获取各类描述符后需分配一个堆来存储它们。只需创建带有VK_BUFFER_USAGE_DESCRIPTOR_HEAP_BIT_EXT及VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT标识的VkBuffer该缓冲区即为描述符堆。后续仅需将描述符二进制数据块移入该堆即可。在堆中获取描述符调用vkWriteResourceDescriptorsEXT()获取描述符数据后需将其写入堆内存有 3 种实现方式若堆内存支持主机可见性可通过vkMapMemory()映射堆内存再用memcpy()复制描述符数据。若堆内存支持主机可见性可直接将vkWriteResourceDescriptorsEXT()的pDescriptors→address指向描述符堆。在 GPU 上传输描述符例如通过vkCmdCopyBuffer()或在着色器中直接写入。注意若在 GPU 上向堆写入数据后续 GPU 读取堆中描述符前需通过VK_ACCESS_2_RESOURCE_HEAP_READ_BIT_EXT或VK_ACCESS_2_SAMPLER_HEAP_READ_BIT_EXT执行正确的同步操作。描述符大小每种VkDescriptorType对应的描述符数据块大小不同。为简化操作可通过VkPhysicalDeviceDescriptorHeapPropertiesEXT获取以下预定义大小samplerDescriptorSize采样器描述符大小。bufferDescriptorSize缓冲区描述符大小。imageDescriptorSize图像描述符大小。在部分特定场景中若驱动支持可通过vkGetPhysicalDeviceDescriptorSizeEXT()获取更紧凑的描述符大小以节省内存。绑定到命令缓冲区录制vkCommandBuffer时需使用与vkCmdBindDescriptorSets()或vkCmdBindDescriptorBuffersEXT()功能相当的命令两类堆分别对应两个基本相同的调用vkCmdBindSamplerHeapEXT()绑定采样器堆。vkCmdBindResourceHeapEXT()绑定资源堆。heapRange字段指定待绑定堆的内存范围同时需指定 “预留范围reserved range”—— 该内存区域供驱动存储内部所需的描述符应用程序禁止访问。可通过minResourceHeapReservedRange和minSamplerHeapReservedRange属性获取所需的最小预留内存大小。以下示例展示两类堆的绑定数值为简化示例// 绑定资源堆 vkCmdBindResourceHeapEXT( commandBuffer, pipelineBindPoint, (VkDescriptorHeapRangeEXT){ .address 0x1000, // 堆起始地址 .size 0x80, // 堆大小 .reservedRangeOffset 0x40, // 预留范围偏移 .reservedRangeSize 0x20 // 预留范围大小 } ); // 绑定采样器堆 vkCmdBindSamplerHeapEXT( commandBuffer, pipelineBindPoint, (VkDescriptorHeapRangeEXT){ .address 0x4020, // 堆起始地址 .size 0x60, // 堆大小 .reservedRangeOffset 0, // 预留范围偏移 .reservedRangeSize 0x20 // 预留范围大小 } );该示例也体现了应用程序可控制驱动预留范围的内存位置。重新绑定新堆在同一个命令缓冲区中多次调用vkCmdBindResourceHeapEXT()时驱动需切换堆该操作开销较大应尽量避免。VK_EXT_descriptor_heap扩展的设计目标是应用程序已知堆的绑定位置可直接将新描述符数据复制到堆的对应偏移量处无需重新绑定堆。将堆映射到现有着色器为方便应用程序过渡到VK_EXT_descriptor_heap扩展无需修改现有着色器代码。VK_EXT_descriptor_heap扩展不再需要VkDescriptorSetLayout和VkPipelineLayout而是通过VkShaderDescriptorSetAndBindingMappingInfoEXT将绑定堆中的内存位置与 SPIR-V 中的DescriptorSet/Binding装饰器关联。该映射关系需在vkCreate*Pipelines()或vkCreateShadersEXT()时提供。VkDescriptorMappingSourceEXTVkDescriptorMappingSourceEXT包含多种映射选项初看可能较为复杂可将其分为三类理解堆访问Heap AccessVK_DESCRIPTOR_MAPPING_SOURCE_HEAP_WITH_CONSTANT_OFFSET_EXTVK_DESCRIPTOR_MAPPING_SOURCE_HEAP_WITH_PUSH_INDEX_EXTVK_DESCRIPTOR_MAPPING_SOURCE_HEAP_WITH_INDIRECT_INDEX_EXTVK_DESCRIPTOR_MAPPING_SOURCE_HEAP_WITH_INDIRECT_INDEX_ARRAY_EXT内联访问Inline AccessVK_DESCRIPTOR_MAPPING_SOURCE_RESOURCE_HEAP_DATA_EXTVK_DESCRIPTOR_MAPPING_SOURCE_PUSH_DATA_EXTVK_DESCRIPTOR_MAPPING_SOURCE_PUSH_ADDRESS_EXTVK_DESCRIPTOR_MAPPING_SOURCE_INDIRECT_ADDRESS_EXT着色器记录Shader Record光线追踪专用VK_DESCRIPTOR_MAPPING_SOURCE_HEAP_WITH_SHADER_RECORD_INDEX_EXTVK_DESCRIPTOR_MAPPING_SOURCE_SHADER_RECORD_DATA_EXTVK_DESCRIPTOR_MAPPING_SOURCE_SHADER_RECORD_ADDRESS_EXTVkDescriptorMappingSourceEXT 堆访问为直观展示映射工作原理先搭建测试环境堆中包含 8 个描述符分别指向 8 个数据负载。本示例中单个统一缓冲区存储 8 个uvec4每个描述符单独引用一个uvec4便于后续示例中明确读取的数据对象。描述符和uvec4均为 16 字节仅为巧合实际应用中不会为每个描述符仅绑定 16 字节内存过于浪费。着色器尝试从描述符数组中读取 2 个描述符// VkDescriptorSetAndBindingMappingEXT配置 // descriptorSet 0; // firstBinding 0; // bindingCount 2; layout(set 0, binding 0) uniform UBO { uvec4 payload; } u_buffers[2]; void main() { // 示例目的确定x和y的取值 uvec4 x u_buffers[0].payload; uvec4 y u_buffers[1].payload; }VK_DESCRIPTOR_MAPPING_SOURCE_HEAP_WITH_CONSTANT_OFFSET_EXT结果x vec4(2)y vec4(6)计算方式偏移量 堆偏移量 着色器索引 × 堆数组步长着色器索引shaderIndex此处指描述符数组u_buffers[]的索引因当前为 set/binding 0。u_buffers [0] 偏移量 0x20 (0 × 0x40)u_buffers [1] 偏移量 0x20 (1 × 0x40)VK_DESCRIPTOR_MAPPING_SOURCE_HEAP_WITH_PUSH_INDEX_EXT结果x vec4(4)y vec4(5)计算方式偏移量 堆偏移量 推送索引 × 堆索引步长着色器索引 × 堆数组步长推送偏移量pushOffset 8对应推送索引pushIndex 0x10。u_buffers [0] 偏移量 0x20 (0x10 × 2) (0 × 0x10)u_buffers [1] 偏移量 0x20 (0x10 × 2) (1 × 0x10)VK_DESCRIPTOR_MAPPING_SOURCE_HEAP_WITH_INDIRECT_INDEX_EXT结果x vec4(3)y vec4(5)计算方式偏移量 堆偏移量 间接索引 × 堆索引步长着色器索引 × 堆数组步长推送偏移量pushOffset 16对应间接地址indirectAddress 0x4000该地址必须是某个VkBuffer的有效VkDeviceAddress。对indirectAddress应用地址偏移量addressOffset 0x40得到最终地址0x4040。0x4040处的uint32_t值为0x20即间接索引indirectIndex。u_buffers [0] 偏移量 0x10 (0x20 × 1) (0 × 0x20)u_buffers [1] 偏移量 0x10 (0x20 × 1) (1 × 0x20)VK_DESCRIPTOR_MAPPING_SOURCE_HEAP_WITH_INDIRECT_INDEX_ARRAY_EXT结果x vec4(6)y vec4(2)计算方式偏移量 堆偏移量 间接索引 × 堆索引步长与上例类似推送偏移量pushOffset 16对应间接地址indirectAddress 0x4000应用地址偏移量addressOffset 0x40后得到0x4040。从0x4040开始每个着色器索引shaderIndex对应下一个uint32_t从间接统一缓冲区中获取偏移量。u_buffers [0] 偏移量 0x00 (0x20 × 1)u_buffers [1] 偏移量 0x00 (0x60 × 1)VkDescriptorMappingSourceEXT 内联访问这类映射与 VK_EXT_inline_uniform_block 类似无需描述符即可直接读取数据存在两个核心限制仅支持统一缓冲区只读。着色器中不能使用描述符数组。新着色器代码示例// VkDescriptorSetAndBindingMappingEXT::bindingCount 1; layout(set 0, binding 0) uniform UBO { uvec4 payload[2]; } u_buffer; void main() { // 示例目的确定x和y的取值 uvec4 x u_buffer.payload[0]; uvec4 y u_buffer.payload[1]; }VK_DESCRIPTOR_MAPPING_SOURCE_RESOURCE_HEAP_DATA_EXT结果x vec4(2)y vec4(3)计算逻辑推送偏移量pushOffset 12对应偏移量0x10与堆偏移量heapOffset 0x80相加得到最终偏移量0x90。后续偏移量由着色器对u_buffer的访问位置决定payload[1]在u_buffer结构体中的偏移量为 160x10字节因此实际访问地址为0x90 0x10。VK_DESCRIPTOR_MAPPING_SOURCE_PUSH_DATA_EXT使用与上例相同的着色器直接从推送数据中加载无需依赖堆。结果x vec4(2)y vec4(3)VK_DESCRIPTOR_MAPPING_SOURCE_PUSH_ADDRESS_EXT结果x vec4(2)y vec4(3)与上例类似仅增加了一层间接引用。VK_DESCRIPTOR_MAPPING_SOURCE_INDIRECT_ADDRESS_EXT结果x vec4(2)y vec4(3)与上例类似再增加一层间接引用。VkDescriptorMappingSourceEXT 着色器记录待补充 —— 光线追踪相关章节无类型着色器模型上述VkShaderDescriptorSetAndBindingMappingInfoEXT的用法旨在提供向后兼容性。VK_EXT_descriptor_heap扩展还支持一种新的 “无类型untyped” 使用方式此处 “无类型” 指通过 VK_KHR_shader_untyped_pointers 扩展使用 “无类型指针” 访问描述符。GLSL 示例如下// 有类型Typed // 标准方式指定set/binding位置 layout(set 0, binding 0) buffer SSBO { vec4 payload; } s_buffers[]; layout(set 0, binding 1) buffer UBO { vec4 payload; } u_buffers[]; // ----- // 无类型Untyped // 直接绑定到堆 layout(descriptor_heap) buffer SSBO { vec4 payload; } s_buffers[]; layout(descriptor_heap) uniform UBO { vec4 payload; } u_buffers[];将堆映射到无类型指针着色器“无类型” 模式下描述符数组直接绑定到堆的内存位置。着色器代码示例// VkPhysicalDeviceDescriptorHeapPropertiesEXT::bufferDescriptorSize 16 (0x10) layout(descriptor_heap) buffer SSBO { vec4 payload; } s_buffers[]; void main() { s_buffers[6].payload vec4(1.0); }映射关系如下掌握每种描述符类型的步长如前所述不同类型的描述符大小不同因此着色器中每个描述符数组的 “数组步长” 也不同。与 DX12/HLSL 不同所有描述符数组大小相同虽更便捷但内存浪费严重Vulkan 中需为每种描述符数组单独设置步长。// VK_DESCRIPTOR_TYPE_SAMPLER 类型描述符数组 layout(descriptor_heap) uniform sampler Samplers[]; // VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE 类型描述符数组 layout(descriptor_heap) uniform texture2D Textures[]; // VK_DESCRIPTOR_TYPE_STORAGE_BUFFER 类型描述符数组 layout(descriptor_heap) buffer ssbo { uint data; } Buffers[];例如访问Samplers[3]、Textures[3]或Buffer[3]时相对于堆起始地址的偏移量可能不同。假设描述符大小如下bufferDescriptorSize 16samplerDescriptorSize 32imageDescriptorSize 64则堆中每个描述符数组的步长应分别设置为上述大小