交换链重建

      +

      介绍

      我们现在的应用程序已经成功绘制了一个三角形,但还有一些情况没有正确处理。 窗口表面可能会发生变化,导致交换链与其不再兼容。 造成这种情况的原因之一是窗口大小的改变。 我们必须捕获这些事件并重新创建交换链。

      重建交换链

      创建一个新的 recreateSwapChain 函数,调用 createSwapChain 和所有依赖于交换链或窗口大小的对象的创建函数。

      void recreateSwapChain() {
          device.waitIdle();
      
          createSwapChain();
          createImageViews();
      }

      我们首先调用 vkDeviceWaitIdle,因为就像上一章一样,我们不应该触碰可能仍在使用的资源。 显然,我们需要重新创建交换链本身。 图像视图需要重新创建,因为它们直接基于交换链图像。

      为了确保在重新创建这些对象之前清理旧版本,我们应该将一些清理代码移到一个单独的函数中,我们可以从 recreateSwapChain 函数调用它。 让我们称之为 cleanupSwapChain

      void cleanupSwapChain() {
      
      }
      
      void recreateSwapChain() {
          device.waitIdle();
      
          cleanupSwapChain();
      
          createSwapChain();
          createImageViews();
      }

      注意,为了简单起见,我们这里不重新创建渲染通道。 理论上,交换链图像格式可能在应用程序的生命周期内发生变化,例如, 当将窗口从标准范围移动到高动态范围显示器时。 这可能需要应用程序重新创建渲染通道,以确保正确反映动态范围之间的变化。

      我们将所有作为交换链刷新的一部分重新创建的对象的清理代码从 cleanup 移到 cleanupSwapChain

      void cleanupSwapChain() {
          swapChainImageViews.clear();
          swapChain = nullptr;
      }
      
      void cleanup() {
          cleanupSwapChain();
      
          glfwDestroyWindow(window);
          glfwTerminate();
      }

      注意,在 chooseSwapExtent 中,我们已经查询了新的窗口分辨率,以确保交换链图像具有(新的)正确大小,所以不需要修改 chooseSwapExtent(记住,我们在创建交换链时已经必须使用 glfwGetFramebufferSize 来获取表面的像素分辨率)。

      重建交换链就是这么简单! 然而,这种方法的缺点是我们需要在创建新的交换链之前停止所有渲染。 可以在旧交换链的图像上的绘制命令仍在进行时创建新的交换链。 您需要将旧交换链传递给 VkSwapchainCreateInfoKHR 结构的 oldSwapchain 字段,并在使用完毕后立即销毁旧交换链。

      次优或过时的交换链

      现在我们只需要弄清楚何时需要重建交换链,并调用我们新的 recreateSwapChain 函数。 幸运的是,Vulkan 通常会在呈现期间告诉我们交换链不再足够。 vkAcquireNextImageKHRvkQueuePresentKHR 函数可以返回以下特殊值来表示这一点。

      • VK_ERROR_OUT_OF_DATE_KHR:交换链与表面不再兼容,不能再用于渲染。 通常在窗口调整大小后发生。

      • VK_SUBOPTIMAL_KHR:交换链仍然可以成功呈现到表面,但表面属性不再完全匹配。

      auto [result, imageIndex] = swapChain.acquireNextImage( UINT64_MAX, *presentCompleteSemaphores[currentFrame], nullptr );
      
      if (result == vk::Result::eErrorOutOfDateKHR) {
          recreateSwapChain();
          return;
      }
      if (result != vk::Result::eSuccess && result != vk::Result::eSuboptimalKHR) {
          throw std::runtime_error("failed to acquire swap chain image!");
      }

      如果在尝试获取图像时发现交换链已经过时,那么就不可能再向其呈现了。 因此,我们应该立即重建交换链,并在下一个 drawFrame 调用中重试。

      如果交换链是次优的,您也可以决定这样做,但在这种情况下,我选择继续,因为我们已经获取了一个图像。 VK_SUCCESSVK_SUBOPTIMAL_KHR 都被视为"成功"返回代码。

      result = presentQueue.presentKHR( presentInfoKHR );
      if (result == vk::Result::eErrorOutOfDateKHR || result == vk::Result::eSuboptimalKHR) {
          framebufferResized = false;
          recreateSwapChain();
      } else if (result != vk::Result::eSuccess) {
          throw std::runtime_error("failed to present swap chain image!");
      }
      
      currentFrame = (currentFrame + 1) % MAX_FRAMES_IN_FLIGHT;

      vkQueuePresentKHR 函数返回相同的值,含义也相同。 在这种情况下,如果交换链是次优的,我们也会重建交换链,因为我们希望获得最佳结果。

      修复死锁

      如果我们现在尝试运行代码,可能会遇到死锁。 调试代码,我们发现应用程序到达 vkWaitForFences 但永远不会继续。 这是因为当 vkAcquireNextImageKHR 返回 VK_ERROR_OUT_OF_DATE_KHR 时,我们重建交换链然后从 drawFrame 返回。 但在此之前,当前帧的栅栏已经被等待并重置。 由于我们立即返回,没有提交工作执行,栅栏永远不会被信号通知,导致 vkWaitForFences 永远停止。

      幸运的是,有一个简单的修复方法。 延迟重置栅栏,直到我们确定会提交工作。 因此,如果我们提前返回,栅栏仍然是有信号的,下次我们使用相同的栅栏对象时,vkWaitForFences 不会死锁。

      drawFrame 的开头现在应该是这样的:

      vkWaitForFences(device, 1, &inFlightFences[currentFrame], VK_TRUE, UINT64_MAX);
      
      uint32_t imageIndex;
      VkResult result = vkAcquireNextImageKHR(device, swapChain, UINT64_MAX, imageAvailableSemaphores[currentFrame], VK_NULL_HANDLE, &imageIndex);
      
      if (result == VK_ERROR_OUT_OF_DATE_KHR) {
          recreateSwapChain();
          return;
      } else if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) {
          throw std::runtime_error("failed to acquire swap chain image!");
      }
      
      // 只有在提交工作时才重置栅栏
      vkResetFences(device, 1, &inFlightFences[currentFrame]);

      显式处理调整大小

      虽然许多驱动程序和平台在窗口调整大小后自动触发 VK_ERROR_OUT_OF_DATE_KHR,但这并不能保证一定会发生。 这就是为什么我们将添加一些额外的代码来显式处理调整大小。 首先,添加一个新的成员变量,标记已发生调整大小:

      std::vector<vk::raii::Fence> inFlightFences;
      
      bool framebufferResized = false;

      然后应该修改 drawFrame 函数,也检查这个标志:

      if (result == vk::Result::eErrorOutOfDateKHR || result == vk::Result::eSuboptimalKHR) {
          framebufferResized = false;
          recreateSwapChain();
      } else if (result != vk::Result::eSuccess) {
          ...
      }

      vkQueuePresentKHR 之后执行此操作很重要,以确保信号量处于一致状态,否则已发出信号的信号量可能永远不会被正确等待。 现在,要实际检测调整大小,我们可以使用 GLFW 框架中的 glfwSetFramebufferSizeCallback 函数来设置回调:

      void initWindow() {
          glfwInit();
      
          glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
      
          window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr);
          glfwSetFramebufferSizeCallback(window, framebufferResizeCallback);
      }
      
      static void framebufferResizeCallback(GLFWwindow* window, int width, int height) {
      
      }

      我们创建一个 static 函数作为回调的原因是因为 GLFW 不知道如何正确调用带有正确 this 指针的成员函数到我们的 HelloTriangleApplication 实例。

      然而,我们在回调中确实得到了对 GLFWwindow 的引用,还有另一个 GLFW 函数允许您在其中存储任意指针:glfwSetWindowUserPointer

      window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr);
      glfwSetWindowUserPointer(window, this);
      glfwSetFramebufferSizeCallback(window, framebufferResizeCallback);

      现在可以从回调中使用 glfwGetWindowUserPointer 检索此值,以正确设置标志:

      static void framebufferResizeCallback(GLFWwindow* window, int width, int height) {
          auto app = reinterpret_cast<HelloTriangleApplication*>(glfwGetWindowUserPointer(window));
          app->framebufferResized = true;
      }

      现在尝试运行程序并调整窗口大小,看看帧缓冲区是否确实随窗口正确调整大小。

      处理最小化

      还有一种情况可能导致交换链过时,那就是一种特殊的窗口调整大小:窗口最小化。 这种情况特殊,因为它会导致帧缓冲区大小为 0。 在本教程中,我们将通过扩展 recreateSwapChain 函数来处理这种情况,暂停直到窗口再次处于前台:

      void recreateSwapChain() {
          int width = 0, height = 0;
          glfwGetFramebufferSize(window, &width, &height);
          while (width == 0 || height == 0) {
              glfwGetFramebufferSize(window, &width, &height);
              glfwWaitEvents();
          }
      
          device.waitIdle();
      
          ...
      }

      初始调用 glfwGetFramebufferSize 处理大小已经正确且 glfwWaitEvents 没有等待对象的情况。

      恭喜,您现在已经完成了您的第一个行为良好的 Vulkan 程序! 在 下一章 中,我们将摆脱顶点着色器中的硬编码顶点,实际使用顶点缓冲区。