交换链重建
介绍
我们现在的应用程序已经成功绘制了一个三角形,但是还有一些情况它尚未正确处理。窗口表面可能会发生变化,导致交换链不再与之兼容。其中一个可能导致这种情况发生的原因是窗口大小的改变。我们必须捕获这些事件并重建交换链。
重建交换链
创建一个新的 recreateSwapChain
函数,该函数调用 createSwapChain
以及所有依赖于交换链或窗口大小的对象的创建函数。
void recreateSwapChain() {
vkDeviceWaitIdle(device);
createSwapChain();
createImageViews();
createFramebuffers();
}
我们首先调用 vkDeviceWaitIdle
,因为就像上一章一样,我们不应该触碰可能仍在使用的资源。显然,我们将不得不重建交换链本身。图像视图需要重建,因为它们直接基于交换链图像。最后,帧缓冲直接依赖于交换链图像,因此也必须重建。
为了确保在重建这些对象之前清理旧版本,我们应该将一些清理代码移动到一个单独的函数中,我们可以从 recreateSwapChain
函数中调用它。我们称之为 cleanupSwapChain
void cleanupSwapChain() {
}
void recreateSwapChain() {
vkDeviceWaitIdle(device);
cleanupSwapChain();
createSwapChain();
createImageViews();
createFramebuffers();
}
请注意,为了简单起见,我们在这里不重建渲染通道。理论上,交换链图像格式有可能在应用程序的生命周期内发生变化,例如,当将窗口从标准范围移动到高动态范围监视器时。这可能需要应用程序重建渲染通道,以确保动态范围之间的变化得到正确反映。
我们将把作为交换链刷新一部分重建的所有对象的清理代码从 cleanup
移动到 cleanupSwapChain
void cleanupSwapChain() {
for (size_t i = 0; i < swapChainFramebuffers.size(); i++) {
vkDestroyFramebuffer(device, swapChainFramebuffers[i], nullptr);
}
for (size_t i = 0; i < swapChainImageViews.size(); i++) {
vkDestroyImageView(device, swapChainImageViews[i], nullptr);
}
vkDestroySwapchainKHR(device, swapChain, nullptr);
}
void cleanup() {
cleanupSwapChain();
vkDestroyPipeline(device, graphicsPipeline, nullptr);
vkDestroyPipelineLayout(device, pipelineLayout, nullptr);
vkDestroyRenderPass(device, renderPass, nullptr);
for (size_t i = 0; i < MAX_FRAMES_IN_FLIGHT; i++) {
vkDestroySemaphore(device, renderFinishedSemaphores[i], nullptr);
vkDestroySemaphore(device, imageAvailableSemaphores[i], nullptr);
vkDestroyFence(device, inFlightFences[i], nullptr);
}
vkDestroyCommandPool(device, commandPool, nullptr);
vkDestroyDevice(device, nullptr);
if (enableValidationLayers) {
DestroyDebugUtilsMessengerEXT(instance, debugMessenger, nullptr);
}
vkDestroySurfaceKHR(instance, surface, nullptr);
vkDestroyInstance(instance, nullptr);
glfwDestroyWindow(window);
glfwTerminate();
}
请注意,在 chooseSwapExtent
中,我们已经查询了新的窗口分辨率,以确保交换链图像具有(新的)正确大小,因此无需修改 chooseSwapExtent
(请记住,在创建交换链时,我们已经必须使用 glfwGetFramebufferSize
获取表面以像素为单位的分辨率)。
这就是重建交换链所需的全部!然而,这种方法的缺点是,我们需要在创建新的交换链之前停止所有渲染。可以在旧交换链中的图像上绘制命令仍在进行中时创建新的交换链。您需要将之前的交换链传递给 VkSwapchainCreateInfoKHR
结构体中的 oldSwapChain
字段,并在完成使用旧交换链后立即销毁它。
次优或过期的交换链
现在我们只需要弄清楚何时需要重建交换链并调用新的 recreateSwapChain
函数。幸运的是,Vulkan 通常会在呈现期间告诉我们交换链不再足够。vkAcquireNextImageKHR
和 vkQueuePresentKHR
函数可以返回以下特殊值来指示这一点。
-
VK_ERROR_OUT_OF_DATE_KHR
:交换链已变得与表面不兼容,并且不能再用于渲染。通常在窗口大小调整后发生。 -
VK_SUBOPTIMAL_KHR
:交换链仍然可以成功呈现到表面,但表面属性不再完全匹配。
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!");
}
如果在尝试获取图像时发现交换链已过期,则不再可能呈现到它。因此,我们应该立即重建交换链,并在下一个 drawFrame
调用中重试。
您也可以决定在交换链次优时这样做,但我选择在这种情况下继续进行,因为我们已经获取了一个图像。VK_SUCCESS
和 VK_SUBOPTIMAL_KHR
都被认为是“成功”返回代码。
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!");
}
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!");
}
// Only reset the fence if we are submitting work
vkResetFences(device, 1, &inFlightFences[currentFrame]);
显式处理窗口大小调整
尽管许多驱动程序和平台在窗口大小调整后会自动触发 VK_ERROR_OUT_OF_DATE_KHR
,但这并非保证会发生。这就是为什么我们将添加一些额外的代码来显式处理大小调整。首先添加一个新的成员变量,用于标记已发生大小调整
std::vector<VkFence> inFlightFences;
bool framebufferResized = false;
然后应该修改 drawFrame
函数以也检查此标志
if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR || framebufferResized) {
framebufferResized = false;
recreateSwapChain();
} else if (result != VK_SUCCESS) {
...
}
重要的是在 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();
}
vkDeviceWaitIdle(device);
...
}
对 glfwGetFramebufferSize
的初始调用处理大小已正确的情况,而 glfwWaitEvents
将无事可等待。
恭喜,您现在已经完成了您的第一个行为良好的 Vulkan 程序!在下一章中,我们将摆脱顶点着色器中的硬编码顶点,并实际使用顶点缓冲。