问题说明
vkQueuePresentKHR 的等待信号量很容易被误用,但幸运的是,修复方法通常也很简单:为每个交换链图像分配一个 “提交完成” 信号量,而不是按帧缓冲数量分配。
本章讨论一种安全复用 vkQueuePresentKHR 等待信号量(即 VkPresentInfoKHR::pWaitSemaphores 中指定的信号量)的方式。这里的 “安全” 指 Vulkan 规范保证信号量已不再被使用,可以被复用。
从 Vulkan SDK 1.4.313 开始,校验层会报告 present 等待信号量未被安全使用的情况。该问题通常以 VUID-vkQueueSubmit-pSignalSemaphores-00067 报错出现,或提示:your VkSemaphore is being signaled by VkQueue, but it may still be in use by VkSwapchainKHR
首先我们需要理解 present 等待信号量的特殊性。Vulkan 同步的核心是执行顺序,通过等待信号、轮询等方式实现。我们等待的对象必须能发出状态变更信号或提供查询机制。
例如:
- vkQueueSubmit 可指定一个 fence,当工作负载完成时触发,应用可在 CPU(主机)侧等待该 fence。
- vkQueueSubmit 也可触发信号量,这些信号量仅能在 GPU(设备)侧等待(时间线信号量可在主机侧等待)。
vkQueuePresentKHR 与 vkQueueSubmit 系列函数不同:它不提供触发信号量或 fence 的机制(无额外扩展时)。这意味着:
如果 vkQueuePresentKHR 可以触发信号,等待该信号即可确认 present 队列操作已完成(包括对 VkPresentInfoKHR::pWaitSemaphores 的等待)。
总结:何时可以安全复用 present 等待信号量并不直观。
解决方案讨论
好消息是:存在一种简单方法能保证呈现操作已完成,尽管不如显式等待直接。
通过 vkAcquireNextImageKHR 获取图像索引,然后等待其信号量或 fence,即可保证使用该图像索引的上一次呈现操作已完成,其中包括对 VkPresentInfoKHR::pWaitSemaphores 的等待,因此对应信号量可以安全复用。
安全复用 present 等待信号量的流程:
为什么很多应用会出错?原因很可能是:只要稍微偏离 “你触发、我等待” 的最直观同步方式,复杂度就会上升,逻辑变得不明显,进而容易出错。
常见错误:按帧缓冲数量(如双缓冲、三缓冲)管理 present 等待信号量。应用通常用 vkQueueSubmit 的 fence 同步帧缓冲,等待该 fence 即可确认对应命令缓冲与帧资源已不再使用。但 Vulkan 规范不保证等待 vkQueueSubmit 的 fence 也能同步呈现操作。
呈现资源的复用应依赖 vkAcquireNextImageKHR 或额外扩展(文末提及),而非 vkQueueSubmit 的 fence。
以下伪代码展示常见错误(在特定驱动上可能运行正常,但违反 Vulkan 规范):
c
运行
// !! 错误代码示例 !!
const kNumberOfFramesInFlight = 2
VkSemaphore submit_semaphores[kNumberOfFramesInFlight]
while (!quit) {
// 等待帧 fence
// 这允许复用帧资源,但不包括呈现资源
VkFence frame_fence = frame_fences[frame_index]
vkWaitForFences(frame_fence)
vkResetFences(frame_fence)
…
// 警告:此处按当前帧缓冲索引获取未使用的提交信号量
// 通常假设等待上一帧后,submit_semaphores 就不再被该帧的 vkQueuePresentKHR 使用
// 这不一定成立
VkSemaphore submit_semaphore = submit_semaphores[frame_index]
VkSubmitInfo submit_info
submit_info.pSignalSemaphores = &submit_semaphore
vkQueueSubmit(queue, &submit_info)
// 警告:submit_semaphore 可能仍被之前的某一次呈现操作占用
VkPresentInfo present_info
present_info.pWaitSemaphores = &submit_semaphore
vkQueuePresentKHR(queue, &present_info)
frame_index = (frame_index + 1) % kNumberOfFramesInFlight
}
修复方法非常简单:
在修复 vkcube 这类简单应用时,修复步骤就像上面描述的一样直接。对于复杂引擎架构则可能有所不同。
以下伪代码展示正确复用呈现等待信号量的渲染帧流程,适用于所有 Vulkan 版本:
// !! 正确代码示例 !!
VkImage swapchain_images[num_swapchain_images]
// 按当前帧缓冲索引管理的资源
const kNumberOfFramesInFlight = 2
VkFence frame_fences[kNumberOfFramesInFlight];
VkSemaphore acquire_semaphores[kNumberOfFramesInFlight];
VkCommandBuffer command_buffers[kNumberOfFramesInFlight];
int frame_index = 0; // 0..kNumberOfFramesInFlight-1
// 被 QueuePresent 等待的信号量按交换链图像数量缓冲
VkSemaphore submit_semaphores[swapchain_image_count]
while (!quit) {
VkFence frame_fence = frame_fences[frame_index]
vkWaitForFences(frame_fence)
vkResetFences(frame_fence)
uint32_t image_index;
VkSemaphore acquire_semaphore = acquire_semaphores[frame_index]
vkAcquireNextImageKHR(swapchain, acquire_semaphore, &image_index)
// 用获取到的交换链图像索引访问提交信号量
// 本例中这是唯一用 image_index 索引的资源
// 其他所有资源(包括 acquire_semaphore)都用当前帧缓冲索引
VkSemaphore submit_semaphore = submit_semaphores[image_index]
VkCommandBuffer command_buffer = command_buffers[frame_index]
vkBeginCommandBuffer(command_buffer)
RecordCommands(command_buffer)
vkEndCommandBuffer(command_buffer)
VkSubmitInfo submit_info
submit_info.pWaitSemaphores = &acquire_semaphore
submit_info.pCommandBuffers = &command_buffer
submit_info.pSignalSemaphores = &submit_semaphore
vkQueueSubmit(queue, &submit_info, frame_fence)
VkPresentInfo present_info
present_info.pWaitSemaphores = &submit_semaphore
present_info.pSwapchains = &swapchain
present_info.pImageIndices = &image_index
vkQueuePresent(queue, &present_info)
frame_index = (frame_index + 1) % kNumberOfFramesInFlight
}
VK_EXT_swapchain_maintenance1 扩展
上面的代码用于说明不依赖额外扩展如何处理交换链等待信号量。支持 VK_EXT_swapchain_maintenance1 的实现提供了另一种方案。
该扩展让 vkQueuePresentKHR 更接近 vkQueueSubmit,允许指定一个应用可等待的 fence。
VK_EXT_swapchain_maintenance1 还解决了原生 Vulkan 中没有良好方案的问题:退出时释放交换链资源。
通常应用会调用 vkDeviceWaitIdle 或 vkQueueWaitIdle,并认为此时删除交换链信号量与交换链本身是安全的。问题在于:WaitIdle 函数基于 fence 定义 —— 它们只等待通过带 fence 参数的函数提交的工作负载。原生 vkQueuePresent 不提供 fence 参数。
理论上,这意味着 vkDeviceWaitIdle 无法保证释放交换链资源是安全的。实践中应用仍会这样做,因为没有更好选择。这也是校验层在此场景下不会报错的原因。
VK_EXT_swapchain_maintenance1 扩展修复了该问题。通过等待呈现 fence,应用可安全释放交换链资源。启用该扩展后,若应用退出流程依赖 vkDeviceWaitIdle 或 vkQueueWaitIdle 释放交换链资源,而非使用呈现 fence,校验层会报错。

