Drawing a triangle/Swap chain recreation

目录

Introduction

Recreating the swap chain

Suboptimal or out-of-date swap chain

Fixing a deadlock

Handling resizes explicitly

Handling minimization


Introduction

我们现在已经成功地绘制了一个三角形,但有些情况下它还没有处理好。窗口表面可能会发生变化,使得交换链不再与其兼容。导致这种情况发生的原因之一是窗口的大小发生变化。我们必须捕捉这些事件并重新创建交换链。

Recreating the swap chain

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

void recreateSwapChain() {
    vkDeviceWaitIdle(device);

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

我们首先调用vkDeviceWaitIdle,因为就像在上一章中一样,我们不应该触摸可能仍在使用的资源。显然,我们必须重新创建交换链本身。需要重新创建图像视图,因为它们直接基于交换链图像。最后,帧缓冲区直接依赖于交换链图像,因此也必须重新创建。

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

void cleanupSwapChain() {

}

void recreateSwapChain() {
    vkDeviceWaitIdle(device);

    cleanupSwapChain();

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

请注意,为了简单起见,我们没有在此处重新创建renderpass。理论上,交换链图像格式可以在应用程序的生命周期内改变,例如,当将窗口从标准范围移动到高动态范围监视器时。这可能需要应用程序重新创建渲染过程,以确保正确反映动态范围之间的更改。

我们将把作为交换链刷新的一部分重新创建的所有对象的清理代码从清理移动到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中,我们已经查询了新的窗口分辨率,以确保交换链图像具有(新的)正确的大小,因此无需修改chooseSwap Extent(请记住,在创建交换链时,我们已经必须使用glfwGetFramebufferSize获取以像素为单位的曲面分辨率)。

这就是重新创建交换链所需的一切!然而,这种方法的缺点是,我们需要在创建新的交换链之前停止所有渲染。在旧交换链的图像上绘制命令仍在运行时,可以创建新的交换链。您需要将之前的交换链传递到VkSwapchainCreateInfoKHR结构中的oldSwapChain字段,并在使用完旧交换链后立即销毁它。

Suboptimal or out-of-date swap chain

现在我们只需要确定何时需要重新创建交换链,并调用新的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!");
}

如果在尝试获取图像时交换链已过时,则无法再呈现给它。因此,我们应立即重新创建交换链,并在下一次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函数返回具有相同含义的相同值。在这种情况下,如果交换链是次优的,我们也将重新创建,因为我们希望得到最好的结果。

Fixing a deadlock

如果我们现在尝试运行代码,可能会遇到死锁。调试代码时,我们发现应用程序到达vkWaitForFences,但从未继续过它。这是因为当vkAcquireNextImageKHR返回VK_ERROR_OUT_OF_DATE_KHR时,我们重新创建swapchain,然后从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]);

Handling resizes explicitly

尽管许多驱动程序和平台在调整窗口大小后会自动触发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) {

}

我们创建一个静态函数作为回调的原因是,GLFW不知道如何使用指向HelloTriangleApplication实例的正确指针正确调用成员函数。

然而,我们确实在回调中获得了对GLFW窗口的引用,并且还有另一个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;
}

现在尝试运行程序并调整窗口大小,以查看帧缓冲区是否确实与窗口一起正确调整了大小。

Handling minimization

还有一种情况是,交换链可能会过时,这是一种特殊的窗口调整:窗口最小化。这种情况很特殊,因为它将导致帧缓冲区大小为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项目!在下一章中,我们将去除顶点着色器中的硬编码顶点,并实际使用顶点缓冲区。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值