[译]Vulkan教程(20)重建交换链

Posted bitzhuwei

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[译]Vulkan教程(20)重建交换链相关的知识,希望对你有一定的参考价值。

[译]Vulkan教程(20)重建交换链

Swap chain recreation 重建交换链

Introduction 入门

The application we have now successfully draws a triangle, but there are some circumstances that it isn‘t handling properly yet. It is possible for the window surface to change such that the swap chain is no longer compatible with it. One of the reasons that could cause this to happen is the size of the window changing. We have to catch these events and recreate the swap chain.

我们现在的程序成功地绘制了一个三角形,但是有的情况它处理的不合适。窗口surface可能改变,使得交换链不再与之兼容。可能的原因之一是,窗口的大小改变了。我们必须捕捉这些事件,并重建交换链。

Recreating the swap chain 重建交换链

Create a new recreateSwapChain function that calls createSwapChain and all of the creation functions for the objects that depend on the swap chain or the window size.

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

void recreateSwapChain() 
    vkDeviceWaitIdle(device);
 
    createSwapChain();
    createImageViews();
    createRenderPass();
    createGraphicsPipeline();
    createFramebuffers();
    createCommandBuffers();

 

We first call vkDeviceWaitIdle, because just like in the last chapter, we shouldn‘t touch resources that may still be in use. Obviously, the first thing we‘ll have to do is recreate the swap chain itself. The image views need to be recreated because they are based directly on the swap chain images. The render pass needs to be recreated because it depends on the format of the swap chain images. It is rare for the swap chain image format to change during an operation like a window resize, but it should still be handled. Viewport and scissor rectangle size is specified during graphics pipeline creation, so the pipeline also needs to be rebuilt. It is possible to avoid this by using dynamic state for the viewports and scissor rectangles. Finally, the framebuffers and command buffers also directly depend on the swap chain images.

我们首先调用vkDeviceWaitIdle,因为像上一章一样,我们不应该触碰资源that可能在使用的。显然,我们要做的第一件事是,重建交换链本身。Image视图需要被重建,因为它们是直接基于交换链image的。Render pass需要被重建,因为它依赖交换链image的格式。交换链image的格式很少随着窗口大小之类的改变而变,但是还是应该处理一下。视口和裁剪区域大小在图形管道创建过程中指定,所以管道也需要被重建。有可能避免这个by使用动态状态for视口和裁剪。最后,帧缓存和命令buffer也直接依赖交换链image。

To make sure that the old versions of these objects are cleaned up before recreating them, we should move some of the cleanup code to a separate function that we can call from the recreateSwapChain function. Let‘s call itcleanupSwapChain:

在重建之前,为确保旧对象被清理干净,我们应当将清理代码移动到一个单独的函数that我们可以在recreateSwapChain 函数中调用。我们称它为cleanupSwapChain

void cleanupSwapChain() 
 

 
void recreateSwapChain() 
    vkDeviceWaitIdle(device);
 
    cleanupSwapChain();
 
    createSwapChain();
    createImageViews();
    createRenderPass();
    createGraphicsPipeline();
    createFramebuffers();
    createCommandBuffers();

 

we‘ll move the cleanup code of all objects that are recreated as part of a swap chain refresh from cleanup to cleanupSwapChain:

我们将清理代码中会被重建的对象当作交换链刷新的一部分,从cleanup 移动到cleanupSwapChain

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

 

We could recreate the command pool from scratch, but that is rather wasteful. Instead I‘ve opted to clean up the existing command buffers with the vkFreeCommandBuffers function. This way we can reuse the existing pool to allocate the new command buffers.

我们可以从零开始重建命令池,但是那太浪费了。我选择用vkFreeCommandBuffers 函数清理已有的命令buffer。这样我们可以重用已有的池来分配新的命令buffer。

To handle window resizes properly, we also need to query the current size of the framebuffer to make sure that the swap chain images have the (new) right size. To do that change the chooseSwapExtent function to take the actual size into account:

为了恰当地处理窗口大小改变问题,我们也需要查询帧缓存当前的大小to确保交换链image有(新的)正确的大小。为此,修改chooseSwapExtent 函数to使用实际的大小:

 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  
 8         VkExtent2D actualExtent = 
 9             static_cast<uint32_t>(width),
10             static_cast<uint32_t>(height)
11         ;
12  
13         ...
14     
15 

 

That‘s all it takes to recreate the swap chain! However, the disadvantage of this approach is that we need to stop all rendering before creating the new swap chain. It is possible to create a new swap chain while drawing commands on an image from the old swap chain are still in-flight. You need to pass the previous swap chain to the oldSwapChain field in the VkSwapchainCreateInfoKHR struct and destroy the old swap chain as soon as you‘ve finished using it.

重建交换链要做的就这些!但是,这个方式的缺点是我们需要停止所有的渲染后才能创建新的交换链。其实可以创建新交换链的同时,保持绘制命令在旧交换链上仍旧在继续。你需要传入之前的交换链到VkSwapchainCreateInfoKHR 结构体的oldSwapChain 字段,在用完旧交换链后立即销毁它。

Suboptimal or out-of-date swap chain 次优或过期的交换链

Now we just need to figure out when swap chain recreation is necessary and call our new recreateSwapChainfunction. Luckily, Vulkan will usually just tell us that the swap chain is no longer adequate during presentation. The vkAcquireNextImageKHR and vkQueuePresentKHR functions can return the following special values to indicate this.

  • VK_ERROR_OUT_OF_DATE_KHR: The swap chain has become incompatible with the surface and can no longer be used for rendering. Usually happens after a window resize.
  • VK_SUBOPTIMAL_KHR: The swap chain can still be used to successfully present to the surface, but the surface properties are no longer matched exactly.

现在我们需要确认,什么时候需要重建交换链并调用recreateSwapChainfunction函数。幸运的是,Vulkan一般会在呈现期间告诉我们交换链不再胜任了。vkAcquireNextImageKHR 和vkQueuePresentKHR 函数会返回下述特殊值来表示这一情况。

  • VK_ERROR_OUT_OF_DATE_KHR:交换链已经与surface不兼容,不能再被用于渲染。一般在窗口改变大小后发生。
  • VK_SUBOPTIMAL_KHR:交换链还可以被用于成功地呈现surface,但是surface属性已经不严格匹配了。
1 VkResult result = vkAcquireNextImageKHR(device, swapChain, std::numeric_limits<uint64_t>::max(), imageAvailableSemaphores[currentFrame], VK_NULL_HANDLE, &imageIndex);
2  
3 if (result == VK_ERROR_OUT_OF_DATE_KHR) 
4     recreateSwapChain();
5     return;
6  else if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) 
7     throw std::runtime_error("failed to acquire swap chain image!");
8 

 

If the swap chain turns out to be out of date when attempting to acquire an image, then it is no longer possible to present to it. Therefore we should immediately recreate the swap chain and try again in the next drawFrame call.

如果交换链过期了when尝试请求image时,那么它就不可能呈现image了。因此我们应当立即重建交换链并在下一次的drawFrame 调用中再次尝试。

However, if we abort drawing at this point then the fence will never have been submitted with vkQueueSubmit and it‘ll be in an unexpected state when we try to wait for it later on. We could recreate the fences as part of swap chain recreation, but it‘s easier to move the vkResetFences call:

但是,如果我们在此时中止绘制,那么fence就永远不会被vkQueueSubmit 提交,它将处于不可预知的状态when我们稍后尝试等待它时。我们可以重建fence,作为重建交换链的一部分,但是取消调用vkResetFences 更简单:

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

 

You could also decide to do that if the swap chain is suboptimal, but I‘ve chosen to proceed anyway in that case because we‘ve already acquired an image. Both VK_SUCCESS and VK_SUBOPTIMAL_KHR are considered "success" return codes.

你也可以这样做if交换链是次优的,但是我选择这样,因为我们已经请求了一个image。VK_SUCCESS 和VK_SUBOPTIMAL_KHR 都被认为是“成功”的返回码。

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

 

The vkQueuePresentKHR function returns the same values with the same meaning. In this case we will also recreate the swap chain if it is suboptimal, because we want the best possible result.

vkQueuePresentKHR 函数返回相同的值with相同的含义。此时,我们也要重建交换链if它是次优的,因为我们想要最好的结果。

Handling resizes explicitly 显式地处理resize

Although many drivers and platforms trigger VK_ERROR_OUT_OF_DATE_KHR automatically after a window resize, it is not guaranteed to happen. That‘s why we‘ll add some extra code to also handle resizes explicitly. First add a new member variable that flags that a resize has happened:

尽管许多驱动和平台自动触发VK_ERROR_OUT_OF_DATE_KHR  after窗口resize后,这不保证它一定发生。这就是为什么我们添加一些外的代码to显式地处理resize。首先添加新成员变量that标记resize是否发生了:

std::vector<VkFence> inFlightFences;
size_t currentFrame = 0;
 
bool framebufferResized = false;

 

The drawFrame function should then be modified to also check for this flag:

drawFrame 函数应当修改一下,检查这个标志:

if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR || framebufferResized) 
    framebufferResized = false;
    recreateSwapChain();
 else if (result != VK_SUCCESS) 
    ...

 

It is important to do this after vkQueuePresentKHR to ensure that the semaphores are in a consistent state, otherwise a signalled semaphore may never be properly waited upon. Now to actually detect resizes we can use the glfwSetFramebufferSizeCallback function in the GLFW framework to set up a callback:

重要的是要在vkQueuePresentKHR 之后做这件事to确保semaphore处于一致的状态,否则一个有信号的semaphore可能永远不能被恰当地等待。现在为了实际地检测resize,我们可以用GLFW框架的glfwSetFramebufferSizeCallback 函数to设置一个回调:

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) 
 

 

The reason that we‘re creating a static function as a callback is because GLFW does not know how to properly call a member function with the right this pointer to our HelloTriangleApplication instance.

我们创建static 函数作为回调的原因是,GLFW不知道如何恰当地调用成员函数with正确的this 指针to我们的HelloTriangleApplication 实例。

However, we do get a reference to the GLFWwindow in the callback and there is another GLFW function that allows you to store an arbitrary pointer inside of it: glfwSetWindowUserPointer:

但是, 我们在回调中有个GLFWwindow 的引用,另一个GLFW函数允许你保存任何指针到glfwSetWindowUserPointer内:

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

 

This value can now be retrieved from within the callback with glfwGetWindowUserPointer to properly set the flag:

这个值现在可以被回调函数检索with glfwGetWindowUserPointer  to恰当地设置标志:

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

 

Now try to run the program and resize the window to see if the framebuffer is indeed resized properly with the window.

现在尝试运行程序,调整窗口大小,看看帧缓存是否真的随着窗口恰当地resize了。

Handling minimization 处理最小化

There is another case where a swap chain may become out of data and that is a special kind of window resizing: window minimization. This case is special because it will result in a frame buffer size of 0. In this tutorial we will handle that by pausing until the window is in the foreground again by extending the recreateSwapChainfunction:

还有另一个情况where交换链可能没有数据,这是一种特殊的窗口resize:窗口最小化。这很特殊,因为它会导致一帧的buffer大小为0。本教程中我们将处理这个我那天by暂停until窗口再次出现在前台by扩展recreateSwapChainfunction函数:

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

 

Congratulations, you‘ve now finished your very first well-behaved Vulkan program! In the next chapter we‘re going to get rid of the hardcoded vertices in the vertex shader and actually use a vertex buffer.

恭喜,你现在已经完成了你的第一个表现良好的Vulkan程序!下一章我们将脱离顶点shader中硬编码的顶点,使用真正的顶点buffer。

C++ code / Vertex shader / Fragment shader

 

 

以上是关于[译]Vulkan教程(20)重建交换链的主要内容,如果未能解决你的问题,请参考以下文章

[译]Vulkan教程(06)验证层

[译]Vulkan教程(17)帧缓存

[译]Vulkan教程(08)逻辑设备和队列

[译]Vulkan教程(21)顶点input描述

Vulkan迷惑问题-交换链中获取图片vkAcquireNextImageKHR的ImageIndex 与 currentFrame之间的关系

Vulkan Tutorial 08 交换链