Vulkan之CommandBuffer

0)引言

我们的渲染操作,实际上是通过一系列的命令的形式来实现的。命令记录在commandBuffer里面,讲命令记录完毕后,会将命令提交到某个队列中执行,以实现渲染。

而vulkan的实际操作中并不是直接创建commandBuffer(这样比较低效,同时不利于buffer的管理),而是通过创建commandBuffer pool,然后再从commandBuffer Pool中分配commandBuffer来实现的。

1) commandBuffer Pool创建

1
2
3
4
5
VKAPI_ATTR VkResult VKAPI_CALL vkCreateCommandPool(
VkDevice device,
const VkCommandPoolCreateInfo* pCreateInfo,
const VkAllocationCallbacks* pAllocator,
VkCommandPool* pCommandPool);
1
2
3
4
5
6
typedef struct VkCommandPoolCreateInfo {
VkStructureType sType; //VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO
const void* pNext; // nullptr, 这个跟其他命令中的一样,指向另一个结构用于扩展
VkCommandPoolCreateFlags flags; // 标志位,决定了pool以及从pool中分配的commandBuffer的行为
uint32_t queueFamilyIndex; // 队列的索引,这个队列源于我们在创建屋里设备是选择的队列族
} VkCommandPoolCreateInfo;

flags标志位的取值范围

1
2
3
4
5
6
typedef enum VkCommandPoolCreateFlagBits {
VK_COMMAND_POOL_CREATE_TRANSIENT_BIT = 0x00000001, // 表示使用时间短,使用完将很快还给pool
VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT = 0x00000002, // 表示允许单个commandBuffer可通过重置二重用
VK_COMMAND_POOL_CREATE_PROTECTED_BIT = 0x00000004,
VK_COMMAND_POOL_CREATE_FLAG_BITS_MAX_ENUM = 0x7FFFFFFF
} VkCommandPoolCreateFlagBits;

2) commandBuffer

之前说了,commandBuffer是由command Pool分配的,分配命令如下:

1
2
3
4
VKAPI_ATTR VkResult VKAPI_CALL vkAllocateCommandBuffers(
VkDevice device,
const VkCommandBufferAllocateInfo* pAllocateInfo,
VkCommandBuffer* pCommandBuffers);

单纯来说,这个名利只是用来分配空间,实际上一般有多少个swapchainImage,我们就会设置多少个commandBuffer,因此pCommandBuffers实际上是一个数组的指针,这里vkAllocateCommandBuffers分配了整个数组的空间

1
2
3
4
5
6
7
typedef struct VkCommandBufferAllocateInfo {
VkStructureType sType; //VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO
const void* pNext; // nullptr, 这个跟其他命令中的一样,指向另一个结构用于扩展
VkCommandPool commandPool;
VkCommandBufferLevel level;
uint32_t commandBufferCount; // commandBuffer的个数
} VkCommandBufferAllocateInfo;

这个结构的其他内容都好理解,这个level是指的commandBuffer的级别,目前只有两种,主要是VK_COMMAND_BUFFER_LEVEL_PRIMARY, secondaryBuffer后续会讲到,也是非常有用的。

1
2
3
4
5
typedef enum VkCommandBufferLevel {
VK_COMMAND_BUFFER_LEVEL_PRIMARY = 0,
VK_COMMAND_BUFFER_LEVEL_SECONDARY = 1,
VK_COMMAND_BUFFER_LEVEL_MAX_ENUM = 0x7FFFFFFF
} VkCommandBufferLevel;

3) command record

当我们创建好了commandBuffer后,下面的一步操作就是命令的录制了,这个标准流程如下图所示:

这个图中的部分步骤是可以省略的,比如说设置scissor,viewport等,下面我们来看一下这些命令录制的内容

a) 首先是Begin comamndBuffer,用来启动命令缓冲区,将其重置为初始状态

1
2
3
VKAPI_ATTR VkResult VKAPI_CALL vkBeginCommandBuffer(
VkCommandBuffer commandBuffer,
const VkCommandBufferBeginInfo* pBeginInfo);
1
2
3
4
5
6
typedef struct VkCommandBufferBeginInfo {
VkStructureType sType; //VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO
const void* pNext; //nullptr
VkCommandBufferUsageFlags flags; //告诉vulkan命令缓冲区将会如何使用
const VkCommandBufferInheritanceInfo* pInheritanceInfo;
} VkCommandBufferBeginInfo;
1
2
3
4
5
6
typedef enum VkCommandBufferUsageFlagBits {
VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT = 0x00000001, // commandBuffer只会记录和执行一次,然后销毁
VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT = 0x00000002, // 在renderpass中使用,但是只在secondary commandBuffer中有效
VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT = 0x00000004, // commandBuffer可以多次执行或者暂停
VK_COMMAND_BUFFER_USAGE_FLAG_BITS_MAX_ENUM = 0x7FFFFFFF // 用于开启secondary commandBuffer,定义哪些状态是从调用当前secondary commandBuffer的primary commandBuffer中继承的。
} VkCommandBufferUsageFlagBits;

b)begin renderpass

启动renderpass,也就意味着启动了一个渲染流程。

1
2
3
4
VKAPI_ATTR void VKAPI_CALL vkCmdBeginRenderPass(
VkCommandBuffer commandBuffer,
const VkRenderPassBeginInfo* pRenderPassBegin,
VkSubpassContents contents);

1
2
3
4
5
6
7
8
9
typedef struct VkRenderPassBeginInfo {
VkStructureType sType; // VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO
const void* pNext;
VkRenderPass renderPass;
VkFramebuffer framebuffer;
VkRect2D renderArea; // 渲染的区域
uint32_t clearValueCount;
const VkClearValue* pClearValues; // 渲染前的清屏设置
} VkRenderPassBeginInfo;

c)绑定pipeline

1
2
3
4
VKAPI_ATTR void VKAPI_CALL vkCmdBindPipeline(
VkCommandBuffer commandBuffer,
VkPipelineBindPoint pipelineBindPoint, // 是图形管线,还是计算管线,或者光线追踪管线的标志位
VkPipeline pipeline);

d)绑定顶点缓存

1
2
3
4
5
6
VKAPI_ATTR void VKAPI_CALL vkCmdBindVertexBuffers(
VkCommandBuffer commandBuffer,
uint32_t firstBinding, // 第一个绑定点的索引,实际上指的vertexBuffer数组中的索引
uint32_t bindingCount, // 绑定的vertexBuffer的数量
const VkBuffer* pBuffers, // 指针,指向一个数组,这个数组中的内容是vertexBuffer
const VkDeviceSize* pOffsets); // 指针,指向一个数组,这个数组中的内容是Buffer的偏移量

e)绑定索引缓存

1
2
3
4
5
VKAPI_ATTR void VKAPI_CALL vkCmdBindIndexBuffer(
VkCommandBuffer commandBuffer,
VkBuffer buffer,
VkDeviceSize offset, // indexBuffer中的起始索引偏移量
VkIndexType indexType); // 索引的数据类型
1
2
3
4
5
6
7
8
typedef enum VkIndexType {
VK_INDEX_TYPE_UINT16 = 0,
VK_INDEX_TYPE_UINT32 = 1,
VK_INDEX_TYPE_NONE_KHR = 1000165000,
VK_INDEX_TYPE_UINT8_EXT = 1000265000,
VK_INDEX_TYPE_NONE_NV = VK_INDEX_TYPE_NONE_KHR,
VK_INDEX_TYPE_MAX_ENUM = 0x7FFFFFFF
} VkIndexType;

f)绑定描述符集

1
2
3
4
5
6
7
8
9
VKAPI_ATTR void VKAPI_CALL vkCmdBindDescriptorSets(
VkCommandBuffer commandBuffer,
VkPipelineBindPoint pipelineBindPoint, // 管线类型
VkPipelineLayout layout, // 管线布局
uint32_t firstSet, // 描述符集中的第一个值得索引
uint32_t descriptorSetCount, //描述符集的个数
const VkDescriptorSet* pDescriptorSets, // 描述符集
uint32_t dynamicOffsetCount, // 动态描述符集的个数
const uint32_t* pDynamicOffsets); // 动态描述符集的偏移

实际上DynamicOffsets是有很大的用处的,这个后面的章节会介绍。

g)设置Viewport和Scissor

实际上每次绘制是可以重新设置Viewport和Scissor的。

1
2
3
4
5
VKAPI_ATTR void VKAPI_CALL vkCmdSetViewport(
VkCommandBuffer commandBuffer,
uint32_t firstViewport,
uint32_t viewportCount,
const VkViewport* pViewports); // 一个指向VkViewport数组的指针,内容是要设置的viewport

1
2
3
4
5
VKAPI_ATTR void VKAPI_CALL vkCmdSetScissor(
VkCommandBuffer commandBuffer,
uint32_t firstScissor,
uint32_t scissorCount,
const VkRect2D* pScissors); // 一个指向VkRect2D数组的指针,内容是要设置的scissor

h)绘制

下面两个函数的区别就在于一个是基于索引的绘制,一个是直接绘制vertexBuffer,没有索引

1
2
3
4
5
6
7
VKAPI_ATTR void VKAPI_CALL vkCmdDrawIndexed(
VkCommandBuffer commandBuffer,
uint32_t indexCount, // 要绘制的顶点数
uint32_t instanceCount, // 要绘制的实例数
uint32_t firstIndex, // 第一个索引的位置
int32_t vertexOffset, // 在索引到顶点缓冲区之前添加到顶点索引的值
uint32_t firstInstance); //要绘制的第一个实例的实例ID

1
2
3
4
5
6
VKAPI_ATTR void VKAPI_CALL vkCmdDraw(
VkCommandBuffer commandBuffer,
uint32_t vertexCount,
uint32_t instanceCount,
uint32_t firstVertex,
uint32_t firstInstance);

除了这些还有vkCmdDrawIndirect,vkCmdDrawIndexedIndirect这两个间接绘制的命令,我们后续说明

i)结束render pass

1
2
VKAPI_ATTR void VKAPI_CALL vkCmdEndRenderPass(
VkCommandBuffer commandBuffer);

j)结束命令录制

1
2
VKAPI_ATTR VkResult VKAPI_CALL vkEndCommandBuffer(
VkCommandBuffer commandBuffer);

4)其他命令的录制

实际上上面记录的是渲染一张图像的命令绘制,其实还有其他的命令绘制方式,但是大体上的流程是一致的。如缓冲区间的数据的复制,移动等等。

5)回收命令缓冲区

我们调用vkAllocateCommandBuffers分配commandBuffer,当我们不再使用时,可以调用vkFreeCommandBuffers将commandBuffer归还到Pool里面,但是这个操作是个很重量级的操作。如果我们记录的命令是相似的命令的话,我们可以直接调用vkResetCommandBuffer来重置缓冲区,这个命令是将CommandBuffer重新设置到初始状态。

1
2
3
VKAPI_ATTR VkResult VKAPI_CALL vkResetCommandBuffer(
VkCommandBuffer commandBuffer,
VkCommandBufferResetFlags flags); // 重置缓冲区的附加操作

1
2
3
4
5
VKAPI_ATTR void VKAPI_CALL vkFreeCommandBuffers(
VkDevice device,
VkCommandPool commandPool,
uint32_t commandBufferCount,
const VkCommandBuffer* pCommandBuffers);

下面的命令可以直接将Pool中的所有commandBuffer全部重置。

1
2
3
4
VKAPI_ATTR VkResult VKAPI_CALL vkResetCommandPool(
VkDevice device,
VkCommandPool commandPool,
VkCommandPoolResetFlags flags);

6)命令提交

我们绘制了命令是没用的,需要将其提交到queue中才能使其产生效果。

1
2
3
4
5
VKAPI_ATTR VkResult VKAPI_CALL vkQueueSubmit(
VkQueue queue,
uint32_t submitCount,
const VkSubmitInfo* pSubmits,
VkFence fence);

1
2
3
4
5
6
7
8
9
10
11
typedef struct VkSubmitInfo {
VkStructureType sType; // VK_STRUCTURE_TYPE_SUBMIT_INFO
const void* pNext;
uint32_t waitSemaphoreCount;
const VkSemaphore* pWaitSemaphores; // 等待的信号量
const VkPipelineStageFlags* pWaitDstStageMask; // 队列开始执行前需要等待的管线阶段
uint32_t commandBufferCount;
const VkCommandBuffer* pCommandBuffers;
uint32_t signalSemaphoreCount;
const VkSemaphore* pSignalSemaphores; // 发出的信号量
} VkSubmitInfo;

vkQueueSubmit这里真正需要注意的就是Semaphore,这个需要好好理解(见前面的章节)。

7)commandBuffer Pool的销毁

1
vkDestroyCommandPool(device, commandPool, nullptr);
显示 Gitment 评论