Link

窗口大小调整

在 Vulkan 中,我们必须自己处理窗口大小调整。作为第 0 章的一部分,我们已经有了最小化的代码,但是调整窗口大小要复杂得多。

当窗口调整大小时,交换链将变为无效,并且对交换链进行的 Vulkan 操作(例如 vkAcquireNextImageKHRvkQueuePresentKHR)可能会失败,并出现 VK_ERROR_OUT_OF_DATE_KHR 错误。我们必须正确处理这些错误,并确保我们可以使用新的大小重新创建交换链。

为了提高效率,我们不会重新分配绘制图像。现在我们只有一个绘制图像和深度图像,但是在更发达的引擎上,它可能会更多,并且重新创建所有这些可能会非常麻烦。相反,我们在启动时使用预设大小创建绘制图像和深度图像,然后在窗口较小时绘制到其中的一部分,或者在窗口较大时将其放大。由于我们没有重新分配而是仅渲染到角落,因此我们还可以使用相同的逻辑来执行动态分辨率,这是一种有用的性能扩展方式,并且对于调试非常方便。我们正在使用 VkCmdBlit 将渲染从绘制图像复制到交换链,并且它会执行缩放,因此在这里效果很好。这种缩放不是最高质量的,因为通常您会希望对放大执行一些更复杂的逻辑,例如应用一些锐化或进行伪抗锯齿作为该缩放的一部分。Imgui UI 仍将直接渲染到交换链图像中,因此它将始终以原生分辨率渲染。

让我们首先在创建窗口时启用可调整大小的标志。然后我们可以看看如果我们尝试调整大小会发生什么。

VulkanEngine::init 的顶部,更改 window_flags,使其具有可调整大小的标志。

SDL_WindowFlags window_flags = (SDL_WindowFlags)(SDL_WINDOW_VULKAN | SDL_WINDOW_RESIZABLE);

SDL 处理窗口大小调整的操作系统部分,因此我们现在可以执行此操作。运行引擎并尝试调整窗口大小。

它应该给出错误并在我们在 vkAcquireNextImageKHRvkQueuePresentKHR 上拥有的 VK_CHECK 宏上崩溃。错误将是 VK_ERROR_OUT_OF_DATE_KHR。因此,要处理大小调整,如果看到该错误,我们需要停止渲染,并在发生这种情况时重建交换链。

首先,将 resize_requested 布尔值添加到 VulkanEngine 类。

在 draw() 函数中,替换对 vkAcquireNextImageKHR 的调用以检查错误代码。

	VkResult e = vkAcquireNextImageKHR(_device, _swapchain, 1000000000, get_current_frame()._swapchainSemaphore, nullptr, &swapchainImageIndex);
	if (e == VK_ERROR_OUT_OF_DATE_KHR) {
        resize_requested = true;       
		return ;
	}

同样以相同的方式替换函数末尾对 vkQueuePresentKHR 的调用。但是我们不添加 return,因为我们已经在函数末尾。

VkResult presentResult = vkQueuePresentKHR(_graphicsQueue, &presentInfo);
if (presentResult == VK_ERROR_OUT_OF_DATE_KHR) {
    resize_requested = true;
}

现在,当我们发生这种情况时将停止渲染,您可以调整图像大小,它不会崩溃,但也不会再次绘制并且只会冻结图像。

让我们向 VulkanEngine 添加一个 resize_swapchain() 函数以重新创建交换链。

void VulkanEngine::resize_swapchain()
{
	vkDeviceWaitIdle(_device);

	destroy_swapchain();

	int w, h;
	SDL_GetWindowSize(_window, &w, &h);
	_windowExtent.width = w;
	_windowExtent.height = h;

	create_swapchain(_windowExtent.width, _windowExtent.height);

	resize_requested = false;
}

要调整交换链的大小,我们首先等待 GPU 完成所有渲染命令。我们不想在 gpu 仍在处理图像和视图时更改它们。然后我们销毁交换链,然后我们从 SDL 查询窗口大小并再次创建它。

现在我们需要在主 run() 循环中在图像调整大小时调用此函数。

在 SDL 事件循环和 freeze_rendering 检查之后,在它也调用任何 NewFrame imgui 函数之前添加此代码。

if (resize_requested) {
	resize_swapchain();
}

现在我们已经实现了大小调整,请尝试一下。您应该能够缩小图像大小而不会遇到错误。但是,如果您将窗口放大,它将失败。我们正在超出绘制图像的大小,并且它尝试在界限之外渲染。我们可以通过实现 _drawExtent 变量并确保将其最大化为绘制图像的大小来解决此问题。

_drawExtent 添加到 VulkanEngine 类,并将 renderScale float 添加到我们将用于动态分辨率的类中。

VkExtent2D _drawExtent;
float renderScale = 1.f;

回到 draw() 函数,我们在其开头计算绘制范围,而不是使用绘制图像范围。

_drawExtent.height = std::min(_swapchainExtent.height, _drawImage.imageExtent.height) * renderScale;
_drawExtent.width= std::min(_swapchainExtent.width, _drawImage.imageExtent.width) * renderScale;

现在我们将向 imgui 添加一个滑块来控制此绘制比例参数。

run() 函数中,在计算背景参数的 imgui 窗口内,将此代码添加到顶部

if (ImGui::Begin("background")) {
ImGui::SliderFloat("Render Scale",&renderScale, 0.3f, 1.f);
//other code
}

这将为我们提供一个可编辑的渲染比例滑块,范围从 0.3 到 1.f。我们不想超过 1,因为它会破坏分辨率。

运行它,并尝试调整窗口大小并调整渲染比例。您将看到现在您可以最大化或移动窗口并在动态更改其分辨率。

我们设置的绘制图像有点小,但是如果您愿意,请尝试从在 init_swapchain() 中创建绘制图像的位置增加绘制图像的大小。将 drawImageExtent 设置为您的显示器分辨率,而不是硬编码为小尺寸的 _windowExtent。

有了这个,我们完成了第 3 章,可以继续下一章了。

下一篇: 第 4 章:新的描述符抽象