[译]Vulkan教程(25)描述符布局和buffer
Descriptor layout and buffer 描述符布局和buffer
Introduction 入门
We're now able to pass arbitrary attributes to the vertex shader for each vertex, but what about global variables? We're going to move on to 3D graphics from this chapter on and that requires a model-view-projection matrix. We could include it as vertex data, but that's a waste of memory and it would require us to update the vertex buffer whenever the transformation changes. The transformation could easily change every single frame.
我们现在可以传递任意属性到顶点shaderfor每个订单,但是全局变量呢?从本章开始我们要折腾3D图形了,这需要用到模型-视图-投影矩阵。我们可以把它放到顶点数据里,但是那就浪费内存了,它还会要求我们随着变换的改变而更新顶点buffer。而变换是很可能每一帧都在改变的。
The right way to tackle this in Vulkan is to use resource descriptors. A descriptor is a way for shaders to freely access resources like buffers and images. We're going to set up a buffer that contains the transformation matrices and have the vertex shader access them through a descriptor. Usage of descriptors consists of three parts:
Vulkan中处理这个问题的正确方法是用资源描述符。描述符是一种让shader*地读写资源(例如buffer和image)的方式。我们要设置一个buffer,它包含变换矩阵,让顶点shader通过描述符来读取它。描述符的用法包含3部分:
- Specify a descriptor layout during pipeline creation在管理创建期间指定描述符布局
- Allocate a descriptor set from a descriptor pool 从描述符池分配描述符set
- Bind the descriptor set during rendering 在渲染期间绑定描述符set
The descriptor layout specifies the types of resources that are going to be accessed by the pipeline, just like a render pass specifies the types of attachments that will be accessed. A descriptor set specifies the actual buffer or image resources that will be bound to the descriptors, just like a framebuffer specifies the actual image views to bind to render pass attachments. The descriptor set is then bound for the drawing commands just like the vertex buffers and framebuffer.
描述符布局指定了这样的类型that要被管道读写,就像render pass指定了附件的类型that要被读写。描述符布局指定了要被绑定到描述符的buffer或image资源,就像帧缓存指定了要绑定到render pass附件的image视图。描述符set绑定for绘制命令-就像顶点buffer和帧缓存。
There are many types of descriptors, but in this chapter we'll work with uniform buffer objects (UBO). We'll look at other types of descriptors in future chapters, but the basic process is the same. Let's say we have the data we want the vertex shader to have in a C struct like this:
有许多类型的描述符,但是本章我们只用uniform buffer对象(UBO)。我们将在后续章节介绍其他类型的描述符,但基本流程是一样的。假设我们想给顶点shader的数据有这样的结构:
struct UniformBufferObject {
glm::mat4 model;
glm::mat4 view;
glm::mat4 proj;
};
Then we can copy the data to a VkBuffer
and access it through a uniform buffer object descriptor from the vertex shader like this:
然后我们可以复制数据到一个VkBuffer
,通过一个unform buffer对象描述符在顶点shader中读写它,代码如下:
layout(binding = ) uniform UniformBufferObject {
mat4 model;
mat4 view;
mat4 proj;
} ubo; void main() {
gl_Position = ubo.proj * ubo.view * ubo.model * vec4(inPosition, 0.0, 1.0);
fragColor = inColor;
}
We're going to update the model, view and projection matrices every frame to make the rectangle from the previous chapter spin around in 3D.
我们每帧都要更新model view和projection矩阵to让矩阵在3D空间旋转。
Vertex shader 顶点shader
Modify the vertex shader to include the uniform buffer object like it was specified above. I will assume that you are familiar with MVP transformations. If you're not, see the resource mentioned in the first chapter.
修改顶点shader,让它包含uniform buffer对象,像上面指定的那样。我假设你熟悉MVP变换。如果你不熟悉,看第一章提及的资料。
#version
#extension GL_ARB_separate_shader_objects : enable layout(binding = ) uniform UniformBufferObject {
mat4 model;
mat4 view;
mat4 proj;
} ubo; layout(location = ) in vec2 inPosition;
layout(location = ) in vec3 inColor; layout(location = ) out vec3 fragColor; void main() {
gl_Position = ubo.proj * ubo.view * ubo.model * vec4(inPosition, 0.0, 1.0);
fragColor = inColor;
}
Note that the order of the uniform
, in
and out
declarations doesn't matter. The binding
directive is similar to the location
directive for attributes. We're going to reference this binding in the descriptor layout. The line with gl_Position
is changed to use the transformations to compute the final position in clip coordinates. Unlike the 2D triangles, the last component of the clip coordinates may not be 1
, which will result in a division when converted to the final normalized device coordinates on the screen. This is used in perspective projection as the perspective division and is essential for making closer objects look larger than objects that are further away.
注意uniform
、in
和out
的顺序是无所谓的。描述属性的binding
指令和location
指令类似。我们要在描述符布局里引用这个绑定。用的行被修改了,它用变换来计算clip坐标的最终位置。不像2D三角形,clip坐标的最后一个元素可能不是1
,这会导致除法when转换为最终的标准设备坐标,呈现到屏幕。这用于透视投影,被称为透视除法,这是让近处的物体看起来更大的本质。
Descriptor set layout 描述符set布局
The next step is to define the UBO on the C++ side and to tell Vulkan about this descriptor in the vertex shader.
下一步是在C++端定义UBO,告诉Vulkan在顶点shader中的这个描述符。
struct UniformBufferObject {
glm::mat4 model;
glm::mat4 view;
glm::mat4 proj;
};
We can exactly match the definition in the shader using data types in GLM. The data in the matrices is binary compatible with the way the shader expects it, so we can later just memcpy
a UniformBufferObject
to a VkBuffer
.
使用GLM中的数据类型,我们可以严格匹配shader中的定义。矩阵中的数据是与shader要求的编码兼容的,所以我们可以之后直接memcpy
一个UniformBufferObject
到VkBuffer
。
We need to provide details about every descriptor binding used in the shaders for pipeline creation, just like we had to do for every vertex attribute and its location
index. We'll set up a new function to define all of this information called createDescriptorSetLayout
. It should be called right before pipeline creation, because we're going to need it there.
创建管道时,我们需要提供shader中每个描述符绑定的细节,就像我们为每个顶点属性及其location
索引做的那样。我们要设置一个新函数createDescriptorSetLayout
to定义所有这些信息。它应当在创建管道之前被调用,因为我们会在那里用到它。
void initVulkan() {
...
createDescriptorSetLayout();
createGraphicsPipeline();
...
} ... void createDescriptorSetLayout() { }
Every binding needs to be described through a VkDescriptorSetLayoutBinding
struct.
每个绑定需要通过VkDescriptorSetLayoutBinding
一个结构体来描述。
void createDescriptorSetLayout() {
VkDescriptorSetLayoutBinding uboLayoutBinding = {};
uboLayoutBinding.binding = ;
uboLayoutBinding.descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER;
uboLayoutBinding.descriptorCount = ;
}
The first two fields specify the binding
used in the shader and the type of descriptor, which is a uniform buffer object. It is possible for the shader variable to represent an array of uniform buffer objects, and descriptorCount
specifies the number of values in the array. This could be used to specify a transformation for each of the bones in a skeleton for skeletal animation, for example. Our MVP transformation is in a single uniform buffer object, so we're using a descriptorCount
of 1
.
前2个字段指定shader中使用的binding
和描述符的类型,这里是个UBO。Shader遍历有可能代表一个UBO数组,descriptorCount
指定数组的元素数。举个例子,这可以用于在骨骼动画中为每个骨骼指定一个变换。我们的MVP变换在一个单独的VBO里,所以我们设置descriptorCount
为1
。
uboLayoutBinding.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
We also need to specify in which shader stages the descriptor is going to be referenced. The stageFlags
field can be a combination of VkShaderStageFlagBits
values or the value VK_SHADER_STAGE_ALL_GRAPHICS
. In our case, we're only referencing the descriptor from the vertex shader.
我们还要指定描述符会被哪个shader阶段引用。stageFlags
字段可以是VkShaderStageFlagBits
值的组合或VK_SHADER_STAGE_ALL_GRAPHICS
。在本例中,我们只在顶点shader中引用描述符。
uboLayoutBinding.pImmutableSamplers = nullptr; // Optional
The pImmutableSamplers
field is only relevant for image sampling related descriptors, which we'll look at later. You can leave this to its default value.
pImmutableSamplers
字段只与image采样相关的描述符有关,我们以后再说。让它保持默认值就好。
All of the descriptor bindings are combined into a single VkDescriptorSetLayout
object. Define a new class member above pipelineLayout
:
所有的描述符绑定都组合进一个VkDescriptorSetLayout
对象。在pipelineLayout
上边定义新类成员:
VkDescriptorSetLayout descriptorSetLayout;
VkPipelineLayout pipelineLayout;
We can then create it using vkCreateDescriptorSetLayout
. This function accepts a simple VkDescriptorSetLayoutCreateInfo
with the array of bindings:
然后我们可以用vkCreateDescriptorSetLayout
创建它。这个函数接收VkDescriptorSetLayoutCreateInfo
,其引用了绑定数组:
VkDescriptorSetLayoutCreateInfo layoutInfo = {};
layoutInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO;
layoutInfo.bindingCount = ;
layoutInfo.pBindings = &uboLayoutBinding; if (vkCreateDescriptorSetLayout(device, &layoutInfo, nullptr, &descriptorSetLayout) != VK_SUCCESS) {
throw std::runtime_error("failed to create descriptor set layout!");
}
We need to specify the descriptor set layout during pipeline creation to tell Vulkan which descriptors the shaders will be using. Descriptor set layouts are specified in the pipeline layout object. Modify the VkPipelineLayoutCreateInfo
to reference the layout object:
我们需要在创建管道时指定描述符set布局to告诉Vulkan,shader会使用哪些描述符。描述符set布局在管道布局对象中指定。修改VkPipelineLayoutCreateInfo
to引用这个布局对象:
VkPipelineLayoutCreateInfo pipelineLayoutInfo = {};
pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
pipelineLayoutInfo.setLayoutCount = ;
pipelineLayoutInfo.pSetLayouts = &descriptorSetLayout;
You may be wondering why it's possible to specify multiple descriptor set layouts here, because a single one already includes all of the bindings. We'll get back to that in the next chapter, where we'll look into descriptor pools and descriptor sets.
你可能纳闷为什么这里可以指定多个描述符set布局,因为一个就已经包含了所有的绑定了。我们将在下一章讨论这个问题,where我们将研究描述符池和描述符set。
The descriptor layout should stick around while we may create new graphics pipelines i.e. until the program ends:
描述符布局应当持续存在,虽然我们可能创建洗难道图形管道,直到程序结束:
void cleanup() {
cleanupSwapChain(); vkDestroyDescriptorSetLayout(device, descriptorSetLayout, nullptr); ...
}
Uniform buffer
In the next chapter we'll specify the buffer that contains the UBO data for the shader, but we need to create this buffer first. We're going to copy new data to the uniform buffer every frame, so it doesn't really make any sense to have a staging buffer. It would just add extra overhead in this case and likely degrade performance instead of improving it.
下一章我们将指定包含UBO数据的buffer,但是我们需要先创建这个buffer。我们要每帧都复制新数据到UBO,所以使用暂存buffer就毫无道理了。这里会增加一些开销,降低性能。
We should have multiple buffers, because multiple frames may be in flight at the same time and we don't want to update the buffer in preparation of the next frame while a previous one is still reading from it! We could either have a uniform buffer per frame or per swap chain image. However, since we need to refer to the uniform buffer from the command buffer that we have per swap chain image, it makes the most sense to also have a uniform buffer per swap chain image.
我们应当准备多个buffer,因为多个帧可能在同时运行,我们可不想在前一帧还在读buffer时就更新它!我们可以让每帧有一个UBO,或者每个交换链image有个UBO。但是,既然我们需要从每个交换链image都有的命令buffer去引用UBO,最合理的方式就是也让每个交换链image都有1个UBO了。
To that end, add new class members for uniformBuffers
, and uniformBuffersMemory
:
为此,添加新类成员uniformBuffers
和uniformBuffersMemory
:
VkBuffer indexBuffer;
VkDeviceMemory indexBufferMemory; std::vector<VkBuffer> uniformBuffers;
std::vector<VkDeviceMemory> uniformBuffersMemory;
Similarly, create a new function createUniformBuffers
that is called after createIndexBuffer
and allocates the buffers:
类似的,创建新函数createUniformBuffers
that在createIndexBuffer
之后调用-去分配buffer:
void initVulkan() {
...
createVertexBuffer();
createIndexBuffer();
createUniformBuffers();
...
} ... void createUniformBuffers() {
VkDeviceSize bufferSize = sizeof(UniformBufferObject); uniformBuffers.resize(swapChainImages.size());
uniformBuffersMemory.resize(swapChainImages.size()); for (size_t i = ; 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]);
}
}
We're going to write a separate function that updates the uniform buffer with a new transformation every frame, so there will be no vkMapMemory
here. The uniform data will be used for all draw calls, so the buffer containing it should only be destroyed when we stop rendering. Since it also depends on the number of swap chain images, which could change after a recreation, we'll clean it up in cleanupSwapChain
:
我们要写一个单独的函数that每帧用新变换来更新UBO,所以这里不会用vkMapMemory
。uniform数据会被所有的draw call使用,所以包含它的buffer只应在我们停止渲染后才销毁。既然它也依赖交换链image的数量,which会在重建后改变,我们要在cleanupSwapChain
中清理它:
void cleanupSwapChain() {
... for (size_t i = ; i < swapChainImages.size(); i++) {
vkDestroyBuffer(device, uniformBuffers[i], nullptr);
vkFreeMemory(device, uniformBuffersMemory[i], nullptr);
}
}
This means that we also need to recreate it in recreateSwapChain
:
这意味着我们也要在recreateSwapChain
中重建它:
void recreateSwapChain() {
... createFramebuffers();
createUniformBuffers();
createCommandBuffers();
}
Updating uniform data 更新uniform数据
Create a new function updateUniformBuffer
and add a call to it from the drawFrame
function right after we know which swap chain image we're going to acquire:
创建新函数updateUniformBuffer
,在drawFrame
函数中调用它-在我们知道要请求哪个交换链image之后:
void drawFrame() {
... uint32_t imageIndex;
VkResult result = vkAcquireNextImageKHR(device, swapChain, std::numeric_limits<uint64_t>::max(), imageAvailableSemaphores[currentFrame], VK_NULL_HANDLE, &imageIndex); ... updateUniformBuffer(imageIndex); VkSubmitInfo submitInfo = {};
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; ...
} ... void updateUniformBuffer(uint32_t currentImage) { }
This function will generate a new transformation every frame to make the geometry spin around. We need to include two new headers to implement this functionality:
这个函数会在每帧生成一个新的变换to让几何体旋转。我们需要包含2个新的头文件to实现这个功能:
#define GLM_FORCE_RADIANS
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp> #include <chrono>
The glm/gtc/matrix_transform.hpp
header exposes functions that can be used to generate model transformations like glm::rotate
, view transformations like glm::lookAt
and projection transformations like glm::perspective
. The GLM_FORCE_RADIANS
definition is necessary to make sure that functions like glm::rotate
use radians as arguments, to avoid any possible confusion.
glm/gtc/matrix_transform.hpp
头文件暴露了函数that看用于生成模型变换(glm::rotate
)、视口变换(glm::lookAt
)和投影变换(glm::perspective
)。使用GLM_FORCE_RADIANS
定义可确保glm::rotate
这样的函数使用弧度为参数to避免可能的困惑。
The chrono
standard library header exposes functions to do precise timekeeping. We'll use this to make sure that the geometry rotates 90 degrees per second regardless of frame rate.
chrono
标准库头文件暴露了函数to做精确时间记录。我们用它确保几何体每秒旋转90度,无论帧率是多少。
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();
}
The updateUniformBuffer
function will start out with some logic to calculate the time in seconds since rendering has started with floating point accuracy.
函数开始时要计算从开始渲染到现在经过了多少秒,以浮点数级别的精确度。
We will now define the model, view and projection transformations in the uniform buffer object. The model rotation will be a simple rotation around the Z-axis using the time
variable:
我们现在要在UBO中定义model、view和projection变换。模型会用time
变量围绕Z轴旋转:
UniformBufferObject ubo = {};
ubo.model = glm::rotate(glm::mat4(1.0f), time * glm::radians(90.0f), glm::vec3(0.0f, 0.0f, 1.0f));
The glm::rotate
function takes an existing transformation, rotation angle and rotation axis as parameters. The glm::mat4(1.0f)
constructor returns an identity matrix. Using a rotation angle of time * glm::radians(90.0f)
accomplishes the purpose of rotation 90 degrees per second.
glm::rotate
函数接收一个现有的变换,旋转角度和旋转轴为参数。glm::mat4(1.0f)
构造器返回单位矩阵。使用旋转角度time * glm::radians(90.0f)
就实现了每秒旋转90度的目的。
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));
For the view transformation I've decided to look at the geometry from above at a 45 degree angle. The glm::lookAt
function takes the eye position, center position and up axis as parameters.
对于view变换,我觉得从45度角往下观察几何体。glm::lookAt
函数接收眼睛位置、中心点和up轴为参数。
ubo.proj = glm::perspective(glm::radians(45.0f), swapChainExtent.width / (float) swapChainExtent.height, 0.1f, 10.0f);
I've chosen to use a perspective projection with a 45 degree vertical field-of-view. The other parameters are the aspect ratio, near and far view planes. It is important to use the current swap chain extent to calculate the aspect ratio to take into account the new width and height of the window after a resize.
我决定透视投影的fov为45度。其他参数是aspect比例、近面和远面。重要的是,要用当前的交换链textent去计算aspect比例to顾及窗口修改大小后的宽度和高度。
ubo.proj[][] *= -;
GLM was originally designed for OpenGL, where the Y coordinate of the clip coordinates is inverted. The easiest way to compensate for that is to flip the sign on the scaling factor of the Y axis in the projection matrix. If you don't do this, then the image will be rendered upside down.
GLM最初是为OpenGl设计的,其clip坐标的Y坐标是反的。最简单的抵消方式是在投影矩阵上翻转缩放因子的Y轴。如果你不这样做,那么image会被上下相反地渲染。
All of the transformations are defined now, so we can copy the data in the uniform buffer object to the current uniform buffer. This happens in exactly the same way as we did for vertex buffers, except without a staging buffer:
所有的变换都已定义,我们可以复制UBO中的数据到当前uniform buffer了。这与我们之前处理顶点buffer时相同,只不过没有暂存buffer:
void* data;
vkMapMemory(device, uniformBuffersMemory[currentImage], , sizeof(ubo), , &data);
memcpy(data, &ubo, sizeof(ubo));
vkUnmapMemory(device, uniformBuffersMemory[currentImage]);
Using a UBO this way is not the most efficient way to pass frequently changing values to the shader. A more efficient way to pass a small buffer of data to shaders are push constants. We may look at these in a future chapter.
这样使用UBO不是最高效的方式to传递shader频繁改变的值。一个更高效的传递少量数据的方式是push常量。我们可能在以后的章节讨论它。
In the next chapter we'll look at descriptor sets, which will actually bind the VkBuffer
s to the uniform buffer descriptors so that the shader can access this transformation data.
下一章我们将讨论描述符set,which会实际上绑定VkBuffer
s到uniform buffer描述符,这样shader就可以读到变换数据了。