目錄
- UniformBuffer
- DescriptorSetLayout 和 VkBuffer
- 頂點著色器定義
- 描述符布局(DescriptorSetLayout)
- 創建 UniformBuffer
- 描述符池(DescriptorSet Pool)
- 描述符集(DescriptorSet)
- 更新描述符集
- 使用描述符集
- 使用多個 Descriptor
UniformBuffer
本篇文檔是通過 Uniform Buffer
的使用進一步加深對 DescriptorSet
的理解
Vulkan
中,描述符是一種在著色器中訪問資源(比如緩沖區,圖像,采樣器等)的機制或者協議
每個描述符(Descriptor
)對應一個資源,代表 GPU
內存中的資源,比如 Uniform Buffer
, storage Buffer
, Texture
,Sampler
等
Vulkan
描述符集(VkDescriptorSet
)表示著色器可以與之交互的資源的集合,著色器是通過描述符讀取和解析資源中的數據,著色器中的綁定點和相應的描述符集中的綁定點必須一一對應
DescriptorSetLayout 和 VkBuffer
現在我們已經可以傳遞頂點的屬性(坐標和顏色等)給到頂點著色器,對于一些所有頂點都共享的屬性,比如頂點的變換矩陣,將其作為頂點屬性為每一個頂點都傳遞一份顯然是很低效的
Vulkan
提供了資源描述符(resource descriptor
)來解決這個問題,資源描述符是用來在著色器中訪問緩沖和圖像數據的一種方式,我們可以將變換矩陣存儲在一個緩沖中,然后通過描述符在著色器中訪問它,使用描述符需要進行下面三部分的設置:
- 在管線(
pipeline Creation
)創建時指定描述符布局(DescriptorSetLayout
) - 從描述符池(
DescriptorSet Pool
)中份分配描述符集(DescriptorSet
) - 渲染時綁定描述符集(
update DescriptorSet
)
描述符布局(DescriptorSetLayout
)用于指定可以被管線訪問的資源類型,類似于渲染流程指定可以被訪問的附著類型
描述符集指定要綁定到描述符上的緩沖和圖像資源,類似于幀緩存指定綁定到渲染流程附著上的圖像視圖
(just like a framebuffer specifies the actual image views to bind to render pass attachments
)
Note
: 本質上是一種定義資源如何訪問的機制或者協議
最后將描述符集綁定到繪制的指令上,類似綁定頂點緩沖和幀緩存到繪制指令上
有多種類型的描述符,在這里, 只使用到了 Uniform
緩沖對象(UBO
), 也有其他類型的描述符,它們的使用方式和 Uniform
緩沖對象類似
我們先用結構體定義我們在著色器中使用的 Uniform
的數據:
struct UniformBufferObject {glm::mat4 model;glm::mat4 view;glm::mat4 proj;
}
我們將要使用的 uniform
數據復制到 VkBuffer
中,然后通過一個 uniform
緩沖對象描述符(DescriptorSet
)在頂點著色器中訪問它:
layout(binding = 0) uniform UniformBufferObejct {mat4 model;mat4 view;mat4 proj;
}void main() {gl_Position = ubo.proj * ubo.view *ubo.model*vec4(inPostion, 0.0, 1.0)fragColor = inColor;
}
在現在的 demo
中,我們在每一幀更新模型(Model
),視圖(View
),投影矩陣(Projection
),可以讓矩陣在三維空間內進行旋轉
頂點著色器定義
#version 450
#extension GL_ARB_separate_shader_object :enablelayout(binding = 0) uniform UniformBufferObject {mat4 model;mat4 view;mat4 proj;
}layout(location = 0) in vec2 inPostion;
layout(location = 1) in vec3 inColor;layout(location = 0) out vec3 fragColorout gl_PerVertex {vec4 gl_Postion;
}void main() {gl_Position = ubo.proj + ubo.view + ubo.model * vec4(inPostion, 0,0, 1.0);fragColor = inColor;
}
uniform
,in
和 out
定義在著色器中出現的順序可以是任意的,任意代碼中 binding
修飾符類似于我們對頂點屬性使用的 location
修飾符,我們會在描述符布局引用這個 binding
值
gl_Position
使用變換矩陣最終得到矩形在三維空間內的裁剪坐標
描述符布局(DescriptorSetLayout)
我們需要在管線創建的時候提供著色器使用的每一個描述符綁定信息,
首先需要使用 createDescriptorSetLayout
的函數,并在管線創建前調用
void initVulkan() {createDecriptorSetLayout();createGraphicPipeline();
}void createDescriptorSetLayout() {
}
使用 vkDescriptorSetLayoutBinding
結構體來描述每一個綁定操作
void createDescriptorSetLayout() {VkSescriptorSetLayoutBinding ubolayoutBinding = {};uboLayoutBinding.binding = 0;uboLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;uboLayoutBinding.descriptorCount = 1;
}
binding
和 descriptorType
用于指定著色器使用的描述符綁定和描述符類型,這里我們指定的是一個 uniform
緩沖對象,
也可以使用 uniform
數組傳遞到著色器中,我們可以使用數組來制定骨骼動畫(skeletal aniamtion
)中使用的所有變換矩陣,
我們的 MVP
矩陣只需要使用一個 uniform
緩沖對象,所以我們將 descriptorCount
的值設置為 1
uboLayoutBinding.pImmutableSamplers = nullptr;
uboLayoutBinding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
我們還需要指定描述符在哪一個著色器階段被使用,stageFlags
這里我們只是在 Vertex Shader
中使用,
pImmutableSamplers
成員變量僅用于和圖像采樣相關的描述符
調用 vkCreateDescriptorSetLayout
函數創建 VkDescriptorSetLayout
對象,vkCreateDescriptorSetLayout
函數以 VkDescriptorSetLayoutCreateInfo
結構體作為參數
VkDescriptorSetLayoutCreateInfo layoutInfo = {};
layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
layoutInfo.bindingCount = 1;
layoutInfo.pBindings = &uboLayoutBinding;if (vkCreateDescriptorSetLayout(device, &layoutInfo, nullptr, &descriptorSetLayout) != VK_SUCCESS) {throw std::runtime_error("failed to create descriptor set layout!");
}
同時我們需要在創建 GraphicPipeline
的時候指定 DescriptorSetLayout
,也可以指定多個 DescriptorSetLayout
VkPipelineLayoutCreateInfo pipelineLayoutInfo = {};
pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
pipelineLayoutInfo.setLayoutCount = 1;
pipelineLayoutInfo.pSetLayouts = &descriptorSetLayout;
創建 UniformBuffer
我們需要創建包含 UniformBuffer
的緩沖對象(Uniform Buffer
),然后在每一幀中將新的 UBO
數據復制到 uniform
緩沖,由于需要頻繁的更新數據,使用暫存并不會帶來性能的提升
由于我們需要并行渲染多幀的緣故,我們需要多個 uniform
緩沖,來滿足多幀并行渲染的需要,我們可以并行渲染每一幀或者一個交換鏈圖像使用獨立的 uniform
緩沖對象
VkBuffer indexBuffer;
VkDeviceMemory indexBufferMemory;std::vector<VkBuffer> uniformBuffers;
std::vector<VkDeviceMemory> uniformBuffersMemory;void createUniformBuffer() {VkDeviceSize bufferSize = sizeof(UniformBufferObject);uniformBuffers.resize(swapChainImages.size());uniformBuffersMemory.resize(swapChainImages.size());for (size_t i = 0; i < swapChainImages.size(); i++) {createBuffer(bufferSize, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, uniformBuffers[i], uniformBuffersMemory[i]);}
}
最后更新 UniformBuffer
只需要將數據拷貝到 UniformBuffer Memory
對象的虛擬地址空間中
void* data;
vkMapMemory(device, uniformBuffersMemory[currentImage], 0, sizeof(ubo), 0, &data);
memcpy(data, &ubo, sizeof(ubo));
vkUnmapMemory(device, uniformBuffersMemory[currentImage]);
描述符池(DescriptorSet Pool)
描述符集不能被直接創建,需要通過描述符池(DescriptorSet Pool
)來分配,這里使用 createDescriptorPool
的函數來進行描述符池的創建
我們使用 VkDescriptorPoolSize
來決定 我們使用的 DescriptorSet
類型和數量
poolSize
是根據 swapChainImages
中的 image
的數量來決定的
VkDescriptorPoolSize poolSize = {};
poolSize.type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
poolSize.descriptorCount = static_cast<uint32_t>(swapChainImages.size());VkDescriptorPoolCreateInfo poolInfo = {};
poolInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO;
poolInfo.poolSizeCount = 1;
poolInfo.pPoolSizes = &poolSize;
poolInfo.maxSets = static_cast<uint32_t>(swapChainImages.size());VkDescriptorPool descriptorPool;...if (vkCreateDescriptorPool(device, &poolInfo, nullptr, &descriptorPool) != VK_SUCCESS) {throw std::runtime_error("failed to create descriptor pool!");
}
描述符集(DescriptorSet)
DescriptorSet
的分配(Allocate
)需要我們使用 vkAllocateDescriptorSets
分配出來,我們使用 VkDescriptorSetAllocateInfo
結構體
需要指定分配 DescriptorSet
使用的 DescriptorSetPool
,需要分配的描述符集數量,以及它們使用的 DescriptorSetLayout
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();VkDescriptorPool descriptorPool;
std::vector<VkDescriptorSet> descriptorSets;
...
descriptorSets.resize(swapChainImages.size());
if (vkAllocateDescriptorSets(device, &allocInfo, &descriptorSets[0]) != VK_SUCCESS) {throw std::runtime_error("failed to allocate descriptor sets!");
}
DescriptorSet
會在 DescriptorSetPool
銷毀的時候自動被銷毀,所以不需要我們顯式的清除
vkAllocateDescriptorSets
函數分配地描述符集對象,每一個都帶有 uniform
緩沖描述符(對應一個 uniform
的 vkBuffer
)
我們通過 vkDescriptorBufferInfo
結構體來配置引用的 vkBuffer
VkDescriptorBufferInfo
結構體可指定緩沖對象和可以訪問的數據范圍
for (size_t i = 0; i < swapChainImages.size(); i++) {VkDescriptorBufferInfo bufferInfo = {};bufferInfo.buffer = uniformBuffers[i];bufferInfo.offset = 0;bufferInfo.range = sizeof(UniformBufferObject);
}
如果需要使用整個緩沖,可以使將 range
成員變量范圍設置為 VK_WHOLE_SIZE
更新描述符集
VkWriteDescriptorSet descriptorWrite = {};
descriptorWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
descriptorWrite.dstSet = descriptorSets[i];
descriptorWrite.dstBinding = 0;
descriptorWrite.dstArrayElement = 0;
descriptorWrite.pBufferInfo = &bufferInfo;
descriptorWrite.pImageInfo = nullptr; // Optional
descriptorWrite.pTexelBufferView = nullptr; // Optional
dstSet
和 dstBinding
成員變量用于指定要更新的 DescriptorSet
和 綁定點(bindings
)
需要注意的是DescriptorSet
可以使用數組,所以我們需要指定數組的第一個元素作為索引,這里我們沒有使用,所以將索引指定為 0
pBufferInfo
成員變量用于指定描述符引用的緩沖數據,pImageInfo
成員變量用于指定描述符引用的圖像數據,
pTexelBufferView
成員變量 用于指定描述符引用的緩沖視圖,這里我們只使用了 pBufferInfo
成員變量
最后使用 vkUpdateDescriptorSets
更新描述符集
vkUpdateDescriptorSets(device, 1, &descriptorWrite, 0, nullptr);
vkUpdateDescriptorSets
函數可以接受兩個數組作為參數;
VkWriteDescriptorSet
結構體數組和 VkCopyDescriptorSet
結構體數組,后者被用來復制(copy
)描述符對
使用描述符集
現在修改 createCommandBuffer
函數為每個交換鏈圖像綁定對應的描述符集,這需要調用 cmdBindDescriptorSets
完成,需要在調用 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);
和頂點緩沖,索引緩沖不同,描述符集合并不是圖像管線所獨有的,所以我們需要指定我們綁定的是圖形管線還是計算管線,管線之后的參數是描述符使用的布局
后面的三個參數用于指定: 描述符集的第一個元素索引,綁定的描述符集的個數,以及用于綁定的描述符集數組,最后兩個參數用于指定動態描述符的數組偏移
使用多個 Descriptor
DescriptorSet 本身就是集合的概念,也就是可以創建 Descriptor 數組對應到一個 DescriptorSet 的綁定點上
VkDescriptorSetLayoutBinding binding = {};
binding.binding = 0; // 綁定點
binding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
binding.descriptorCount = 8; // 綁定了 8 個 uniform buffer
binding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;VkDescriptorSetLayoutCreateInfo layoutInfo = {};
layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
layoutInfo.bindingCount = 1;
layoutInfo.pBindings = &binding;
vkCreateDescriptorSetLayout(device, &layoutInfo, nullptr, &descriptorSetLayout);// glsl access descriptorset array
layout(set = 0, binding = 0) uniform UniformBuffer {mat4 model;vec4 color;
} ubo[8];void main() {mat4 modelMatrix = ubo[3].model; // 訪問第4個元素vec4 objectColor = ubo[gl_InstanceIndex].color; // 按實例索引訪問
}
也可以在一個綁定點上使用不同的 DescriptorSet index
,對應的 glsl
代碼如下
// 三個不同的descriptor set,但都使用binding = 0
layout(set = 0, binding = 0) uniform UniformBuffer { ... } cameraUBO;
layout(set = 1, binding = 0) uniform UniformBuffer { ... } modelUBO;
layout(set = 2, binding = 0) uniform sampler2D albedoTexture;
最后再更新一下 DescriptorSet
的示意圖,加深理解: