0)引言
我们的渲染操作,实际上是通过一系列的命令的形式来实现的。命令记录在commandBuffer里面,讲命令记录完毕后,会将命令提交到某个队列中执行,以实现渲染。
而vulkan的实际操作中并不是直接创建commandBuffer(这样比较低效,同时不利于buffer的管理),而是通过创建commandBuffer pool,然后再从commandBuffer Pool中分配commandBuffer来实现的。
1) commandBuffer Pool创建
1 | VKAPI_ATTR VkResult VKAPI_CALL vkCreateCommandPool( |
1 | typedef struct VkCommandPoolCreateInfo { |
flags标志位的取值范围1
2
3
4
5
6typedef 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
4VKAPI_ATTR VkResult VKAPI_CALL vkAllocateCommandBuffers(
VkDevice device,
const VkCommandBufferAllocateInfo* pAllocateInfo,
VkCommandBuffer* pCommandBuffers);
单纯来说,这个名利只是用来分配空间,实际上一般有多少个swapchainImage,我们就会设置多少个commandBuffer,因此pCommandBuffers实际上是一个数组的指针,这里vkAllocateCommandBuffers分配了整个数组的空间1
2
3
4
5
6
7typedef 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
5typedef 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 | VKAPI_ATTR VkResult VKAPI_CALL vkBeginCommandBuffer( |
1 | typedef struct VkCommandBufferBeginInfo { |
1 | typedef enum VkCommandBufferUsageFlagBits { |
b)begin renderpass
启动renderpass,也就意味着启动了一个渲染流程。1
2
3
4VKAPI_ATTR void VKAPI_CALL vkCmdBeginRenderPass(
VkCommandBuffer commandBuffer,
const VkRenderPassBeginInfo* pRenderPassBegin,
VkSubpassContents contents);
1 | typedef struct VkRenderPassBeginInfo { |
c)绑定pipeline
1 | VKAPI_ATTR void VKAPI_CALL vkCmdBindPipeline( |
d)绑定顶点缓存
1 | VKAPI_ATTR void VKAPI_CALL vkCmdBindVertexBuffers( |
e)绑定索引缓存
1 | VKAPI_ATTR void VKAPI_CALL vkCmdBindIndexBuffer( |
1 | typedef enum VkIndexType { |
f)绑定描述符集
1 | VKAPI_ATTR void VKAPI_CALL vkCmdBindDescriptorSets( |
实际上DynamicOffsets是有很大的用处的,这个后面的章节会介绍。
g)设置Viewport和Scissor
实际上每次绘制是可以重新设置Viewport和Scissor的。1
2
3
4
5VKAPI_ATTR void VKAPI_CALL vkCmdSetViewport(
VkCommandBuffer commandBuffer,
uint32_t firstViewport,
uint32_t viewportCount,
const VkViewport* pViewports); // 一个指向VkViewport数组的指针,内容是要设置的viewport
1 | VKAPI_ATTR void VKAPI_CALL vkCmdSetScissor( |
h)绘制
下面两个函数的区别就在于一个是基于索引的绘制,一个是直接绘制vertexBuffer,没有索引1
2
3
4
5
6
7VKAPI_ATTR void VKAPI_CALL vkCmdDrawIndexed(
VkCommandBuffer commandBuffer,
uint32_t indexCount, // 要绘制的顶点数
uint32_t instanceCount, // 要绘制的实例数
uint32_t firstIndex, // 第一个索引的位置
int32_t vertexOffset, // 在索引到顶点缓冲区之前添加到顶点索引的值
uint32_t firstInstance); //要绘制的第一个实例的实例ID
1 | VKAPI_ATTR void VKAPI_CALL vkCmdDraw( |
除了这些还有vkCmdDrawIndirect,vkCmdDrawIndexedIndirect这两个间接绘制的命令,我们后续说明
i)结束render pass
1 | VKAPI_ATTR void VKAPI_CALL vkCmdEndRenderPass( |
j)结束命令录制
1 | VKAPI_ATTR VkResult VKAPI_CALL vkEndCommandBuffer( |
4)其他命令的录制
实际上上面记录的是渲染一张图像的命令绘制,其实还有其他的命令绘制方式,但是大体上的流程是一致的。如缓冲区间的数据的复制,移动等等。
5)回收命令缓冲区
我们调用vkAllocateCommandBuffers分配commandBuffer,当我们不再使用时,可以调用vkFreeCommandBuffers将commandBuffer归还到Pool里面,但是这个操作是个很重量级的操作。如果我们记录的命令是相似的命令的话,我们可以直接调用vkResetCommandBuffer来重置缓冲区,这个命令是将CommandBuffer重新设置到初始状态。1
2
3VKAPI_ATTR VkResult VKAPI_CALL vkResetCommandBuffer(
VkCommandBuffer commandBuffer,
VkCommandBufferResetFlags flags); // 重置缓冲区的附加操作
1 | VKAPI_ATTR void VKAPI_CALL vkFreeCommandBuffers( |
下面的命令可以直接将Pool中的所有commandBuffer全部重置。1
2
3
4VKAPI_ATTR VkResult VKAPI_CALL vkResetCommandPool(
VkDevice device,
VkCommandPool commandPool,
VkCommandPoolResetFlags flags);
6)命令提交
我们绘制了命令是没用的,需要将其提交到queue中才能使其产生效果。1
2
3
4
5VKAPI_ATTR VkResult VKAPI_CALL vkQueueSubmit(
VkQueue queue,
uint32_t submitCount,
const VkSubmitInfo* pSubmits,
VkFence fence);
1 | typedef struct VkSubmitInfo { |
vkQueueSubmit这里真正需要注意的就是Semaphore,这个需要好好理解(见前面的章节)。
7)commandBuffer Pool的销毁
1 | vkDestroyCommandPool(device, commandPool, nullptr); |