重新创建交换链

简介

我们的程序现在成功地画出了一个三角形,但是还有一些没有正确处理的东西。表面有可能发生了改变而交换链没有一同改变导致二者不兼容。导致这一情况的原因之一是窗口大小发生了改变。我们需要捕获这些事件然后重新创建交换链。

重新创建交换链

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

  1. void recreateSwapChain() {
  2. vkDeviceWaitIdle(device);
  3. createSwapChain();
  4. createImageViews();
  5. createRenderPass();
  6. createGraphicsPipeline();
  7. createFramebuffers();
  8. createCommandBuffers();
  9. }

首先,我们调用了vkDeviceWaitIdle,因为与上一章类似,我们不应该修改那些可能正在被使用的资源。显然,我们需要做的第一件事情是重新创建交换链本身。图像视图(image view)需要被重新创建,因为它们是直接基于交换链图像的。渲染过程(render pass)需要被重新创建,因为它们依赖于叫交换链图像的格式。尽管类似于改变窗口大小这种操作很少会导致交换链图像的格式发生变化,但是还是应该处理一下。视口和裁剪矩形的大小是在图形渲染管线创建时指定的,因此图形渲染管线也需要重新建立。如果为视口和裁剪矩形启用动态设置(dynamic state)的话可以避免重建整个图形渲染管线。最后,帧缓冲和命令缓冲也是直接基于交换链图像的。

为保证在重新创建这些对象之前其旧版本已经被清除,我们需要把一部分清除(cleanup)代码挪出来,写成一个单独的函数,这样我们就可以在recreateSwapChain函数里面调用了。给新函数起个名字叫cleanupSwapChain

  1. void cleanupSwapChain() {
  2. }
  3. void recreateSwapChain() {
  4. vkDeviceWaitIdle(device);
  5. cleanupSwapChain();
  6. createSwapChain();
  7. createImageViews();
  8. createRenderPass();
  9. createGraphicsPipeline();
  10. createFramebuffers();
  11. createCommandBuffers();
  12. }

我们会把重新创建交换链时涉及的所有需要重新创建的对象的清除代码从cleanup挪到cleanupSwapChain里:

  1. void cleanupSwapChain() {
  2. for (size_t i = 0; i < swapChainFramebuffers.size(); i++) {
  3. vkDestroyFramebuffer(device, swapChainFramebuffers[i], nullptr);
  4. }
  5. vkFreeCommandBuffers(device, commandPool, static_cast<uint32_t>(commandBuffers.size()), commandBuffers.data());
  6. vkDestroyPipeline(device, graphicsPipeline, nullptr);
  7. vkDestroyPipelineLayout(device, pipelineLayout, nullptr);
  8. vkDestroyRenderPass(device, renderPass, nullptr);
  9. for (size_t i = 0; i < swapChainImageViews.size(); i++) {
  10. vkDestroyImageView(device, swapChainImageViews[i], nullptr);
  11. }
  12. vkDestroySwapchainKHR(device, swapChain, nullptr);
  13. }
  14. void cleanup() {
  15. cleanupSwapChain();
  16. for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
  17. vkDestroySemaphore(device, renderFinishedSemaphores[i], nullptr);
  18. vkDestroySemaphore(device, imageAvailableSemaphores[i], nullptr);
  19. vkDestroyFence(device, inFlightFences[i], nullptr);
  20. }
  21. vkDestroyCommandPool(device, commandPool, nullptr);
  22. vkDestroyDevice(device, nullptr);
  23. if (enableValidationLayers) {
  24. DestroyDebugReportCallbackEXT(instance, callback, nullptr);
  25. }
  26. vkDestroySurfaceKHR(instance, surface, nullptr);
  27. vkDestroyInstance(instance, nullptr);
  28. glfwDestroyWindow(window);
  29. glfwTerminate();
  30. }

我们可以从头开始重新创建命令池,但是这样相当浪费(资源)。作为替代,我选择了使用vkFreeCommandBuffers函数清除现有的命令缓冲。这样我们就可以利用已经创建的命令池来分配新的命令缓冲。

为了正确处理窗口大小的改变,我们还需要来查询帧缓冲现在的大小来保证交换链图像的大小是正确的(新的)。为此我们修改chooseSwapExtent函数来考虑实际大小:

  1. VkExtent2D chooseSwapExtent(const VkSurfaceCapabilitiesKHR& capabilities) {
  2. if (capabilities.currentExtent.width != std::numeric_limits<uint32_t>::max()) {
  3. return capabilities.currentExtent;
  4. } else {
  5. int width, height;
  6. glfwGetFramebufferSize(window, &width, &height);
  7. VkExtent2D actualExtent = {
  8. static_cast<uint32_t>(width),
  9. static_cast<uint32_t>(height)
  10. };
  11. ...
  12. }
  13. }

这就是重新创建交换链所需要的全部步骤!然而这种方式的缺点在于我们需要停止所有渲染操作直到新交换链创建完成。在创建新交换链的同时,可以让旧交换链上的绘制命令继续进行。你需要通过VkSwapchainCreateInfoKHR结构体的oldSwapChain字段来传递之前的交换链,然后在你用完旧交换链的时候销毁它。

未经优化的或过期的交换链

现在我们只需要找出什么时候需要重新创建交换链,然后调用我们新的recreateSwapChain函数就行了。幸运的是,Vulkan通常会在显示过程中告知我们交换链已经不再适用。vkAcquireNextImageKHRvkQueuePresentKHR函数可以返回如下的特殊值来表明这一点:

  • VK_ERROR_OUT_OF_DATE_KHR:交换链已经与表面不兼容且无法再用于渲染。通常会在窗口大小改变后发生。
  • VK_SUBOPTIMAL_KHR:交换链可以继续用于渲染,但是其与表面属性不再完全匹配。
  1. VkResult result = vkAcquireNextImageKHR(device, swapChain, std::numeric_limits<uint64_t>::max(), imageAvailableSemaphores[currentFrame], VK_NULL_HANDLE, &imageIndex);
  2. if (result == VK_ERROR_OUT_OF_DATE_KHR) {
  3. recreateSwapChain();
  4. return;
  5. } else if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) {
  6. throw std::runtime_error("failed to acquire swap chain image!");
  7. }

如果交换链在试图获取一个新图像的时候过期,那么它将无法再用于显示。因此我们应该立即重建交换链并且在下一次调用drawFrame函数时重试。

然而,如果我们在这时放弃了绘制,那么屏障(fence)就不会通过vkQueueSubmit函数进行提交,在稍后我们尝试等待它的时候其可能处于一个意外的状态。我们可以把重新创建屏障作为重新创建交换链的一个部分,不过移动vkResetFences的调用更简单:

  1. vkResetFences(device, 1, &inFlightFences[currentFrame]);
  2. if (vkQueueSubmit(graphicsQueue, 1, &submitInfo, inFlightFences[currentFrame]) != VK_SUCCESS) {
  3. throw std::runtime_error("failed to submit draw command buffer!");
  4. }

你也可以在交换链不再适用的时候这么做,但是我选择在这时继续渲染,因为我们已经请求到了一个图像。VK_SUCCESSVK_SUBOPTIMAL_KHR都可以认为是“成功”的返回值。

  1. result = vkQueuePresentKHR(presentQueue, &presentInfo);
  2. if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR) {
  3. recreateSwapChain();
  4. } else if (result != VK_SUCCESS) {
  5. throw std::runtime_error("failed to present swap chain image!");
  6. }
  7. currentFrame = (currentFrame + 1) % MAX_FRAMES_IN_FLIGHT;

vkQueuePresentKHR函数返回同样的值,意思也是一样的。在这种情况下如果交换链变得不再适用我们也会重新创建交换链,因为我们想尽可能获得好的结果。

显式处理大小的改变

尽管在窗口大小改变之后,大多数驱动和平台都会自动触发VK_ERROR_OUT_OF_DATE_KHR,但这种行为是不受保证的。这也就是我们要添加一些额外的代码来显式处理大小的改变的理由。首先添加一个新的成员变量来表示大小是否被改变:

  1. std::vector<VkFence> inFlightFences;
  2. size_t currentFrame = 0;
  3. bool framebufferResized = false;

drawFrame函数需要被修改以检查此标志:

  1. if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR || framebufferResized) {
  2. framebufferResized = false;
  3. recreateSwapChain();
  4. } else if (result != VK_SUCCESS) {
  5. ...
  6. }

vkQueuePresentKHR之后再执行这一操作非常重要,这样可以确保信号量处于一致的状态,否则可能永远无法正确等待一个改变了信号的信号量。现在为了检测大小的改变,我们可以使用GLFW框架中的glfwSetFramebufferSizeCallback函数来设置一个回调函数:

  1. void initWindow() {
  2. glfwInit();
  3. glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
  4. window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr);
  5. glfwSetFramebufferSizeCallback(window, framebufferResizeCallback);
  6. }
  7. static void framebufferResizeCallback(GLFWwindow* window, int width, int height) {
  8. }

之所以创建了一个static函数是因为GLFW不知道该如何使用正确的、指向我们的HelloTriangleApplication实例的this指针来正确地调用成员函数。

不过,我们在回调函数得到了一个GLFWwindow的引用,并且有另一个函数允许你向它里面保存一个任意类型的指针:glfwSetWindowUserPointer

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

这个值现在可以在回调函数里通过glfwGetWindowUserPointer取回,然后正确地设置标志:

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

现在尝试运行程序并且调整窗口大小来看看帧缓冲的大小是否的确随着窗口大小改变了。

处理最小化

还有一种情况会导致交换链过期,并且这也是一种特殊的窗口大小改变:窗口最小化。这种情况特殊在它会导致帧缓冲的大小变为0。在这篇教程中我们会以暂停渲染直到窗口回到前台的方式处理最小化,这需要扩展recreateSwapChain函数:

  1. void recreateSwapChain() {
  2. int width = 0, height = 0;
  3. while (width == 0 || height == 0) {
  4. glfwGetFramebufferSize(window, &width, &height);
  5. glfwWaitEvents();
  6. }
  7. vkDeviceWaitIdle(device);
  8. ...
  9. }

恭喜,你现在完成了你的第一个表现良好的Vulkan程序!在下一章里我们会摆脱硬编码在顶点着色器里的顶点,然后开始使用顶点缓冲。

C++代码/顶点着色器/片段着色器