本节的代码是 15-draw_cube.cpp
你快完成了!
下面是让你的Vulkan图像出现在屏幕上的最后步骤:
在开始绘制任何东西之前,样例程序需要一个目标交换链图像来呈现。vkAcquireNextImageKHR()
函数用于将索引放入交换链图像列表中,因此它知道哪个framebuffer会被用作渲染目标。即下一个可供渲染的图像。
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);
对于第一帧,可能不需要使用信号量,因为交换链中的所有图像都是可用的。但是,在继续向GPU提交命令之前,确保图像已经准备好仍然是一种很好的做法。如果这个样例被更改为渲染多个帧,比如动画,那么就有必要等到硬件完成后再使用它。
请注意,您现在没有等待任何东西。你只是在创建这个信号量,并将它与图像关联起来,这样就可以使用信号量来延迟命令缓冲区的提交,直到图像准备好为止。
您已经在前一节中定义了渲染过程,因此在命令缓冲区中放置一个“开始渲染过程”命令就可以开始渲染过程,这很简单:
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()
将其转换为记录模式。
您提供了先前定义的渲染过程,以及由vkAcquireNextImageKHR()
返回的索引所选择的framebuffer。
清空值被初始化,并将背景颜色设置为黑灰色,且深度缓冲区被设置成“远”值(clear_values
)。
其余的信息都在info.render_pass
中,正如您在前面设置的那样,然后您继续插入这个命令,以启动渲染过程到命令缓冲区。
下一步将管线绑定到命令缓冲区:
vkCmdBindPipeline(info.cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, info.pipeline);
您在前一节中定义了管道,并在这将其绑定,然后告诉GPU如何渲染稍后将要出现的图形。
VK_PIPELINE_BIND_POINT_GRAPHICS
告诉GPU这是一个图形管线而不是计算管线。
注意,由于这个命令是一个命令缓冲区命令,所以一个程序可以定义多个图形管道,并在单个命令缓冲区中切换它们。
回想一下,我们前面定义的描述符集描述了着色程序期望如何找到它的输入数据,比如MVP转换。把这些信息告诉GPU:
vkCmdBindDescriptorSets(info.cmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
info.pipeline_layout, 0, 1,
info.desc_set.data(), 0, NULL);
请再次注意,如果您想要更改shader程序如何找到其数据,您可以在命令缓冲区的中间绑定不同的描述符。例如,如果您想要改变命令缓冲区中间的转换,您可以使用不同的描述符来指向不同的MVP转换。
您创建了一个顶点缓冲,并在“顶点缓冲区”示例中填充了顶点数据。在这里,告诉GPU如何找到它:
const VkDeviceSize offsets[1] = {0};
vkCmdBindVertexBuffers(info.cmd, 0, 1, &info.vertex_buffer.buf, offsets);
这个命令将一个或多个顶点缓冲绑定到命令缓冲区。在这种情况下,您只绑定一个缓冲区。
之前指出,viewport和scissors 是动态的状态,这意味着它们可以用一个命令缓冲命令来设置。所以,你需要把它们设置在这里。下面是用来设置viewport的 init_viewports()
中的代码:
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 矩形的代码和上述类似
最好将这些值设为动态的,因为执行过程中窗口的大小发生变化,那么许多应用程序需要更改这些值。这就避免了在窗口大小发生变化时重新构建管线。
最后,发出一个绘图命令,告诉GPU将顶点发送到管线中,并完成渲染过程。
vkCmdDraw(info.cmd, 12 * 3, 1, 0, 0);
vkCmdEndRenderPass(info.cmd);
vkCmdDraw
命令告诉GPU一次性画36个顶点。你已经在管线章节中配置了几何装配,去渲染独立的三角形,所以意味着最终有12个三角形被绘制。
vkCmdEndRenderPass
命令标志着渲染过程的结束,但是命令缓冲区仍然是“开放的”,并且示例还没有停止记录命令。
当GPU在渲染时,目标交换链的图像布局是VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL
,这是GPU渲染的最佳布局。在本教程之前的章节定义渲染过程时,你在定义子过程里设置了这个布局。但是这个布局可能不是将图像扫描到显示设备的最佳布局。例如,用于渲染的最优GPU内存布局可能是“平铺的”,正如本教程的“渲染过程”部分所讨论的那样。但是显示硬件可能更喜欢线性内存布局。您可以使用VK_IMAGE_LAYOUT_PRESENT_SRC_KHR
布局来指定图像将要呈现给显示器。
您已经在渲染过程章节中处理了这个布局转换,在颜色图像附件的描述中指定finalLayout
为 VK_IMAGE_LAYOUT_PRESENT_SRC_KHR
:
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;
注意,还有另一种方法可以通过在命令缓冲器中记录另一个内存屏障命令来完成这种布局转换。这种替代方法在某些情况下可能很有用,例如在不使用渲染过程的队列提交的情况下。这种情况的一个例子可以在copy_blit_image
示例中找到,它不是本教程的一部分,但是可以在与这些教程示例相同的文件夹中找到。
本例中,你是用一个渲染过程,但你仍然可以使用这个可选的方式。通过设置initialLayout
与finalLayout
为相同的颜色附件:
attachments[0].initialLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
attachments[0].finalLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
这就要求您使用另一个管线内存屏障执行这个转换,就像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
示例中找到。
一旦该命令在渲染过程结束后执行,图像缓冲区就可以显示了。当然,您不需要转换深度缓冲图像布局。
请记住,您还没有向GPU发送任何命令。您刚刚将它们记录到命令缓冲区中。但是现在你已经完成了记录:
res = vkEndCommandBuffer(info.cmd);
你需要创建一个围栏,你可以用它来判断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);
现在我们可以提交命令缓冲区:
const VkCommandBuffer cmd_bufs[] = {info.cmd};
VkPipelineStageFlags pipe_stage_flags =
VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_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_BOTTOM_OF_PIPE_BIT
管线中完成命令执行的最后一步。
imageAcquiredSemaphore
用于等待图像在绘图前准备好,正如本节的顶部所解释的那样。这使得驱动程序在imageAcquiredSemaphore
上等待,以知道交换链图像是可用的。然后,它将命令提交给GPU。当GPU执行这些命令时,它会通知栅drawFence
,表明绘制已经完成了。
vkWaitForFences()
等待命令缓冲区完成执行。
它在循环里被调用,以防命令比预期花费更长的时间完成。
do {
res = vkWaitForFences(info.device, 1, &drawFence, VK_TRUE, FENCE_TIMEOUT);
} while (res == VK_TIMEOUT);
在这一点上,您知道交换链的图像缓冲区已经准备好呈现给显示器了。
将交换链的图像呈现给显示器非常简单:
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);
您现在应该在屏幕上看到一个立方体!
© Copyright 2016-2017 LunarG, Inc