交换链重建

引言

我们当前的应用已成功绘制出一个三角形,但还存在一些未妥善处理的情况。窗口表面可能发生变化,导致交换链不再与其兼容。引发这种情况的原因之一是窗口尺寸的改变。我们必须捕获这些事件并重新创建交换链。

重新创建交换链

创建一个新的 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 (auto framebuffer : swapChainFramebuffers) {
        vkDestroyFramebuffer(device, framebuffer, nullptr);
    }

    for (auto imageView : swapChainImageViews) {
        vkDestroyImageView(device, imageView, 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 通常会在呈现过程中告知我们交换链已不再适用。vkAcquireNextImageKHRvkQueuePresentKHR 函数可能返回以下特殊值来提示这种情况:

  • 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_SUCCESSVK_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 无法正确调用带有指向 HelloTriangleApplication 实例的 this 指针的成员函数。

不过,我们确实在回调函数中获得了 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 程序!下一章我们将移除顶点着色器中的硬编码顶点数据,开始实际使用顶点缓冲区。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐