为什么单个深度缓冲区足以满足这个 vulkan 交换链渲染循环?

clu*_*aru 3 3d graphics vulkan

我正在关注https://vulkan-tutorial.com/ 上的 vulkan 教程和深度缓冲章节,作者 Alexander Overvoorde 提到“我们只需要一个深度图像,因为一次只运行一个绘制操作。” 这就是我的问题所在。

在过去的几天里,我阅读了许多关于 Vulkan 同步的 SO 问题和文章/博客文章,但我似乎无法得出结论。到目前为止,我收集到的信息如下:

在 gpu 上执行相同子通道中的绘制调用,就好像它们按顺序执行一样,但前提是它们绘制到帧缓冲区(我不记得我在哪里读到的,这可能是 youtube 上的技术演讲,所以我是对此不是 100% 确定)。据我了解,这更多是 GPU 硬件行为,而不是 Vulkan 行为,因此这基本上意味着上述情况总体上是正确的(包括跨子通道甚至渲染通道)——这将回答我的问题,但我不能没有找到任何明确的信息。

我最接近回答我的问题的是OP 似乎接受的这个reddit 评论,但理由基于两件事:

  • “在高层有一个队列刷新,确保之前提交的渲染通道完成”

  • “渲染通道本身描述了它们作为外部依赖读取和写入的附件”

我既没有看到任何高级队列刷新(除非在规范中我一生都找不到某种明确的队列),也没有看到渲染通道描述对其附件的依赖关系的地方 - 它描述了附件,但没有描述依赖项(至少不是明确的)。说明书的相关章节我看了很多遍,但感觉语言不够清晰,初学者无法完全掌握。

如果可能,我也非常感谢 Vulkan 规范引用。

编辑:澄清一下,最后一个问题是:什么同步机制保证在当前绘制调用完成之前不提交下一个命令缓冲区中的绘制调用?

j00*_*0hi 7

恐怕,不得不说 Vulkan 教程是错误的。在当前状态下,不能保证仅使用单个深度缓冲区时没有内存危害。然而,它只需要很小的改变,这样只有一个深度缓冲区就足够了。


下面我们来分析一下代码中执行的相关步骤drawFrame

我们有两个不同的队列:presentQueueandgraphicsQueueMAX_FRAMES_IN_FLIGHT并发帧。我指的是“飞行中索引” cf(代表currentFrame = (currentFrame + 1) % MAX_FRAMES_IN_FLIGHT)。我正在使用sem1sem2来表示不同的信号量fence数组和栅栏数组。

伪代码中的相关步骤如下:

vkWaitForFences(..., fence[cf], ...);
vkAcquireNextImageKHR(..., /* signal when done: */ sem1[cf], ...);
vkResetFences(..., fence[cf]);
vkQueueSubmit(graphicsQueue, ...
    /* wait for: */ sem1[cf], /* wait stage: *, COLOR_ATTACHMENT_OUTPUT ...
    vkCmdBeginRenderPass(cb[cf], ...);
      Subpass Dependency between EXTERNAL -> 0:
          srcStages = COLOR_ATTACHMENT_OUTPUT,
          srcAccess = 0, 
          dstStages = COLOR_ATTACHMENT_OUTPUT,
          dstAccess = COLOR_ATTACHMENT_WRITE
      ...
      vkCmdDrawIndexed(cb[cf], ...);
      (Implicit!) Subpass Dependency between 0 -> EXTERNAL:
          srcStages = ALL_COMMANDS,
          srcAccess = COLOR_ATTACHMENT_WRITE|DEPTH_STENCIL_WRITE, 
          dstStages = BOTTOM_OF_PIPE,
          dstAccess = 0
    vkCmdEndRenderPass(cb[cf]);
    /* signal when done: */ sem2[cf], ...
    /* signal when done: */ fence[cf]
);
vkQueuePresent(presentQueue, ... /* wait for: */ sem2[cf], ...);
Run Code Online (Sandbox Code Playgroud)

绘制调用在一个队列上执行:graphicsQueue. 我们必须检查上面的命令在graphicsQueue理论上是否可以重叠。

让我们考虑graphicsQueue前两帧按时间顺序发生的事件:

img[0] -> sem1[0] signal -> t|...|ef|fs|lf|co|b -> sem2[0] signal, fence[0] signal
img[1] -> sem1[1] signal -> t|...|ef|fs|lf|co|b -> sem2[1] signal, fence[1] signal
Run Code Online (Sandbox Code Playgroud)

其中t|...|ef|fs|lf|co|b代表不同的管道阶段,绘制调用通过:

  • t ... TOP_OF_PIPE
  • ef ... EARLY_FRAGMENT_TESTS
  • fs ... FRAGMENT_SHADER
  • lf ... LATE_FRAGMENT_TESTS
  • co ... COLOR_ATTACHMENT_OUTPUT
  • b ... BOTTOM_OF_PIPE

虽然有可能是之间的隐含相关性sem2[i] signal -> presentsem1[i+1],当交换链只提供了一个形象,这仅适用(或如果它总是提供相同的图像)。在一般情况下,这是不能假设的。这意味着,在第一帧移交给 之后,没有任何东西会延迟后续帧的立即进展present。围栏也无济于事,因为在 之后fence[i] signal,代码等待fence[i+1],即在一般情况下也不会阻止后续帧的进展。

我的意思是:第二帧开始同时渲染到第一帧,据我所知,没有什么可以阻止它同时访问深度缓冲区。


修复:

但是,如果我们只想使用单个深度缓冲区,我们可以修复教程的代码:我们想要实现的是eflf阶段在恢复之前等待前一个绘制调用完成。即我们要创建以下场景:

img[0] -> sem1[0] signal -> t|...|ef|fs|lf|co|b -> sem2[0] signal, fence[0] signal
img[1] -> sem1[1] signal -> t|...|________|ef|fs|lf|co|b -> sem2[1] signal, fence[1] signal
Run Code Online (Sandbox Code Playgroud)

其中_表示等待操作。

为了实现这一点,我们必须添加一个屏障来防止后续帧同时执行EARLY_FRAGMENT_TESTLATE_FRAGMENT_TEST阶段。只有一个队列执行绘制调用,因此只有其中的命令graphicsQueue需要屏障。“屏障”可以通过使用子通道依赖项来建立:

vkWaitForFences(..., fence[cf], ...);
vkAcquireNextImageKHR(..., /* signal when done: */ sem1[cf], ...);
vkResetFences(..., fence[cf]);
vkQueueSubmit(graphicsQueue, ...
    /* wait for: */ sem1[cf], /* wait stage: *, EARLY_FRAGMENT_TEST...
    vkCmdBeginRenderPass(cb[cf], ...);
      Subpass Dependency between EXTERNAL -> 0:
          srcStages = EARLY_FRAGMENT_TEST|LATE_FRAGMENT_TEST,
          srcAccess = DEPTH_STENCIL_ATTACHMENT_WRITE, 
          dstStages = EARLY_FRAGMENT_TEST|LATE_FRAGMENT_TEST,
          dstAccess = DEPTH_STENCIL_ATTACHMENT_WRITE|DEPTH_STENCIL_ATTACHMENT_READ
      ...
      vkCmdDrawIndexed(cb[cf], ...);
      (Implicit!) Subpass Dependency between 0 -> EXTERNAL:
          srcStages = ALL_COMMANDS,
          srcAccess = COLOR_ATTACHMENT_WRITE|DEPTH_STENCIL_WRITE, 
          dstStages = BOTTOM_OF_PIPE,
          dstAccess = 0
    vkCmdEndRenderPass(cb[cf]);
    /* signal when done: */ sem2[cf], ...
    /* signal when done: */ fence[cf]
);
vkQueuePresent(presentQueue, ... /* wait for: */ sem2[cf], ...);
Run Code Online (Sandbox Code Playgroud)

这应该graphicsQueue在不同帧的绘制调用之间建立适当的屏障。因为它是一个EXTERNAL -> 0-type subpass 依赖,我们可以确定 renderpass-external 命令是同步的(即与前一帧同步)。

更新:也必须将等待阶段sem1[cf]从 更改COLOR_ATTACHMENT_OUTPUTEARLY_FRAGMENT_TEST。这是因为布局转换发生在以下vkCmdBeginRenderPass时间:第一个同步范围(srcStagessrcAccess)之后和第二个同步范围(dstStagesdstAccess)之前。因此,交换链图像必须已经在那里可用,以便布局转换发生在正确的时间点。