很抱歉,我不得不说Vulkan教程是错误的。在当前状态下,不能保证仅使用一个深度缓冲区时没有内存危险。然而,只需进行非常小的更改,就可以确保仅需要一个深度缓冲区。
让我们分析在drawFrame中执行的相关代码步骤。
我们有两个不同的队列:presentQueue和graphicsQueue,以及MAX_FRAMES_IN_FLIGHT并发帧。我用cf(表示currentFrame =(currentFrame + 1)%MAX_FRAMES_IN_FLIGHT)来引用“in flight index”。我使用sem1和sem2来表示不同的信号量数组,fence用于存储栅栏的数组。
伪代码中的相关步骤如下:
vkWaitForFences(..., fence[cf], ...);
vkAcquireNextImageKHR(..., sem1[cf], ...);
vkResetFences(..., fence[cf]);
vkQueueSubmit(graphicsQueue, ...
sem1[cf], sem2[cf], ...
fence[cf]
);
vkQueuePresent(presentQueue, ... 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
阶段等待上一个绘制调用完成。也就是说,我们想要创建以下情况:
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
中的命令需要屏障。可以使用子通道依赖性来建立“屏障”:
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
类型的子通道依赖关系,我们可以确保renderpass-external命令已经被同步(即与上一帧同步)。
更新:此外,sem1 [cf]
的等待阶段必须从COLOR_ATTACHMENT_OUTPUT
更改为EARLY_FRAGMENT_TEST
。这是因为布局转换发生在vkCmdBeginRenderPass
时间:在第一个同步范围(srcStages
和srcAccess
)之后,在第二个同步范围(dstStages
和dstAccess
)之前。因此,交换链图像必须已经可用,以便布局转换在正确的时间点发生。
vkDraw*
会变得非常麻烦。其他所有内容都必须显式同步。你的应用程序示例要么正确地执行了显式同步,要么不符合规范。无论哪种情况,这都与问题主题无关。 - krOoze