介绍

我们现在的应用程序已经成功绘制了一个三角形,但是还有一些情况它尚未正确处理。窗口表面可能会发生变化,导致交换链不再与之兼容。其中一个可能导致这种情况发生的原因是窗口大小的改变。我们必须捕获这些事件并重建交换链。

重建交换链

创建一个新的 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 通常会在呈现期间告诉我们交换链不再足够。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 不知道如何使用正确的 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 程序!在下一章中,我们将摆脱顶点着色器中的硬编码顶点,并实际使用顶点缓冲。

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