简述 在上一篇里,我们已经成功绘制了一个颜色渐变的三角形,并将其显示在窗口上了。但是窗口Surface可能会发生变化,从而使交换链不再与之兼容,比如是窗口大小的变化。所以我们必须捕获这些事件并重新创建交换链。
一. 重建交换链 创建一个新的recreateSwapChain函数,该函数调用createSwapChain和所有依赖于交换链或窗口大小的对象的创建函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void cleanupSwapChain () {} void recreateSwapChain () { vkDeviceWaitIdle (device); cleanupSwapChain (); createSwapChain (); createImageViews (); createRenderPass (); createGraphicsPipeline (); createFramebuffers (); createCommandBuffers (); }
首先调用vkDeviceWaitIdle,确保不触及可能仍在使用的资源。显然,我们必须做的第一件事是重新创建交换链本身。
需要重新创建图像视图,因为它们直接基于交换链图像。渲染通道需要重新创建,因为它取决于交换链图像的格式。交换链的图像格式很少在窗口调整等操作期间发生改变,但它仍然应该被处理。视口和剪刀矩形大小是在图形管道创建时指定的,因此管道也需要重新构建。可以通过使用动态状态的视图和剪刀矩形来避免这种情况。最后,帧缓冲区和命令缓冲区也直接依赖于交换链图像。
为了确保这些对象的旧版本在重新创建它们之前得到清理,我们应该将一些清理代码移到一个单独的函数中,我们可以从recreateSwapChain函数调用这个函数cleanupSwapChain。
1.1 cleanupSwapChain 将所有和交换链相关的资源从cleanup函数中移到此函数内:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 void cleanupSwapChain () { for (auto framebuffer : swapChainFramebuffers) { vkDestroyFramebuffer (device, framebuffer, nullptr ); } vkFreeCommandBuffers (device, commandPool, static_cast <uint32_t >(commandBuffers.size ()), commandBuffers.data ()); vkDestroyPipeline (device, graphicsPipeline, nullptr ); vkDestroyPipelineLayout (device, pipelineLayout, nullptr ); vkDestroyRenderPass (device, renderPass, nullptr ); for (auto imageView : swapChainImageViews) { vkDestroyImageView (device, imageView, nullptr ); } vkDestroySwapchainKHR (device, swapChain, nullptr ); } void cleanup () { cleanupSwapChain (); ... }
可以从头重新创建命令池,但相当浪费。所以选择使用vkFreeCommandBuffers函数清理现有的命令缓冲区, 这样可以重用现有的池来分配新的命令缓冲区。
1.2 获取窗口最新大小 为了正确地处理窗口的大小,我们还需要查询framebuffer的当前大小,以确保交换链图像具有(新的)正确的大小。为了做到这一点,改变chooseSwapExtent函数来考虑实际的大小:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 VkExtent2D chooseSwapExtent (const VkSurfaceCapabilitiesKHR& capabilities) { if (capabilities.currentExtent.width != std::numeric_limits<uint32_t >::max ()) { return capabilities.currentExtent; } else { int width, height; glfwGetFramebufferSize (window, &width, &height); VkExtent2D actualExtent = { static_cast <uint32_t >(width), static_cast <uint32_t >(height)}; actualExtent.width = std::max (capabilities.minImageExtent.width, std::min (capabilities.maxImageExtent.width, actualExtent.width)); actualExtent.height = std::max (capabilities.minImageExtent.height, std::min (capabilities.maxImageExtent.height, actualExtent.height)); return actualExtent; } }
通过glfwGetFramebufferSize函数来获取当前窗口大小。
这就是重建交换链所需要的全部!然而,这种方法的缺点是:需要在创建新的交换链之前停止所有的呈现。当从旧的交换链在图像上绘制命令时,可以创建一个新的交换链。需要将之前的交换链传递给VkSwapchainCreateInfoKHR结构中的oldswarechain字段,并在使用完旧的交换链后立即销毁它。
1.3 次优或过时的交换链 现在,我们只需要确定何时需要重新创建交换链,并调用新的recreateSwapChain函数。幸运的是,Vulkan通常会告诉我们交换链在显示过程中不再足够。vkAcquireNextImageKHR和vkQueuePresentKHR函数可以返回以下特殊值来表示这一点:
VK_ERROR_OUT_OF_DATE_KHR: 交换链已经变得与表面不兼容,不能再用于渲染。通常发生在窗口大小调整之后。
VK_SUBOPTIMAL_KHR:交换链仍然可以成功地呈现到表面,但是表面的属性不再完全匹配。
1 2 3 4 5 6 7 8 9 10 VkResult result = vkAcquireNextImageKHR (device, swapChain, std::numeric_limits<uint64_t >::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!" ); }
如果交换链在试图获取映像时已经过期,那么就不可能再向其呈现。因此,应该立即重新创建交换链,并在下一个drawFrame调用中再次尝试。然而,如果在这中止绘图,那么栅栏将永远不会通过vkqueuessubmit提交,当我们稍后尝试等待它时,它将处于一个意想不到的状态。我们可以重建fence作为交换链重建的一部分,但是移动vkResetFences调用更容易:
1 2 3 4 5 vkResetFences (device, 1 , &inFlightFences[currentFrame]);if (vkQueueSubmit (graphicsQueue, 1 , &submitInfo, inFlightFences[currentFrame]) != VK_SUCCESS) { throw std::runtime_error ("failed to submit draw command buffer!" ); }
把fence放在vkQueueSubmit之前,而不是在vkWaitForFences后立刻调用。这是什么原理呢?
1.3.1 fence fence是一种同步原语,可用于将依赖项从队列插入到主机。fence有两种状态——有信号的和没有信号的, fence可以作为队列提交命令执行的一部分发出信号。
使用vkResetFences可以将fence置为unsignal状态。主机可以通过vkWaitForFences命令来等待fence,并且可以通过vkGetFenceStatus来查询当前的状态。
如果vkWaitForFences被调用时条件被满足,那么vkWaitForFences会立即返回。如果在vkWaitForFences被调用的时候条件没有被满足,那么vkWaitForFences将会阻塞并等待到超时纳秒,直到条件被满足。这里的条件就是fence状态是不是signal状态。vkQueueSubmit会将fence置为signal状态,那么vkWaitForFences就会通过。
所以,当vkWaitForFences之后立刻调用vkResetFences,那么当vkAcquireNextImageKHR发生异常导致返回时,下次在进入drawFrame调用vkWaitForFences就永远处于等待状态了。
1.3.2 vkQueuePresentKHR 如果交换链不是最优的,也可以继续呈现,因为我们已经获得了一个映像。VK_SUCCESS和VK_SUBOPTIMAL_KHR都被认为是“成功”返回码。
1 2 3 4 5 6 result = vkQueuePresentKHR (presentQueue, &presentInfo); if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR) { recreateSwapChain (); } else if (result != VK_SUCCESS) { throw std::runtime_error ("failed to present swap chain image!" ); }
vkQueuePresentKHR函数返回具有相同含义的相同值。 在这种情况下,如果交换链不是次优的,为获得最好的结果,最好重新创建交换链。
1.4 主动处理窗口变化 尽管许多驱动程序和平台在调整窗口大小后会自动触发VK_ERROR_OUT_OF_DATE_KHR,但不能保证一定会发生这种情况。所以最好通过监听窗口变化来主动重建交换链。
添加一个新的成员变量,该变量指示已调整大小:
1 2 3 4 5 6 7 8 9 10 bool framebufferResized = false ;void drawFrame () { ... result = vkQueuePresentKHR (presentQueue, &presentInfo); if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR || framebufferResized) { framebufferResized = false ; recreateSwapChain (); ... }
1.4.1 监听窗口变化 要实际检测窗口大小调整,可以使用GLFW框架中的glfwSetFramebufferSizeCallback函数来设置回调:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void initWindow () { glfwInit (); glfwWindowHint (GLFW_CLIENT_API, GLFW_NO_API); window = glfwCreateWindow (WIDTH, HEIGHT, "Vulkan" , nullptr , nullptr ); glfwSetWindowUserPointer (window, this ); glfwSetFramebufferSizeCallback (window, framebufferResizeCallback); } static void framebufferResizeCallback (GLFWwindow* window, int width, int height) { auto app = reinterpret_cast <HelloTriangleApplication*>(glfwGetWindowUserPointer (window)); app->framebufferResized = true ; }
因为glfw回调只能通过静态函数实现,所以通过glfwSetWindowUserPointer保存当前实例指针。
1.5 窗口最小化 还有一种特殊状态是,当最小化窗口时,拿到的窗口大小是0, 这样创建出来的帧缓冲区大小也应该是0,根本不需要渲染。所以这里做一个简单的等待处理:
1 2 3 4 5 6 7 8 9 10 void recreateSwapChain () { int width = 0 , height = 0 ; while (width == 0 || height == 0 ) { glfwGetFramebufferSize (window, &width, &height); glfwWaitEvents (); } vkDeviceWaitIdle (device); ... }
突然有点好奇这个绘制的刷新率,通过在drawFrame里嵌入函数computeRefreshRate来计算刷新率:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void computeRefreshRate () { static float fps = 0 ; static int64_t count = 0 ; static int64_t lastCount = 0 ; static auto lastTimestamp = std::chrono::high_resolution_clock::now (); static auto now = std::chrono::high_resolution_clock::now (); count++; now = std::chrono::high_resolution_clock::now (); float duration = std::chrono::duration_cast<std::chrono::duration<float >>(now-lastTimestamp).count (); if (duration >= 1 ) { lastTimestamp = now; fps = (count - lastCount)/duration; lastCount = count; std::cout<<"computeRefreshRate: fps=" <<fps<<", count=" <<count<<std::endl; } }
算出来高达4k,看起来这个fence也并没有同步gpu显示,只是不停的提交。
那么如果我们想吧这个实时刷新率显示在我们程序的左上角,该怎么做呢?