我正在关注 vulkan 教程 https://vulkan-tutorial.com/并在 depth buffering chapter ,作者 Alexander Overvoorde 提到“我们只需要一张深度图像,因为一次只有一个绘制操作在运行。”这就是我的问题所在。
在过去的几天里,我阅读了许多关于 Vulkan 同步的 SO 问题和文章/博客文章,但我似乎无法得出结论。到目前为止,我收集到的信息如下:
在 gpu 上执行相同子 channel 中的绘制调用,就好像它们按顺序执行一样,但前提是它们绘制到帧缓冲区(我不记得我在哪里读到的,这可能是 youtube 上的技术演讲,所以我是对此不是 100% 确定)。据我了解,这更多是 GPU 硬件行为,而不是 Vulkan 行为,因此这基本上意味着上述情况总体上是正确的(包括跨子 channel 甚至渲染 channel )——这将回答我的问题,但我不能没有找到任何明确的信息。
我最接近回答问题的是这个 reddit comment OP似乎接受,但理由基于两件事:
我既没有看到任何高级队列刷新(除非在规范中我一生都找不到某种明确的队列),也没有看到渲染 channel 描述对其附件的依赖关系的地方 - 它描述了附件,但没有描述依赖项(至少不是明确的)。说明书的相关章节我看了很多遍,但是感觉语言不够清晰,初学者无法完全掌握。
如果可能,我也非常感谢 Vulkan 规范引用。
编辑:澄清一下,最后一个问题是:
什么同步机制保证下一个命令缓冲区中的绘制调用直到当前绘制调用完成才提交?
最佳答案
恐怕,不得不说 Vulkan 教程是错误的。在当前状态下,不能保证仅使用单个深度缓冲区时没有内存危害。然而,它只需要很小的改变,这样只有一个深度缓冲区就足够了。
下面我们来分析一下drawFrame
内执行的代码的相关步骤.
我们有两个不同的队列:presentQueue
和 graphicsQueue
, 和 MAX_FRAMES_IN_FLIGHT
并发帧。我用 cf
指代“飞行中索引” (代表 currentFrame = (currentFrame + 1) % MAX_FRAMES_IN_FLIGHT
)。我正在使用 sem1
和 sem2
表示不同的信号量数组和 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], ...);
绘制调用在一个队列上执行:
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
哪里
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 -> present
之间的隐式依赖和 sem1[i+1]
,这仅适用于交换链仅提供一张图像(或始终提供相同图像)的情况。在一般情况下,这是不能假设的。这意味着,没有什么会延迟 立即将第一帧移交给present
之后的后续帧的进展.围栏也无济于事,因为在 fence[i] signal
之后,代码等待 fence[i+1]
,即在一般情况下也不会阻止后续帧的进展。我的意思是:第二帧开始渲染 同时到第一帧,据我所知,没有什么可以阻止它同时访问深度缓冲区。
修复:
但是,如果我们只想使用单个深度缓冲区,我们可以修复教程的代码:我们想要实现的是
ef
和 lf
阶段在恢复之前等待前一个绘制调用完成。 IE。我们要创建以下场景: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
哪里
_
表示等待操作。为了实现这一点,我们必须添加一个屏障来防止后续帧执行
EARLY_FRAGMENT_TEST
和 LATE_FRAGMENT_TEST
同时阶段。只有一个队列执行绘制调用,因此只有 graphicsQueue
中的命令需要一个屏障。 “屏障”可以通过使用子 channel 依赖项来建立: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], ...);
这应该在
graphicsQueue
上建立一个适当的障碍。在不同帧的绘制调用之间。因为它是EXTERNAL -> 0
-type subpass 依赖,我们可以确保 renderpass-external 命令是同步的(即与前一帧同步)。更新:也是
sem1[cf]
的等待阶段必须从 COLOR_ATTACHMENT_OUTPUT
更改至 EARLY_FRAGMENT_TEST
.这是因为布局转换发生在 vkCmdBeginRenderPass
时间:第一个同步范围之后( srcStages
和 srcAccess
)和第二个同步范围之前( dstStages
和 dstAccess
)。因此,交换链图像必须已经在那里可用,以便布局转换发生在正确的时间点。
关于graphics - 为什么单个深度缓冲区足以满足这个 vulkan 交换链渲染循环?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/62371266/