Ch3-6 描述符
描述符(descriptor)是用于描述着色器中所使用的资源(缓冲区、贴图、采样器)的一种不透明数据类型。
在使用描述符时以描述符集的形式(VkDescriptorSet)是成套使用的,描述符集则从描述符池(VkDescriptorPool)中分配。
管线必须具有相应的描述符布局(VkDescriptorSetLayout)才能绑定相应描述符集。
描述符类型
描述符有以下类型:
版本要求 |
VkDescriptorType 的枚举项 |
---|---|
1.0 |
VK_DESCRIPTOR_TYPE_SAMPLER 表示采样器 |
1.0 |
VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER 表示带采样器的被采样图像 |
1.0 |
VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE 表示被采样图像 |
1.0 |
VK_DESCRIPTOR_TYPE_STORAGE_IMAGE 表示storage图像(任意着色器可读,可在计算着色器中进行写入) |
1.0 |
VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER 表示uniform纹素缓冲区(着色器中只读) |
1.0 |
VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER 表示storage纹素缓冲区(任意着色器可读,可在计算着色器中进行写入) |
1.0 |
VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER 表示uniform缓冲区(在着色器中只读) |
1.0 |
VK_DESCRIPTOR_TYPE_STORAGE_BUFFER 表示storage缓冲区(可在任意着色器中指定其可读写性) |
1.0 |
VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC 表示动态uniform缓冲区 |
1.0 |
VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC 表示动态storage缓冲区 |
1.0 |
VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT 表示输入附件 |
1.3 |
VK_DESCRIPTOR_TYPE_INLINE_UNIFORM_BLOCK 表示内联uniform块 |
-
在写入描述符时,可以指定uniform缓冲区和storage缓冲区的offset,而动态uniform缓冲区和动态storage缓冲区是在此基础上,在绑定时,可以再多指定一个offset的缓冲区。这意味着,你可以将多个具有同类数据的uniform/storage缓冲区放进单个动态uniform/storage缓冲区中,然后在绑定时指定不同的offset来使用。
-
内联uniform块不需要缓冲区,而是能通过使得VkWriteDescriptorSet的pNext指向一个VkWriteDescriptorSetInlineUniformBlock结构体,在写入描述符时直接更新内联uniform块的数据,适用于少量数据。相比push constant,适合不会经常变更的数据。
Descriptor Set Layout
描述符布局(VkDescriptorSetLayout)包含了管线如何使用描述符的信息。
创建描述符布局
用vkCreateDescriptorSetLayout(...)创建描述符布局:
VkResult VKAPI_CALL vkCreateDescriptorSetLayout(...) 的参数说明 |
|
---|---|
VkDevice device |
逻辑设备的handle |
const VkDescriptorSetLayoutCreateInfo* pCreateInfo |
指向VkDescriptorSetLayout的创建信息 |
const VkAllocationCallbacks* pAllocator |
|
VkCommandPool* pSetLayout |
若执行成功,将描述符布局的handle写入*pSetLayout |
struct VkDescriptorSetLayoutCreateInfo 的成员说明 |
|
---|---|
VkStructureType sType |
结构体的类型,本处必须是VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO |
const void* pNext |
如有必要,指向一个用于扩展该结构体的结构体 |
VkDescriptorSetLayoutCreateFlags flags |
见后文 |
uint32_t bindingCount |
VkDescriptorSetLayoutBinding的个数 |
const VkDescriptorSetLayoutBinding* pBindings |
指向VkDescriptorSetLayoutBinding的数组,用于指定着色器中各个binding所对应的描述符类型 |
-
若flags中包含VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT(需Vulkan1.2),则需要使得pNext指向一个VkDescriptorSetLayoutBindingFlagsCreateInfo结构体并指定VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT,以及在创建相应描述符池时,也应该注明VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT(见后文“更新描述符集”)。
struct VkDescriptorSetLayoutBinding 的成员说明 |
|
---|---|
uint32_t binding |
对应着色器中的binding |
VkDescriptorType descriptorType |
描述符类型 |
uint32_t descriptorCount |
描述符的个数(描述符可以构成数组),或uniform块的容量(仅限VK_DESCRIPTOR_TYPE_INLINE_UNIFORM_BLOCK) |
VkShaderStageFlags stageFlags |
会访问本结构体指定的描述符的阶段 |
const VkSampler* pImmutableSamplers |
用于指定不可变更的采样器,见后文 |
若描述符类型为VK_DESCRIPTOR_TYPE_SAMPLER或VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,则用pImmutableSamplers指定descriptorCount个不可变更的采样器,无需指定则pImmutableSamplers为nullptr。
不可变更的采样器的具体效果是:
pImmutableSamplers所指向的一系列采样器的handle被保存到描述符布局中,使得相应binding的采样器一直是所指定的采样器(之后无需也不可再更新),若描述符类型为VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,更新描述符时无视VkDescriptorImageInfo::sampler。
注意,须确保不可变更采样器的生存期不短于描述符布局。
封装为descriptorSetLayout类
向VKBase.h,vulkan命名空间中添加以下代码:
class descriptorSetLayout { VkDescriptorSetLayout handle = VK_NULL_HANDLE; public: descriptorSetLayout() = default; descriptorSetLayout(VkDescriptorSetLayoutCreateInfo& createInfo) { Create(createInfo); } descriptorSetLayout(descriptorSetLayout&& other) noexcept { MoveHandle; } ~descriptorSetLayout() { DestroyHandleBy(vkDestroyDescriptorSetLayout); } //Getter DefineHandleTypeOperator; DefineAddressFunction; //Non-const Function result_t Create(VkDescriptorSetLayoutCreateInfo& createInfo) { createInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; VkResult result = vkCreateDescriptorSetLayout(graphicsBase::Base().Device(), &createInfo, nullptr, &handle); if (result) outStream << std::format("[ descriptorSetLayout ] ERROR\nFailed to create a descriptor set layout!\nError code: {}\n", int32_t(result)); return result; } };
Descriptor Set
描述符集(VkDescriptorSet)顾名思义,是在着色器中被成套使用的一系列描述符的集合。
更新描述符集
用vkUpdateDescriptorSets(...)更新描述符集:
void VKAPI_CALL vkUpdateDescriptorSets(...) 的参数说明 |
|
---|---|
VkDevice device |
逻辑设备的handle |
uint32_t descriptorWriteCount |
VkWriteDescriptorSet的个数 |
const VkWriteDescriptorSet* pDescriptorWrites |
指向VkWriteDescriptorSet的数组,用于指定写入描述符集的操作 |
uint32_t descriptorCopyCount |
VkCopyDescriptorSet的个数 |
const VkCopyDescriptorSet* pDescriptorCopies |
指向VkCopyDescriptorSet的数组,用于指定复制描述符集的操作 |
通常应当在录制绑定描述符的命令之前,更新描述符。
自Vulkan1.2起,可在绑定描述符后、提交命令缓冲区前更新描述符,则该描述符集必须从创建时注明了VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT的描述符池分配,并且创建相应描述符布局时也需要注明VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT。
写入描述符集
struct VkWriteDescriptorSet 的成员说明 |
|
---|---|
VkStructureType sType |
结构体的类型,本处必须是VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET |
const void* pNext |
如有必要,指向一个用于扩展该结构体的结构体 |
VkDescriptorSet dstSet |
需要被更新的描述符集的handle |
uint32_t dstBinding |
指定要更新描述符中的哪个binding |
uint32_t dstArrayElement |
指定从数组中哪个元素开始更新(适用于更新某一类描述符构成的数组的情况) |
uint32_t descriptorCount |
需要更新的描述符的个数(适用于更新某一类描述符构成的数组的情况) |
VkDescriptorType descriptorType |
描述符的类型 |
const VkDescriptorImageInfo* pImageInfo |
若描述符类型是采样器、图像或带采样器的图像,向pImageInfo所指数组填入相关信息,否则为nullptr |
const VkDescriptorBufferInfo* pBufferInfo |
若描述符类型是uniform或storage缓冲区(或对应的动态缓冲区),向pBufferInfo所指数组填入相关信息,否则为nullptr |
const VkBufferView* pTexelBufferView |
若描述符类型为storage或uniform纹素缓冲区,向pTexelBufferView所指数组填入各个缓冲区视图,否则为nullptr |
-
若描述符类型为内联uniform块,descriptorCount与pNext所指向VkWriteDescriptorSetInlineUniformBlock结构体中的dataSize一致(这似乎有点多此一举?)。
struct VkDescriptorImageInfo 的成员说明 |
|
---|---|
VkSampler sampler |
若描述符类型为采样器或带采样器的图像,在此填入采样器的handle,否则为VK_NULL_HANDLE |
VkImageView imageView |
若描述符类型为图像或带采样器的图像或输入附件,在此填入图像视图的handle,否则为VK_NULL_HANDLE |
VkImageLayout imageLayout |
若描述符类型为图像或带采样器的图像或输入附件,在此填入图像的内存布局,否则本项被无视 |
struct VkDescriptorBufferInfo 的成员说明 |
|
---|---|
VkBuffer buffer |
缓冲区的handle |
VkDeviceSize offset |
在着色器中访问缓冲区时的起始点,对应Vulkan的缓冲区中,距离缓冲区起始位置的距离(即用来定义一个映射关系) |
VkDeviceSize range |
可通过该描述符访问的缓冲区的大小范围,若使用特殊值VK_WHOLE_SIZE,则为从offset到缓冲区末尾的全部范围 |
-
这里的offset旨在使用描述符前,定义一个映射关系。动态uniform缓冲区或动态storage缓冲区的动态offset是在用vkCmdBindDescriptorSets(...)绑定描述符时指定的,最终着色器中访问动态缓冲区时的起始点,由这两个offset之合确定。
-
若多个描述符想引用同一个缓冲区中的不同部分,需满足对齐要求:若提供的缓冲区类型是uniform缓冲区/动态uniform缓冲区,则offset必须为VkPhysicalDeviceProperties::limits::minUniformBufferOffsetAlignment的整数倍。相应地,若提供的缓冲区类型是storage缓冲区/动态storage缓冲区,则offset必须为VkPhysicalDeviceProperties::limits::minStorageBufferOffsetAlignment的整数倍。
复制描述符集
struct VkCopyDescriptorSet 的成员说明 |
|
---|---|
VkStructureType sType |
结构体的类型,本处必须是VK_STRUCTURE_TYPE_COPY_DESCRIPTOR_SET |
const void* pNext |
如有必要,指向一个用于扩展该结构体的结构体 |
VkDescriptorSet srcSet |
源描述符集的handle |
uint32_t srcBinding |
指定从源描述符集中的哪个binding复制 |
uint32_t srcArrayElement |
指定从源描述符集的数组中哪个元素开始复制(适用于更新某一类描述符构成的数组的情况) |
VkDescriptorSet dstSet |
目标描述符集的handle |
uint32_t dstBinding |
指定复制到目标描述符集中的哪个binding |
uint32_t dstArrayElement |
指定复制到目标描述符集的数组中的起始位置(适用于更新某一类描述符构成的数组的情况) |
uint32_t descriptorCount |
需要复制的描述符的个数(适用于更新某一类描述符构成的数组的情况) |
封装为descriptorSet类
向VKBase.h,vulkan命名空间中添加以下代码:
class descriptorSet { friend class descriptorPool; VkDescriptorSet handle = VK_NULL_HANDLE; public: descriptorSet() = default; descriptorSet(descriptorSet&& other) noexcept { MoveHandle; } //Getter DefineHandleTypeOperator; DefineAddressFunction; //Const Function void Write(arrayRef<const VkDescriptorImageInfo> descriptorInfos, VkDescriptorType descriptorType, uint32_t dstBinding = 0, uint32_t dstArrayElement = 0) const { VkWriteDescriptorSet writeDescriptorSet = { .dstSet = handle, .dstBinding = dstBinding, .dstArrayElement = dstArrayElement, .descriptorCount = uint32_t(descriptorInfos.Count()), .descriptorType = descriptorType, .pImageInfo = descriptorInfos.Pointer() }; Update(writeDescriptorSet); } void Write(arrayRef<const VkDescriptorBufferInfo> descriptorInfos, VkDescriptorType descriptorType, uint32_t dstBinding = 0, uint32_t dstArrayElement = 0) const { VkWriteDescriptorSet writeDescriptorSet = { .dstSet = handle, .dstBinding = dstBinding, .dstArrayElement = dstArrayElement, .descriptorCount = uint32_t(descriptorInfos.Count()), .descriptorType = descriptorType, .pBufferInfo = descriptorInfos.Pointer() }; Update(writeDescriptorSet); } void Write(arrayRef<const VkBufferView> descriptorInfos, VkDescriptorType descriptorType, uint32_t dstBinding = 0, uint32_t dstArrayElement = 0) const { VkWriteDescriptorSet writeDescriptorSet = { .dstSet = handle, .dstBinding = dstBinding, .dstArrayElement = dstArrayElement, .descriptorCount = uint32_t(descriptorInfos.Count()), .descriptorType = descriptorType, .pTexelBufferInfo = descriptorInfos.Pointer() }; Update(writeDescriptorSet); } void Write(arrayRef<const bufferView> descriptorInfos, VkDescriptorType descriptorType, uint32_t dstBinding = 0, uint32_t dstArrayElement = 0) const { Write({ descriptorInfos[0].Address(), descriptorInfos.Count() }, descriptorType, dstBinding, dstArrayElement); } //Static Function static void Update(arrayRef<VkWriteDescriptorSet> writes, arrayRef<VkCopyDescriptorSet> copies = {}) { for (auto& i : writes) i.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; for (auto& i : copies) i.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; vkUpdateDescriptorSets( graphicsBase::Base().Device(), writes.Count(), writes.Pointer(), copies.Count(), copies.Pointer()); } };
Descriptor Pool
描述符池(VkDescriptorPool)用于分配描述符集。
创建描述符池
用vkCreateDescriptorPool(...)创建描述符池:
VkResult VKAPI_CALL vkCreateDescriptorPool(...) 的参数说明 |
|
---|---|
VkDevice device |
逻辑设备的handle |
const VkDescriptorPoolCreateInfo* pCreateInfo |
指向VkDescriptorSetLayout的创建信息 |
const VkAllocationCallbacks* pAllocator |
|
VkDescriptorPool* pDescriptorPool |
若执行成功,将描述符池的handle写入*pDescriptorPool |
struct VkDescriptorPoolCreateInfo 的成员说明 |
|
---|---|
VkStructureType sType |
结构体的类型,本处必须是VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO |
const void* pNext |
如有必要,指向一个用于扩展该结构体的结构体 |
VkDescriptorPoolCreateFlags flags |
见后文 |
uint32_t maxSets |
该描述符池所能分配的描述符集的最大个数 |
uint32_t poolSizeCount |
VkDescriptorPoolSize的个数 |
const VkDescriptorPoolSize* pPoolSizes |
指向VkDescriptorPoolSize的数组,用于指定所需的各种描述符的个数 |
版本要求 |
|
---|---|
1.0 |
VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT 表示可以释放从描述符池中分配的描述符集,资源会被返还到描述符池中 |
1.2 |
VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT 如前文所述,表示从该描述符池分配的描述符集,可以在被绑定后、提交命令缓冲区前被更新 |
struct VkDescriptorPoolSize 的成员说明 |
|
---|---|
VkDescriptorType type |
描述符的类型 |
uint32_t descriptorCount |
所需该种描述符的个数 |
分配描述符集
用vkAllocateDescriptorSets(...)分配描述符集:
VkResult VKAPI_CALL vkAllocateDescriptorSets(...) 的参数说明 |
|
---|---|
VkDevice device |
逻辑设备的handle |
const VkDescriptorSetAllocateInfo* pAllocateInfo |
指向VkDescriptorSet的分配信息 |
VkDescriptorSet* pDescriptorSet |
若执行成功,将描述符集的handle写入pDescriptorSets所指数组 |
struct VkDescriptorSetAllocateInfo 的成员说明 |
|
---|---|
VkStructureType sType |
结构体的类型,本处必须是VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO |
const void* pNext |
如有必要,指向一个用于扩展该结构体的结构体 |
VkDescriptorPool descriptorPool |
描述符池的handle |
uint32_t descriptorSetCount |
所需分配的描述符集的个数 |
const VkDescriptorSetLayout* pSetLayouts |
所需分配的各个描述符集对应的描述符布局 |
释放描述符集
用vkFreeDescriptorSets(...)释放描述符集:
VkResult VKAPI_CALL vkFreeDescriptorSets 的参数说明 |
|
---|---|
VkDevice device |
逻辑设备的handle |
VkDescriptorPool descriptorPool |
描述符池的handle |
uint32_t descriptorSetCount |
所需释放的描述符集的个数 |
const VkDescriptorSet* pDescriptorSets |
指向由所需释放的描述符集的handle构成的数组 |
-
该函数只会返回VK_SUCCESS。
封装为descriptorPool类
向VKBase.h,vulkan命名空间中,descriptorSet的定义后添加以下代码:
class descriptorPool { VkDescriptorPool handle = VK_NULL_HANDLE; public: descriptorPool() = default; descriptorPool(VkDescriptorPoolCreateInfo& createInfo) { Create(createInfo); } descriptorPool(uint32_t maxSetCount, arrayRef<const VkDescriptorPoolSize> poolSizes, VkDescriptorPoolCreateFlags flags = 0) { Create(maxSetCount, poolSizes, flags); } descriptorPool(descriptorPool&& other) noexcept { MoveHandle; } ~descriptorPool() { DestroyHandleBy(vkDestroyDescriptorPool); } //Getter DefineHandleTypeOperator; DefineAddressFunction; //Const Function result_t AllocateSets(arrayRef<VkDescriptorSet> sets, arrayRef<const VkDescriptorSetLayout> setLayouts) const { if (sets.Count() != setLayouts.Count()) if (sets.Count() < setLayouts.Count()) { outStream << std::format("[ descriptorPool ] ERROR\nFor each descriptor set, must provide a corresponding layout!\n"); return VK_RESULT_MAX_ENUM;//没有合适的错误代码,别用VK_ERROR_UNKNOWN } else outStream << std::format("[ descriptorPool ] WARNING\nProvided layouts are more than sets!\n"); VkDescriptorSetAllocateInfo allocateInfo = { .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO, .descriptorPool = handle, .descriptorSetCount = uint32_t(sets.Count()), .pSetLayouts = setLayouts.Pointer() }; VkResult result = vkAllocateDescriptorSets(graphicsBase::Base().Device(), &allocateInfo, sets.Pointer()); if (result) outStream << std::format("[ descriptorPool ] ERROR\nFailed to allocate descriptor sets!\nError code: {}\n", int32_t(result)); return result; } result_t AllocateSets(arrayRef<VkDescriptorSet> sets, arrayRef<const descriptorSetLayout> setLayouts) const { return AllocateSets( sets, { setLayouts[0].Address(), setLayouts.Count() }); } result_t AllocateSets(arrayRef<descriptorSet> sets, arrayRef<const VkDescriptorSetLayout> setLayouts) const { return AllocateSets( { &sets[0].handle, sets.Count() }, setLayouts); } result_t AllocateSets(arrayRef<descriptorSet> sets, arrayRef<const descriptorSetLayout> setLayouts) const { return AllocateSets( { &sets[0].handle, sets.Count() }, { setLayouts[0].Address(), setLayouts.Count() }); } result_t FreeSets(arrayRef<VkDescriptorSet> sets) const { VkResult result = vkFreeDescriptorSets(graphicsBase::Base().Device(), handle, sets.Count(), sets.Pointer()); memset(sets.Pointer(), 0, sets.Count() * sizeof(VkDescriptorSet)); return result;//Though vkFreeDescriptorSets(...) can only return VK_SUCCESS } result_t FreeSets(arrayRef<descriptorSet> sets) const { return FreeSets({ &sets[0].handle, sets.Count() }); } //Non-const Function result_t Create(VkDescriptorPoolCreateInfo& createInfo) { createInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; VkResult result = vkCreateDescriptorPool(graphicsBase::Base().Device(), &createInfo, nullptr, &handle); if (result) outStream << std::format("[ descriptorPool ] ERROR\nFailed to create a descriptor pool!\nError code: {}\n", int32_t(result)); return result; } result_t Create(uint32_t maxSetCount, arrayRef<const VkDescriptorPoolSize> poolSizes, VkDescriptorPoolCreateFlags flags = 0) { VkDescriptorPoolCreateInfo createInfo = { .flags = flags, .maxSets = maxSetCount, .poolSizeCount = uint32_t(poolSizes.Count()), .pPoolSizes = poolSizes.Pointer() }; return Create(createInfo); } };