欢迎光临
我们一直在努力

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,校验层会报错。

    赞(0)
    未经允许不得转载:171主机测评 » Synchronization -- Swapchain Semaphore Reuse
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址