原文链接:https://vulkan.lunarg.com/doc/sdk/1.2.131.2/windows/tutorial/html/15-draw_cube.html
本章节的代码文件是 15-draw_cube.cpp
你快要完成了!
这是把你的 Vulkan 图像上屏的最终步骤了:
在绘制东西之前,示例程序需要一个目标 swapchain image 来进行渲染。
vkAcquireNextImageKHR()
函数是用来获取 swapchain image 列表里的一个索引,所以它知道哪一个 framebuffer 用来作为渲染目标。这是可渲染的下一个 image。
res = vkCreateSemaphore(info.device, &imageAcquiredSemaphoreCreateInfo,
NULL, &imageAcquiredSemaphore);
// Get the index of the next available swapchain image:
res = vkAcquireNextImageKHR(info.device, info.swap_chain, UINT64_MAX,
imageAcquiredSemaphore, VK_NULL_HANDLE,
&info.current_buffer);
对于第一帧,很可能不需要使用 semaphore,因为 swapchain 里的所有 image 很可能都是可用的。但是在处理真正的GPU命令提交之前确保 image 准备好了依然是很好的做法,稍后我们会做这些。如果该示例被改成了渲染多帧的,就像动画,那么就变成了再次使用一个image之前必须要等待硬件处理完成。
注意你不是现在要等待所有东西。你只是创建了 semaphore 并且把它和 image 连接起来,以使得 semaphore 可以被用来推迟 command buffer 的提交直到 image 准备好。
你已经在前面的章节中定义了 render pass,所以通过输入一个开启 render pass 的命令到 command buffer 里来开始 render pass 是相当简单的:
VkRenderPassBeginInfo rp_begin;
rp_begin.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
rp_begin.pNext = NULL;
rp_begin.renderPass = info.render_pass;
rp_begin.framebuffer = info.framebuffers[info.current_buffer];
rp_begin.renderArea.offset.x = 0;
rp_begin.renderArea.offset.y = 0;
rp_begin.renderArea.extent.width = info.width;
rp_begin.renderArea.extent.height = info.height;
rp_begin.clearValueCount = 2;
rp_begin.pClearValues = clear_values;
vkCmdBeginRenderPass(info.cmd, &rp_begin, VK_SUBPASS_CONTENTS_INLINE);
注意在本示例的前面你已经通过调用 init_command_buffer()
和 execute_begin_command()
创建了一个 command buffer 并把它置为了录入模式。
你通过之前定义的 render pass 和 通过 vkAcquireNextImageKHR()
返回的索引选择的 framebuffer。
clear values 被初始化来设置背景颜色为暗灰色,并且设置 depth buffer 为它的“极远”值(clear_values)。
剩下需要的信息是在 info.render_pass里,如你之前设置的那样,然后你可以继续将该命令插入 command buffer 里来开始 render pass。
接下来绑定 pipeline 和 command buffer:
vkCmdBindPipeline(info.cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, info.pipeline);
你在前面的章节中定义了pipeline,并在这里绑定它,来告诉GPU如何渲染即将到来的图形元。
VK_PIPELINE_BIND_POINT_GRAPHICS
告诉GPU这是一个图形pipeline,而不是一个计算pipeline。
注意因为该命令是一个command buffer命令,对一个程序来说,定义几个图形 pipeline 然后在单个 command buffer 里来回切换是可能的。
再次说明,我们先前定义的 descriptor set 描述了 shader 程序想要如何找到它的输入数据,例如 MVP 变换。在这里把那些信息给到 GPU:
vkCmdBindDescriptorSets(info.cmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
info.pipeline_layout, 0, 1,
info.desc_set.data(), 0, NULL);
再次强调,如果你想要改变 shader 程序寻找它数据的方式,你可以在 command buffer 中绑定不同的 descriptors。例如,如果你想要在 command buffer 中更改变换,你可以使用一个不同的 descriptor 指向一个不同的 MVP 变换。
在 vertex_buffer 示例中,你创建了一个 vertex buffer 并且用顶点数据填充它。在这里,告诉GPU如何找到它:
const VkDeviceSize offsets[1] = {0};
vkCmdBindVertexBuffers(info.cmd, 0, 1, &info.vertex_buffer.buf, offsets);
这些命令绑定 vertex buffer 或者 buffer 到 command buffer。在当前情况下,你只绑定了一个 buffer,但其实可以绑定多个。
你在前面指出 viewport 和 scissor 是动态状态,这意味着它们可以用 command buffer 命令设置。所以,你需要在这里进行设置。这是在 init_viewports() 里设置 viewport 的代码:
info.viewport.height = (float)info.height;
info.viewport.width = (float)info.width;
info.viewport.minDepth = (float)0.0f;
info.viewport.maxDepth = (float)1.0f;
info.viewport.x = 0;
info.viewport.y = 0;
vkCmdSetViewport(info.cmd, 0, NUM_VIEWPORTS, &info.viewport);
设置 scissors rectangle 的代码是相似的。
使它们动态变化可能比较好,因为很多应用如果在运行的时候,window改变了尺寸,就需要改变它们的值。这避免了 window 尺寸改变的时候必须重建 pipeline。
最终,发出一个绘制命令,告诉GPU发送顶点到 pipeline 里,并且结束 render pass:
vkCmdDraw(info.cmd, 12 * 3, 1, 0, 0);
vkCmdEndRenderPass(info.cmd);
vkCmdDraw 命令告诉 GPU 一次绘制36个顶点。你已经配置了 pipeline 原始装配部分为绘制一系列独立的三角形,所以这代表了绘制12个三角形。
vkCmdEndRenderPass 命令标志了 render pass 的结束,但是 command buffer 依然“开启”着,示例还没有结束录入命令。
在 GPU 渲染的时候,目标 swapchain image 布局是 VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL,这对 GPU 渲染时最好的布局。你在本教程前面一章中定义 render pass 的时候,在 subpass 定义里设置了该布局。但是该布局对于显示器硬件扫描图像到显示设备上来说可能不是最好的布局。例如,对于渲染的最佳GPU内存布局可能是“平铺”,如本教程 render_pass 一章中讨论的那样。但是显示器硬件为了扫描内存可能更适合线性内存布局。你使用 VK_IMAGE_LAYOUT_PRESENT_SRC_KHR
布局来指定image是要呈现到显示器上的。
通过指定 color image attachment 的 description 中 finalLayout 为 VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,你已经在 render_pass 章节中处理过该布局转换。
VkAttachmentDescription attachments[2];
attachments[0].format = info.format;
attachments[0].samples = NUM_SAMPLES;
attachments[0].loadOp = VK_ATTACHMENT_LOAD_OP_CLEAR;
attachments[0].storeOp = VK_ATTACHMENT_STORE_OP_STORE;
attachments[0].stencilLoadOp = VK_ATTACHMENT_LOAD_OP_DONT_CARE;
attachments[0].stencilStoreOp = VK_ATTACHMENT_STORE_OP_DONT_CARE;
attachments[0].initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
attachments[0].finalLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
attachments[0].flags = 0;
注意还有另外一种方式来完成这种布局转换,就是通过向 command buffer 里录入 memory barrier 命令。这种备用方法在特定情况下可能很有用,例如在不使用 render pass 的队列提交中。这种情况的例子可以在 copy_blit_image 示例中找到,不是本教程的一部分,但是可以在本教程示例的同一个目录下找到。
在本示例中,你用了一个 render pass,但是如果你在 render_pass 示例中创建 render pass 的地方,把 color attachment 的 finalLayout 置为和 initialLayout 相同,那么你仍然可以使用这种备用方法:
attachments[0].initialLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
attachments[0].finalLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
那么这就要求你用另一种 pipeline memory barrier 执行该转换,和 set_image_layout() 执行的布局转换大部分一样:
VkImageMemoryBarrier prePresentBarrier = {};
prePresentBarrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
prePresentBarrier.pNext = NULL;
prePresentBarrier.srcAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
prePresentBarrier.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT;
prePresentBarrier.oldLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
prePresentBarrier.newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR;
prePresentBarrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
prePresentBarrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
prePresentBarrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
prePresentBarrier.subresourceRange.baseMipLevel = 0;
prePresentBarrier.subresourceRange.levelCount = 1;
prePresentBarrier.subresourceRange.baseArrayLayer = 0;
prePresentBarrier.subresourceRange.layerCount = 1;
prePresentBarrier.image = info.buffers[info.current_buffer].image;
vkCmdPipelineBarrier(info.cmd, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT, 0, 0, NULL, 0,
NULL, 1, &prePresentBarrier);
以上代码不在本示例中,但是可以在 copy_blit_image 示例中找到。
一旦 render pass 结束以后该命令处理完,图像缓存就准备好了被显示。当然,你不需要转换 depth buffer 的图像布局。
记住你还没有真正的向GPU发送任何命令。你仅仅是把它们录入了 command buffer 里。但是现在你已经完成了录入:
res = vkEndCommandBuffer(info.cmd);
你需要创建一个 fence,这是你用来区分 GPU 何时做完的。你需要知道 GPU 做完了,好让你不会过早地开始刷到显示器上。
VkFenceCreateInfo fenceInfo;
VkFence drawFence;
fenceInfo.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
fenceInfo.pNext = NULL;
fenceInfo.flags = 0;
vkCreateFence(info.device, &fenceInfo, NULL, &drawFence);
现在我们可以提交 command buffer 了:
const VkCommandBuffer cmd_bufs[] = {info.cmd};
VkPipelineStageFlags pipe_stage_flags =
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
VkSubmitInfo submit_info[1] = {};
submit_info[0].pNext = NULL;
submit_info[0].sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submit_info[0].waitSemaphoreCount = 1;
submit_info[0].pWaitSemaphores = &imageAcquiredSemaphore;
submit_info[0].pWaitDstStageMask = &pipe_stage_flags;
submit_info[0].commandBufferCount = 1;
submit_info[0].pCommandBuffers = cmd_bufs;
submit_info[0].signalSemaphoreCount = 0;
submit_info[0].pSignalSemaphores = NULL;
res = vkQueueSubmit(info.queue, 1, submit_info, drawFence);
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT
是最终颜色值从 pipeline 输出的阶段。我们使用 imageAcquiredSemaphore
在 color attachment output 阶段等待,直到 swapchain image 可用来写入颜色。
当 GPU 处理完了命令,它会通知 drawFence 表明绘制完成了。
vkWaitForFences() 等待 command buffer 处理完成。它在这里是被循环调用的,以防完成这些命令耗时比预期要长得多,在这个简单的示例中应该不会出现这种情况。
do {
res = vkWaitForFences(info.device, 1, &drawFence, VK_TRUE, FENCE_TIMEOUT);
} while (res == VK_TIMEOUT);
此时,你知道 swapchain image buffer 准备好呈现到显示器上了。
呈现 swapchain image 到显示器上很简单:
VkPresentInfoKHR present;
present.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
present.pNext = NULL;
present.swapchainCount = 1;
present.pSwapchains = &info.swap_chain;
present.pImageIndices = &info.current_buffer;
present.pWaitSemaphores = NULL;
present.waitSemaphoreCount = 0;
present.pResults = NULL;
res = vkQueuePresentKHR(info.queue, &present);
现在,你应该能在屏幕上看到一个立方体了!