Swapchain recreation

In the previous chapter we cleared the screen. Before we draw our first triangle, we will do an extra little thing: make our window resizable. However, there is a problem: we created our swapchain with a fixed size, so when we resize our window, the current swapchain will no longer work properly, or at all. The solution is swapchain recreation.

This tutorial is in open beta. There may be bugs in the code and misinformation and inaccuracies in the text. If you find any, feel free to open a ticket on the repo of the code samples.

Introduction

If we resize our window, the size of the window and the size of the swapchain images will no longer match. The swapchain and the presentation engine can handle this in two ways:

Both of these can happen and it's all implementation dependent. Vulkan notifies our application about these events in the return value of two functions: vkAcquireNextImageKHR and vkQueuePresentKHR. Both of these events have their own return value:

When we receive these return values from any of these functions, we can recreate our swapchain, acquire an image from the new swapchain and retry rendering and presentation.

Let's say we need to recreate the swapchain. How do we know what the new image size should be? Like I said in a previous chapter, the currentExtent field of VkSurfaceCapabilitiesKHR is unusable, so we need another way. This is where the windowing API comes in. It sends a message to the application about window resizes as well. SDL2 exposes this, so we can reliably extract the information from there.

After some experimentation I came up with the following solution: If a window resized message comes in, we check the new width and height, and if there is a change, we store the new dimensions and request swapchain recreation. If the swapchain needs recreation, we recreate it at the beginning of the frame. If the image acquisition is suboptimal, we do the rendering and attempt to present suboptimally. If the image acquisition fails because the swapchain is out of date, we terminate the current frame and retry rendering only in the next frame.

The fact whether swapchain recreation was requested will be stored in a bool value. Requesting swapchain recreation means setting this value to true. If this value is true, we recreate the swapchain, and set the bool variable to false.

Our plan for a frame in bulletpoints looks like this:

Make the window resizable

We can make our window resizable using the resizable() function on our window builder.


    // Creating SDL2 window
    let width = 800;
    let height = 600;

    let sdl = sdl2::init().unwrap();
    let video = sdl.video().unwrap();
    let window = video.window(
        "Tutorial",
        width,
        height
    ).vulkan().resizable().build().unwrap();

Extracting swapchain and framebuffer creation

When the swapchain needs recreating, we need to do a very similar procedure as we did during the initial creation with a little twist:

Since copy pasting this into the game loop would be a brutal code duplication even for a tutorial, I will extract the swapchain creation into a utility function and just call it during initialization and swapchain recreation. In the website I will omit lots of code from the function bodies that are unchanged since the previous tutorial, but the full code will be available in the sample application.

It will expect a few extra parameters: we supply the preferred surface format and the old swpachain. This is where we place the suboptimal or out of date swapchain during recreation. If it is not a null pointer, we destroy the old swapchain after creating the new one.


//
// Swapchain creation
//

struct SwapchainResult
{
    swapchain: VkSwapchainKHR,
    width: u32,
    height: u32
}

unsafe fn create_swapchain(
    chosen_phys_device: VkPhysicalDevice,
    surface: VkSurfaceKHR,
    device: VkDevice,
    old_swapchain: VkSwapchainKHR, // This is new
    width: u32, // This is new
    height: u32, // This is new
    format: VkFormat, // This is new
    chosen_graphics_queue_family: u32,
    chosen_present_queue_family: u32
) -> SwapchainResult
{
    let mut surface_capabilities = VkSurfaceCapabilitiesKHR::default();
    unsafe
    {
        vkGetPhysicalDeviceSurfaceCapabilitiesKHR(
            chosen_phys_device,
            surface,
            &mut surface_capabilities
        )
    };

    // Query surface formats
    let mut surface_format_count: u32 = 0;
    unsafe
    {
        vkGetPhysicalDeviceSurfaceFormatsKHR(
            chosen_phys_device,
            surface,
            &mut surface_format_count,
            core::ptr::null_mut()
        )
    };

    let mut surface_formats = vec![VkSurfaceFormatKHR::default(); surface_format_count as usize];
    unsafe
    {
        vkGetPhysicalDeviceSurfaceFormatsKHR(
            chosen_phys_device,
            surface,
            &mut surface_format_count,
            surface_formats.as_mut_ptr()
        )
    };

    let mut chosen_surface_format = None;
    for surface_format in surface_formats.iter()
    {
        let mut format_properties = VkFormatProperties::default();
        unsafe
        {
            vkGetPhysicalDeviceFormatProperties(
                chosen_phys_device,
                surface_format.format,
                &mut format_properties
            );
        }

        if format_properties.optimalTilingFeatures & VK_FORMAT_FEATURE_COLOR_ATTACHMENT_BIT as VkFormatFeatureFlags == 0
        {
            continue;
        }

        if surface_format.format == format && // format is no longer hardcoded
           surface_format.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR
        {
            chosen_surface_format = Some(*surface_format);
        }
    }

    let chosen_surface_format = chosen_surface_format.expect("Could not find suitable surface format.");

    // ...

    let swapchain_image_count = surface_capabilities.minImageCount;

    let min_width = surface_capabilities.minImageExtent.width;
    let max_width = surface_capabilities.maxImageExtent.width;

    let min_height = surface_capabilities.minImageExtent.height;
    let max_height = surface_capabilities.maxImageExtent.height;

    let swapchain_create_info = VkSwapchainCreateInfoKHR {
        sType: VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR,
        flags: 0x0,
        pNext: core::ptr::null(),
        surface: surface,
        minImageCount: swapchain_image_count,
        imageFormat: chosen_surface_format.format,
        imageColorSpace: chosen_surface_format.colorSpace,
        imageExtent: VkExtent2D {
            width: min_width.max(max_width.min(width)),
            height: min_height.max(max_height.min(height))
        },
        imageArrayLayers: 1,
        imageUsage: VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT as VkImageUsageFlags,
        imageSharingMode: image_sharing_mode,
        queueFamilyIndexCount: queue_families.len() as u32,
        pQueueFamilyIndices: queue_families.as_ptr(),
        preTransform: surface_capabilities.currentTransform,
        compositeAlpha: VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR,
        presentMode: chosen_present_mode,
        clipped: VK_TRUE,
        oldSwapchain: old_swapchain // This is new
    };

    println!("Creating swapchain.");
    let mut swapchain = core::ptr::null_mut();
    let result = unsafe
    {
        vkCreateSwapchainKHR(
            device,
            &swapchain_create_info,
            core::ptr::null_mut(),
            &mut swapchain
        )
    };

    if result != VK_SUCCESS
    {
        panic!("Failed to create swapchain. Error: {:?}.", result);
    }

    if old_swapchain != core::ptr::null_mut()
    {
        println!("Deleting old swapchain.");
        unsafe
        {
            vkDestroySwapchainKHR(
                device,
                old_swapchain,
                core::ptr::null_mut()
            );
        }
    }

    SwapchainResult {
        swapchain,
        width: width,
        height: height
    }
}

The new swapchain will contain new images that we need to get from the new swapchain. Framebuffers and image views cannot be modified to refer to the new images, so we need to destroy the old image views and framebuffers and create new ones. Therefore I extract both the creation and the destruction of the image views and framebuffers as well.

Creation...


//
// Getting swapchain images and framebuffer creation
//

unsafe fn create_framebuffers(
    device: VkDevice,
    width: u32,
    height: u32,
    format: VkFormat,
    render_pass: VkRenderPass,
    swapchain: VkSwapchainKHR,
    swapchain_imgs: &mut Vec<VkImage>,
    swapchain_img_views: &mut Vec<VkImageView>,
    framebuffers: &mut Vec<VkFramebuffer>
)
{
    let mut swapchain_img_count: u32 = 0;
    let result = unsafe
    {
        vkGetSwapchainImagesKHR(
            device,
            swapchain,
            &mut swapchain_img_count,
            core::ptr::null_mut()
        )
    };

    if result != VK_SUCCESS
    {
        panic!("Failed to get swapchain images. Error: {:?}.", result);
    }

    if swapchain_imgs.len() < swapchain_img_count as usize
    {
        swapchain_imgs.resize(swapchain_img_count as usize, core::ptr::null_mut());
    }

    let result = unsafe
    {
        vkGetSwapchainImagesKHR(
            device,
            swapchain,
            &mut swapchain_img_count,
            swapchain_imgs.as_mut_ptr()
        )
    };

    if result != VK_SUCCESS
    {
        panic!("Failed to get swapchain images. Error: {:?}.", result);
    }

    swapchain_img_views.reserve(swapchain_imgs.len());
    for (i, swapchain_img) in swapchain_imgs.iter().enumerate()
    {
        // Image view creation code from previous tutorials...
    }

    framebuffers.reserve(swapchain_imgs.len());
    for (i, swapchain_img_view) in swapchain_img_views.iter().enumerate()
    {
        // Framebuffer creation code from previous tutorials...
    }
}

I did not duplicate the image view and framebuffer creation here. Just copy paste your own code into the right places!

Destruction...


unsafe fn destroy_framebuffers(
    device: VkDevice,
    swapchain_img_views: &mut Vec<VkImageView>,
    framebuffers: &mut Vec<VkFramebuffer>
)
{
    for swapchain_framebuffer in framebuffers.iter()
    {
        println!("Deleting framebuffer.");
        unsafe
        {
            vkDestroyFramebuffer(
                device,
                *swapchain_framebuffer,
                core::ptr::null_mut()
            );
        }
    }

    framebuffers.clear();

    for swapchain_img_view in swapchain_img_views.iter()
    {
        println!("Deleting swapchain image views.");
        unsafe
        {
            vkDestroyImageView(
                device,
                *swapchain_img_view,
                core::ptr::null_mut()
            );
        }
    }

    swapchain_img_views.clear();
}

Now let's just invoke these during initialization! Here is the swapchain creation.


    //
    // Swapchain creation
    //

    let format = VK_FORMAT_B8G8R8A8_UNORM;
    let SwapchainResult {
        mut swapchain,
        mut width,
        mut height
    } = unsafe
    {
        create_swapchain(
            chosen_phys_device,
            surface,
            device,
            core::ptr::null_mut(),
            width,
            height,
            format,
            chosen_graphics_queue_family,
            chosen_present_queue_family
        )
    };

Framebuffer and image view creation...


    //
    // Getting swapchain images and framebuffer creation
    //

    let mut swapchain_imgs = Vec::new();
    let mut swapchain_img_views = Vec::new();
    let mut framebuffers = Vec::new();
    unsafe
    {
        create_framebuffers(
            device,
            width,
            height,
            format,
            render_pass,
            swapchain,
            &mut swapchain_imgs,
            &mut swapchain_img_views,
            &mut framebuffers
        );
    }

...and destruction...



    //
    // Cleanup
    //

    // ...

    unsafe
    {
        destroy_framebuffers(
            device,
            &mut swapchain_img_views,
            &mut framebuffers
        );
    }

    // ...

Everything should work the same way so far.

Swapchain recreation at the beginning of the frame

Let's place a mutable bool before our game loop that tells us whether the previous frame required a swapchain recreation and an if statement before image acquisition where the recreation logic will be placed. Also let's handle window resized events!


    //
    // Game loop
    //

    let mut recreate_swapchain = false;
    let mut current_frame_index = 0;

    let mut event_pump = sdl.event_pump().unwrap();
    'main: loop
    {
        for event in event_pump.poll_iter()
        {
            match event
            {
                sdl2::event::Event::Quit { .. } =>
                {
                    break 'main;
                }
                sdl2::event::Event::Window { win_event, .. } =>
                {
                    if let sdl2::event::WindowEvent::Resized(new_width, new_height) = win_event
                    {
                        let new_width = new_width as u32;
                        let new_height = new_height as u32;
                        if new_width != width || new_height != height
                        {
                            width = new_width;
                            height = new_height;
                            recreate_swapchain = true;
                        }
                    }
                }
                _ =>
                {}
            }
        }

        //
        // Rendering
        //

        //
        // Recreate swapchain if needed
        //

        if recreate_swapchain
        {
            // ...
        }

        //
        // Waiting for previous frame
        //

        // ...
    }

The window resized event is a special case of the variant sdl2::event::Event::Window. There is a variable inside, win_event, which in the case of a window resize event will be the variant sdl2::event::WindowEvent::Resized. It will contain the new window dimensions.

Let's create the most problematic part of our code! If there is a need to recreate our swapchain, we first want to wait until every previous frame finished being rendered, because destroying a swapchain while its images are still being used by the GPU is a bad idea.

We will wait for the signaling of our rendering finished fences. Why is that a problem? It doesn't tell us about whether the presentation engine still uses some of our swapchain images, and whether they are free to be destroyed. As mentioned in the previous chapter, this was a limitation of the VK_KHR_swapchain extension, and to fix it a new extension called VK_EXT_swapchain_maintenance1 was created to get notified about it by submitting a fence during present submission, but these tutorials are not adapted to it. Still it's worth keeping in mind for your own application.


        //
        // Rendering
        //

        //
        // Recreate swapchain if needed
        //

        if recreate_swapchain
        {
            let mut fences = Vec::with_capacity(frame_count);
            for (frame_index, frame_submitted) in frame_submitted.iter_mut().enumerate()
            {
                if *frame_submitted
                {
                    fences.push(rendering_finished_fences[frame_index]);
                    *frame_submitted = false;
                }
            }

            let result = unsafe
            {
                vkWaitForFences(
                    device,
                    fences.len() as u32,
                    fences.as_ptr(),
                    VK_TRUE,
                    core::u64::MAX
                )
            };

            if result != VK_SUCCESS
            {
                panic!("Error while waiting for fences. Error: {:?}.", result);
            }

            let result = unsafe
            {
                vkResetFences(
                    device,
                    fences.len() as u32,
                    fences.as_ptr()
                )
            };

            if result != VK_SUCCESS
            {
                panic!("Error while resetting fences. Error: {:?}.", result);
            }

            // ...
        }

Once the previous frames complete, we destroy our framebuffers, since they reference the images of the old swapchain. Then we recreate the swapchain, let the recreate function destroy the old one, and create new framebuffers for the new swapchain.


        //
        // Rendering
        //

        //
        // Recreate swapchain if needed
        //

        if recreate_swapchain
        {
            // ...

            unsafe
            {
                destroy_framebuffers(
                    device,
                    &mut swapchain_img_views,
                    &mut framebuffers
                );
            }

            let SwapchainResult {
                swapchain: new_swapchain,
                width: new_width,
                height: new_height
            } = unsafe
            {
                create_swapchain(
                    chosen_phys_device,
                    surface,
                    device,
                    swapchain,
                    width,
                    height,
                    format,
                    chosen_graphics_queue_family,
                    chosen_present_queue_family
                )
            };

            swapchain = new_swapchain;
            width = new_width;
            height = new_height;

            swapchain_imgs.clear();

            unsafe
            {
                create_framebuffers(
                    device,
                    width,
                    height,
                    format,
                    render_pass,
                    swapchain,
                    &mut swapchain_imgs,
                    &mut swapchain_img_views,
                    &mut framebuffers
                );
            }

            if swapchain_imgs.len() != frame_count
            {
                panic!("New swapchain has a different amount of images than the old one.");
            }

            recreate_swapchain = false;
        }

...at the end we set recreate_swapchain to false, and we're done. Now let's go to our image acquisition and present to detect whether the swapchain needs recreation.

Attempting to acquire a swapchain image

Like I said previously, one of the two places where Vulkan notifies us about whether the swapchain needs to be recreated is where we acquire the swapchain image. Like I said the strategy is the following: if the return value of vkAcquireNextImageKHR is VK_SUBOPTIMAL_KHR, we continue with rendering the image and try to present suboptimally. However if the result is VK_ERROR_OUT_OF_DATE_KHR, we skip the current frame, do not try to render and present and recreate at the beginning of the next frame.

The reason why we settle with presenting suboptimally in case of VK_SUBOPTIMAL_KHR, is that while VK_ERROR_OUT_OF_DATE_KHR is considered an error, VK_SUBOPTIMAL_KHR is considered as a success. While in case of an error the semaphore we supply will be intact, in case of success it will be signaled when the image is ready and I have not found a nice way to unsignal it other than using it as a wait semaphore in a subsequent queue submit, which is something we do after command buffer recording.

Now it's time to get to coding!


        //
        // Acquire image
        //

        let mut image_index: u32 = 0;
        let result = unsafe
        {
            vkAcquireNextImageKHR(
                device,
                swapchain,
                core::u64::MAX,
                image_acquired_sems[current_frame_index],
                core::ptr::null_mut(),
                &mut image_index
            )
        };

        if result != VK_SUCCESS
        {
            if result == VK_ERROR_OUT_OF_DATE_KHR
            {
                recreate_swapchain = true;
                continue;
            }
            else if result == VK_SUBOPTIMAL_KHR
            {
                recreate_swapchain = true;
            }
            else
            {
                panic!("Fatal error while acquiring image: {:?}", result);
            }
        }

Done. Now let's go to the second place where things can go wrong, the present submission.

Attempting to present

When we queue a present request, vkQueuePresentKHR also returns if our swapchain is suboptimal or out of date. Here no matter whether we get a VK_SUBOPTIMAL_KHR or a VK_ERROR_OUT_OF_DATE_KHR, the spec is clear enough that we don't have to worry about synchronization primitives being left in an uncertain state, and we can go with simplicity.

You might worry whether the rendering finished queue will be unsignaled in case of an error, but the spec is clear: ..if the presentation request is rejected by the presentation engine with an error VK_ERROR_OUT_OF_DATE_KHR, VK_ERROR_FULL_SCREEN_EXCLUSIVE_MODE_LOST_EXT, or VK_ERROR_SURFACE_LOST_KHR, the set of queue operations are still considered to be enqueued and thus any semaphore wait operation specified in VkPresentInfoKHR will execute when the corresponding queue operation is complete. So it gets unsignaled and the semaphore will be in a usable state even if present fails.

The code looks like this.


        //
        // Submit
        //

        // ...

        {
            let swapchains = [
                swapchain
            ];
            let image_indices = [
                image_index
            ];
            let rendering_finished_sem = [
                rendering_finished_sems[current_frame_index]
            ];

            let present_info = VkPresentInfoKHR {
                sType: VK_STRUCTURE_TYPE_PRESENT_INFO_KHR,
                pNext: core::ptr::null(),
                waitSemaphoreCount: rendering_finished_sem.len() as u32,
                pWaitSemaphores: rendering_finished_sem.as_ptr(),
                swapchainCount: swapchains.len() as u32,
                pSwapchains: swapchains.as_ptr(),
                pImageIndices: image_indices.as_ptr(),
                pResults: core::ptr::null_mut()
            };

            let result = unsafe
            {
                vkQueuePresentKHR(
                    present_queue,
                    &present_info
                )
            };

            if result != VK_SUCCESS
            {
                if result == VK_SUBOPTIMAL_KHR || result == VK_ERROR_OUT_OF_DATE_KHR
                {
                    recreate_swapchain = true;
                }
                else
                {
                    panic!("Fatal error while submitting present: {:?}.", result);
                }
            }
        }

That's it. Now our window is resizable and our swapchain will be recreated when needed.

Wrapping up

Now our application can recreate the swapchain when we resize our window. We factored out swapchain, image view and framebuffer creation. Then we learned how to handle window resized events and how to recreate our swapchain on window resized events, or if image acquisition or present submit signals a problematic swapchain.

In the next tutorial we are finally going to draw our first triangle.

The sample code for this tutorial can be found here.

The tutorial continues here.