参考资料
简述
我们现在可以为每个顶点传递任意属性到顶点着色器,但是全局变量呢?我们将从本章开始讨论三维图形,这需要一个模型视图投影矩阵。我们可以将其作为顶点数据包含,但这是对内存的浪费,而且每当变换发生变化时,都需要我们更新顶点缓冲区。然而这种转换可能在每一帧都有。
在Vulkan中解决这个问题的正确方法是使用资源描述符(resource descriptor)。描述符是着色器自由访问缓冲区和图像等资源的一种方式。我们将设置一个包含变换矩阵的缓冲区,并让顶点着色器通过描述符访问它们。描述符的使用包括三个部分:
- 在管道创建期间指定描述符布局
- 从描述符池分配描述符集
- 渲染期间绑定描述符集
描述符是表示着色器资源的不透明数据结构,比如缓冲区、缓冲区视图、图像视图、采样器或组合图像采样器。描述符被组织成描述符集,这些描述符集在命令记录期间被绑定,以便在后续的绘制命令中使用。每个描述符集中内容的安排由描述符集布局决定,该布局决定了可以在其中存储哪些描述符。管道可使用的描述符集布局序列在管道布局中指定。每个管道对象最多可以使用maxBoundDescriptorSets(参见限制)描述符集。
描述符布局指定管道要访问的资源类型,就像渲染过程指定要访问的附件类型一样。描述符集指定将绑定到描述符的实际缓冲区或图像资源,就像帧缓冲区指定要绑定到渲染过程附件的实际图像视图一样。然后为绘图命令绑定描述符集,就像顶点缓冲区和帧缓冲区一样。
着色器通过装饰有描述符集和绑定数的变量访问资源,这些变量将它们连接到描述符集中的描述符。着色器接口到绑定描述符集的映射在着色器资源接口部分描述。着色器也可以通过64位地址使用物理存储缓冲区访问,而不需要通过描述符来访问缓冲区。
描述符有很多种类型,这里使用统一缓冲区对象(UBO)。如下所示:
struct UniformBufferObject {
glm::mat4 model;
glm::mat4 view;
glm::mat4 proj;
};
我们可以使用GLM中的数据类型精确匹配着色器中的定义。矩阵中的数据与着色器期望的方式是二进制兼容的,因此我们可以稍后将UniformBufferObject的memcpy转换为VkBuffer。
需要更改顶点着色器:
#version 450
#extension GL_ARB_separate_shader_objects : enable
layout(binding = 0) uniform UniformBufferObject {
mat4 model;
mat4 view;
mat4 proj;
} ubo;
layout(location = 0) in vec2 inPosition;
layout(location = 1) in vec3 inColor;
layout(location = 0) out vec3 fragColor;
void main() {
gl_Position = ubo.proj * ubo.view * ubo.model * vec4(inPosition, 0.0, 1.0);
fragColor = inColor;
}
绑定指令类似于属性的位置指令。 我们将在描述符布局中引用此绑定。 更改了带有gl_Position的行,以使用转换来计算剪辑坐标中的最终位置。 与2D三角形不同,剪辑坐标的最后一个分量可能不是1,这在转换为屏幕上的最终归一化设备坐标时将导致除法。 这在透视投影中用作透视划分,对于使较近的对象看起来比较远的对象看起来更大,这是必不可少的。
一. 描述符集布局
我们需要提供着色器中用于管道创建的每个描述符绑定的详细信息,就像我们必须为每个顶点属性及其位置索引所做的那样。我们将设置一个新函数来定义所有这些信息,称为createDescriptorSetLayout。在创建管道之前应该调用:
void initVulkan() {
...
createDescriptorSetLayout();
createGraphicsPipeline();
...
}
void createDescriptorSetLayout() {
VkDescriptorSetLayoutBinding uboLayoutBinding = {};
// 指定在着色器中使用的绑定
uboLayoutBinding.binding = 0;
// 描述符的类型
uboLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
uboLayoutBinding.descriptorCount = 1;
// 指定描述符将在顶点着色器阶段被引用
uboLayoutBinding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
// pImmutableSamplers仅与图像采样描述符有关
uboLayoutBinding.pImmutableSamplers = nullptr;
}
每个绑定都需要通过VkDescriptorSetLayoutBinding结构来描述。前两个字段指定在着色器中使用的绑定和描述符的类型,该描述符是一个统一的缓冲区对象。着色器变量可能表示一个统一缓冲区对象的数组,而描述符计数指定该数组中值的数量。 例如,这可用于为骨骼动画指定骨骼中每个骨骼的变换。 我们的MVP转换位于单个统一缓冲区对象中,因此我们使用的描述符数为1。
1.1 VkDescriptorSetLayoutBinding
typedef struct VkDescriptorSetLayoutBinding {
uint32_t binding;
VkDescriptorType descriptorType;
uint32_t descriptorCount;
VkShaderStageFlags stageFlags;
const VkSampler* pImmutableSamplers;
} VkDescriptorSetLayoutBinding;
- binding是此条目的绑定号,并且与着色器阶段中具有相同绑定号的资源相对应。
- descriptorType是VkDescriptorType,它指定用于此绑定的资源描述符的类型。
- descriptorCount是绑定中包含的描述符数量,在着色器中以数组形式访问,除非描述符类型为VK_DESCRIPTOR_TYPE_INLINE_UNIFORM_BLOCK_EXT,在这种情况下,描述符计数是嵌入式统一块的字节大小。如果描述符计数为零,则此绑定条目被保留,并且不得使用设置的布局在任何管道内通过任何绑定从任何阶段访问资源。
- stageFlags成员是VkShaderStageFlagBits的位掩码,用于指定哪些管道着色器阶段可以访问此绑定的资源。 VK_SHADER_STAGE_ALL是一种简写形式,用于指定所有定义的着色器阶段,包括扩展定义的任何其他阶段,都可以访问该资源。如果stageFlags中未包含着色器阶段,则不得使用设置的布局在任何管道中通过此绑定从该阶段访问资源。除了限于片段着色器的输入附件之外,对于阶段的哪些组合可以使用描述符绑定没有任何限制,特别是图形阶段和计算阶段都可以使用绑定。
- pImmutableSamplers影响采样器的初始化。如果描述符类型指定VK_DESCRIPTOR_TYPE_SAMPLER或VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER类型描述符,则可以使用pImmutableSamplers初始化一组不可变的采样器。不可变的采样器永久绑定到设置的布局中,不得更改。不允许使用不可变采样器更新VK_DESCRIPTOR_TYPE_SAMPLER描述符,并且使用不可变采样器更新VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER描述符不会修改采样器(将更新图像视图,但会忽略采样器更新)。如果pImmutableSamplers不为NULL,则它指向一个采样器句柄数组,该数组将被复制到set布局中并用于相应的绑定。仅采样器句柄被复制;在最终使用集合布局以及使用它创建的任何描述符池和集合之前,不得破坏采样器对象。如果pImmutableSamplers为NULL,则采样器插槽是动态的,必须使用此布局将采样器句柄绑定到描述符集中。如果描述符类型不是这些描述符类型之一,则将忽略pImmutableSamplers。
1.1.1 VkDescriptorType
其中描述符的类型VkDescriptorType有如下取值:
typedef enum VkDescriptorType {
VK_DESCRIPTOR_TYPE_SAMPLER = 0,
VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER = 1,
VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE = 2,
VK_DESCRIPTOR_TYPE_STORAGE_IMAGE = 3,
VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER = 4,
VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER = 5,
VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER = 6,
VK_DESCRIPTOR_TYPE_STORAGE_BUFFER = 7,
VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC = 8,
VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC = 9,
VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT = 10,
VK_DESCRIPTOR_TYPE_INLINE_UNIFORM_BLOCK_EXT = 1000138000,
VK_DESCRIPTOR_TYPE_ACCELERATION_STRUCTURE_NV = 1000165000,
VK_DESCRIPTOR_TYPE_MAX_ENUM = 0x7FFFFFFF
} VkDescriptorType;
- VK_DESCRIPTOR_TYPE_SAMPLER: 指定采样器描述符
- VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER: 指定组合图像采样器描述符
- VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE: 指定采样图像描述符
- VK_DESCRIPTOR_TYPE_STORAGE_IMAGE: 指定存储映像描述符
- VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER: 指定统一纹理像素缓冲区描述符
- VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER: 指定存储纹理元素缓冲区描述符
- VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER: 统一缓冲区描述符
- VK_DESCRIPTOR_TYPE_STORAGE_BUFFER: 指定存储缓冲区描述符
- VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC: 指定动态统一缓冲区描述符
- VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC: 指定动态存储缓冲区描述符
- VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT: 指定输入附件描述符
- VK_DESCRIPTOR_TYPE_INLINE_UNIFORM_BLOCK_EXT: 指定内联统一块
1.1.2 VkShaderStageFlags
需要指定一个或多个着色器阶段的命令和结构使用位对应于阶段的位掩码来指定。可以设置为指定着色器阶段的位有:
typedef enum VkShaderStageFlagBits {
VK_SHADER_STAGE_VERTEX_BIT = 0x00000001,
VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT = 0x00000002,
VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT = 0x00000004,
VK_SHADER_STAGE_GEOMETRY_BIT = 0x00000008,
VK_SHADER_STAGE_FRAGMENT_BIT = 0x00000010,
VK_SHADER_STAGE_COMPUTE_BIT = 0x00000020,
VK_SHADER_STAGE_ALL_GRAPHICS = 0x0000001F,
VK_SHADER_STAGE_ALL = 0x7FFFFFFF,
VK_SHADER_STAGE_RAYGEN_BIT_NV = 0x00000100,
VK_SHADER_STAGE_ANY_HIT_BIT_NV = 0x00000200,
VK_SHADER_STAGE_CLOSEST_HIT_BIT_NV = 0x00000400,
VK_SHADER_STAGE_MISS_BIT_NV = 0x00000800,
VK_SHADER_STAGE_INTERSECTION_BIT_NV = 0x00001000,
VK_SHADER_STAGE_CALLABLE_BIT_NV = 0x00002000,
VK_SHADER_STAGE_TASK_BIT_NV = 0x00000040,
VK_SHADER_STAGE_MESH_BIT_NV = 0x00000080,
VK_SHADER_STAGE_FLAG_BITS_MAX_ENUM = 0x7FFFFFFF
} VkShaderStageFlagBits;
- VK_SHADER_STAGE_VERTEX_BIT: 顶点阶段
- VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT: 细分控制阶段
- VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT: 细分评估阶段
- VK_SHADER_STAGE_GEOMETRY_BIT: 几何图形阶段
- VK_SHADER_STAGE_FRAGMENT_BIT: 片段阶段
- VK_SHADER_STAGE_COMPUTE_BIT: 计算阶段
- VK_SHADER_STAGE_ALL_GRAPHICS: 用作速记的位的组合,用于指定上面定义的所有图形阶段(计算阶段除外)
- VK_SHADER_STAGE_ALL: 用作简写的位的组合,用于指定设备支持的所有着色器阶段,包括扩展引入的所有其他阶段
- VK_SHADER_STAGE_TASK_BIT_NV: 任务阶段
- VK_SHADER_STAGE_MESH_BIT_NV: 网格阶段
- VK_SHADER_STAGE_RAYGEN_BIT_NV: 射线生成阶段
- VK_SHADER_STAGE_ANY_HIT_BIT_NV: 任何命中阶段
- VK_SHADER_STAGE_CLOSEST_HIT_BIT_NV: 最接近的命中阶段
- VK_SHADER_STAGE_MISS_BIT_NV: 未命中阶段
- VK_SHADER_STAGE_INTERSECTION_BIT_NV: 相交阶段
- VK_SHADER_STAGE_CALLABLE_BIT_NV: 可调用阶段
1.2 创建VkDescriptorSetLayout
所有描述符绑定都合并到一个vkDescriptorSetLayout对象中:
VkDescriptorSetLayout descriptorSetLayout;
void createDescriptorSetLayout() {
VkDescriptorSetLayoutBinding uboLayoutBinding = {};
// 指定在着色器中使用的绑定
uboLayoutBinding.binding = 0;
// 描述符的类型
uboLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
uboLayoutBinding.descriptorCount = 1;
// 指定描述符将在顶点着色器阶段被引用
uboLayoutBinding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
// pImmutableSamplers仅与图像采样描述符有关
uboLayoutBinding.pImmutableSamplers = nullptr;
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!");
}
}
1.2.1 VkDescriptorSetLayoutCreateInfo
typedef struct VkDescriptorSetLayoutCreateInfo {
VkStructureType sType;
const void* pNext;
VkDescriptorSetLayoutCreateFlags flags;
uint32_t bindingCount;
const VkDescriptorSetLayoutBinding* pBindings;
} VkDescriptorSetLayoutCreateInfo;
- sType就是这种结构的类型, VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO
- pNext为NULL或指向特定于扩展的结构的指针
- flags是VkDescriptorSetLayoutCreateFlagBits的位掩码,用于指定描述符集布局创建的选项
- bindingCount是pBindings中的元素数
- pBindings是指向VkDescriptorSetLayoutBinding结构数组的指针
typedef enum VkDescriptorSetLayoutCreateFlagBits {
VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT = 0x00000002,
VK_DESCRIPTOR_SET_LAYOUT_CREATE_PUSH_DESCRIPTOR_BIT_KHR = 0x00000001,
VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT_EXT = VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT,
VK_DESCRIPTOR_SET_LAYOUT_CREATE_FLAG_BITS_MAX_ENUM = 0x7FFFFFFF
} VkDescriptorSetLayoutCreateFlagBits;
- VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT: 指定不得使用此布局分配描述符集,而是由vkCmdPushDescriptorSetKHR推送描述符
- VK_DESCRIPTOR_SET_LAYOUT_CREATE_PUSH_DESCRIPTOR_BIT_KHR: 指定描述符集使用此布局必须从创建一个描述符池分配VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT位集。描述符集布局创建这部分设置有备用限制描述符的最大数量每级和per-pipeline布局。non-UpdateAfterBind限制仅计数在没有此标志的情况下创建的集合中的描述符。UpdateAfterBind限制计算所有描述符,但是限制可能高于非UpdateAfterBind限制。
1.2.2 vkCreateDescriptorSetLayout
描述符集布局对象由零个或多个描述符绑定的数组定义。每个单独的描述符绑定由描述符类型、绑定中描述符数量的计数(数组大小)、可以访问绑定的一组着色器阶段以及(如果使用不可变采样器)采样器描述符数组指定。
创建描述符集布局可以使用函数: vkCreateDescriptorSetLayout
VkResult vkCreateDescriptorSetLayout(
VkDevice device,
const VkDescriptorSetLayoutCreateInfo* pCreateInfo,
const VkAllocationCallbacks* pAllocator,
VkDescriptorSetLayout* pSetLayout);
- device: 创建描述符集布局的逻辑设备
- pCreateInfo: 指向VkDescriptorSetLayoutCreateInfo结构的指针,它指定了描述符集布局对象的状态
- pAllocator: 控制主机内存分配
- pSetLayout: 指向VkDescriptorSetLayout句柄的指针,在这个句柄中返回结果描述符集布局对象
当然通过vkCreate*创建的对象或资源,一般需要显示销毁:
void cleanup() {
cleanupSwapChain();
vkDestroyDescriptorSetLayout(device, descriptorSetLayout, nullptr);
...
}
1.3 管道指定描述符集布局
我们需要在管道创建期间指定描述符集布局,以告诉Vulkan着色器将使用哪些描述符。描述符集布局在管道布局对象中指定。修改VkPipelineLayoutCreateInfo以引用布局对象:
VkPipelineLayoutCreateInfo pipelineLayoutInfo = {};
pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
pipelineLayoutInfo.setLayoutCount = 1;
pipelineLayoutInfo.pSetLayouts = &descriptorSetLayout;
这里留个悬念,为什么可以指定多个描述符集布局。
1.4 统一缓存
我们将指定包含着色器的UBO数据的缓冲区,但是我们需要首先创建这个缓冲区。我们将在每一帧将新数据复制到统一缓冲区,因此使用暂存缓冲区实际上没有任何意义。在这种情况下,它只会增加额外的开销,而且可能会降低性能。
我们应该有多个缓冲区,因为多个帧可能在同一时间绘制,我们不想更新缓冲区,准备下一帧,而前一帧仍在读取它!我们可以为每个帧或每个交换链图像提供统一的缓冲区。然而,由于我们需要从每个交换链映像所拥有的命令缓冲区引用统一缓冲区,因此最好也为每个交换链映像创建一个统一缓冲区。
VkBuffer indexBuffer;
VkDeviceMemory indexBufferMemory;
std::vector<VkBuffer> uniformBuffers;
std::vector<VkDeviceMemory> uniformBuffersMemory;
void initVulkan() {
...
createVertexBuffer();
createIndexBuffer();
createUniformBuffers();
...
}
void createUniformBuffers() {
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],
VK_SHARING_MODE_EXCLUSIVE);
}
}
我们将编写一个单独的函数,在每一帧用一个新的转换来更新统一缓冲区,所以这里没有vkMapMemory。
统一数据将被用于所有的draw调用,所以包含它的缓冲区只有在我们停止渲染时才会被销毁。因为它也取决于交换链图像的数量,这可能会在重新创建后改变,所以在cleanupSwapChain中清理它:
void cleanupSwapChain() {
...
for (size_t i = 0; i < uniformBuffers.size(); i++) {
vkDestroyBuffer(device, uniformBuffers[i], nullptr);
vkFreeMemory(device, uniformBuffersMemory[i], nullptr);
}
}
void recreateSwapChain() {
...
createFramebuffers();
createUniformBuffers();
createCommandBuffers();
}
1.5 更新统一缓存数据
在绘制更新交换链帧的时候更新统一缓存数据:
void drawFrame() {
...
updateUniformBuffer(imageIndex);
VkSubmitInfo submitInfo = {};
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
...
}
void updateUniformBuffer(uint32_t currentImage) {
}
updateUniformBuffer函数将在每帧生成一个新的变换,以使几何体旋转。
// 确保glm::rotate之类的函数使用弧度作为参数是必要的,以避免任何可能的混淆
#define GLM_FORCE_RADIANS
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
// chrono标准库标头公开了执行精确计时的功能
#include <chrono>
void updateUniformBuffer(uint32_t currentImage) {
static auto startTime = std::chrono::high_resolution_clock::now();
auto currentTime = std::chrono::high_resolution_clock::now();
// 计算时长
float time = std::chrono::duration<float, std::chrono::seconds::period>(currentTime - startTime).count();
UniformBufferObject ubo = {};
// 在统一缓冲区对象中定义模型,视图和投影转换。 使用时间变量,模型旋转将是围绕Z轴的简单旋转
// 意思是每秒旋转90度
ubo.model = glm::rotate(glm::mat4(1.0f), time * glm::radians(90.0f), glm::vec3(0.0f, 0.0f, 1.0f));
// 设置视图角度,从上方以45度角查看几何图形。 glm :: lookAt函数将眼睛位置,中心位置和上轴作为参数。
ubo.view = glm::lookAt(glm::vec3(2.0f, 2.0f, 2.0f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 0.0f, 1.0f));
// 使用具有45度垂直视场的透视投影。
// 其他参数是长宽比,近视平面和远视平面。 重要的是使用当前交换链范围来计算纵横比,以考虑调整大小后窗口的新宽度和高度。
ubo.proj = glm::perspective(glm::radians(45.0f), swapChainExtent.width / (float) swapChainExtent.height, 0.1f, 10.0f);
// GLM最初是为OpenGL(左手坐标系)设计的,将其中坐标的Y坐标反转。 最简单的补偿方法是在投影矩阵中翻转Y轴缩放比例上的符号。
// 如果不这样做,那么图像将被倒置呈现。
ubo.proj[1][1] *= -1;
// 将统一缓冲区对象中的数据复制到当前的统一缓冲区中。 与使用顶点缓冲区的方式完全相同,只是不需要暂存缓冲区(因为每帧都要更新):
void* data;
vkMapMemory(device, uniformBuffersMemory[currentImage], 0, sizeof(ubo), 0, &data);
memcpy(data, &ubo, sizeof(ubo));
vkUnmapMemory(device, uniformBuffersMemory[currentImage]);
}
当然此时编译运行程序是不成功的,因为我们仅仅是更新数据,但是没有将描述符集绑定到图形管道中。
二. 描述符
前面我们创建了描述符集布局,描述了可以绑定的描述符的类型,现在我们给统一缓冲区的每个缓冲创建一个描述符集,然后将其绑定到统一缓冲区描述符中。
2.1 描述符池
描述符集无法直接创建,它们必须从命令缓冲区之类的池中分配。描述符集又称为描述符池。 我们将编写一个新函数createDescriptorPool进行设置
void initVulkan() {
...
createUniformBuffers();
createDescriptorPool();
...
}
void createDescriptorPool() {
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());
// 创建描述符池
if (vkCreateDescriptorPool(device, &poolInfo, nullptr, &descriptorPool) != VK_SUCCESS) {
throw std::runtime_error("failed to create descriptor pool!");
}
}
首先需要使用VkDescriptorPoolSize结构来描述我们的描述符集将包含哪些描述符类型以及其中有多少个描述符类型。
2.1.1 VkDescriptorPoolSize
typedef struct VkDescriptorPoolSize {
VkDescriptorType type;
uint32_t descriptorCount;
} VkDescriptorPoolSize;
- type是描述符的类型
- descriptorCount是要分配的该类型的描述符数。如果类型是VK_DESCRIPTOR_TYPE_INLINE_UNIFORM_BLOCK_EXT,则descriptorCount是要为此类型的描述符分配的字节数
2.1.2 VkDescriptorPoolCreateInfo
typedef struct VkDescriptorPoolCreateInfo {
VkStructureType sType;
const void* pNext;
VkDescriptorPoolCreateFlags flags;
uint32_t maxSets;
uint32_t poolSizeCount;
const VkDescriptorPoolSize* pPoolSizes;
} VkDescriptorPoolCreateInfo;
- sType是此结构的类型, VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO
- pNext是NULL或指向扩展特定结构的指针
- flags是VkDescriptorPoolCreateFlagBits的位掩码,用于指定池中某些受支持的操作
- maxSets是可以从池中分配的描述符集的最大数量
- poolSizeCount是pPoolSizes中的元素数
- pPoolSizes是一个指向VkDescriptorPoolSize结构数组的指针,每个结构都包含一个描述符类型和要在池中分配的该类型的描述符数量
2.1.3 vkCreateDescriptorPool
描述符池维护着一个描述符池,从中分配描述符集。 描述符池是外部同步的,这意味着应用程序不得同时从多个线程中的同一池中分配和/或释放描述符集。
VkResult vkCreateDescriptorPool(
VkDevice device,
const VkDescriptorPoolCreateInfo* pCreateInfo,
const VkAllocationCallbacks* pAllocator,
VkDescriptorPool* pDescriptorPool);
- device: 创建描述符池的逻辑设备
- pCreateInfo: 指向VkDescriptorPoolCreateInfo结构的指针,该结构指定描述符池对象的状态
- pAllocator: 内存分配
- pDescriptorPool: 指向VkDescriptorPool句柄的指针,在该句柄中返回生成的描述符池对象
别忘了手动清理描述符池:
void cleanupSwapChain() {
...
vkDestroyDescriptorPool(device, descriptorPool, nullptr);
}
void recreateSwapChain() {
...
createUniformBuffers();
createDescriptorPool();
createCommandBuffers();
}
2.2 描述符集
有了描述符池就可以分配描述符集了。为此添加createDescriptorSets函数:
VkDescriptorPool descriptorPool;
std::vector<VkDescriptorSet> descriptorSets;
void initVulkan() {
...
createDescriptorPool();
createDescriptorSets();
...
}
void recreateSwapChain() {
...
createDescriptorPool();
createDescriptorSets();
...
}
void createDescriptorSets() {
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();
// 重置大小
descriptorSets.resize(swapChainImages.size());
// 内存分配描述符集
if (vkAllocateDescriptorSets(device, &allocInfo, descriptorSets.data()) != VK_SUCCESS) {
throw std::runtime_error("failed to allocate descriptor sets!");
}
// 配置描述符
for (size_t i = 0; i < descriptorSets.size(); i++) {
// 引用缓冲区的描述符(例如我们的统一缓冲区描述符)使用VkDescriptorBufferInfo结构进行配置
// 指定缓冲区以及其中包含描述符数据的区域。
VkDescriptorBufferInfo bufferInfo = {};
// 绑定缓冲区
bufferInfo.buffer = uniformBuffers[i];
bufferInfo.offset = 0;
bufferInfo.range = sizeof(UniformBufferObject);
VkWriteDescriptorSet descriptorWrite = {};
descriptorWrite.sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET;
descriptorWrite.dstSet = descriptorSets[i];
descriptorWrite.dstBinding = 0;
descriptorWrite.dstArrayElement = 0;
descriptorWrite.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
descriptorWrite.descriptorCount = 1;
descriptorWrite.pBufferInfo = &bufferInfo;
descriptorWrite.pImageInfo = nullptr; // Optional
descriptorWrite.pTexelBufferView = nullptr; // Optional
// 应用描述符集更新
vkUpdateDescriptorSets(device, 1, &descriptorWrite, 0, nullptr);
}
}
描述符集分配用VkDescriptorSetAllocateInfo结构描述。需要指定要从中分配的描述符池、要分配的描述符集的数量以及基于它们的描述符布局。
2.2.1 VkDescriptorSetAllocateInfo
typedef struct VkDescriptorSetAllocateInfo {
VkStructureType sType;
const void* pNext;
VkDescriptorPool descriptorPool;
uint32_t descriptorSetCount;
const VkDescriptorSetLayout* pSetLayouts;
} VkDescriptorSetAllocateInfo;
- sType是此结构的类型
- pNext是NULL或指向扩展特定结构的指针
- descriptorPool是从中分配集合的池
- descriptorSetCount确定要从池中分配的描述符集的数量
- pSetLayouts是一个指向描述符集布局数组的指针,每个成员指定如何分配相应的描述符集
2.2.2 vkAllocateDescriptorSets
VkResult vkAllocateDescriptorSets(
VkDevice device,
const VkDescriptorSetAllocateInfo* pAllocateInfo,
VkDescriptorSet* pDescriptorSets);
- device: 拥有描述符池的逻辑设备
- pAllocateInfo: 指向VkDescriptorSetAllocateInfo结构的指针,该结构描述分配参数
- pDescriptorSets: 指向VkDescriptorSet句柄数组的指针,在该数组中返回生成的描述符集对象
无需手动清理描述符集,因为在销毁描述符池时,会自动释放描述符集。 对vkAllocateDescriptorSets的调用将分配描述符集,每个描述符集具有一个统一的缓冲区描述符。
2.2.3 VkDescriptorBufferInfo
typedef struct VkDescriptorBufferInfo {
VkBuffer buffer;
VkDeviceSize offset;
VkDeviceSize range;
} VkDescriptorBufferInfo;
- buffer是缓冲区资源
- offset是从缓冲区开始的偏移量(以字节为单位)。 通过此描述符访问缓冲存储器将使用相对于此起始偏移量的寻址
- range是用于此描述符更新的大小(以字节为单位),或者是VK_WHOLE_SIZE以使用从偏移量到缓冲区末尾的范围
2.2.4 VkWriteDescriptorSet
typedef struct VkWriteDescriptorSet {
VkStructureType sType;
const void* pNext;
VkDescriptorSet dstSet;
uint32_t dstBinding;
uint32_t dstArrayElement;
uint32_t descriptorCount;
VkDescriptorType descriptorType;
const VkDescriptorImageInfo* pImageInfo;
const VkDescriptorBufferInfo* pBufferInfo;
const VkBufferView* pTexelBufferView;
} VkWriteDescriptorSet;
- sType是此结构的类型, VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET
- pNext是NULL或指向扩展特定结构的指针
- dstSet是要更新的目标描述符集
- dstBinding是该集合内的描述符绑定
- dstArrayElement是该数组中的起始元素。如果由dstSet和dstBinding标识的描述符绑定的描述符类型为VK_DESCRIPTOR_TYPE_INLINE_UNIFORM_BLOCK_EXT,则dstArrayElement指定绑定内的起始字节偏移量
- descriptorCount是要更新的描述符的数量(pImageInfo,pBufferInfo或pTexelBufferView中的元素数量,或者与pNext链中的VkWriteDescriptorSetInlineUniformBlockEXT结构的dataSize成员匹配的值,或者与pNext中的VkWriteDescriptorSetAccelerationStructureNV结构的AccelerationStructureCount匹配的值。链 )。如果由dstSet和dstBinding标识的描述符绑定的描述符类型为VK_DESCRIPTOR_TYPE_INLINE_UNIFORM_BLOCK_EXT,则描述符计数指定要更新的字节数
- descriptorType是VkDescriptorType,用于指定pImageInfo,pBufferInfo或pTexelBufferView中每个描述符的类型,如下所述。它必须与在dstBinding中为dstSet的VkDescriptorSetLayoutBinding中指定的类型相同。描述符的类型还控制描述符从哪个数组获取
- pImageInfo是指向VkDescriptorImageInfo结构数组的指针
- pBufferInfo是指向VkDescriptorBufferInfo结构数组的指针
- pTexelBufferView是指向VkBufferView句柄数组的指针
2.2.5 vkUpdateDescriptorSets
内存分配后,描述符集可以使用写和复制操作的组合进行更新。 要更新描述符集,调用:vkUpdateDescriptorSets
void vkUpdateDescriptorSets(
VkDevice device,
uint32_t descriptorWriteCount,
const VkWriteDescriptorSet* pDescriptorWrites,
uint32_t descriptorCopyCount,
const VkCopyDescriptorSet* pDescriptorCopies);
- device是更新描述符集的逻辑设备
- descriptorWriteCount是pDescriptorWrites数组中元素的数量
- pDescriptorWrites是指向VkWriteDescriptorSet结构数组的指针,该结构描述了要写入的描述符集
- descriptorCopyCount是pDescriptorCopies数组中元素的数量
- pDescriptorCopies是指向VkCopyDescriptorSet结构数组的指针,该结构描述了要在其间复制的描述符集
2.3 使用描述符集
现在,我们需要更新createCommandBuffers函数,以将每个交换链图像的正确描述符集实际绑定到具有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);
与顶点和索引缓冲区不同,描述符集不是图形管线所独有的。因此,我们需要指定是否要将描述符集绑定到图形或计算管道–vkCmdBindDescriptorSets。
现在运行程序,是看不到任何内容的。问题在于,由于我们在投影矩阵中进行了Y翻转,因此现在以顺时针顺序而不是逆时针顺序绘制了顶点。这将导致背面剔除,并阻止绘制任何几何图形。
在createGraphicsPipeline函数中VkPipelineRasterizationStateCreateInfo中修改frontFace来更正此问题:
rasterizer.cullMode = VK_CULL_MODE_BACK_BIT;
rasterizer.frontFace = VK_FRONT_FACE_COUNTER_CLOCKWISE;
frontFace是VkFrontFace结构体内的类型:
typedef enum VkFrontFace {
VK_FRONT_FACE_COUNTER_CLOCKWISE = 0,
VK_FRONT_FACE_CLOCKWISE = 1,
VK_FRONT_FACE_MAX_ENUM = 0x7FFFFFFF
} VkFrontFace;
- VK_FRONT_FACE_COUNTER_CLOCKWISE 指定具有正面积的三角形被认为是朝前的
- VK_FRONT_FACE_CLOCKWISE 指定具有负面积的三角形被认为是朝前的
如何计算面积的正负,后续研究。现在运行程序可以看到我们的图像在沿着逆时针旋转~
2.3.1 vkCmdBindDescriptorSets
绑定描述符集调用 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);
- commandBuffer是描述符集将绑定到的命令缓冲区
- pipelineBindPoint是一个VkPipelineBindPoint,它指示描述符是由图形管线还是由计算管线使用。 每个图形和计算都有一组单独的绑定点,因此绑定一个不会干扰另一个
- layout是一个VkPipelineLayout对象,用于对绑定进行编程
- firstSet是要绑定的第一个描述符集的集号
- descriptorSetCount是pDescriptorSets数组中元素的数量
- pDescriptorSets是指向VkDescriptorSet对象的句柄数组的指针,该对象描述了要写入的描述符集
- dynamicOffsetCount是pDynamicOffsets数组中的动态偏移量
- pDynamicOffsets是指向指定动态偏移量的uint32_t值数组的指针
vkCmdBindDescriptorSets导致编号为[firstSet…firstSet + descriptorSetCount-1]的集合使用存储在pDescriptorSets [0…descriptorSetCount-1]中的绑定用于后续渲染命令(根据pipelineBindPoint计算或图形)。以前通过这些集合应用的任何绑定都不再有效。
绑定后,描述符集会影响命令缓冲区中后续图形或计算命令的渲染,直到将不同的集绑定到相同的集编号,或者直到该集受到干扰(如管线布局兼容性中所述)为止。
在记录绘制或分派命令以使用该管道执行时,必须为管道中任何着色器访问的所有设定编号绑定一个兼容的描述符集。但是,如果管道中的所有着色器都不静态使用具有特定集合号的任何绑定,则即使该管道编号包括该集合号的非平凡描述符集合布局,也不需要为该集合号绑定任何描述符集。
如果要绑定的任何集合包括动态统一缓冲区或存储缓冲区,则pDynamicOffsets会为每个集合中每个动态描述符类型绑定中的每个数组元素包含一个元素。从pDynamicOffsets中获取值的顺序是:集合N的所有条目都在集合N + 1之前;在一个集合中,条目按描述符集合布局中的绑定号排序;在绑定数组中,元素是有序的。 dynamicOffsetCount必须等于要绑定的集合中动态描述符的总数。
用于动态统一和存储缓冲区绑定的有效偏移量是从pDynamicOffsets获取的相对偏移量与缓冲区的基地址加描述符集中的基本偏移量之和。动态统一和存储缓冲区绑定的范围是描述符集中指定的缓冲区范围。
每个pDescriptorSet都必须与layout指定的管道布局兼容。用于编程绑定的布局还必须与后续图形或计算命令中使用的管线兼容,如“管线布局兼容性”部分中所定义。
调用vkCmdBindDescriptorSets绑定的描述符集内容可能在以下时间使用:
- 对于使用VK_DESCRIPTOR_BINDING_UPDATE_AFTER_BIND_BIT位置1创建的描述符绑定,在将命令缓冲区提交到队列时,在着色器执行结果绘制和调度时或在两者之间的任何时间,内容都可能被消耗。
- 在命令的主机执行期间,或在着色器执行结果绘制和派发期间,或之间的任何时间。
因此,在描述符集合绑定的内容可能被消耗的第一个时间点和该命令在队列上完成执行之间,不得更改(由更新命令覆盖或释放)描述符集绑定的内容。
在执行vkCmdBindDescriptorSets时,pDynamicOffsets的内容将立即消耗。一旦所有待定用途都已完成,就可以更新和重用描述符集。
三. 总结
描述符的使用包括三个部分:
- 在管道创建期间指定描述符布局
- 从描述符池分配描述符集
- 渲染期间绑定描述符集
所谓描述符,就是用来描述着色器资源的不透明数据结构,比如缓冲区、缓冲区视图、图像视图、采样器或组合图像采样器。
接下来,我们尝试一些更让人激动的东西–贴图。