Synchronization -- Swapchain Semaphore Reuse

Synchronization -- Swapchain Semaphore Reuse 问题说明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 的机制无额外扩展时。这意味着无法直接等待 present 完成信号。无法知道VkPresentInfoKHR::pWaitSemaphores是否仍被呈现操作占用。如果vkQueuePresentKHR可以触发信号等待该信号即可确认 present 队列操作已完成包括对VkPresentInfoKHR::pWaitSemaphores的等待。总结何时可以安全复用 present 等待信号量并不直观。解决方案讨论好消息是存在一种简单方法能保证呈现操作已完成尽管不如显式等待直接。通过vkAcquireNextImageKHR获取图像索引然后等待其信号量或 fence即可保证使用该图像索引的上一次呈现操作已完成其中包括对VkPresentInfoKHR::pWaitSemaphores的等待因此对应信号量可以安全复用。安全复用 present 等待信号量的流程调用vkAcquireNextImageKHR获取图像索引。在某一批vkQueueSubmit中等待来自vkAcquireNextImageKHR的信号量也可等待 fence但会引入额外主机同步点。等待完成后即可安全复用信号量。为什么很多应用会出错原因很可能是只要稍微偏离 “你触发、我等待” 的最直观同步方式复杂度就会上升逻辑变得不明显进而容易出错。常见错误按帧缓冲数量如双缓冲、三缓冲管理 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 }修复方法非常简单submit_semaphores数组按交换链图像数量分配而非帧缓冲数量。使用获取到的交换链图像索引访问该数组而非当前帧缓冲索引。在修复 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校验层会报错。