交换链重建
介绍
我们现在的应用程序已经成功绘制了一个三角形,但还有一些情况没有正确处理。 窗口表面可能会发生变化,导致交换链与其不再兼容。 造成这种情况的原因之一是窗口大小的改变。 我们必须捕获这些事件并重新创建交换链。
重建交换链
创建一个新的 recreateSwapChain 函数,调用 createSwapChain 和所有依赖于交换链或窗口大小的对象的创建函数。
void recreateSwapChain() {
device.waitIdle();
createSwapChain();
createImageViews();
}
我们首先调用 vkDeviceWaitIdle,因为就像上一章一样,我们不应该触碰可能仍在使用的资源。
显然,我们需要重新创建交换链本身。
图像视图需要重新创建,因为它们直接基于交换链图像。
为了确保在重新创建这些对象之前清理旧版本,我们应该将一些清理代码移到一个单独的函数中,我们可以从 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 来获取表面的像素分辨率)。
重建交换链就是这么简单!
然而,这种方法的缺点是我们需要在创建新的交换链之前停止所有渲染。
可以在旧交换链的图像上的绘制命令仍在进行时创建新的交换链。
您需要将旧交换链传递给 VkSwapchainCreateInfoKHR 结构的 oldSwapchain 字段,并在使用完毕后立即销毁旧交换链。
次优或过时的交换链
现在我们只需要弄清楚何时需要重建交换链,并调用我们新的 recreateSwapChain 函数。
幸运的是,Vulkan 通常会在呈现期间告诉我们交换链不再足够。
vkAcquireNextImageKHR 和 vkQueuePresentKHR 函数可以返回以下特殊值来表示这一点。
-
VK_ERROR_OUT_OF_DATE_KHR:交换链与表面不再兼容,不能再用于渲染。 通常在窗口调整大小后发生。 -
VK_SUBOPTIMAL_KHR:交换链仍然可以成功呈现到表面,但表面属性不再完全匹配。
auto [result, imageIndex] = swapChain.acquireNextImage( UINT64_MAX, *presentCompleteSemaphores[currentFrame], nullptr );
if (result == vk::Result::eErrorOutOfDateKHR) {
recreateSwapChain();
return;
}
if (result != vk::Result::eSuccess && result != vk::Result::eSuboptimalKHR) {
throw std::runtime_error("failed to acquire swap chain image!");
}
如果在尝试获取图像时发现交换链已经过时,那么就不可能再向其呈现了。
因此,我们应该立即重建交换链,并在下一个 drawFrame 调用中重试。
如果交换链是次优的,您也可以决定这样做,但在这种情况下,我选择继续,因为我们已经获取了一个图像。
VK_SUCCESS 和 VK_SUBOPTIMAL_KHR 都被视为"成功"返回代码。
result = presentQueue.presentKHR( presentInfoKHR );
if (result == vk::Result::eErrorOutOfDateKHR || result == vk::Result::eSuboptimalKHR) {
framebufferResized = false;
recreateSwapChain();
} else if (result != vk::Result::eSuccess) {
throw std::runtime_error("failed to present swap chain image!");
}
currentFrame = (currentFrame + 1) % MAX_FRAMES_IN_FLIGHT;
vkQueuePresentKHR 函数返回相同的值,含义也相同。
在这种情况下,如果交换链是次优的,我们也会重建交换链,因为我们希望获得最佳结果。
修复死锁
如果我们现在尝试运行代码,可能会遇到死锁。
调试代码,我们发现应用程序到达 vkWaitForFences 但永远不会继续。
这是因为当 vkAcquireNextImageKHR 返回 VK_ERROR_OUT_OF_DATE_KHR 时,我们重建交换链然后从 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!");
}
// 只有在提交工作时才重置栅栏
vkResetFences(device, 1, &inFlightFences[currentFrame]);
显式处理调整大小
虽然许多驱动程序和平台在窗口调整大小后自动触发 VK_ERROR_OUT_OF_DATE_KHR,但这并不能保证一定会发生。
这就是为什么我们将添加一些额外的代码来显式处理调整大小。
首先,添加一个新的成员变量,标记已发生调整大小:
std::vector<vk::raii::Fence> inFlightFences;
bool framebufferResized = false;
然后应该修改 drawFrame 函数,也检查这个标志:
if (result == vk::Result::eErrorOutOfDateKHR || result == vk::Result::eSuboptimalKHR) {
framebufferResized = false;
recreateSwapChain();
} else if (result != vk::Result::eSuccess) {
...
}
在 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) {
}
我们创建一个 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 片段着色器