狸村 Mystic Web · 狸村
目录

引言

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

重新创建交换链

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

void recreateSwapChain()
{
    device.waitIdle();

    createSwapChain();
    createImageViews();
}

我们首先调用 vk::raii::Device::waitIdle,因为就像上一章一样,我们不应该触碰可能仍在使用的资源。 显然,我们必须重新创建交换链本身。 图像视图需要重新创建,因为它们是直接基于交换链图像的。

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

void cleanupSwapChain()
{
}

void recreateSwapChain()
{
    device.waitIdle();

    cleanupSwapChain();

    createSwapChain();
    createImageViews();
}

注意,为简单起见,我们在这里不重新创建渲染通道。 理论上,在应用程序的生命周期中,交换链图像格式可能会发生变化,例如,将窗口从标准范围显示器移动到高动态范围显示器时。 这可能需要应用程序重新创建渲染通道,以确保动态范围之间的变化得到正确反映。

我们将把所有在交换链刷新时需要重新创建的对象的清理代码从 cleanup 移动到 cleanupSwapChain

void cleanupSwapChain()
{
    swapChainImageViews.clear();
    swapChain = nullptr;
}

void cleanup()
{
    cleanupSwapChain();

    glfwDestroyWindow(window);
    glfwTerminate();
}

请注意,在 chooseSwapExtent 中我们已经查询了新的窗口分辨率,以确保交换链图像具有(新的)正确大小,因此无需修改 chooseSwapExtent(记住,我们在创建交换链时已经必须使用 glfwGetFramebufferSize 来获取表面的像素分辨率)。

重新创建交换链就是这样! 然而,这种方法的缺点是我们需要在创建新交换链之前停止所有渲染。 有可能在旧交换链的图像上绘制命令仍在执行时创建新的交换链。 你需要将先前的交换链传递给 vk::SwapchainCreateInfoKHR 结构体中的 oldSwapchain 字段,并在使用完旧交换链后尽快销毁它。

次优或过时的交换链

现在我们只需要确定何时需要重建交换链,并调用我们的新 recreateSwapChain 函数。 幸运的是,Vulkan 通常会在呈现时告诉我们交换链已不再适用。 vk::raii::SwapchainKHR::acquireNextImagevk::raii::Queue::presentKHR 函数可以返回以下特殊值来表示这一点。

  • vk::Result::eErrorOutOfDateKHR:交换链已与表面不兼容,无法再用于渲染。通常发生在窗口调整大小之后。请注意,“vk::Result::eErrorOutOfDateKHR”实际上是一个错误代码,默认情况下会触发异常。通过定义“VULKAN_HPP_HANDLE_ERROR_OUT_OF_DATE_AS_SUCCESS”,它被视为成功代码,因此可以由“vk::raii::SwapchainKHR::acquireNextImage”和“vk::raii::Queue::presentKHR”返回。

  • vk::Result::eSuboptimalKHR:交换链仍可用于成功呈现到表面,但表面属性不再完全匹配。

auto [result, imageIndex] = swapChain.acquireNextImage(UINT64_MAX, *presentCompleteSemaphores[frameIndex], nullptr);

if (result == vk::Result::eErrorOutOfDateKHR)
{
  recreateSwapChain();
  return;
}
if (result != vk::Result::eSuccess && result != vk::Result::eSuboptimalKHR)
{
  assert(result == vk::Result::eTimeout || result == vk::Result::eNotReady);
  throw std::runtime_error("failed to acquire swap chain image!");
}

如果在尝试获取图像时交换链已过时,则无法再向其呈现。 因此,我们应该立即重新创建交换链,并在下一次 drawFrame 调用中重试。

如果交换链是次优的,你也可以决定这样做,但因为我我们已经获取了一张图像,所以我选择在这种情况下继续。 vk::Result::eSuccessvk::Result::eSuboptimalKHR 都被视为“成功”返回码。

result = queue.presentKHR(presentInfoKHR);
if ((result == vk::Result::eSuboptimalKHR) || (result == vk::Result::eErrorOutOfDateKHR))
{
  recreateSwapChain();
}
else
{
  // 除了 eSuccess 之外没有其他成功代码;任何错误代码 presentKHR 已经抛出了异常。
  assert(result == vk::Result::eSuccess);
}

vk::raii::Queue::presentKHR 函数返回相同的值,含义也相同。 在这种情况下,如果交换链是次优的,我们也会重新创建它,因为我们希望获得尽可能好的结果。

const vk::PresentInfoKHR presentInfoKHR{.waitSemaphoreCount = 1,
                                        .pWaitSemaphores    = &*renderFinishedSemaphores[imageIndex],
                                        .swapchainCount     = 1,
                                        .pSwapchains        = &*swapChain,
                                        .pImageIndices      = &imageIndex};
result = queue.presentKHR(presentInfoKHR);
if ((result == vk::Result::eSuboptimalKHR) || (result == vk::Result::eErrorOutOfDateKHR))
{
  recreateSwapChain();
}
else
{
  // 除了 eSuccess 之外没有其他成功代码;任何错误代码 presentKHR 已经抛出了异常。
  assert(result == vk::Result::eSuccess);
}

修复死锁

如果我们现在尝试运行代码,有可能会遇到死锁。 调试代码,我们发现应用程序到达了 vk::raii::Device::waitForFences,但从未继续执行下去。 这是因为当 vk::raii::SwapchainKHR::acquireNextImage 返回 vk::Result::eErrorOutOfDateKHR 时,我们重新创建了交换链,然后从 drawFrame 返回。 但在此之前,当前帧的围栏已被等待并重置。 由于我们立即返回,没有工作被提交执行,围栏将永远不会被 signaled,导致 vk::raii::Device::waitForFences 永远停止。

幸运的是,有一个简单的修复方法。 延迟重置围栏,直到我们确定会提交工作为止。 这样,如果我们提前返回,围栏仍然处于 signaled 状态,下次我们使用同一个围栏对象时,vk::raii::Device::waitForFences 就不会死锁。

现在 drawFrame 的开头应该如下所示:

auto fenceResult = device.waitForFences(*inFlightFences[frameIndex], vk::True, UINT64_MAX);
if (fenceResult != vk::Result::eSuccess)
{
  throw std::runtime_error("failed to wait for fence!");
}

auto [result, imageIndex] = swapChain.acquireNextImage(UINT64_MAX, *presentCompleteSemaphores[frameIndex], nullptr);

if (result == vk::Result::eErrorOutOfDateKHR)
{
    recreateSwapChain();
    return;
}
if (result != vk::Result::eSuccess && result != vk::Result::eSuboptimalKHR)
{
    assert(result == vk::Result::eTimeout || result == vk::Result::eNotReady);
    throw std::runtime_error("failed to acquire swap chain image!");
}

// 只有在我们提交工作时才重置围栏
device.resetFences(*inFlightFences[frameIndex]);

显式处理调整大小

尽管许多驱动程序和平台在窗口调整大小后会自动触发 vk::Result::eErrorOutOfDateKHR,但这并不保证会发生。 这就是为什么我们要添加一些额外的代码来显式处理调整大小。 首先,添加一个新的成员变量,标记是否发生了调整大小:

bool framebufferResized = false;

然后应修改 drawFrame 函数以检查此标志:

if (result == vk::Result::eErrorOutOfDateKHR || result == vk::Result::eSuboptimalKHR || framebufferResized)
{
    framebufferResized = false;
    recreateSwapChain();
}
else
{
    ...
}

重要的是在 vk::raii::Queue::presentKHR 之后执行此操作,以确保信号量处于一致状态,否则 signaled 的信号量可能永远不会被正确等待。 现在,要实际检测调整大小,我们可以使用 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();
    }

    device.waitIdle();

    ...
}

首次调用 glfwGetFramebufferSize 处理了尺寸已经正确的情况,而 glfwWaitEvents 则没有等待内容。

恭喜,你现在已经完成了你的第一个行为规范的 Vulkan 程序! 在下一章中,我们将摆脱顶点着色器中硬编码的顶点,并实际使用顶点缓冲。

Vulkan 是 Khronos Group Inc. 的注册商标

教程内容版权归原作者,遵循 CC BY-SA 4.0;本站独立代码及设计除外。