Vulkan教程 - 13 重建交換鏈

現在我們的程序能成功繪製三角形了,但是還有一些情況,它還不能很好地處理。窗口表面可能會改變,導致交換鏈與其不兼容。這種事情發生的可能原因之一是窗口的大小改變了。我們要能抓取這些事件,然後重建交換鏈。

創建一個recreateSwapChain方法,它會調用createSwapChain以及爲依賴交換鏈或者窗口大小的對象調用所有創建方法。

void recreateSwapChain() {
    vkDeviceWaitIdle(device);

    cleanupSwapChain();
    createSwapChain();
    createImageViews();
    createRenderPass();
    createGraphicsPipeline();
    createFramebuffers();
    createCommandBuffers();
}

我們先調用vkDeviceWaitIdle,因爲就和上一章一樣,我們不應該接觸還在用的資源。顯然,第一件要做的事就是創建交換鏈本身,圖像視圖需要重建是因爲它們直接基於交換鏈圖像。渲染通道需要重建因爲它依賴於交換鏈圖像的格式。操作如改變窗口大小之類的很少會讓交換鏈圖像格式改變,但是也是需要處理的。視口和裁剪矩形尺寸是在圖形管線創建的過程中指定的,所以管線也需要重建。爲視口和裁剪矩形使用動態狀態可能就不用這麼做了。最終,幀緩衝和命令緩衝也直接依賴於交換鏈圖像。

爲了確定舊版本對象在重建之前已經清除了,我們應該將cleanup中的一些代碼移動到另一個的方法中,以便我們從recreateSwapChain方法中調用。現在把它叫做cleanupSwapChain。

我們從cleanup中移走被創建作爲交換鏈一部分的所有對象的清理代碼到cleanupSwapChain中:

void cleanupSwapChain() {
    for (size_t i = 0; i < swapChainFramebuffers.size(); i++) {
        vkDestroyFramebuffer(device, swapChainFramebuffers[i], nullptr);
    }

    vkFreeCommandBuffers(device, commandPool, static_cast<uint32_t>(commandBuffers.size()),
        commandBuffers.data());

    vkDestroyPipeline(device, graphicsPipeline, nullptr);
    vkDestroyPipelineLayout(device, pipelineLayout, nullptr);
    vkDestroyRenderPass(device, renderPass, nullptr);

    for (size_t i = 0; i < swapChainImageViews.size(); i++) {
        vkDestroyImageView(device, swapChainImageViews[i], nullptr);
    }

    vkDestroySwapchainKHR(device, swapChain, nullptr);
}

對應cleanup變成了下面的樣子:

void cleanup() {
    cleanupSwapChain();

    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();
}

我們可以完全重建命令池,不過就有些浪費了。我用了vkFreeCommandBuffers方法來清理已有的命令緩衝,這也就能重用已有的命令池來分配新的命令緩衝了。

爲了能較好處理窗口大小改變的問題,我們還要查詢當前準緩衝大小來確保交換鏈圖像有正確的大小。修改chooseSwapExtent方法,考慮實際大小:

VkExtent2D chooseSwapExtent(const VkSurfaceCapabilitiesKHR& capabilities) {
    if (capabilities.currentExtent.width != std::numeric_limits<uint32_t>::max()) {
        return capabilities.currentExtent;
    } else {
        int width, height;
        glfwGetFramebufferSize(window, &width, &height);

        VkExtent2D actualExtent = { static_cast<uint32_t>(width), 
            static_cast<uint32_t>(height)};
        actualExtent.width = std::max(capabilities.minImageExtent.width,
            std::min(capabilities.maxImageExtent.width, actualExtent.width));
        actualExtent.height = std::max(capabilities.minImageExtent.height,
            std::min(capabilities.maxImageExtent.height, actualExtent.height));

        return actualExtent;
    }
}

這就是重建交換鏈的所有步驟!但是,不足之處是我們要在建立新的交換鏈之前停止所有的渲染。其實當來自舊的交換鏈的圖像上的繪製命令還在準備中的時候也可以創建新的交換鏈。你需要將之前交換鏈傳給VkSwapchainCreateInfoKHR結構體的oldSwapChain字段,並且要在使用完畢後儘快銷燬。

現在我們就只要弄清楚什麼時候有必要重建交換鏈,然後調用recreateSwapChain即可。幸運的是,Vulkan通常會告訴我們呈現的時候交換鏈不夠用了。vkAcquireNextImageKHR和vkQueuePresentKHR方法會返回以下的值表示發生了該情況:

VK_ERROR_OUT_OF_DATE_KHR:交換鏈已經和表面不兼容了,而且不能繼續用於渲染。這通常在調整窗口大小後發生;

VK_SUBOPTIMAL_KHR:交換鏈還是成功被用於呈現到表面,但是表面屬性不再精確匹配。

VkResult result = vkAcquireNextImageKHR(device, swapChain,
    std::numeric_limits<uint64_t>::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!");
}

當嘗試獲取一個圖像的時候發現交換鏈過時,那就不能向它呈現東西了。因此我們要立即重建交換鏈了,然後在下一次drawFrame中繼續嘗試。

但是,如果我們放棄了此時的繪製,柵欄就永遠不會用vkQueueSubmit提交上去,之後我們想要等待它的時候他就是一個不可知的狀態。我們可以重建柵欄,作爲重建交換鏈的一部分,但是移動vkResetFences調用更容易一些:

vkResetFences(device, 1, &inFlightFences[currentFrame]);

if (vkQueueSubmit(graphicsQueue, 1, &submitInfo, inFlightFences[currentFrame]) != VK_SUCCESS) {
    throw std::runtime_error("failed to draw command buffer!");
}

你也可以在交換鏈次佳的時候這麼做,但是我選擇還是繼續執行,因爲我們已經取得了一個圖像。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!");
}

vkQueuePresentKHR方法返回同樣的值,也是一樣的意思。這種情況下如果它是次佳的,我們要重建交換鏈,因爲我們想要最好的結果。

儘管許多驅動和平臺會在窗口大小改變的時候自動觸發VK_ERROR_OUT_OF_DATE_KHR,但是也不保證就一定會發生。所以我們要添加額外的代碼來明確處理大小改變的事情。首先添加一個成員變量來標記大小改變的事情發生了:

bool framebufferResized = false;

drawFrame方法應該修改如下來檢查該標記:

if (result == VK_ERROR_OUT_OF_DATE_KHR || result == VK_SUBOPTIMAL_KHR || framebufferResized) {
    framebufferResized = false;
    recreateSwapChain();
    return;
} else if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) {
    throw std::runtime_error("failed to acquire swap chain image!");
}

在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不知道如何正確使用指向我們的HelloTriangleApplication實例的this指針調用一個類方法。

但是我們確實可以得到一個回調中的GLFWwindows的引用,而且有另一個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;
    while (width == 0 || height == 0) {
        glfwGetFramebufferSize(window, &width, &height);
        glfwWaitEvents();
    }

    vkDeviceWaitIdle(device);

    createSwapChain();
    createImageViews();
    createRenderPass();
    createGraphicsPipeline();
    createFramebuffers();
    createCommandBuffers();
}

恭喜你完成了第一個能良好運行的Vulkan程序。後面的章節我們會消滅硬編碼的頂點,真正使用起來頂點緩衝。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章