Vulkan 下的指令,比如绘制指令和内存传输指令并不是直接通过函数调用执行的。我们需要将所有要执行的操作记录在一个指令缓冲对象,然后提交给可以执行这些操作的队列才能执行。这使得我们可以在程序初始化时就准备好所有要指定的指令序列,在渲染时直接提交执行。也使得多线程提交指令变得更加容易。我们只需要在需要指定执行的使用,将指令缓冲对象提交给 Vulkan 处理接口。
示例demo:
//指令池对象,管理指令缓冲对象使用的内存,并负责指令缓冲对象的分配--11
VkCommandPool commandPool ;
//存储创建的指令缓冲对象,指令缓冲对象会在指令池对象被清除时自动被清除--11
std::vector<VkCommandBuffer> commandBuffers;
//创建指令池--11
void createCommandPool(){
/**
指令缓冲对象在被提交给我们之前获取的队列后,被 Vulkan 执行。每
个指令池对象分配的指令缓冲对象只能提交给一个特定类型的队列。在这
里,我们使用的是绘制指令,它可以被提交给支持图形操作的队列。
有下面两种用于指令池对象创建的标记,
可以提供有用的信息给Vulkan的驱动程序进行一定优化处理:
VK_COMMAND_POOL_CREATE_TRANSIENT_BIT:
使用它分配的指令缓冲对象被频繁用来记录新的指令
(使用这一标记可能会改变帧缓冲对象的内存分配策略)
VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT:
指令缓冲对象之间相互独立,不会被一起重置。
不使用这一标记,指令缓冲对象会被放在一起重置
这里我们只在程序初始化时记录指令到指令缓冲对象,
然后在程序的主循环中执行指令,所以,我们不使用上面这两个标记
*/
QueueFamilyIndices queueFamilyIndices =
findQueueFamilies(physicalDevice);
VkCommandPoolCreateInfo poolInfo = {};
poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
poolInfo.queueFamilyIndex = queueFamilyIndices.graphicsFamily;
poolInfo.flags = 0;
//指令池对象的创建
if(vkCreateCommandPool(device,&poolInfo,nullptr,
&commandPool) != VK_SUCCESS){
throw std::runtime_error("failed to create command pool!");
}
}
/**
指令缓冲对象,用来记录绘制指令
由于绘制操作是在帧缓冲上进行的,我们需要为交换链中的每一个图像分配一个指令缓冲对象
*/
//创建指令缓冲对象--11
void createCommandBuffers(){
commandBuffers.resize(swapChainFramebuffers.size());
//指定分配使用的指令池和需要分配的指令缓冲对象个数
VkCommandBufferAllocateInfo allocInfo = {};
allocInfo.sType =
VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
allocInfo.commandPool = commandPool;
/**
level 成员变量用于指定分配的指令缓冲对象是主要指令缓冲对象还是辅助指令缓冲对象:
VK_COMMAND_BUFFER_LEVEL_PRIMARY:
可以被提交到队列进行执行,但不能被其它指令缓冲对象调用。
VK_COMMAND_BUFFER_LEVEL_SECONDARY:
不能直接被提交到队列进行执行,但可以被主要指令缓冲对象调用执行
在这里,我们没有使用辅助指令缓冲对象,但辅助治理给缓冲对象的
好处是显而易见的,我们可以把一些常用的指令存储在辅助指令缓冲对象,
然后在主要指令缓冲对象中调用执行
*/
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
allocInfo.commandBufferCount = (uint32_t)commandBuffers.size();
//分配指令缓冲对象
if(vkAllocateCommandBuffers(device,&allocInfo,
commandBuffers.data())!= VK_SUCCESS){
throw std::runtime_error("failed to allocate command buffers!");
}
//记录指令到指令缓冲
for(size_t i=0;i<commandBuffers.size();i++){
//指定一些有关指令缓冲的使用细节
VkCommandBufferBeginInfo beginInfo = {};
beginInfo.sType =
VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
/**
flags 成员变量用于指定我们将要怎样使用指令缓冲。它的值可以是下面这些:
VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT:
指令缓冲在执行一次后,就被用来记录新的指令.
VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT:
这是一个只在一个渲染流程内使用的辅助指令缓冲.
VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT:
在指令缓冲等待执行时,仍然可以提交这一指令缓冲
*/
beginInfo.flags =
VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT;
//用于辅助指令缓冲,可以用它来指定从调用它的主要指令缓冲继承的状态
beginInfo.pInheritanceInfo = nullptr;
//指令缓冲对象记录指令后,调用vkBeginCommandBuffer函数会重置指令缓冲对象
//开始指令缓冲的记录操作
if(vkBeginCommandBuffer(commandBuffers[i],&beginInfo)!=VK_SUCCESS){
throw std::runtime_error(
"failed to begin recording command buffer.");
}
//指定使用的渲染流程对象
VkRenderPassBeginInfo renderPassInfo = {};
renderPassInfo.sType =VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
//指定使用的渲染流程对象
renderPassInfo.renderPass = renderPass;
//指定使用的帧缓冲对象
renderPassInfo.framebuffer = swapChainFramebuffers[i];
/**
renderArea指定用于渲染的区域。位于这一区域外的像素数据会处于未定义状态。
通常,我们将这一区域设置为和我们使用的附着大小完全一样.
*/
renderPassInfo.renderArea.offset = {0,0};
renderPassInfo.renderArea.extent = swapChainExtent;
VkClearValue clearColor = {0.0f, 0.0f, 0.0f, 1.0f};
//指定标记后,使用的清除值
renderPassInfo.clearValueCount = 1;
renderPassInfo.pClearValues = &clearColor;
/**
所有可以记录指令到指令缓冲的函数的函数名都带有一个 vkCmd 前缀,
并且这些函数的返回值都是 void,也就是说在指令记录操作完全结束前,
不用进行任何错误处理。
这类函数的第一个参数是用于记录指令的指令缓冲对象。第二个参数
是使用的渲染流程的信息。最后一个参数是用来指定渲染流程如何提供绘
制指令的标记,它可以是下面这两个值之一:
VK_SUBPASS_CONTENTS_INLINE:
所有要执行的指令都在主要指令缓冲中,没有辅助指令缓冲需要执行
VK_SUBPASS_CONTENTS_SECONDARY_COMMAND_BUFFERS:
有来自辅助指令缓冲的指令需要执行。
*/
//开始一个渲染流程
vkCmdBeginRenderPass( commandBuffers[i], &renderPassInfo,
VK_SUBPASS_CONTENTS_INLINE) ;
//绑定图形管线,第二个参数用于指定管线对象是图形管线还是计算管线
vkCmdBindPipeline(commandBuffers[i],VK_PIPELINE_BIND_POINT_GRAPHICS,
graphicsPipeline ) ;
/**
至此,我们已经提交了需要图形管线执行的指令,以及片段着色器使用的附着
*/
/**
vkCmdDraw参数:
1.记录有要执行的指令的指令缓冲对象
2. vertexCount:
尽管这里我们没有使用顶点缓冲,但仍然需要指定三个顶点用于三角形的绘制。
3.instanceCount:用于实例渲染,为 1 时表示不进行实例渲染
4.firstVertex:用于定义着色器变量 gl_VertexIndex 的值
5.firstInstance:用于定义着色器变量 gl_InstanceIndex 的值
*/
//开始调用指令进行三角形的绘制操作
vkCmdDraw( commandBuffers [ i ] , 3 , 1 , 0 , 0) ;
//结束渲染流程
vkCmdEndRenderPass( commandBuffers [ i ] ) ;
//结束记录指令到指令缓冲
if(vkEndCommandBuffer(commandBuffers[i]) != VK_SUCCESS){
throw std::runtime_error("failed to record command buffer!");
}
}
}
//销毁指令池对象--11
vkDestroyCommandPool(device,commandPool,nullptr);
基本命令缓冲区操作
在其他图形API中,应用程序可以通过进行API调用来设置诸如线宽之类的属性glLineWidth()。在幕后,驱动程序将此API调用转换为特定于GPU的命令,并将命令放入命令缓冲区。驱动程序还通过在应用程序视图之外创建和销毁命令缓冲区来管理命令缓冲区。最终,驱动程序将命令缓冲区“提交”到GPU以处理命令。
在Vulkan中,您创建一个命令缓冲区并进行类似的Vulkan API调用vkCmdSetLineWidth()以向命令缓冲区添加命令。由于每个GPU都有自己的“指令集”,因此驱动程序仍需要做一些工作来生成GPU特定的指令来设置线宽。
这里,驱动程序确定要插入命令缓冲区的适当二进制GPU指令,以指示GPU使用5的线宽来绘制后续行。您不需要查看实际的命令缓冲区内容,因为驱动程序正在为您执行这部分GPU编程。
命令缓冲池
由于创建和销毁单个命令缓冲区可能很昂贵,因此Vulkan使用命令缓冲池来管理命令缓冲区。使用命令缓冲池的动机包括:
1.某些应用程序使用短命令命令缓冲区,这意味着它们经常被创建和销毁。专用池分配器通常可以更有效地处理这些分配模式。
2.命令缓冲区内存的特殊之处在于它必须对CPU和GPU都可见。在许多系统中,内存到处理器(CPU或GPU)的映射只能使用大粒度来完成,这意味着小的命令缓冲区可能会浪费大量内存。
3.内存映射很昂贵,因为它通常涉及修改页表和使TLB缓存无效。最好映射一个较大的命令缓冲池并在其中分配单个命令缓冲区,而不是单独映射每个命令缓冲区。
命令缓冲池和队列系列
驱动程序使用适合于读取命令缓冲存储器的GPU硬件的内存分配属性来分配命令缓冲池。此类属性的示例包括内存对齐要求和缓存行为。
如果GPU硬件中存在多个硬件队列(如物理设备队列系列所述),则驱动程序可能需要分配具有不同内存分配属性的命令缓冲池,这些属性特定于每个GPU硬件队列。只要驱动程序知道包含命令缓冲区将使用的队列的队列系列,驱动程序就会为您处理这些详细信息。
使用命令缓冲区
创建命令缓冲区后,通过调用开始“录制” vkBeginCommandBuffer()。调用此函数会将命令缓冲区置于“记录”状态,并允许您调用将命令插入命令缓冲区的许多“vkCmd *”函数之一。您已经vkCmdSetLineWidth()在本节中看到了此示例。另一个例子是vkCmdDraw(),它告诉GPU绘制一些顶点。当您完成将命令插入命令缓冲区后,您将调用 vkEndCommandBuffer()以指示您已完成并将命令缓冲区从记录状态中取出并使其可供使用。
您将在后面的部分中看到实际填充命令缓冲区的代码。
完成命令缓冲区记录不会使GPU做任何事情。为了让GPU处理命令缓冲区,您必须使用它将其提交到GPU的队列中vkQueueSubmit()。在向GPU提交命令缓冲区之前,还有很多事情需要设置,这将在本教程的最后一节中进行
创建命令缓冲区
///5.创建命令缓冲区
//使用创建设备时要使用的队列
//实际上,您必须为应用程序打算使用的每个唯一队列系列创建命令缓冲池。
//由于在创建设备时仅指定了一个队列系列,因此一个命令缓冲池足以满足这些样本的要求
uint32_t graphics_queue_family_index=queue_info.queueFamilyIndex;
//命令缓冲池只能与一个队列系列相关联,构建命令缓冲区信息
VkCommandPoolCreateInfo cmd_pool_info = {};
cmd_pool_info.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
cmd_pool_info.pNext = NULL;
//指定队列系列索
cmd_pool_info.queueFamilyIndex = graphics_queue_family_index;
cmd_pool_info.flags = 0;
VkCommandPool cmd_pool;
//创建命令缓冲池
res = vkCreateCommandPool(device, &cmd_pool_info, NULL, &cmd_pool);
assert(res == VK_SUCCESS);
//从命令缓冲池中分配命令缓冲区
VkCommandBufferAllocateInfo cmd = {};
cmd.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
cmd.pNext = NULL;
cmd.commandPool = cmd_pool;
cmd.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
cmd.commandBufferCount = 1;//个数
VkCommandBuffer cmdBuffer;
//创建命令缓冲区
res = vkAllocateCommandBuffers(device, &cmd, &cmdBuffer);
assert(res == VK_SUCCESS);
VkCommandBuffer cmd_bufs[1] = {cmdBuffer};
vkFreeCommandBuffers(device, cmd_pool, 1, cmd_bufs);//释放资源
vkDestroyCommandPool(device, cmd_pool, NULL);//销毁资源