Vulkan教程 – 13 重建交换链

现在我们的程序能成功绘制三角形了,但是还有一些情况,它还不能很好地处理。窗口表面可能会改变,导致交换链与其不兼容。这种事情发生的可能原因之一是窗口的大小改变了。我们要能抓取这些事件,然后重建交换链。

创建一个recreateSwapChain方法,它会调用createSwapChain以及为依赖交换链或者窗口大小的对象调用所有创建方法。

void recreateSwapChain() {
    vkDeviceWaitIdle(device);

    cleanupSwapChain();
    createSwapChain();
    createImageViews();
    createRenderPass();
    createGraphicsPipeline();
    createFramebuffers();
    createCommandBuffers();
}

我们先调用vkDeviceWaitIdle,因为就和上一章一样,我们不应该接触还在用的资源。显然,第一件要做的事就是创建交换链本身,图像视图需要重建是因为它们直接基于交换链图像。渲染通道需要重建因为它依赖于交换链图像的格式。操作如改变窗口大小之类的很少会让交换链图像格式改变,但是也是需要处理的。视口和裁剪矩形尺寸是在图形管线创建的过程中指定的,所以管线也需要重建。为视口和裁剪矩形使用动态状态可能就不用这么做了。最终,帧缓冲和命令缓冲也直接依赖于交换链图像。

为了确定旧版本对象在重建之前已经清除了,我们应该将cleanup中的一些代码移动到另一个的方法中,以便我们从recreateSwapChain方法中调用。现在把它叫做cleanupSwapChain。

我们从cleanup中移走被创建作为交换链一部分的所有对象的清理代码到cleanupSwapChain中:

void cleanupSwapChain() {
    for (size_t i = 0; i < swapChainFramebuffers.size(); i++) {
        vkDestroyFramebuffer(device, swapChainFramebuffers[i], 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 (size_t i = 0; i < swapChainImageViews.size(); i++) {
        vkDestroyImageView(device, swapChainImageViews[i], nullptr);
    }

    vkDestroySwapchainKHR(device, swapChain, nullptr);
}

对应cleanup变成了下面的样子:

void cleanup() {
    cleanupSwapChain();

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

我们可以完全重建命令池,不过就有些浪费了。我用了vkFreeCommandBuffers方法来清理已有的命令缓冲,这也就能重用已有的命令池来分配新的命令缓冲了。

为了能较好处理窗口大小改变的问题,我们还要查询当前准缓冲大小来确保交换链图像有正确的大小。修改chooseSwapExtent方法,考虑实际大小:

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

这就是重建交换链的所有步骤!但是,不足之处是我们要在建立新的交换链之前停止所有的渲染。其实当来自旧的交换链的图像上的绘制命令还在准备中的时候也可以创建新的交换链。你需要将之前交换链传给VkSwapchainCreateInfoKHR结构体的oldSwapChain字段,并且要在使用完毕后尽快销毁。

现在我们就只要弄清楚什么时候有必要重建交换链,然后调用recreateSwapChain即可。幸运的是,Vulkan通常会告诉我们呈现的时候交换链不够用了。vkAcquireNextImageKHR和vkQueuePresentKHR方法会返回以下的值表示发生了该情况:

VK_ERROR_OUT_OF_DATE_KHR:交换链已经和表面不兼容了,而且不能继续用于渲染。这通常在调整窗口大小后发生;

VK_SUBOPTIMAL_KHR:交换链还是成功被用于呈现到表面,但是表面属性不再精确匹配。

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中继续尝试。

但是,如果我们放弃了此时的绘制,栅栏就永远不会用vkQueueSubmit提交上去,之后我们想要等待它的时候他就是一个不可知的状态。我们可以重建栅栏,作为重建交换链的一部分,但是移动vkResetFences调用更容易一些:

vkResetFences(device, 1, &inFlightFences[currentFrame]);

if (vkQueueSubmit(graphicsQueue, 1, &submitInfo, inFlightFences[currentFrame]) != VK_SUCCESS) {
    throw std::runtime_error("failed to draw command buffer!");
}

你也可以在交换链次佳的时候这么做,但是我选择还是继续执行,因为我们已经取得了一个图像。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!");
}

vkQueuePresentKHR方法返回同样的值,也是一样的意思。这种情况下如果它是次佳的,我们要重建交换链,因为我们想要最好的结果。

尽管许多驱动和平台会在窗口大小改变的时候自动触发VK_ERROR_OUT_OF_DATE_KHR,但是也不保证就一定会发生。所以我们要添加额外的代码来明确处理大小改变的事情。首先添加一个成员变量来标记大小改变的事情发生了:

bool framebufferResized = false;

drawFrame方法应该修改如下来检查该标记:

if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR || framebufferResized) {
    framebufferResized = false;
    recreateSwapChain();
    return;
} else if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) {
    throw std::runtime_error("failed to acquire swap chain image!");
}

在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不知道如何正确使用指向我们的HelloTriangleApplication实例的this指针调用一个类方法。

但是我们确实可以得到一个回调中的GLFWwindows的引用,而且有另一个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;
    while (width == 0 || height == 0) {
        glfwGetFramebufferSize(window, &width, &height);
        glfwWaitEvents();
    }

    vkDeviceWaitIdle(device);

    createSwapChain();
    createImageViews();
    createRenderPass();
    createGraphicsPipeline();
    createFramebuffers();
    createCommandBuffers();
}

恭喜你完成了第一个能良好运行的Vulkan程序。后面的章节我们会消灭硬编码的顶点,真正使用起来顶点缓冲。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片