void recreateSwapChain()
{
device.waitIdle();
createSwapChain();
createImageViews();
}
我们现在的应用程序已经成功绘制了一个三角形,但仍有一些情况尚未妥善处理。 窗口表面可能会发生变化,使得交换链不再与之兼容。 导致这种情况发生的原因之一是窗口大小的改变。 我们必须捕获这些事件并重新创建交换链。
创建一个新的 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::acquireNextImage 和 vk::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::eSuccess 和 vk::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 程序! 在下一章中,我们将摆脱顶点着色器中硬编码的顶点,并实际使用顶点缓冲。
C++ 代码 / Slang 着色器 / GLSL 顶点着色器 / GLSL 片段着色器
Vulkan 是 Khronos Group Inc. 的注册商标
教程内容版权归原作者,遵循 CC BY-SA 4.0;本站独立代码及设计除外。