我在https://vulkan-tutorial.com/上的vulkan教程中以及深度缓冲一章中都遵循过,作者Alexander Overvoorde提到:“我们只需要一个深度图像,因为一次只运行一个绘制操作。” 这就是我的问题所在。
在过去的几天里,我已经阅读了许多有关Vulkan同步的SO问题和文章/博客帖子,但是我似乎无法得出结论。到目前为止,我收集的信息如下:
在同一子通道中的绘图调用就像在顺序上一样在gpu上执行,但是仅当它们绘制到帧缓冲区时才有效(我无法确切回忆起我读到的内容,这可能是youtube上的技术话题,所以我对此不是100%肯定)。据我了解,这是GPU硬件行为而不是Vulkan行为,因此从本质上讲,这通常是正确的(包括跨子通道甚至是渲染通道),这可以回答我的问题,但我可以做到”在这方面找不到任何明确的信息。
我得到的最接近答案的答案是OP似乎接受的有关reddit的评论,但理由基于两点:
“高级别的队列刷新可确保先前提交的渲染过程完成”
“渲染过程本身描述了作为外部依赖项从中读取和写入的附件”
我既看不到任何高级队列刷新(除非规范中存在我无法找到的某种显式队列刷新),也看不到渲染通道描述对其附件的依赖关系的地方-它描述了附件,但没有描述依赖性(至少不是明确地)。我已经多次阅读了规范的相关章节,但是我觉得这种语言不够清晰,以至于初学者无法完全掌握。
如果可能,我也非常感谢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
andlf
阶段在恢复之前等待上一个draw调用完成。即我们要创建以下方案:
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
require中的命令需要一个屏障。可以通过使用子传递依赖项来建立“障碍”:
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子传递依赖项,所以我们可以确保renderpass-external命令是同步的(即与上一帧同步)。
更新:等待的阶段sem1[cf]
也必须从更改COLOR_ATTACHMENT_OUTPUT
为EARLY_FRAGMENT_TEST
。这是因为布局转换是在vkCmdBeginRenderPass
时间上发生的:在第一个同步作用域(srcStages
和srcAccess
)之后,在第二个同步作用域(dstStages
和dstAccess
)之前。因此,交换链映像必须已经在那里可用,以便布局转换在正确的时间点发生。
本文收集自互联网,转载请注明来源。
如有侵权,请联系[email protected] 删除。
我来说两句