重建交换链
介绍
我们的应用程序现在已经成功的绘制出了三角形,可还有一些场景没有被正确处理.窗口表面会发生让交换链不再兼容的改变。比如说窗口大小的修改。我们需要重建交换链来处理这些事件。
交换链重建
创建一个新函数 recreateSwapChain 在里面调用 createSwapchain 以及重新创建所有依赖于交换链或窗口尺寸的对象。
void recreateSwapChain(){
vkDeviceWaitIdle(device);
createSwapChain();
createImageViews();
createFramebuffers();
}
首先调用 vkDeviceWaitIdle ,如前面说过的,我们并不会直接操作正在使用的资源,所以需要等待操作全部运行完毕。很明显,首先要重建交换链本身,图像视图也需要被重建因为其直接依赖于交换链提供的图像,最后帧缓冲对象也直接依赖于交换链中的图像,所以也需要被重新创建。
为了确保老的这些对象被正确清理掉,需要将 cleanup 代码中的一些操作提取出一个单独的函数, 以便于我们可以在 recreateSwapChain 中重新调用它们,我们将提出的函数命名为 cleanupSwapchain():
void cleanupSwapChain() {
}
void recreateSwapChain() {
vkDeviceWaitIdle(device);
cleanupSwapChain();
createSwapChain();
createImageViews();
createFramebuffers();
}
注意我们这里并没有重建渲染通道,理论上,在应用程序的生命周期内也可能发生交换链的格式被改变的情况,例如,切换到了一台高动态显式范围的显示器上,这也许就需要应用程序去重建渲染通道,以保证适配正确的显式设备。
我们将cleanup中关联交换链的对象都提取到 cleanupSwapChain()中:
void cleanupSwapChain() {
for (auto framebuffer : swapChainFramebuffers) {
vkDestroyFramebuffer(device, framebuffer, nullptr);
}
for (auto imageView : swapChainImageViews) {
vkDestroyImageView(device, imageView, 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 已经查询了新窗口的尺寸,以确保交换链的图像也有匹配的大小,所以我们并不需要修改 chooseSwapExtent 函数(已经使用 glfwGetFramebufferSize 获取到了正确的窗口的实际大小).
以上便是要重建交换链所有的内容了,此种方法的缺点是,我们需要停止所有的渲染任务去等待交换链重建完成。也可以在绘制中去创建新的交换链,你需要把之前的交换链作为参数传递给 VkSwapchainCreateInfoKHR 的 oldSwapchain 字段,并在你不使用它时尽早销毁它。
交换链过时或并非最优
现在我们需要找出何时重建交换链,调用新建的 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!");
}
如果交换链在请求可用的图像索引时返回 VK_ERROR_OUT_OF_DATE_KHR ,表示可能不能再用于渲染的呈现,我们需要立即重建交换链,再下一帧 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 函数返回与 vkAcquireNextImageKHR 相同的值,本例中哪怕是次优场景,我们也选择了重建交换链.
处理死锁
如果现在运行这段代码,可能会发生死锁问题,调试代码,会发现程序在 vkWaitFences 函数中卡住了,这是因为当 vkAcquireNextImageKHR 返回 VK_ERROR_OUT_OF_DATE_KHR 的时候,会从 drawFrame 直接退出并重建交换链,可是在那发生前当前这一帧的fence还没有被重置,我们立即就返回了,这个工作没有完成,fence将永远不会变成有信号态,从而在 vkWaitForFences 停住.
一个简单的解决方案是将重置fence的操作延后,放到异常的返回之后,这样fence在异常返回发生时,依然是有信号状态,不会因为相同的fence对象而引发死锁.
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]);
显式处理尺寸改变
尽管很多平台的驱动会在窗口改变时自动触发 VK_ERROR_OUT_OF_DATE_KHR ,但这并不保证是一定会发生的。所以我们需要显式的添加一些代码处理窗口改变事件,首先添加一个新的成员变量标识是否出发了窗口改变事件framebufferResized
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库本身并不知道如何正确地调用一个类的成员函数。但是,我们可以得到一个 GLFWwindow 的引用,可以使用 glfwSetWindowUserPointer 去存储任意一个指针。
window = glfwCreateWindow(WIDTH, HEIGHT, "Vulkan", nullptr, nullptr);
glfwSetWindowUserPointer(window, this);
glfwSetFramebufferSizeCallback(window, framebufferResizeCallback);
这个值可以在回调中被接收,从而找到需要的对象指针
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();
}
vkDeviceWaitIdle(device);
...
}
首先调用一次 glfwGetFramebufferSize 去处理正常的窗口尺寸修改事件,循环中的 glfwWaitEvents 用于阻塞线程,等待事件.
恭喜,现在已经完成了一个完备的Vulkan程序!下一章,我们将要摆脱硬编码在顶点着色器中的顶点数据,转而使用真正的顶点缓冲.