Drew
Drew

Reputation: 8963

How to recreate swapchain after vkAcquireNextImageKHR is VK_SUBOPTIMAL_KHR?

This vulkan tutorial discusses swapchain recreation:

You could also decide to [recreate the swapchain] that if the swap chain is suboptimal, but I've chosen to proceed anyway in that case because we've already acquired an image.

My question is: how would one recreate the swapchain and not proceed in this case of VK_SUBOPTIMAL_KHR?

To see what I mean, let's look at the tutorial's render function:

void 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_SUBOPTIMAL_KHR) { createSwapchain(); ??? } */
        } else if (result != VK_SUCCESS && result != VK_SUBOPTIMAL_KHR) {
            throw std::runtime_error("failed to acquire swap chain image!");
        }

        if (imagesInFlight[imageIndex] != VK_NULL_HANDLE) {
            vkWaitForFences(device, 1, &imagesInFlight[imageIndex], VK_TRUE, UINT64_MAX);
        }
        imagesInFlight[imageIndex] = inFlightFences[currentFrame];

        VkSubmitInfo submitInfo{};
        submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;

        VkSemaphore waitSemaphores[] = {imageAvailableSemaphores[currentFrame]};
        VkPipelineStageFlags waitStages[] = {VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT};
        submitInfo.waitSemaphoreCount = 1;
        submitInfo.pWaitSemaphores = waitSemaphores;
        submitInfo.pWaitDstStageMask = waitStages;

        submitInfo.commandBufferCount = 1;
        submitInfo.pCommandBuffers = &commandBuffers[imageIndex];

        VkSemaphore signalSemaphores[] = {renderFinishedSemaphores[currentFrame]};
        submitInfo.signalSemaphoreCount = 1;
        submitInfo.pSignalSemaphores = signalSemaphores;

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

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

        VkPresentInfoKHR presentInfo{};
        presentInfo.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;

        presentInfo.waitSemaphoreCount = 1;
        presentInfo.pWaitSemaphores = signalSemaphores;

        VkSwapchainKHR swapChains[] = {swapChain};
        presentInfo.swapchainCount = 1;
        presentInfo.pSwapchains = swapChains;

        presentInfo.pImageIndices = &imageIndex;

        result = vkQueuePresentKHR(presentQueue, &presentInfo);

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

        currentFrame = (currentFrame + 1) % MAX_FRAMES_IN_FLIGHT;
    }

The trouble is as follows:

  1. vkAcquireImageKHR succeeds, signaling the semaphore and returning a valid, suboptimal image
  2. Recreate the swapchain
  3. We can't present the image from 1 with the swapchain from 2 due to VUID-VkPresentInfoKHR-pImageIndices-01430. We need to call vkAcquireImageKHR again to get a new image.
  4. When we call vkAcquireImageKHR again, the semaphore is in the signaled state which is not allowed (VUID-vkAcquireNextImageKHR-semaphore-01286), we need to 'unsignal' it.

Is the best solution here to destroy and recreate the semaphore?

Upvotes: 2

Views: 2092

Answers (2)

Aelarion
Aelarion

Reputation: 417

I know this question is a tad dated, but I came across the same issue and ended up here from Google searches.

To build on krOoze's suggestion of destroying and re-creating synchronization objects, I definitely agree. This portion of the application is most likely an outlier path, and certainly not something you are doing all the time.

If following vulkan-tutorial (e.g., the C API), you could factor out a cleanupSyncObjects function:

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

You could then utilize this in your recreateSwapChain method:

void recreateSwapChain() {
    // handle minimized window logic

    vkDeviceWaitIdle(device);

    cleanupSwapChain();
    cleanupSyncObjects(); // add

    createSwapChain();
    createImageViews();
    createFramebuffers();
    createSyncObjects(); // add
}

And in your cleanup:

void cleanup() {
    // ...
    cleanupSyncObjects();
    // ...
}

The only part I might caveat in the above is that depending on what you are doing, you may want to specifically wait on every queue (you may only have one for graphics) with some kind of timeout. vkDeviceWaitIdle is a bit of a sledgehammer that will wait on every queue with an infinite timeout:

vkDeviceWaitIdle is equivalent to calling vkQueueWaitIdle for all queues owned by device.

vkQueueWaitIdle:

vkQueueWaitIdle is equivalent to having submitted a valid fence to every previously executed queue submission command that accepts a fence, then waiting for all of those fences to signal using vkWaitForFences with an infinite timeout and waitAll set to VK_TRUE.

Essentially you could deadlock your application, which wouldn't be great. If just following the tutorial it's probably not a concern while you try to wrap your head around the API.


Side note on Vulkan C++ Bindings

If using Vulkan-Hpp, vk::UniqueSemaphore and vk::UniqueFence are unique pointer analogs that can be destroyed by simply assigning a new value. If not using the unique pointers, vk::Semaphore and vk::Fence need to be destroyed by vk::Device::destroySemaphore method vk::Device::destroyFence.

The same logic as the C API follows, just with a C++ API (wait for things to not be in use -> destroy -> create again).

Upvotes: 2

krOoze
krOoze

Reputation: 13246

Ad 3: you can use the old images (and swapchain) if you properly use the oldSwapchain parameter when creating the new swapchain. Which is what I assume the tutorial suggests.

Anyway. What I do is that I paranoidly sanitize that toxic semaphore like this:

// cleanup dangerous semaphore with signal pending from vkAcquireNextImageKHR (tie it to a specific queue)
// https://github.com/KhronosGroup/Vulkan-Docs/issues/1059
void cleanupUnsafeSemaphore( VkQueue queue, VkSemaphore semaphore ){
    const VkPipelineStageFlags psw = VK_PIPELINE_STAGE_BOTTOM_OF_PIPE_BIT;
    VkSubmitInfo submit_info = {};
    submit_info.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
    submit_info.waitSemaphoreCount = 1;
    submit_info.pWaitSemaphores = &semaphore;
    submit_info.pWaitDstStageMask;

    vkQueueSubmit( queue, 1, &submit_info, VK_NULL_HANDLE );
}

After that the semaphore can be properly catched with a fence or vkQueueWaitIdle, and then destroyed or reused.

I just destroy them, because the new semaphore count might differ, and I don't really consider swapchain recreation a hotspot (and also I just use vkDeviceWaitIdle in such case).

Upvotes: 5

Related Questions