理解 Vulkan 指令缓存

Vulkan 指令缓存

在 Vulkan 中,指令缓存(Command Buffer)是用于记录和存储一系列绘图和计算指令的对象

这些指令将在 GPU 上执行,可以用于执行不同类型的工作,包括绑定顶点缓存、绑定流水线、录制渲染通道指令、设置视口和裁切矩形、设置绘制指令、执行图像和缓存内容的复制操作等。

指令缓存提交到硬件队列的过程和它们被执行的过程是异步进行的。

指令缓存的类型主要有两种:主指令缓存和次指令缓存。

  • 主指令缓存(primary command buffer):包含次指令缓存,负责执行它们,可以直接提交给队列执行。
  • 次指令缓存(secondary command buffer):不能直接提交给队列,必须嵌套在主指令缓存执行中。适用于记录可重用的指令或者多线程录制指令。

指令池

理解 Vulkan 指令缓存

一个应用程序中指令缓存的数量可能成百上千。Vulkan API 的设计是为了最大化地提升性能,指令缓存的分配通过指令池 VkCommandPool 来完成,从而降低多个指令缓存之间的资源创建带来的性能消耗。

指令缓存可以是一直存在的,它们只需要创建一次,就可以一直反复地使用。如果不想继续使用某一个指令缓存了,可以通过一个简单的休眠指令让它恢复到可复用的状态。

相比传统的“先销毁缓存,再创建一个新的缓存”这样的流程,上述方案是非常高效。

指令缓存的创建

创建指令池

首先,需要创建一个指令池。指令池用于分配指令缓存。

VkCommandPool commandPool;
VkCommandPoolCreateInfo poolInfo = {};
poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
poolInfo.queueFamilyIndex = graphicsQueueFamilyIndex; // 指定队列族索引
poolInfo.flags = 0; // 可以指定一些标志,如 VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT

if (vkCreateCommandPool(device, &poolInfo, nullptr, &commandPool) != VK_SUCCESS) {
    throw std::runtime_error("failed to create command pool!");
}

分配指令缓存

用于分配指令缓存的结构体:

typedef struct VkCommandBufferAllocateInfo {
    VkStructureType           sType;              // 结构体类型,必须为 VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO
    const void*               pNext;              // 可选的扩展指针,通常为 nullptr
    VkCommandPool             commandPool;        // 用于分配命令缓冲区的命令池
    VkCommandBufferLevel      level;              // 命令缓冲区的级别,主级别或次级别
    uint32_t                  commandBufferCount; // 要分配的命令缓冲区数量,你可以一次性分配多个命令缓冲区。
} VkCommandBufferAllocateInfo;

其中 level 这个属性你稍加注意下,level: 指定命令缓冲区的级别。可以是以下两个值之一:

  • VK_COMMAND_BUFFER_LEVEL_PRIMARY: 主命令缓冲区,可以直接提交到队列并执行。
  • VK_COMMAND_BUFFER_LEVEL_SECONDARY: 次级命令缓冲区,不能直接提交,需要在主命令缓冲区内调用。

在创建指令池后,可以从指令池中分配指令缓存。

 VkCommandBufferAllocateInfo allocInfo = {};
 allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
 allocInfo.commandPool = commandPool; // 指定指令池
 allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; // 指定缓存类型
 allocInfo.commandBufferCount = 1; // 分配的指令缓存数量
 
 VkCommandBuffer commandBuffer;
 if (vkAllocateCommandBuffers(device, &allocInfo, &commandBuffer) != VK_SUCCESS) {
     throw std::runtime_error("failed to allocate command buffers!");
 }

释放指令缓存、销毁缓存池

// 释放指令缓存
vkFreeCommandBuffers(device, commandPool, 1, &commandBuffer);
// 销毁命令池
vkDestroyCommandPool(device, commandPool, nullptr);

指令缓存的使用

理解 Vulkan 指令缓存

指令缓存是用来记录指令的,它是通过函数 vkBeginCommandBuffer() 和 vkEndCommandBuffer() 来完成。这两个函数定义了一个范围,在这个范围之内的所有Vulkan指令都会被记录下来。

指令记录的起始位置是通过函数 vkBeginCommandBuffer()设置的。它定义了一个起始位置,在它之后的所有指令调用都会被记录下来,直到我们设置了结束位置(即vkEndCommandBuffer())。

分配指令缓存后,可以开始记录指令。

// 定义命令缓冲区的开始信息结构体
VkCommandBufferBeginInfo beginInfo = {}; // 初始化 VkCommandBufferBeginInfo 结构体
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; // 指定结构体类型为 VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO
4beginInfo.flags = 0; // 设置为 0 或 VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT 表示一次性使用
beginInfo.pInheritanceInfo = nullptr; // 仅在次级命令缓冲区中使用,如果是主命令缓冲区则为 nullptr

// 开始录制命令缓冲区
VkResult result = vkBeginCommandBuffer(commandBuffer, &beginInfo); // 开始录制命令缓冲区,传入初始化的 beginInfo
if (result != VK_SUCCESS) {
    throw std::runtime_error("Failed to begin recording command buffer!"); // 检查录制是否成功,若失败则抛出异常
}

// 录制 Vulkan 指令
// 1. 设置视口(可选)
VkViewport viewport = {}; // 初始化 VkViewport 结构体
viewport.x = 0.0f; // 视口左上角的 X 坐标
viewport.y = 0.0f; // 视口左上角的 Y 坐标
viewport.width = (float)swapChainExtent.width; // 视口的宽度,通常是交换链的宽度
viewport.height = (float)swapChainExtent.height; // 视口的高度,通常是交换链的高度
viewport.minDepth = 0.0f; // 最小深度值
viewport.maxDepth = 1.0f; // 最大深度值
vkCmdSetViewport(commandBuffer, 0, 1, &viewport); // 录制设置视口的指令,绑定到命令缓冲区

// 2. 设置剪裁区域(可选)
VkRect2D scissor = {}; // 初始化 VkRect2D 结构体
scissor.offset = {0, 0}; // 剪裁区域的偏移量,从左上角开始
scissor.extent = swapChainExtent; // 剪裁区域的大小,通常是交换链的尺寸
vkCmdSetScissor(commandBuffer, 0, 1, &scissor); // 录制设置剪裁区域的指令

// 3. 开始渲染通道(Render Pass)
VkRenderPassBeginInfo renderPassInfo = {}; // 初始化 VkRenderPassBeginInfo 结构体
renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO; // 指定结构体类型
renderPassInfo.renderPass = renderPass; // 指定关联的渲染通道
renderPassInfo.framebuffer = framebuffer; // 指定要渲染的帧缓冲区
renderPassInfo.renderArea.offset = {0, 0}; // 渲染区域的起点,通常是从左上角(0, 0)开始
renderPassInfo.renderArea.extent = swapChainExtent; // 渲染区域的大小,通常与交换链一致

VkClearValue clearColor = {{{0.0f, 0.0f, 0.0f, 1.0f}}}; // 定义清屏颜色为黑色
renderPassInfo.clearValueCount = 1; // 清屏的值数量,这里只设置一个清屏颜色
renderPassInfo.pClearValues = &clearColor; // 指向清屏颜色的指针

// 开始录制渲染通道
vkCmdBeginRenderPass(commandBuffer, &renderPassInfo, VK_SUBPASS_CONTENTS_INLINE); // 开始渲染通道的录制

// 4. 绑定图形管线
vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, graphicsPipeline); // 绑定图形管线到命令缓冲区

// 5. 绑定顶点缓冲区(可选)
VkBuffer vertexBuffers[] = {vertexBuffer}; // 顶点缓冲区数组,包含顶点数据的缓冲区
VkDeviceSize offsets[] = {0}; // 偏移量数组,表示从缓冲区的哪个位置开始读取数据
vkCmdBindVertexBuffers(commandBuffer, 0, 1, vertexBuffers, offsets); // 绑定顶点缓冲区,设置偏移量

// 6. 绘制图元
vkCmdDraw(commandBuffer, static_cast<uint32_t>(vertexCount), 1, 0, 0); // 录制绘制指令,指定要绘制的顶点数量

// 结束渲染通道
vkCmdEndRenderPass(commandBuffer); // 结束当前渲染通道

// 结束命令缓冲区的录制
result = vkEndCommandBuffer(commandBuffer); // 结束命令缓冲区的录制
if (result != VK_SUCCESS) {
    throw std::runtime_error("Failed to record command buffer!"); // 检查是否成功录制命令缓冲区,失败则抛出异常
}

提交指令缓存

记录完命令后,可以将指令缓存提交给队列执行。

VkSubmitInfo submitInfo = {};
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &commandBuffer;

if (vkQueueSubmit(graphicsQueue, 1, &submitInfo, VK_NULL_HANDLE) != VK_SUCCESS) {
    throw std::runtime_error("failed to submit draw command buffer!");
}
vkQueueWaitIdle(graphicsQueue); // 等待队列完成执行

//GPU 渲染完成(渲染指令执行完毕)

参考

  • 《Vulkan 学习指南》 — [新加坡] 帕敏德·辛格(Parminder Singh)
  • 《Vulkan 应用开发指南》— [美] 格拉汉姆·塞勒斯(Graham Sellers)等 译者:李晓波 等

本文来自作者投稿,版权归原作者所有。如需转载,请注明出处:https://www.nxrte.com/jishu/52601.html

(0)

相关推荐

发表回复

登录后才能评论