Drawing a triangle/Drawing/Command buffers

目录

Command pools

Command buffer allocation

Command buffer recording

Starting a render pass

Basic drawing commands

Finishing up


Vulkan中的命令,如绘图操作和内存传输,不使用函数调用直接执行。您必须在命令缓冲区对象中记录所有要执行的操作。这样做的好处是,当我们准备好告诉Vulkan我们想做什么时,所有命令都一起提交,Vulkan可以更有效地处理命令,因为所有命令都可以一起使用。此外,如果需要,这允许在多个线程中进行命令记录。

Command pools

在创建命令缓冲区之前,我们必须创建一个命令池。命令池管理用于存储缓冲区的内存,并从中分配命令缓冲区。添加新的类成员以存储VkCommandPool:

VkCommandPool commandPool;

然后创建一个新函数createCommandPool,并在创建帧缓冲区后从initVulkan调用它。

void initVulkan() {
    createInstance();
    setupDebugMessenger();
    createSurface();
    pickPhysicalDevice();
    createLogicalDevice();
    createSwapChain();
    createImageViews();
    createRenderPass();
    createGraphicsPipeline();
    createFramebuffers();
    createCommandPool();
}

...

void createCommandPool() {

}

命令池创建只需要两个参数:

QueueFamilyIndices queueFamilyIndices = findQueueFamilies(physicalDevice);

VkCommandPoolCreateInfo poolInfo{};
poolInfo.sType = VK_STRUCTURE_TYPE_COMMAND_POOL_CREATE_INFO;
poolInfo.flags = VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT;
poolInfo.queueFamilyIndex = queueFamilyIndices.graphicsFamily.value();

命令池有两种可能的标志:

  • VK_COMMAND_POOL_CREATE_TRANSIENT_BIT:提示命令缓冲区经常被新命令重新记录(可能会改变内存分配行为)
  • VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT:允许单独重新记录命令缓冲区,如果没有此标志,则必须一起重置它们

我们将在每一帧记录一个命令缓冲区,因此我们希望能够对其进行重置和重新记录。因此,我们需要为命令池设置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!");
}

使用vkCreateCommandPool函数完成创建命令池。它没有任何特殊参数。整个程序将使用命令在屏幕上绘制对象,因此池只应在最后销毁:

void cleanup() {
    vkDestroyCommandPool(device, commandPool, nullptr);

    ...
}

Command buffer allocation

我们现在可以开始分配命令缓冲区了。

创建VkCommandBuffer对象作为类成员。命令缓冲区将在其命令池被破坏时自动释放,因此我们不需要显式清理。

VkCommandBuffer commandBuffer;

现在,我们将开始使用createCommandBuffer函数来从命令池中分配单个命令缓冲区。

void initVulkan() {
    createInstance();
    setupDebugMessenger();
    createSurface();
    pickPhysicalDevice();
    createLogicalDevice();
    createSwapChain();
    createImageViews();
    createRenderPass();
    createGraphicsPipeline();
    createFramebuffers();
    createCommandPool();
    createCommandBuffer();
}

...

void createCommandBuffer() {

}

使用vkAllocateCommandBuffers函数分配命令缓冲区,该函数将VkCommandBufferAllocateInfo结构作为参数,指定要分配的命令池和缓冲区数量:

VkCommandBufferAllocateInfo allocInfo{};
allocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO;
allocInfo.commandPool = commandPool;
allocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY;
allocInfo.commandBufferCount = 1;

if (vkAllocateCommandBuffers(device, &allocInfo, &commandBuffer) != VK_SUCCESS) {
    throw std::runtime_error("failed to allocate command buffers!");
}

level参数指定分配的命令缓冲区是主命令缓冲区还是辅助命令缓冲区。

  • VK_COMMAND_BUFFER_LEVEL_PRIMARY:可以提交到队列执行,但不能从其他命令缓冲区调用。
  • VK_COMMAND_BUFFER_LEVEL_SECONDARY:无法直接提交,但可以从主命令缓冲区调用。

这里我们不会使用辅助命令缓冲区功能,但您可以想象,重用主命令缓冲区中的常用操作是有帮助的。

因为我们只分配了一个命令缓冲区,所以commandBufferCount参数只有一个。

Command buffer recording

现在,我们将开始处理recordCommandBuffer函数,该函数将要执行的命令写入命令缓冲区。使用的VkCommandBuffer将作为参数传入,以及我们要写入的当前swapchain映像的索引。

void recordCommandBuffer(VkCommandBuffer commandBuffer, uint32_t imageIndex) {

}

我们总是通过调用具有小VkCommandBufferBeginInfo结构的vkBeginCommandBuffer作为参数来开始记录命令缓冲区,该参数指定了有关此特定命令缓冲区用法的一些细节。

VkCommandBufferBeginInfo beginInfo{};
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
beginInfo.flags = 0; // Optional
beginInfo.pInheritanceInfo = nullptr; // Optional

if (vkBeginCommandBuffer(commandBuffer, &beginInfo) != VK_SUCCESS) {
    throw std::runtime_error("failed to begin recording command buffer!");
}

flags参数指定如何使用命令缓冲区。以下值可用:

  • VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT:命令缓冲区将在执行一次后立即重新记录。
  • VK_COMMAND_BUFFER_USAGE_RENDER_PASS_CONTINUE_BIT:这是一个辅助命令缓冲区,将完全位于单个渲染过程中。
  • VK_COMMAND_BUFFER_USAGE_SIMULTANEOUS_USE_BIT:可以在命令缓冲区也已挂起执行时重新提交。

这些标志现在都不适用于我们。

pInheritanceInfo参数仅与辅助命令缓冲区相关。它指定要从调用主命令缓冲区继承的状态。

如果命令缓冲区已经记录了一次,那么对vkBeginCommandBuffer的调用将隐式重置它。以后不可能将命令附加到缓冲区。

Starting a render pass

通过使用vkCmdBeginRenderPass开始渲染过程开始绘制。渲染过程是使用VkRenderPassBeginInfo结构中的一些参数配置的。

VkRenderPassBeginInfo renderPassInfo{};
renderPassInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
renderPassInfo.renderPass = renderPass;
renderPassInfo.framebuffer = swapChainFramebuffers[imageIndex];

第一个参数是渲染过程本身和要绑定的附件。我们为每个交换链图像创建了一个帧缓冲区,将其指定为颜色附件。因此,我们需要为要绘制的swapchain图像绑定帧缓冲区。使用传入的imageIndex参数,我们可以为当前swapchaine图像选择正确的帧缓冲区。

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;

最后两个参数定义了用于VK_ATTACHMENT_LOAD_OP_clear的清除值,我们将其用作颜色附件的加载操作。我已经将透明颜色定义为100%不透明度的黑色。

vkCmdBeginRenderPass(commandBuffer, &renderPassInfo, VK_SUBPASS_CONTENTS_INLINE);

现在可以开始渲染过程。所有记录命令的函数都可以通过其vkCmd前缀识别。它们都返回void,因此在我们完成录制之前不会有错误处理。

每个命令的第一个参数始终是记录命令的命令缓冲区。第二个参数指定我们刚才提供的渲染过程的详细信息。最后一个参数控制如何提供渲染过程中的绘图命令。它可以有两个值之一:

  • VK_SUBPASS_CONTENTS_INLINE:渲染传递命令将嵌入主命令缓冲区本身,并且不会执行辅助命令缓冲区。
  • VK_SUBPASS_CONTENTS_SECONDARY_COMMAND_BUFFERS:渲染过程命令将从辅助命令缓冲区执行。

我们将不使用辅助命令缓冲区,因此我们将使用第一个选项。

Basic drawing commands

我们现在可以绑定图形管道:

vkCmdBindPipeline(commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, graphicsPipeline);

第二个参数指定管道对象是图形管道还是计算管道。我们现在已经告诉Vulkan要在图形管道中执行哪些操作,以及要在片段着色器中使用哪些附件。

正如在固定函数一章中所指出的,我们确实为这个管道指定了动态视口和裁剪状态。因此,在发出绘制命令之前,我们需要在命令缓冲区中设置它们:

VkViewport viewport{};
viewport.x = 0.0f;
viewport.y = 0.0f;
viewport.width = static_cast<float>(swapChainExtent.width);
viewport.height = static_cast<float>(swapChainExtent.height);
viewport.minDepth = 0.0f;
viewport.maxDepth = 1.0f;
vkCmdSetViewport(commandBuffer, 0, 1, &viewport);

VkRect2D scissor{};
scissor.offset = {0, 0};
scissor.extent = swapChainExtent;
vkCmdSetScissor(commandBuffer, 0, 1, &scissor);

现在,我们准备为三角形发出绘制命令:

vkCmdDraw(commandBuffer, 3, 1, 0, 0);

实际的vkCmdDraw函数有点虎头蛇尾,但由于我们事先指定了所有信息,所以它非常简单。除了命令缓冲区之外,它还有以下参数:

  • vertexCount:尽管我们没有顶点缓冲区,但从技术上讲,我们仍有3个顶点需要绘制。
  • instanceCount:用于实例化渲染,如果不这样做,请使用1。
  • firstVertex:用作顶点缓冲区的偏移量,定义gl_VertexIndex的最小值。
  • firstInstance:用作实例渲染的偏移量,定义gl_InstanceIndex的最小值。

Finishing up

渲染过程现在可以结束:

vkCmdEndRenderPass(commandBuffer);

我们已经完成了命令缓冲区的录制:

if (vkEndCommandBuffer(commandBuffer) != VK_SUCCESS) {
    throw std::runtime_error("failed to record command buffer!");
}

在下一章中,我们将编写主循环的代码,它将从交换链获取图像,记录并执行命令缓冲区,然后将完成的图像返回到交换链。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值