Vulkan中的Descriptor、DescriptorSet、DescriptorSetLayout、DescriptorSetLayoutBinding等概念

1、概述

VulkanOpenGL的着色器语言都是glsl,但其仍有不同之处:以uniform为例

  • OpenGL中,其uniform变量是逐shader、逐帧设置的,通过获取特定shader的uniform location,然后从CPU到GPU拷贝数据。如果有多个shader,则这个拷贝操作就需要重复多次。比如相机相关的uniform,每个shader使用的都是一模一样的,但却要每帧每个都上传一遍,很浪费性能
  • vulkan是通过在显存中开辟一块区域,比如uniform buffer,从CPU写入uniform数据到显存中,然后每个shader都读取这块共享区域即可,性能会有提升

为了兼容vulkan API,Opengl也可以用vulkan的方式来处理uniform,这样会略微提高一些性能

如果一切都设置好了,在vulkan风格的GLSL中,shader中读取该uniform buffer的方式为:

layout(set = 0, binding = 0) uniform Camera
{
	mat4 u_View;
	mat4 u_Projection;
};
元素说明
set = 0 binding = 0表示从0号descriptor set的0号binding point上读取uniform buffer
mat4 u_View
mat4 u_Projection
这是该描述符里的成员,两个mat4

2、必须了解的结构体

vulkan的GLSL中,要实现上面这样的shader资源传递,必须了解的几个结构体:

2.1 Descriptor 描述符

传给shader的各种资源(矩阵、图像、变量等), 简单列举几个:

Descriptor作用说明
Uniform Buffers(均匀缓冲区)用于传递Uniform Data给着色器,比如模型变换矩阵、视图矩阵等。
Storage Buffers(存储缓冲区)用于传递读写访问的缓冲区,可用于实现计算着色器中的通用数据存储。
Sampled Images(采样图像)用于传递可在着色器中进行采样的图像。

2.2 DescriptorSet 描述符集

顾名思义,一个描述符的集合,它可以有多个元素,每个元素都是一个描述符。

它可以将程序中的资源(如图像, 缓冲区等)与渲染管线中的shader绑定起来. 在渲染时, 通过将descriptor Set绑定到渲染管线上, 可以将set中存放的资源传递给shader进行渲染。

一个渲染管线可以绑定多个描述符集,对应于shader中的set = 0 set = 1

将描述符集绑定到渲染管线vkCmdBindDescriptorSets
函数签名:

void vkCmdBindDescriptorSets(
    VkCommandBuffer                             commandBuffer,
    VkPipelineBindPoint                         pipelineBindPoint,
    VkPipelineLayout                            layout,
    uint32_t                                    firstSet,
    uint32_t                                    descriptorSetCount,
    const VkDescriptorSet*                      pDescriptorSets,
    uint32_t                                    dynamicOffsetCount,
    const uint32_t*                             pDynamicOffsets
);

调用:

vkCmdBindDescriptorSets(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, layout, 0, 1, &descriptorSet, 0, nullptr);
参数传入参数说明
1commandBuffer命令缓冲区对象,用于记录这条GPU命令
2VK_PIPELINE_BIND_POINT_GRAPHICS管线绑定点,就是渲染管线的类型(图形管线、计算管线等))
3layout渲染管线布局,包含了描述符集布局以及其他信息
40描述符集数组中的第一个描述符集的索引,可以绑定多个set
51要绑定的描述符集数量
6&descriptorSet指向描述符集数组的指针,包含要绑定的描述符集
70动态偏移量的数量,允许在运行时修改描述符中的一些参数
8nullptr指向动态偏移量数组的指针,与动态偏移量相关联的值

2.3 DescriptorSetLayoutBinding 描述符集布局:绑定点

descriptor Set上的每个绑定点的结构信息,每个VkDescriptorSetLayoutBinding对象对应说明一个绑定点的信息,比如绑定点号、绑定点上descriptor个数、类型,如果个数为多个,则绑定的是一个数组

对应于shader中的binding = 0 binding = 1

std::vector<VkDescriptorSetLayoutBinding> layoutBindings

// binding 1
VkDescriptorSetLayoutBinding& layoutBinding = layoutBindings.emplace_back();	
layoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;	
layoutBinding.descriptorCount = 1;	
layoutBinding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
layoutBinding.binding = binding;

// binding 2
auto& layoutBinding = layoutBindings.emplace_back();
layoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;	
layoutBinding.descriptorCount = 1;
layoutBinding.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
layoutBinding.pImmutableSamplers = nullptr;
layoutBinding.binding = binding;

2.4 DescriptorSetLayout 描述符集布局

这个东西,用来说明descriptor Set的布局结构,主要说明有多少个绑定点

descriptorLayoutCreateInfo.bindingCount = (uint32_t)(layoutBindings.size());
descriptorLayouCreateInfot.pBindings =layoutBindings.data();

3、大致步骤总结(代码来自vulkan编程指南)

3.1 首先,需要描述set的布局信息 DescriptorSetLayout

  • 通过多个 DescriptorSetLayoutBinding,指明绑定点的个数,每个VkDescriptorSetLayoutBinding 结构体要说目标绑定点的描述符个数
    void createDescriptorSetLayout() {
    	// binding point 0
        VkDescriptorSetLayoutBinding uboLayoutBinding0{};
        uboLayoutBinding.binding = 0;
        uboLayoutBinding.descriptorCount = 1;	// 一个绑定点准确来说是绑定了一个描述符数组,但这里只用1个
        
        // binding point 1
        VkDescriptorSetLayoutBinding imgLayoutBinding1{};
        samplerLayoutBinding.binding = 1;
        samplerLayoutBinding.descriptorCount = 1;
    	
        std::array<VkDescriptorSetLayoutBinding, 2> bindings = { 
        uboLayoutBinding0, imgLayoutBinding1};
    
        VkDescriptorSetLayoutCreateInfo layoutInfo{};
        layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
        layoutInfo.bindingCount = static_cast<uint32_t>(bindings.size());
        layoutInfo.pBindings = bindings.data();
        
        // 创建DescriptorSetLayout
        vkCreateDescriptorSetLayout(device, &layoutInfo, nullptr, &descriptorSetLayout);
    }
    

3.2 DescirptorSet是通过DescirptorPool来分配的

类似于commandBuffer需要由commandPool来管理内存分配,DescirptorSet的分配同样需要描述符池。

这两种机制都采用了池化的方式,以更有效地管理和分配内存资源。这种资源池的设计有助于减少内存碎片和提高资源的复用率,从而优化了应用程序的性能。

下面展示了如何创建一个简单的 Vulkan 描述符池。
描述符池包含两种类型的描述符:每种类型的描述符数量都设置为1,最大描述符集数量为1。总的来说这个池一共就两个描述符,并且能通过这个池创建最多1个描述符集,当你需要创建新的描述符集的时候,就需要再创建一个pool

void createDescriptorPool() 
{
    // 定义描述符池中的描述符类型和数量
       std::array<VkDescriptorPoolSize, 2> poolSizes{};
       poolSizes[0].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
       poolSizes[0].descriptorCount = 1;
       poolSizes[1].type = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
       poolSizes[1].descriptorCount = 1;
       
       // 创建描述符池的信息结构体
       VkDescriptorPoolCreateInfo poolInfo{};
       poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
       poolInfo.poolSizeCount = poolSizes.size();
       poolInfo.pPoolSizes = poolSizes.data();
       poolInfo.maxSets = 1;	// 本池子只分配1个描述符集,如果需要更多,可以改为n(n<=pooSizes.size()),也可以创建新的池来分配

       vkCreateDescriptorPool(device, &poolInfo, nullptr, &descriptorPool);
   }

在Vulkan中,描述符集是通过描述符池(Descriptor Pool)来分配的。描述符池会预分配一定数量的描述符,当你需要一个新的描述符集时,你可以从描述符池中分配。如果描述符池中的描述符集用完,你需要创建一个新的描述符池。

关于描述符池的一些重要概念:

  • 描述符池大小(maxSets): 在创建描述符池时,你需要指定描述符池能够容纳的最大描述符集数量。这个数量可以根据你的应用程序需求进行调整。

  • 每种描述符类型的数量(poolSizes): 在创建描述符池时,你需要指定每种描述符类型在池中的数量。这些数量的总和不应超过描述符池的最大描述符集数量。

如果你的描述符池中的描述符集最大分配数为1,那么每次你需要新的描述符集时,都需要创建一个新的描述符池。这样的做法有一些潜在的开销,因为每个描述符池都需要额外的资源和管理开销。因此,通常建议根据应用程序的需求,预先分配足够数量的描述符集,以减少频繁创建和销毁描述符池的开销。

请注意,描述符池是在GPU上进行管理的,因此创建和销毁可能涉及到一定的开销。因此,选择描述符池的大小时需要权衡应用程序的需求和性能要求。

比如在池中预分配一堆描述符,每个种类都100个,一共1100个描述符。如果每个描述符集只包含一个描述符(单个绑定点),那么你理论上可以通过这个描述符池分配1100个描述符集

std::vector<VkDescriptorPoolSize > poolSizes =
{
	{ VK_DESCRIPTOR_TYPE_SAMPLER, 100 },				
	{ VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER, 100 },
	{ VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, 100 },
	{ VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, 100 },
	{ VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER, 100 },
	{ VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER, 100 },
	{ VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, 100 },
	{ VK_DESCRIPTOR_TYPE_STORAGE_BUFFER, 100 },
	{ VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC, 100 },
	{ VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC, 100 },
	{ VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT, 100 }
};
VkDescriptorPoolCreateInfo pool_info = {};
pool_info.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
pool_info.flags = VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT;
pool_info.maxSets = 100 * poolSizes .size();
pool_info.poolSizeCount = (uint32_t)poolSizes.size();
pool_info.pPoolSizes = poolSizes;
VK_CHECK_RESULT(vkCreateDescriptorPool(device, &pool_info, nullptr, &descriptorPool));

3.3 创建DescriptorSet

void createDescriptorSets() {
    // descriptorSetLayout和descriptorPool在前两步已经创建好
    VkDescriptorSetAllocateInfo allocInfo{};
    allocInfo.descriptorPool = descriptorPool;
    allocInfo.descriptorSetCount = 1;
    allocInfo.pSetLayouts = &descriptorSetLayout; // 如果需要创建多个set,可以传入layout数组
	
	vkAllocateDescriptorSets(device, &allocInfo, &descriptorSet);
    }
}

3.4 利用WriteDescriptorSet 填充数据

目前set还只是一个空壳、框架,还没有绑定具体的uniform资源或图像资源等
在本文的案例中,首先需要创建具体的描述符并分配显存,具体的分配显存并拷贝数据到该显存块中的流程见这篇文章

在这里,假设已经创建好了对应资源,我们需要创建描述符缓存信息,如果是uniformbuffer,对应VkDescriptorBufferInfo ,如果是image对应VkDescriptorImageInfo

这个info对象其实应该位于资源创建部分代码块中,比如一个类VulkanUniformBuffer,不仅要维护一个VkBuffer对象、一个VkDeviceMemory对象,还应该有一个VkDescriptorBufferInfo 对象,因为这个结构体跟Buffer很亲密。后面跟具体set进行绑定的时候直接引用即可。

这个结构体有三个成员:

  • buffer:该资源的handle
  • offset:在显存中的起始位置偏移量,并不是相对于整块显存,而是相对于该buffer对象绑定的VkDeviceMemory对象的起始位置
  • range:描述符的尺寸(size of bytes)

然后填充VkWriteDescriptorSet,并调用vkUpdateDescriptorSets

// binding 0 : uniform buffer
VkDescriptorBufferInfo bufferInfo{};
bufferInfo.buffer = uniformBuffer;		// 事先在GPU内分配好的显存块,存放着一些uniform数据	
bufferInfo.offset = 0;
bufferInfo.range = sizeof(uniformBuffer);

// binding 1 :texture image sampler
VkDescriptorImageInfo imageInfo{};
imageInfo.imageView = textureImageView;	// 事先在GPU内分配好的显存块,存放着图像数据	
imageInfo.sampler = textureSampler;

// 指定对应的内存资源,需要两个vkWrite结构体
std::array<VkWriteDescriptorSet, 2> descriptorWrites{};

descriptorWrites[0].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
descriptorWrites[0].dstSet = descriptorSet; 	// 目标描述符集,3.3创建的
descriptorWrites[0].dstBinding = 0;             // 绑定点索引
descriptorWrites[0].dstArrayElement = 0;        // 数组元素索引  
descriptorWrites[0].descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
descriptorWrites[0].descriptorCount = 1;
descriptorWrites[0].pBufferInfo = &bufferInfo;

descriptorWrites[1].sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
descriptorWrites[1].dstSet = descriptorSet;
descriptorWrites[1].dstBinding = 1;
descriptorWrites[1].dstArrayElement = 0;
descriptorWrites[1].descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER;
descriptorWrites[1].descriptorCount = 1;
descriptorWrites[1].pImageInfo = &imageInfo;

vkUpdateDescriptorSets(device, static_cast<uint32_t>(descriptorWrites.size()), 
	descriptorWrites.data(), 0, nullptr);

最后,某乎作者MuGdxy暮狗dan的这张图做得是真漂亮
在这里插入图片描述

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

宗浩多捞

您的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值