Why I got white blinking while resizing resizing the images of the swapchain with VK_PRESENT_MODE_FIFO_KHR present mode?

1

When resizing the images of the swapchain (because the window size changed) I am getting some white blinking. I don't really understand why is the source of this issue. I have this issue only while using VK_PRESENT_MODE_FIFO_KHR present mode with my Intel(R) UHD Graphics 630 integrated GPU, I don't have this issue with the GeForce GTX 1050. I am find that having different behaviors depending on the GPU really curious with Vulkan.

Maybe the ideal solution for what I try to achieve is to have a swapchain that always do the size of the screen and blit only the visible part if it is possible to do it?

Here is my swapchain resize code (far from optimal as I redo some operations that can be avoided).

bool resize_swapchain(VK_Renderer* renderer, Window* window) {
    assert(renderer);

    VkResult    res;

    clear_swapchain(renderer);

    // Build the swapchain

    // Get the list of VkFormats that are supported:
    get_enumeration(vkGetPhysicalDeviceSurfaceFormatsKHR,
                    VkSurfaceFormatKHR,
                    surface_formats,
                    "Failed to get physical device surface formats.\n",
                    "Found %d surface formats.\n",
                    renderer->physical_device,
                    renderer->surface);

    // If the format list includes just one entry of VK_FORMAT_UNDEFINED,
    // the surface has no preferred format. Otherwise, at least one
    // supported format will be returned.
    if (surface_formats.size() == 1 && surface_formats[0].format == VK_FORMAT_UNDEFINED) {
        renderer->surface_format = VK_FORMAT_B8G8R8A8_UNORM;
    } else {
        renderer->surface_format = surface_formats[0].format;
    }

    VkSurfaceCapabilitiesKHR    surface_capabilities;

    res = vkGetPhysicalDeviceSurfaceCapabilitiesKHR(renderer->physical_device, renderer->surface, &surface_capabilities);
    if (res != VK_SUCCESS) {
        log(globals.logger, Log_Level::error, "Failed to get physical device surface capabilities.\n");
        clear_swapchain(renderer);
        return false;
    }

    get_enumeration(vkGetPhysicalDeviceSurfacePresentModesKHR,
                    VkPresentModeKHR,
                    present_modes,
                    "Failed to get physical device surface present modes.\n",
                    "Found %d present modes.\n",
                    renderer->physical_device,
                    renderer->surface);

    // width and height are either both 0xFFFFFFFF, or both not 0xFFFFFFFF.
    if (surface_capabilities.currentExtent.width == 0xFFFFFFFF) {
        // If the surface size is undefined, the size is set to
        // the size of the images requested.

        renderer->swapchain_extent.width = window->size.x;
        renderer->swapchain_extent.height = window->size.y;
        if (renderer->swapchain_extent.width < surface_capabilities.minImageExtent.width) {
            renderer->swapchain_extent.width = surface_capabilities.minImageExtent.width;
        } else if (renderer->swapchain_extent.width > surface_capabilities.maxImageExtent.width) {
            renderer->swapchain_extent.width = surface_capabilities.maxImageExtent.width;
        }

        if (renderer->swapchain_extent.height < surface_capabilities.minImageExtent.height) {
            renderer->swapchain_extent.height = surface_capabilities.minImageExtent.height;
        } else if (renderer->swapchain_extent.height > surface_capabilities.maxImageExtent.height) {
            renderer->swapchain_extent.height = surface_capabilities.maxImageExtent.height;
        }
    } else {
        // If the surface size is defined, the swap chain size must match
        renderer->swapchain_extent = surface_capabilities.currentExtent;
    }

    // The FIFO present mode is guaranteed by the spec to be supported
#if defined(FL_PROFILING_MODE)
    VkPresentModeKHR    swapchain_present_mode = VK_PRESENT_MODE_IMMEDIATE_KHR;
#else
    VkPresentModeKHR    swapchain_present_mode = VK_PRESENT_MODE_FIFO_KHR;
#endif

    // Determine the number of VkImage's to use in the swap chain.
    // We need to acquire only 1 presentable image at at time.
    // Asking for minImageCount images ensures that we can acquire
    // 1 presentable image as long as we present it before attempting
    // to acquire another.
    uint32_t    desired_number_of_swapchain_images = surface_capabilities.minImageCount;

    VkSurfaceTransformFlagBitsKHR   surface_transform;
    if (surface_capabilities.supportedTransforms & VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR) {
        surface_transform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
    } else {
        surface_transform = surface_capabilities.currentTransform;
    }

    // Find a supported composite alpha mode - one of these is guaranteed to be set
    VkCompositeAlphaFlagBitsKHR composite_alpha_flag = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
    // TODO change the order if we want to be able to blend the window of our application with the Windows Desktop
    VkCompositeAlphaFlagBitsKHR composite_alpha_flags[4] = {
        VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR,
        VK_COMPOSITE_ALPHA_PRE_MULTIPLIED_BIT_KHR,
        VK_COMPOSITE_ALPHA_POST_MULTIPLIED_BIT_KHR,
        VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR,
    };
    for (uint32_t i = 0; i < sizeof(composite_alpha_flags); i++) {
        if (surface_capabilities.supportedCompositeAlpha & composite_alpha_flags[i]) {
            composite_alpha_flag = composite_alpha_flags[i];
            break;
        }
    }

    VkSwapchainCreateInfoKHR    swapchain_info = {};
    swapchain_info.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
    swapchain_info.pNext = nullptr;
    swapchain_info.surface = renderer->surface;
    swapchain_info.minImageCount = desired_number_of_swapchain_images;
    swapchain_info.imageFormat = renderer->surface_format;
    swapchain_info.imageExtent.width = renderer->swapchain_extent.width;
    swapchain_info.imageExtent.height = renderer->swapchain_extent.height;
    swapchain_info.preTransform = surface_transform;
    swapchain_info.compositeAlpha = composite_alpha_flag;
    swapchain_info.imageArrayLayers = 1;
    swapchain_info.presentMode = swapchain_present_mode;
    swapchain_info.oldSwapchain = nullptr;
    swapchain_info.clipped = true;
    swapchain_info.imageColorSpace = VK_COLORSPACE_SRGB_NONLINEAR_KHR;
    swapchain_info.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
    swapchain_info.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
    swapchain_info.queueFamilyIndexCount = 0;
    swapchain_info.pQueueFamilyIndices = nullptr;

    uint32_t    queue_family_indices[2] = {(uint32_t)renderer->graphics_queue_family_index, (uint32_t)renderer->present_queue_family_index};
    if (renderer->graphics_queue_family_index != renderer->present_queue_family_index) {
        // If the graphics and present queues are from different queue families,
        // we either have to explicitly transfer ownership of images between
        // the queues, or we have to create the swapchain with imageSharingMode
        // as VK_SHARING_MODE_CONCURRENT
        swapchain_info.imageSharingMode = VK_SHARING_MODE_CONCURRENT;
        swapchain_info.queueFamilyIndexCount = 2;
        swapchain_info.pQueueFamilyIndices = queue_family_indices;
        // TODO @Speedup We may want optimize this by using VK_SHARING_MODE_EXCLUSIVE and be explicit about transfert ownership
    }

    res = vkCreateSwapchainKHR(renderer->device, &swapchain_info, nullptr, &renderer->swapchain);
    if (res != VK_SUCCESS) {
        log(globals.logger, Log_Level::error, "Failed to create the swapchain.\n");
        clear_swapchain(renderer);
        return false;
    }
    log(globals.logger, Log_Level::verbose, "Swapchain created with size (%d, %d).\n",
        swapchain_info.imageExtent.width,
        swapchain_info.imageExtent.height);

    get_enumeration(vkGetSwapchainImagesKHR,
                    VkImage,
                    swapchain_images,
                    "Failed to get swapchain images.\n",
                    "Found %d swapchain images.\n",
                    renderer->device,
                    renderer->swapchain);

    renderer->swapchain_buffers.resize(swapchain_images.size());
    for (uint32_t i = 0; i < swapchain_images.size(); i++) {
        renderer->swapchain_buffers[i].image = swapchain_images[i];
    }

    for (uint32_t i = 0; i < swapchain_images.size(); i++) {
        VkImageViewCreateInfo   color_image_view = {};
        color_image_view.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
        color_image_view.pNext = nullptr;
        color_image_view.flags = 0;
        color_image_view.image = renderer->swapchain_buffers[i].image;
        color_image_view.viewType = VK_IMAGE_VIEW_TYPE_2D;
        color_image_view.format = renderer->surface_format;
        color_image_view.components.r = VK_COMPONENT_SWIZZLE_R;
        color_image_view.components.g = VK_COMPONENT_SWIZZLE_G;
        color_image_view.components.b = VK_COMPONENT_SWIZZLE_B;
        color_image_view.components.a = VK_COMPONENT_SWIZZLE_A;
        color_image_view.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
        color_image_view.subresourceRange.baseMipLevel = 0;
        color_image_view.subresourceRange.levelCount = 1;
        color_image_view.subresourceRange.baseArrayLayer = 0;
        color_image_view.subresourceRange.layerCount = 1;

        res = vkCreateImageView(renderer->device, &color_image_view, nullptr, &renderer->swapchain_buffers[i].view);
        if (res != VK_SUCCESS) {
            log(globals.logger, Log_Level::error, "Failed to create image view.\n");
            clear_swapchain(renderer);
            return false;
        }
        log(globals.logger, Log_Level::verbose, "Image view %d created.\n", i);
    }

    // Build the depth buffer

    VkImageCreateInfo   image_info = {};
    const VkFormat      depth_format = VK_FORMAT_D32_SFLOAT;
    VkFormatProperties  format_properties;
    bool                found_memory_type_index;

    vkGetPhysicalDeviceFormatProperties(renderer->physical_device, depth_format, &format_properties);
    if (format_properties.linearTilingFeatures & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT) {
        image_info.tiling = VK_IMAGE_TILING_LINEAR;
    } else if (format_properties.optimalTilingFeatures & VK_FORMAT_FEATURE_DEPTH_STENCIL_ATTACHMENT_BIT) {
        image_info.tiling = VK_IMAGE_TILING_OPTIMAL;
    } else {
        // @TODO choose an other format?
        log(globals.logger, Log_Level::error, "VK_FORMAT_D32_SFLOAT Unsupported.\n");
        clear_swapchain(renderer);
        return false;
    }

    image_info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO;
    image_info.pNext = nullptr;
    image_info.imageType = VK_IMAGE_TYPE_2D;
    image_info.format = depth_format;
    image_info.extent.width = renderer->swapchain_extent.width;
    image_info.extent.height = renderer->swapchain_extent.height;
    image_info.extent.depth = 1;
    image_info.mipLevels = 1;
    image_info.arrayLayers = 1;
    image_info.samples = renderer->sample_count_flag;
    image_info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED;
    image_info.usage = VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT;
    image_info.queueFamilyIndexCount = 0;
    image_info.pQueueFamilyIndices = nullptr;
    image_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE;
    image_info.flags = 0;

    VkMemoryAllocateInfo    memory_allocation_info = {};
    memory_allocation_info.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO;
    memory_allocation_info.pNext = nullptr;
    memory_allocation_info.allocationSize = 0;
    memory_allocation_info.memoryTypeIndex = 0;

    VkImageViewCreateInfo view_info = {};
    view_info.sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO;
    view_info.pNext = nullptr;
    view_info.image = nullptr;
    view_info.format = depth_format;
    view_info.components.r = VK_COMPONENT_SWIZZLE_R;
    view_info.components.g = VK_COMPONENT_SWIZZLE_G;
    view_info.components.b = VK_COMPONENT_SWIZZLE_B;
    view_info.components.a = VK_COMPONENT_SWIZZLE_A;
    view_info.subresourceRange.aspectMask = VK_IMAGE_ASPECT_DEPTH_BIT;
    view_info.subresourceRange.baseMipLevel = 0;
    view_info.subresourceRange.levelCount = 1;
    view_info.subresourceRange.baseArrayLayer = 0;
    view_info.subresourceRange.layerCount = 1;
    view_info.viewType = VK_IMAGE_VIEW_TYPE_2D;
    view_info.flags = 0;

    VkMemoryRequirements    memory_requirements;

    renderer->depth_buffer.format = depth_format;

    /* Create image */
    res = vkCreateImage(renderer->device, &image_info, nullptr, &renderer->depth_buffer.image);
    if (res != VK_SUCCESS) {
        log(globals.logger, Log_Level::error, "Failed to create the depth image.\n");
        clear_swapchain(renderer);
        return false;
    }

    vkGetImageMemoryRequirements(renderer->device, renderer->depth_buffer.image, &memory_requirements);

    memory_allocation_info.allocationSize = memory_requirements.size;
    /* Use the memory properties to determine the type of memory required */
    found_memory_type_index = memory_type_from_properties(renderer, memory_requirements.memoryTypeBits, VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, &memory_allocation_info.memoryTypeIndex);
    if (!found_memory_type_index) {
        log(globals.logger, Log_Level::error, "Failed to find memory type to allocate the depth image.\n");
        clear_swapchain(renderer);
        return false;
    }

    /* Allocate memory */
    res = vkAllocateMemory(renderer->device, &memory_allocation_info, nullptr, &renderer->depth_buffer.memory);
    if (res != VK_SUCCESS) {
        log(globals.logger, Log_Level::error, "Failed to create memory for depth image.\n");
        clear_swapchain(renderer);
        return false;
    }

    /* Bind memory */
    res = vkBindImageMemory(renderer->device, renderer->depth_buffer.image, renderer->depth_buffer.memory, 0);
    if (res != VK_SUCCESS) {
        log(globals.logger, Log_Level::error, "Failed to bind the depth image memory.\n");
        clear_swapchain(renderer);
        return false;
    }

    /* Create image view */
    view_info.image = renderer->depth_buffer.image;
    res = vkCreateImageView(renderer->device, &view_info, nullptr, &renderer->depth_buffer.view);
    if (res != VK_SUCCESS) {
        log(globals.logger, Log_Level::error, "Failed to create the depth image view.\n");
        clear_swapchain(renderer);
        return false;
    }

    log(globals.logger, Log_Level::verbose, "Depth buffer created.\n");

    for (size_t i = 0; i < renderer->scenes.size(); i++) {
        swapchain_resized(renderer->scenes[i], renderer->swapchain_extent.width, renderer->swapchain_extent.height);
    }

    return true;
}

Edit: Maybe my issue is more related on how I submit rendered images to the swapchain or the image acquisition.

    for (size_t i = 0; i < scene->meshes.size(); i++) {
        draw_mesh(scene->meshes[i]);
    }

    // End the Render pass
    vkCmdEndRenderPass(scene->renderer->graphical_command_buffer);

    // End command buffer
    {
        res = vkEndCommandBuffer(scene->renderer->graphical_command_buffer);
    }

    // Execute queue command buffer
    {
        /* Queue the command buffer for execution */
        const VkCommandBuffer   command_buffers[] = {scene->renderer->graphical_command_buffer};
        VkFenceCreateInfo       fence_create_info;
        VkFence                 draw_fence;
        fence_create_info.sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO;
        fence_create_info.pNext = nullptr;
        fence_create_info.flags = 0;
        vkCreateFence(scene->renderer->device, &fence_create_info, nullptr, &draw_fence);

        VkPipelineStageFlags    pipe_stage_flags = VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT;
        VkSubmitInfo            submit_info[1] = {};
        submit_info[0].pNext = nullptr;
        submit_info[0].sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
        submit_info[0].waitSemaphoreCount = 1;
        submit_info[0].pWaitSemaphores = &scene->image_acquired_semaphore;
        submit_info[0].pWaitDstStageMask = &pipe_stage_flags;
        submit_info[0].commandBufferCount = 1;
        submit_info[0].pCommandBuffers = command_buffers;
        submit_info[0].signalSemaphoreCount = 0;
        submit_info[0].pSignalSemaphores = nullptr;

        /* Queue the command buffer for execution */
        res = vkQueueSubmit(scene->renderer->graphics_queue, 1, submit_info, draw_fence);
        assert(res == VK_SUCCESS);

        /* Now present the image in the window */
        VkPresentInfoKHR present;
        present.sType = VK_STRUCTURE_TYPE_PRESENT_INFO_KHR;
        present.pNext = nullptr;
        present.swapchainCount = 1;
        present.pSwapchains = &scene->renderer->swapchain;
        present.pImageIndices = &scene->current_buffer;
        present.pWaitSemaphores = nullptr;
        present.waitSemaphoreCount = 0;
        present.pResults = nullptr;

        /* Make sure command buffer is finished before presenting */
        do {
            res = vkWaitForFences(scene->renderer->device, 1, &draw_fence, VK_TRUE, scene->renderer->draw_fence_timeout_us);
        } while (res == VK_TIMEOUT);
        assert(res == VK_SUCCESS);

        res = vkQueuePresentKHR(scene->renderer->present_queue, &present);
        assert(res == VK_SUCCESS);

        vkDestroyFence(scene->renderer->device, draw_fence, nullptr);
    }

On vulkan-tutorial.com it is write that we should also recreate the command buffers with the swapchain (https://vulkan-tutorial.com/Drawing_a_triangle/Swap_chain_recreation), is it really mandatory?

c++
vulkan
asked on Stack Overflow Nov 11, 2019 by Xavier Bigand • edited Nov 17, 2019 by tambre

1 Answer

2

I'm the guy from that reddit thread. I'm not sure 100% if we have the exact same issue, but I can explain what I was dealing with and how I worked around it.

So the issue here has several layers. The first one is that window resizing on Windows blocks the message queue because it needs to capture all the input events for itself. So to work around this you need to make your window update asynchronous, for example through threads.

Now your rendering and window resized work asynchronously, which is great, until someone resizes the windows while you are halfway through rendering the new frame. This IMMEDIATELY causes the swapchain to be VK_ERROR_OUT_OF_DATE_KHR, making you unable to present your render result to the screen. This can cause a variety of different artifacts on the surface, depening on GPU vendor, driver version, and even between different GPUs from the same vendor. This is quite literally undefined behavior. But flickering is definitely one of the common results, where it simply doesn't show anything on the surface until a new successful suspend. So far I have not found a single vendor that supports VK_SUBOPTIMAL_KHR to allow you to keep rendering.

A naive solution would be to give the window full control over the framerate instead, but would give very poor and inconsistent frame timings, especially when going over 60hz. You want the rendering to run as fast as it can, with as little latency as possible.

So before I go into the solution, lets summarize the requirements:

  • The application (including rendering) doesn't freeze when resizing.
  • The window does not resize between Acquire and Present
  • The frame timings are not controlled by the window message queue*

* When not resizing

You might have noticed the asterisk on the last requirement. This is because we will have to make a small compromise. The idea is that we only let the window take control over the frame timings when it is resizing. Outside of that we can draw as fast as possible, since nothing else can invalidate the swapchain in between.

To do this I used Fibers. You can think of fibers as a stack without a thread. You can then jump from the fiber to a different fiber and back. Remember that the message queue (specifically GetMessage/PeekMessage calls) doesn't return when resizing? Well, you can jump out of that loop and back in using fibers! Combined with a timer that causes the switch, we can synchronously update the window as well as render frames. Here is a sample from my code:

LRESULT Window::Impl::WndProc(HWND a_HWND, UINT a_Message, WPARAM a_WParam, LPARAM a_LParam)
{
    switch (a_Message)
    {
        case WM_ENTERSIZEMOVE:
        SetTimer(a_HWND, 0, 1, NULL);
        break;

        case WM_EXITSIZEMOVE:
        KillTimer(a_HWND, 0);
        break;

        case WM_TIMER:
        m_MainFiber.Switch();
        break;

        case WM_MOVE:
        if (m_MoveCallback)
        {
            m_MoveCallback(m_This, Vector2i(static_cast<int16_t>(LOWORD(a_LParam)), static_cast<int16_t>(HIWORD(a_LParam))));
        }
        break;

        case WM_SIZE:
        switch (a_WParam)
        {
            case SIZE_MINIMIZED:
            if (m_MinimizeCallback)
            {
                m_MinimizeCallback(m_This);
            }
            break;

            case SIZE_MAXIMIZED:
            if (m_MaximizeCallback)
            {
                m_MaximizeCallback(m_This);
            }
            break;
        }

        if (m_ResizeCallback)
        {
            m_ResizeCallback(m_This, Vector2i(static_cast<int16_t>(LOWORD(a_LParam)), static_cast<int16_t>(HIWORD(a_LParam))));
        }
        break;

        case WM_CLOSE:
        if (m_CloseCallback)
        {
            m_CloseCallback(m_This);
        }
        break;
    }

    if (a_Message == WM_CLOSE)
    {
        return 0;
    }

    return DefWindowProcW(a_HWND, a_Message, a_WParam, a_LParam);
}

As you can see, it is actually pretty simple. Start a timer when resize starts, stop it when resize ends, and switch back to the original fiber when it triggers.

Here is the fiber callback itself:

void Window::Impl::FiberCallback()
{
    MSG msg;

    for (;;)
    {
        if (PeekMessageW(&msg, m_Window, 0, 0, PM_REMOVE) != 0)
        {
            TranslateMessage(&msg);
            DispatchMessageW(&msg);
        }
        else
        {
            m_MainFiber.Switch();
        }
    }
}

And then the actual polling is as simple as this:

void Window::PollEvents()
{
    m_Impl->m_MessageFiber.Switch();
}

This should make PollEvents always return immediately when not resizing, and after the timer expires when you are resizing. It also completely avoids threading since it all runs on the same thread, it just switches between stacks.

Leave a comment if something is unclear, and I hope it solves your problem.

answered on Stack Overflow Nov 16, 2019 by Rick de Water

User contributions licensed under CC BY-SA 3.0