[译]Vulkan教程(26)描述符池和set
[译]Vulkan教程(26)描述符池和set
Descriptor pool and sets 描述符池和set
Introduction 入门
The descriptor layout from the previous chapter describes the type of descriptors that can be bound. In this chapter we're going to create a descriptor set for each VkBuffer
resource to bind it to the uniform buffer descriptor.
从之前的章节可知,描述符布局描述了描述符可被绑定的类型。本章我们要为每个VkBuffer
资源创建描述符set,以绑定它到uniform buffer描述符。
Descriptor pool 描述符池
Descriptor sets can't be created directly, they must be allocated from a pool like command buffers. The equivalent for descriptor sets is unsurprisingly called a descriptor pool. We'll write a new function createDescriptorPool
to set it up.
描述符set不能被直接创建,它们必须从池中分配,像命令buffer一样。豪不意外,描述符set的池叫做描述符池。我们要写一个新函数createDescriptorPool
来设置它。
void initVulkan() { ... createUniformBuffers(); createDescriptorPool(); ... } ... void createDescriptorPool() { }
We first need to describe which descriptor types our descriptor sets are going to contain and how many of them, using VkDescriptorPoolSize
structures.
我们先要描述,我们的描述符set会包含哪些描述类型,包含多少-用VkDescriptorPoolSize
结构体。
VkDescriptorPoolSize poolSize = {}; poolSize.type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; poolSize.descriptorCount = static_cast<uint32_t>(swapChainImages.size());
We will allocate one of these descriptors for every frame. This pool size structure is referenced by the main VkDescriptorPoolCreateInfo
:
我们将为每帧分配一个描述符。池的大小结构体被VkDescriptorPoolCreateInfo
引用。
VkDescriptorPoolCreateInfo poolInfo = {}; poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO; poolInfo.poolSizeCount = 1; poolInfo.pPoolSizes = &poolSize;
Aside from the maximum number of individual descriptors that are available, we also need to specify the maximum number of descriptor sets that may be allocated:
除了独立描述符的可用的最大数量,我们也需要指定可被分配的描述符set的最大数量:
poolInfo.maxSets = static_cast<uint32_t>(swapChainImages.size());;
The structure has an optional flag similar to command pools that determines if individual descriptor sets can be freed or not: VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT
. We're not going to touch the descriptor set after creating it, so we don't need this flag. You can leave flags
to its default value of 0
.
结构体有一个可选的标志-与命令池类似that决定单独的描述符set是否可被释放:VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT
。创建描述符set后,我们不会再碰它,所以我们不需要这个标志。让它保持默认值0
即可。
VkDescriptorPool descriptorPool; ... if (vkCreateDescriptorPool(device, &poolInfo, nullptr, &descriptorPool) != VK_SUCCESS) { throw std::runtime_error("failed to create descriptor pool!"); }
Add a new class member to store the handle of the descriptor pool and call vkCreateDescriptorPool
to create it. The descriptor pool should be destroyed when the swap chain is recreated because it depends on the number of images:
添加类成员to保存描述符池的句柄,调用vkCreateDescriptorPool
来创建它。当交换链创建时,描述符池应当被销毁,因为它依赖image的数量。
void cleanupSwapChain() { ... for (size_t i = 0; i < swapChainImages.size(); i++) { vkDestroyBuffer(device, uniformBuffers[i], nullptr); vkFreeMemory(device, uniformBuffersMemory[i], nullptr); } vkDestroyDescriptorPool(device, descriptorPool, nullptr); }
And recreated in recreateSwapChain
:
在recreateSwapChain
中重建:
void recreateSwapChain() { ... createUniformBuffers(); createDescriptorPool(); createCommandBuffers(); }
Descriptor set 描述符set
We can now allocate the descriptor sets themselves. Add a createDescriptorSets
function for that purpose:
我们现在可以分配描述符set了。为此,添加createDescriptorSets
函数:
1 void initVulkan() { 2 ... 3 createDescriptorPool(); 4 createDescriptorSets(); 5 ... 6 } 7 8 void recreateSwapChain() { 9 ... 10 createDescriptorPool(); 11 createDescriptorSets(); 12 ... 13 } 14 15 ... 16 17 void createDescriptorSets() { 18 19 }
A descriptor set allocation is described with a VkDescriptorSetAllocateInfo
struct. You need to specify the descriptor pool to allocate from, the number of descriptor sets to allocate, and the descriptor layout to base them on:
描述符set的分配用VkDescriptorSetAllocateInfo
结构体来描述。你需要指定从哪个描述符池分配,要分配的描述符set的数量,还有基于它们的描述符布局:
std::vector<VkDescriptorSetLayout> layouts(swapChainImages.size(), descriptorSetLayout); VkDescriptorSetAllocateInfo allocInfo = {}; allocInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; allocInfo.descriptorPool = descriptorPool; allocInfo.descriptorSetCount = static_cast<uint32_t>(swapChainImages.size()); allocInfo.pSetLayouts = layouts.data();
In our case we will create one descriptor set for each swap chain image, all with the same layout. Unfortunately we do need all the copies of the layout because the next function expects an array matching the number of sets.
本例中,我们将为交换链的每个image创建一个描述符set,其布局全相同。不幸的是,我们需要所有布局的副本,因为下一个函数要求一个与set数量相同的数组。
Add a class member to hold the descriptor set handles and allocate them with vkAllocateDescriptorSets
:
添加类成员to记录描述符set句柄,用vkAllocateDescriptorSets
分配它们:
VkDescriptorPool descriptorPool; std::vector<VkDescriptorSet> descriptorSets; ... descriptorSets.resize(swapChainImages.size()); if (vkAllocateDescriptorSets(device, &allocInfo, descriptorSets.data()) != VK_SUCCESS) { throw std::runtime_error("failed to allocate descriptor sets!"); }
You don't need to explicitly clean up descriptor sets, because they will be automatically freed when the descriptor pool is destroyed. The call to vkAllocateDescriptorSets
will allocate descriptor sets, each with one uniform buffer descriptor.
你不需要显式地清理描述符set,因为它们会被自动地释放when描述符池被销毁。调用vkAllocateDescriptorSets
会分配描述符set,每个带一个uniform buffer描述符。
The descriptor sets have been allocated now, but the descriptors within still need to be configured. We'll now add a loop to populate every descriptor:
描述符set现在已经分配了,但是描述符还没有配置好。现在我们用一个循环来填充每个描述符:
for (size_t i = 0; i < swapChainImages.size(); i++) { }
Descriptors that refer to buffers, like our uniform buffer descriptor, are configured with a VkDescriptorBufferInfo
struct. This structure specifies the buffer and the region within it that contains the data for the descriptor.
引用buffer的描述符,例如我们的uniform buffer描述符,用VkDescriptorBufferInfo
结构体配置。这个结构体指定了buffer及其区域that包含描述符的数据。
for (size_t i = 0; i < swapChainImages.size(); i++) { VkDescriptorBufferInfo bufferInfo = {}; bufferInfo.buffer = uniformBuffers[i]; bufferInfo.offset = 0; bufferInfo.range = sizeof(UniformBufferObject); }
If you're overwriting the whole buffer, like we are in this case, then it is also possible to use the VK_WHOLE_SIZE
value for the range. The configuration of descriptors is updated using the vkUpdateDescriptorSets
function, which takes an array of VkWriteDescriptorSet
structs as parameter.
如果你要重写整个buffer,像我们这里这样,那么也可以用VK_WHOLE_SIZE
for范围。描述符的配置用vkUpdateDescriptorSets
函数更新,它接收VkWriteDescriptorSet
结构体数组为参数。
VkWriteDescriptorSet descriptorWrite = {}; descriptorWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; descriptorWrite.dstSet = descriptorSets[i]; descriptorWrite.dstBinding = 0; descriptorWrite.dstArrayElement = 0;
The first two fields specify the descriptor set to update and the binding. We gave our uniform buffer binding index 0
. Remember that descriptors can be arrays, so we also need to specify the first index in the array that we want to update. We're not using an array, so the index is simply 0
.
前2个字段指定了要更新和的描述符和绑定。我们指定我们的uniform buffer绑定索引为0
。记住,描述符可以是数组,所以我们也要指定数组的第一个索引that我们想要更新。我们不用数组,所以索引是0
。
descriptorWrite.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; descriptorWrite.descriptorCount = 1;
We need to specify the type of descriptor again. It's possible to update multiple descriptors at once in an array, starting at index dstArrayElement
. The descriptorCount
field specifies how many array elements you want to update.
我们需要再次指定描述符的类型。可以同时更新一个数组里的多个描述符,从索引dstArrayElement
开始。descriptorCount
字段指定了你想要更新数组里的多少个元素。
descriptorWrite.pBufferInfo = &bufferInfo; descriptorWrite.pImageInfo = nullptr; // Optional descriptorWrite.pTexelBufferView = nullptr; // Optional
The last field references an array with descriptorCount
structs that actually configure the descriptors. It depends on the type of descriptor which one of the three you actually need to use. The pBufferInfo
field is used for descriptors that refer to buffer data, pImageInfo
is used for descriptors that refer to image data, and pTexelBufferView
is used for descriptors that refer to buffer views. Our descriptor is based on buffers, so we're using pBufferInfo
.
最后一个字段引用descriptorCount
结构体数组that实际配置描述符。它依赖于描述符的类型(你从三个中选择一个)。pBufferInfo
字段用于引用buffer数据的描述符,pImageInfo
用于引用image数据的描述符,pTexelBufferView
用于引用buffer视图的描述符。我们的描述符基于buffer,所以我们使用pBufferInfo
。
vkUpdateDescriptorSets(device, 1, &descriptorWrite, 0, nullptr);
The updates are applied using vkUpdateDescriptorSets
. It accepts two kinds of arrays as parameters: an array of VkWriteDescriptorSet
and an array of VkCopyDescriptorSet
. The latter can be used to copy descriptors to each other, as its name implies.
用vkUpdateDescriptorSets
进行更新。它接收2个数组为参数:VkWriteDescriptorSet
数组和VkCopyDescriptorSet
数组。后者可以用于复制描述符到另一个,如其名所示。
Using descriptor sets 使用描述符set
We now need to update the createCommandBuffers
function to actually bind the right descriptor set for each swap chain image to the descriptors in the shader with cmdBindDescriptorSets
. This needs to be done before the vkCmdDrawIndexed
call:
我们现在需要跟新函数createCommandBuffers
to用cmdBindDescriptorSets
为交换链的每个image绑定正确的描述符set到shader的描述符。这需要在调用vkCmdDrawIndexed
之前完成:
vkCmdBindDescriptorSets(commandBuffers[i], VK_PIPELINE_BIND_POINT_GRAPHICS, pipelineLayout, 0, 1, &descriptorSets[i], 0, nullptr); vkCmdDrawIndexed(commandBuffers[i], static_cast<uint32_t>(indices.size()), 1, 0, 0, 0);
Unlike vertex and index buffers, descriptor sets are not unique to graphics pipelines. Therefore we need to specify if we want to bind descriptor sets to the graphics or compute pipeline. The next parameter is the layout that the descriptors are based on. The next three parameters specify the index of the first descriptor set, the number of sets to bind, and the array of sets to bind. We'll get back to this in a moment. The last two parameters specify an array of offsets that are used for dynamic descriptors. We'll look at these in a future chapter.
不像顶点和索引buffer,描述符set对图形管道不是唯一的。因此我们需要指定我们十分想绑定描述符set到图形或计算管道。下一个参数是描述符基于的布局。下3个参数指定加一个描述符set的索引、要绑定的set的数量和要绑定的set的数组。我们稍后再来谈它。最后2个参数指定偏移量数组that用于动态描述符。我们将在后续章节再说。
If you run your program now, then you'll notice that unfortunately nothing is visible. The problem is that because of the Y-flip we did in the projection matrix, the vertices are now being drawn in clockwise order instead of counter-clockwise order. This causes backface culling to kick in and prevents any geometry from being drawn. Go to the createGraphicsPipeline
function and modify the frontFace
in VkPipelineRasterizationStateCreateInfo
to correct this:
如果你现在运行程序,那么你会注意到什么都不显示。问题在于我们在投影矩阵做的Y翻转,顶点按顺时针顺序绘制,而不是逆时针。这让背面剔除去掉了所有几何体。找到createGraphicsPipeline
函数,修改VkPipelineRasterizationStateCreateInfo
中的frontFace
为如下:
rasterizer.cullMode = VK_CULL_MODE_BACK_BIT;
rasterizer.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
Run your program again and you should now see the following:
再次运行程序,你应当看到下述情景:
The rectangle has changed into a square because the projection matrix now corrects for aspect ratio. The updateUniformBuffer
takes care of screen resizing, so we don't need to recreate the descriptor set inrecreateSwapChain
.
矩形变成了正方形,因为投影矩阵修正了aspect比例。updateUniformBuffer
处理窗口resize问题,所以我们不需在inrecreateSwapChain
中重建描述符。
Alignment requirements 对齐需求
One thing we've glossed over so far is how exactly the data in the C++ structure should match with the uniform definition in the shader. It seems obvious enough to simply use the same types in both:
我们一直回避的一件事是,C++结构体中的数据如何与shader中的uniform定义匹配。看起来很明显to用相同的类型:
struct UniformBufferObject { glm::mat4 model; glm::mat4 view; glm::mat4 proj; }; layout(binding = 0) uniform UniformBufferObject { mat4 model; mat4 view; mat4 proj; } ubo;
However, that's not all there is to it. For example, try modifying the struct and shader to look like this:
但是,并非完全如此。例如,修改结构体和shader成这样:
struct UniformBufferObject { glm::vec2 foo; glm::mat4 model; glm::mat4 view; glm::mat4 proj; }; layout(binding = 0) uniform UniformBufferObject { vec2 foo; mat4 model; mat4 view; mat4 proj; } ubo;
Recompile your shader and your program and run it and you'll find that the colorful square you worked so far has disappeared! That's because we haven't taken into account the alignment requirements.
重新编译你的shader和程序,运行之,你会发现彩色四边形消失了!这是因为我们没有考虑对齐需求。
Vulkan expects the data in your structure to be aligned in memory in a specific way, for example:
Vulkan期待你的结构体中的数据在内存中以一个特定的方式对齐,例如:
- Scalars have to be aligned by N (= 4 bytes given 32 bit floats). 向量必须对齐N(对于32位浮点数,=4字节)
- A
vec2
must be aligned by 2N (= 8 bytes)vec2
必须对齐2N(=8字节) - A
vec3
orvec4
must be aligned by 4N (= 16 bytes)vec3
或vec4
必须对齐4N(=16字节) - A nested structure must be aligned by the base alignment of its members rounded up to a multiple of 16. 嵌套结构体必须对齐……
- A
mat4
matrix must have the same alignment as avec4
.mat4
矩阵必须与vec4
有相同的对齐
You can find the full list of alignment requirements in the specification.
你可以在里找到完整的对齐需求列表。
Our original shader with just three mat4
fields already met the alignment requirements. As each mat4
is 4 x 4 x 4 = 64 bytes in size, model
has an offset of 0
, view
has an offset of 64 and proj
has an offset of 128. All of these are multiples of 16 and that's why it worked fine.
我们最初的shader有3个mat4
字段,它已经符合了对齐要求。每个mat4
是4 x 4 x 4 = 64字节,model
的偏移量是0
,view
的偏移量是64,proj
的偏移量是128.所以这些都是16的倍数,所以它工作得很好。
The new structure starts with a vec2
which is only 8 bytes in size and therefore throws off all of the offsets. Now model
has an offset of 8
, view
an offset of 72
and proj
an offset of 136
, none of which are multiples of 16. To fix this problem we can use the alignas
specifier introduced in C++11:
新结构体以vec2
开始,它只有8字节,因此脱离了偏移量的规矩。现在model
的偏移量为8
,view
的偏移量为72
,proj
的偏移量为136
,全都不是16的倍数。为修复这个问题,我们可以用C++11引入的alignas
标识符:
struct UniformBufferObject { glm::vec2 foo; alignas(16) glm::mat4 model; glm::mat4 view; glm::mat4 proj; };
If you now compile and run your program again you should see that the shader correctly receives its matrix values once again.
如果你现在编译运行你的程序,你应当会看到shader正确地接收了矩阵值。
Luckily there is a way to not have to think about these alignment requirements most of the time. We can define GLM_FORCE_DEFAULT_ALIGNED_GENTYPES
right before including GLM:
幸运的是,有一个办法可以在大多数时候不需考虑这些对齐需求。我们可以在包含GLM前定义GLM_FORCE_DEFAULT_ALIGNED_GENTYPES
:
#define GLM_FORCE_RADIANS #define GLM_FORCE_DEFAULT_ALIGNED_GENTYPES #include <glm/glm.hpp>
This will force GLM to use a version of vec2
and mat4
that has the alignment requirements already specified for us. If you add this definition then you can remove the alignas
specifier and your program should still work.
这会强制GLM用已经对齐了的vec2
和mat4
。如果你添加这个宏定义,那么你可以去掉alignas
标识符,你的程序仍旧可以工作。
Unfortunately this method can break down if you start using nested structures. Consider the following definition in the C++ code:
不幸的是这个方法会失败if你使用枪套结构体。考虑下述C++代码的定义:
struct Foo { glm::vec2 v; }; struct UniformBufferObject { Foo f1; Foo f2; };
And the following shader definition:
还有下述shader定义:
struct Foo { vec2 v; }; layout(binding = 0) uniform UniformBufferObject { Foo f1; Foo f2; } ubo;
In this case f2
will have an offset of 8
whereas it should have an offset of 16
since it is a nested structure. In this case you must specify the alignment yourself:
此时f2
的偏移量为8
,而它本应当是16
,因为它是嵌套结构体。此时你必须亲自指定对齐:
struct UniformBufferObject { Foo f1; alignas(16) Foo f2; };
These gotchas are a good reason to always be explicit about alignment. That way you won't be caught off guard by the strange symptoms of alignment errors.
这些是个好理由to总数显式地声明对齐。这样你就不会被奇怪的对齐错误搞蒙圈了。
struct UniformBufferObject { alignas(16) glm::mat4 model; alignas(16) glm::mat4 view; alignas(16) glm::mat4 proj; };
Don't forget to recompile your shader after removing the foo
field.
不要在去掉foo
字段之后忘记重新编译你的shader。
Multiple descriptor sets 多个描述符set
As some of the structures and function calls hinted at, it is actually possible to bind multiple descriptor sets simultaneously. You need to specify a descriptor layout for each descriptor set when creating the pipeline layout. Shaders can then reference specific descriptor sets like this:
如某些结构体和函数提示的,实际上可以同时绑定多个描述符set。创建管道布局时,你需要为每个描述符set指定一个描述符布局。然后shader就可以像这样引用特定的描述符set了:
layout(set = 0, binding = 0) uniform UniformBufferObject { ... }
You can use this feature to put descriptors that vary per-object and descriptors that are shared into separate descriptor sets. In that case you avoid rebinding most of the descriptors across draw calls which is potentially more efficient.
你可以用此特性设置逐对象的描述符和在不同描述符set间共享的描述符。那样你就避免了在多次绘制调用中重新绑定大多数描述符which可能更高效。
C++ code / Vertex shader / Fragment shader
[译]Vulkan教程(25)描述符池和set
微信扫码,自愿捐赠。天涯同道,共谱新篇。
微信捐赠不显示捐赠者个人信息,如需要,请注明联系方式。 |