Rendering and presentation
Outline of a frame
渲染每一帧的基本步骤:
- 等待上一帧完成(fence)
- 从swapchain获取图像
- 开始录制到命令缓冲区,BeginRenderPass->bindPipeline->SetViewport->draw,绘制场景到取出的图像上
- 录制结束的命令缓冲区·提交到queue
- 展示交换链图像
Synchronization
Vulkan的核心设计理念是GPU上的执行同步,
事件执行顺序取决于Semaphores和Fences,这意味着调用是Asynchronization异步的,但是每个操作都依赖于前一个操作的完成,因此需要显式地对许多事件进行排序,让它们同步执行,而非异步
Semaphores 信号量
Semaphores用于在队列操作之间添加顺序,有两种信号量,二进制和时间轴
Semaphores在一个队列作为信号,当队列执行完发出信号,在另一个队列等待这个信号发生时执行,当这个新队列开始执行,Semaphores会自动重置为未发送的状态,从而允许再次使用它
VkSemaphore S
vkQueueSubmit(work: A, signal: S, wait: None)
vkQueueSubmit(work: B, signal: None, wait: S)
Fences围栏
上述Semaphores仅能控制GPU的运行,但是CPU通过Fences控制的,被告知GPU什么时候完成了某件事
每当开始新的任务,都可以附加VkFence围栏,当工作完成时,围栏将发出信号。然后,我们可以让CPU等待栅栏发出信号,保证在CPU继续之前工作已经完成
vkResetFences必须手动重置栅栏,使其回到无信号状态,这是因为围栏用于控制CPU的执行,因此CPU可以决定何时重置围栏,而Semaphores则自动等待
VkFence F
vkQueueSubmit(work: A, fence: F)//完成后发出fence
vkWaitForFence(F)//阻止之后的CPU指令操作
vkResetFences();//重置围栏
实例
Fence
inFlightFence确保一次只渲染一帧,
vkWaitForFences接收fence数组,VK_TRUE在返回前需要等待数组的 所有fence发出信号后,超时参数,我们将其设置为64位无符号整数的最大值UINT64_MAX,这有效地禁用了超时
但是这里vkWaitForFences由于放在函数的最开始的指令位置,它在执行时将等待fence的信号,但是,fence信号是在此帧渲染完成后才发出,因此它会无限期的阻止CPU执行,
有一个内置于API中的巧妙的变通方法,fenceInfo.flags = VK_FENCE_CREATE_SIGNALED_BIT;
标志表示在信号状态下创建围栏,以便对vkWaitForFences()的第一次调用立即返回,因为围栏已经发出信号
vkResetFence我们需要手动将栅栏重置为未发送信号的状态
Acquiring image
vkAcquireNextImageKHR交换链是一个扩展特性,所以我们必须使用vk*KHR命名约定的函数
第3 4个参数表示信号量,栅栏(都可选)
最后一个参数指定一个变量,用于输出已变为可用的交换链映像的索引
commandbuffer
vkResetCommandBuffer确保它能够被记录
recordCommandBuffer来记录我们想要的命令
SubmitInfo
有了完整记录的命令缓冲区,我们现在可以提交它了
VkSubmitInfo结构:队列提交和Synchronization同步
waitSemaphores指明需要等待的信号量,和pSignalSemaphores执行结束后发出的 信号量
waitStages在什么阶段等待,VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT等待写入颜色附件的图形管道后
imageAvailableSemaphore表示图像已经从交换链中获取并准备好进行渲染,renderFinishedSemaphore表示渲染已经完成并可以进行呈现
vkQueueSubmit将命令缓冲区提交到图形队列,需要指明fence(可选)
SubpassDependency
VkSubpassDependency指定子过程之间的内存和执行依赖性,有两个内置依赖项负责渲染过程开始和渲染过程结束时的过渡
指定依赖项和依赖子通道的索引,dstSubpass必须始终高于srcSubpass
接下来的两个字段指定要等待的操作以及这些操作发生的阶段,我们需要等待交换链完成对图像的阅读,然后才能访问它
VkSubpassDependency dependency{};//子通道依赖
dependency.srcSubpass = VK_SUBPASS_EXTERNAL;
dependency.dstSubpass = 0;
dependency.srcStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
dependency.srcAccessMask = 0;
dependency.dstStageMask = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
dependency.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
可以将VkSubmitInfok 的PipelineStageFlags waitStages[] = { VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT };更改为VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT以确保渲染通道在图像可用之前不会开始
Presentation
VkPresentInfoKHR
前两个参数指定在呈现发生之前要等待哪些信号量,就像刚才的VkSubmitInfo中指定的一样
接下来的两个参数指定要向其呈现图像的交换链以及每个交换链的图像索引
最后一个可选参数称为pResults。它允许您指定一个VkResult值数组,以检查每个单独的交换链是否成功呈现
通过vkquePresentKHR(presentQueue,&presentInfo);将请求提交到交换链以呈现图像
VkPresentInfoKHR presentInfo{};
presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
presentInfo.waitSemaphoreCount = 1;
presentInfo.pWaitSemaphores = signalSemaphores;
VkSwapchainKHR swapChains[] = { swapChain };
presentInfo.swapchainCount = 1;
presentInfo.pSwapchains = swapChains;
presentInfo.pImageIndices = &imageIndex;
在900多行代码之后,我们终于看到了三角形
Frame synchronization
帧同步
现在我们的渲染循环需要等待上一帧完成,然后才能开始渲染下一帧(同步),这导致CPU不必要的空闲,我们应该允许一个帧的渲染(GPU)不干扰下一个帧的记录(CPU),
因此,我们需要多个commandBuffer、Semaphores和Fence,从而达到并发的目的,提高CPU的使用率
首先定义应该并发处理多少帧,CPU和GPU可以同时处理自己的任务,定义为2,但是并不是并发任务越多越好,如果有3帧或更多帧在运行中,CPU可能因为多个线程争夺而变大运行缓慢
实例
每个帧都应该有自己的commandBuffer、Semaphores和Fence,然后将它们改为vectors
需要修改createCommandBuffer,createSyncObjects,并且需要改为for去清理它们,注意当我们释放命令池时,命令缓冲区已为我们释放,所以命令缓冲区无需额外清理操作
我们还要记录currentFrame索引,修改drawFrame使用这三个数组的currentFrame索引的对象
我们还应该每次都前进到下一帧
currentFrame = (currentFrame + 1) % MAX_FRAMES_IN_FLIGHT;