Vulkan入门(10)-重建交换链.md

参考资料

简述

在上一篇里,我们已经成功绘制了一个颜色渐变的三角形,并将其显示在窗口上了。但是窗口Surface可能会发生变化,从而使交换链不再与之兼容,比如是窗口大小的变化。所以我们必须捕获这些事件并重新创建交换链。

一. 重建交换链

创建一个新的recreateSwapChain函数,该函数调用createSwapChain和所有依赖于交换链或窗口大小的对象的创建函数:

void cleanupSwapChain() {

}

void recreateSwapChain() {
    // 等待队列中所有操作完成
    vkDeviceWaitIdle(device);
    // 先清理后创建
    cleanupSwapChain();

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

首先调用vkDeviceWaitIdle,确保不触及可能仍在使用的资源。显然,我们必须做的第一件事是重新创建交换链本身。

需要重新创建图像视图,因为它们直接基于交换链图像。渲染通道需要重新创建,因为它取决于交换链图像的格式。交换链的图像格式很少在窗口调整等操作期间发生改变,但它仍然应该被处理。视口和剪刀矩形大小是在图形管道创建时指定的,因此管道也需要重新构建。可以通过使用动态状态的视图和剪刀矩形来避免这种情况。最后,帧缓冲区和命令缓冲区也直接依赖于交换链图像。

为了确保这些对象的旧版本在重新创建它们之前得到清理,我们应该将一些清理代码移到一个单独的函数中,我们可以从recreateSwapChain函数调用这个函数cleanupSwapChain。

1.1 cleanupSwapChain

将所有和交换链相关的资源从cleanup函数中移到此函数内:

void cleanupSwapChain() {
    // 释放所有的帧缓冲区
    for (auto framebuffer : swapChainFramebuffers) {
        vkDestroyFramebuffer(device, framebuffer, nullptr);
    }
    // 释放指令缓冲区
    // 可以从头重新创建命令池,但相当浪费。所以选择使用vkFreeCommandBuffers函数清理现有的命令缓冲区。
    // 这样可以重用现有的池来分配新的命令缓冲区。
    vkFreeCommandBuffers(device, commandPool,
            static_cast<uint32_t>(commandBuffers.size()), commandBuffers.data());
    // 销毁图形管道
    vkDestroyPipeline(device, graphicsPipeline, nullptr);
    // 释放管道布局
    vkDestroyPipelineLayout(device, pipelineLayout, nullptr);
    // 释放渲染通道
    vkDestroyRenderPass(device, renderPass, nullptr);
    // 释放交换链对应的图像视图
    for (auto imageView : swapChainImageViews) {
        vkDestroyImageView(device, imageView, nullptr);
    }
    // 释放交换链
    vkDestroySwapchainKHR(device, swapChain, nullptr);
}

void cleanup() {
    cleanupSwapChain();
    ...
}

可以从头重新创建命令池,但相当浪费。所以选择使用vkFreeCommandBuffers函数清理现有的命令缓冲区, 这样可以重用现有的池来分配新的命令缓冲区。

1.2 获取窗口最新大小

为了正确地处理窗口的大小,我们还需要查询framebuffer的当前大小,以确保交换链图像具有(新的)正确的大小。为了做到这一点,改变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;
        }
    }

通过glfwGetFramebufferSize函数来获取当前窗口大小。

这就是重建交换链所需要的全部!然而,这种方法的缺点是:需要在创建新的交换链之前停止所有的呈现。当从旧的交换链在图像上绘制命令时,可以创建一个新的交换链。需要将之前的交换链传递给VkSwapchainCreateInfoKHR结构中的oldswarechain字段,并在使用完旧的交换链后立即销毁它。

1.3 次优或过时的交换链

现在,我们只需要确定何时需要重新创建交换链,并调用新的recreateSwapChain函数。幸运的是,Vulkan通常会告诉我们交换链在显示过程中不再足够。vkAcquireNextImageKHR和vkQueuePresentKHR函数可以返回以下特殊值来表示这一点:

  1. VK_ERROR_OUT_OF_DATE_KHR: 交换链已经变得与表面不兼容,不能再用于渲染。通常发生在窗口大小调整之后。
  2. 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();
            // 已经获得了一个映像。VK_SUCCESS和VK_SUBOPTIMAL_KHR都被认为是“成功”返回码, 也可以去掉return
            return;
        } else if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) {
            throw std::runtime_error("failed to acquire swap chain image!");
        }

如果交换链在试图获取映像时已经过期,那么就不可能再向其呈现。因此,应该立即重新创建交换链,并在下一个drawFrame调用中再次尝试。然而,如果在这中止绘图,那么栅栏将永远不会通过vkqueuessubmit提交,当我们稍后尝试等待它时,它将处于一个意想不到的状态。我们可以重建fence作为交换链重建的一部分,但是移动vkResetFences调用更容易:

        vkResetFences(device, 1, &inFlightFences[currentFrame]);
        // VkQueue是Vulkan中应用程序向GPU提交命令的唯一途径
        if (vkQueueSubmit(graphicsQueue, 1, &submitInfo, inFlightFences[currentFrame]) != VK_SUCCESS) {
            throw std::runtime_error("failed to submit draw command buffer!");
        }

把fence放在vkQueueSubmit之前,而不是在vkWaitForFences后立刻调用。这是什么原理呢?

1.3.1 fence

fence是一种同步原语,可用于将依赖项从队列插入到主机。fence有两种状态——有信号的和没有信号的, fence可以作为队列提交命令执行的一部分发出信号。

使用vkResetFences可以将fence置为unsignal状态。主机可以通过vkWaitForFences命令来等待fence,并且可以通过vkGetFenceStatus来查询当前的状态。

如果vkWaitForFences被调用时条件被满足,那么vkWaitForFences会立即返回。如果在vkWaitForFences被调用的时候条件没有被满足,那么vkWaitForFences将会阻塞并等待到超时纳秒,直到条件被满足。这里的条件就是fence状态是不是signal状态。vkQueueSubmit会将fence置为signal状态,那么vkWaitForFences就会通过。

所以,当vkWaitForFences之后立刻调用vkResetFences,那么当vkAcquireNextImageKHR发生异常导致返回时,下次在进入drawFrame调用vkWaitForFences就永远处于等待状态了。

1.3.2 vkQueuePresentKHR

如果交换链不是最优的,也可以继续呈现,因为我们已经获得了一个映像。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函数返回具有相同含义的相同值。 在这种情况下,如果交换链不是次优的,为获得最好的结果,最好重新创建交换链。

1.4 主动处理窗口变化

尽管许多驱动程序和平台在调整窗口大小后会自动触发VK_ERROR_OUT_OF_DATE_KHR,但不能保证一定会发生这种情况。所以最好通过监听窗口变化来主动重建交换链。

添加一个新的成员变量,该变量指示已调整大小:

bool framebufferResized = false;

void drawFrame() {
    ...
    result = vkQueuePresentKHR(presentQueue, &presentInfo);
    if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR || framebufferResized) {
        framebufferResized = false;
        recreateSwapChain();
    ...
}

1.4.1 监听窗口变化

要实际检测窗口大小调整,可以使用GLFW框架中的glfwSetFramebufferSizeCallback函数来设置回调:

void initWindow() {
    glfwInit();

    glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);

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

因为glfw回调只能通过静态函数实现,所以通过glfwSetWindowUserPointer保存当前实例指针。

1.5 窗口最小化

还有一种特殊状态是,当最小化窗口时,拿到的窗口大小是0, 这样创建出来的帧缓冲区大小也应该是0,根本不需要渲染。所以这里做一个简单的等待处理:

void recreateSwapChain() {
    int width = 0, height = 0;
    while (width == 0 || height == 0) {
        glfwGetFramebufferSize(window, &width, &height);
        glfwWaitEvents();
    }
    vkDeviceWaitIdle(device);

    ...
}

突然有点好奇这个绘制的刷新率,通过在drawFrame里嵌入函数computeRefreshRate来计算刷新率:

void computeRefreshRate() {
    static float fps = 0;
    static int64_t count = 0;
    static int64_t lastCount = 0;
    static auto lastTimestamp = std::chrono::high_resolution_clock::now();
    static auto now = std::chrono::high_resolution_clock::now();

    count++;

    now = std::chrono::high_resolution_clock::now();
    float duration = std::chrono::duration_cast<std::chrono::duration<float>>(now-lastTimestamp).count();
    if (duration >= 1) {
        lastTimestamp = now;
        fps = (count - lastCount)/duration;
        lastCount = count;
        std::cout<<"computeRefreshRate: fps="<<fps<<", count="<<count<<std::endl;
    }
}

算出来高达4k,看起来这个fence也并没有同步gpu显示,只是不停的提交。

那么如果我们想吧这个实时刷新率显示在我们程序的左上角,该怎么做呢?

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值