文章目录
1、概述
Vulkan
与OpenGL
的着色器语言都是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);
参数 | 传入参数 | 说明 |
---|---|---|
1 | commandBuffer | 命令缓冲区对象,用于记录这条GPU命令 |
2 | VK_PIPELINE_BIND_POINT_GRAPHICS | 管线绑定点,就是渲染管线的类型(图形管线、计算管线等)) |
3 | layout | 渲染管线布局,包含了描述符集布局以及其他信息 |
4 | 0 | 描述符集数组中的第一个描述符集的索引,可以绑定多个set |
5 | 1 | 要绑定的描述符集数量 |
6 | &descriptorSet | 指向描述符集数组的指针,包含要绑定的描述符集 |
7 | 0 | 动态偏移量的数量,允许在运行时修改描述符中的一些参数 |
8 | nullptr | 指向动态偏移量数组的指针,与动态偏移量相关联的值 |
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的这张图做得是真漂亮