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_SAMPLERVK_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

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的数组,用于指定所需的各种描述符的个数

版本要求

VkDescriptorPoolCreateFlagBits 的枚举项

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);
    }
};