Vulkan Tutorial 教程翻译(七) 绘制三角形 2.5 重建交换链

141 阅读7分钟

重建交换链

介绍

我们的应用程序现在已经成功的绘制出了三角形,可还有一些场景没有被正确处理.窗口表面会发生让交换链不再兼容的改变。比如说窗口大小的修改。我们需要重建交换链来处理这些事件。

交换链重建

创建一个新函数 recreateSwapChain 在里面调用 createSwapchain 以及重新创建所有依赖于交换链或窗口尺寸的对象。

void recreateSwapChain(){
    vkDeviceWaitIdle(device);

    createSwapChain();
    createImageViews();
    createFramebuffers();
}

首先调用 vkDeviceWaitIdle ,如前面说过的,我们并不会直接操作正在使用的资源,所以需要等待操作全部运行完毕。很明显,首先要重建交换链本身,图像视图也需要被重建因为其直接依赖于交换链提供的图像,最后帧缓冲对象也直接依赖于交换链中的图像,所以也需要被重新创建。

为了确保老的这些对象被正确清理掉,需要将 cleanup 代码中的一些操作提取出一个单独的函数, 以便于我们可以在 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通常会直接告诉我们,当前的交换链不再适配目前的显示组件。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!");
}

如果交换链在请求可用的图像索引时返回 VK_ERROR_OUT_OF_DATE_KHR ,表示可能不能再用于渲染的呈现,我们需要立即重建交换链,再下一帧 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 函数返回与 vkAcquireNextImageKHR 相同的值,本例中哪怕是次优场景,我们也选择了重建交换链.

处理死锁

如果现在运行这段代码,可能会发生死锁问题,调试代码,会发现程序在 vkWaitFences 函数中卡住了,这是因为当 vkAcquireNextImageKHR 返回 VK_ERROR_OUT_OF_DATE_KHR 的时候,会从 drawFrame 直接退出并重建交换链,可是在那发生前当前这一帧的fence还没有被重置,我们立即就返回了,这个工作没有完成,fence将永远不会变成有信号态,从而在 vkWaitForFences 停住.

一个简单的解决方案是将重置fence的操作延后,放到异常的返回之后,这样fence在异常返回发生时,依然是有信号状态,不会因为相同的fence对象而引发死锁.

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 ,但这并不保证是一定会发生的。所以我们需要显式的添加一些代码处理窗口改变事件,首先添加一个新的成员变量标识是否出发了窗口改变事件framebufferResized

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) {

}

我们选择使用静态函数作为回调,是因为GLFW库本身并不知道如何正确地调用一个类的成员函数。但是,我们可以得到一个 GLFWwindow 的引用,可以使用 glfwSetWindowUserPointer 去存储任意一个指针。

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;
}

现在运行程序,修改窗口大小,去看看帧缓冲是否真的随着窗口修改成了对应的尺寸。

处理最小化

还有另外一种场景导致交换链过时失效,那便是一种特殊的窗口尺寸修改,窗口的最小化,这个场景之所以特别是它将是帧缓冲的尺寸变为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++ 代码