a-simple-triangle / Part 21 - Vulkan create swapchain

Vulkan - Create Swapchain

In the previous article I mentioned that the swapchain is a Vulkan component which allows us to render to the screen and have off screen frame buffers that are cycled through, so the renderer can always be preparing the next frame while another is being presented. Creating an instance of a swapchain is a pretty dense coding exercise but we have little choice but to work our way through it as it is a prerequisite to rendering anything in Vulkan.

These sites provide some detailed explanations of the swapchain and ways to acquire one:

In the article about creating a physical device we wrote code to check for the support for the swapchain extension. We also requested the VK_KHR_SWAPCHAIN_EXTENSION_NAME extension when creating the logical device - so our application should be well positioned to successfully create a swapchain by the time we need one. That said, if we cannot create a swapchain it will trigger our fallback flow for our application - reverting to the OpenGL implementation.

Our swapchain will need to have the following properties - some of which will require us to write a few new Vulkan components:


Create the VulkanSwapchain class

Our swapchain will be encapsulated in its own wrapper class - get started by creating vulkan-swapchain.hpp and vulkan-swapchain.cpp in the application/vulkan folder.

Note: We will use the spelling Swapchain rather than SwapChain to align with the approach taken within the Vulkan SDK itself. The lower case c feels a little odd to me, but we will just pretend that Swapchain is a noun in its own right rather than two separate nouns - Swap Chain - just go with it!

Edit the header file with the following:

#pragma once

#include "../../core/graphics-wrapper.hpp"
#include "../../core/internal-ptr.hpp"
#include "../../core/sdl-window.hpp"
#include "vulkan-device.hpp"
#include "vulkan-physical-device.hpp"
#include "vulkan-surface.hpp"

namespace ast
{
    struct VulkanSwapchain
    {
        VulkanSwapchain(const ast::SDLWindow& window,
                        const ast::VulkanPhysicalDevice& physicalDevice,
                        const ast::VulkanDevice& device,
                        const ast::VulkanSurface& surface);

        const vk::SwapchainKHR& getSwapchain() const;

    private:
        struct Internal;
        ast::internal_ptr<Internal> internal;
    };
} // namespace ast

Our swapchain will need to query properties of our window, physical device device and surface to gather all the information it needs. It will also expose a public getSwapchain function. The implementation will be fairly long so we’ll break it down into chunks and build it up as we go.


Acquire swapchain format

We will first work on how to find out what colour format and configuration to apply to our swapchain. The physical device and surface will determine what options are available for us to choose from. Enter the following into vulkan-swapchain.cpp:

#include "vulkan-swapchain.hpp"
#include "../../core/log.hpp"

using ast::VulkanSwapchain;

namespace
{
    struct VulkanSwapchainFormat
    {
        vk::ColorSpaceKHR colorSpace;
        vk::Format colorFormat;
    };

    VulkanSwapchainFormat getFormat(const ast::VulkanPhysicalDevice& physicalDevice,
                                    const ast::VulkanSurface& surface)
    {
        static const std::string logTag{"ast::VulkanSwapchain::getFormat"};

        // We need to make sure that there is at least one surface format compatible with our surface.
        std::vector<vk::SurfaceFormatKHR> availableSurfaceFormats{
            physicalDevice.getPhysicalDevice().getSurfaceFormatsKHR(surface.getSurface())};

        size_t availableFormatCount{availableSurfaceFormats.size()};

        if (availableFormatCount == 0)
        {
            throw std::runtime_error(logTag + ": No compatible surface formats found.");
        }

        // Take the first format as a 'default'.
        vk::SurfaceFormatKHR defaultFormat{availableSurfaceFormats[0]};

        // If there is only one surface with an undefined format, we will manually choose one.
        if (availableFormatCount == 1 && defaultFormat.format == vk::Format::eUndefined)
        {
            ast::log(logTag, "Surface format is undefined: defaulting to eSrgbNonlinear + eR8G8B8Unorm.");
            return VulkanSwapchainFormat{vk::ColorSpaceKHR::eSrgbNonlinear, vk::Format::eR8G8B8Unorm};
        }

        // We will look through the available formats, attempting to prefer the eR8G8B8Unorm type.
        for (const auto& availableFormat : availableSurfaceFormats)
        {
            if (availableFormat.format == vk::Format::eR8G8B8Unorm)
            {
                ast::log(logTag, "Found supported eR8G8B8Unorm surface format.");
                return VulkanSwapchainFormat{availableFormat.colorSpace, availableFormat.format};
            }
        }

        // Otherwise we will just have to use the first available format.
        ast::log(logTag, "Surface format eR8G8B8Unorm not found, using default available format.");
        return VulkanSwapchainFormat{defaultFormat.colorSpace, defaultFormat.format};
    }
} // namespace

struct VulkanSwapchain::Internal
{
    const VulkanSwapchainFormat format;

    Internal(const ast::SDLWindow& window,
             const ast::VulkanPhysicalDevice& physicalDevice,
             const ast::VulkanDevice& device,
             const ast::VulkanSurface& surface)
        : format(::getFormat(physicalDevice, surface)) {}
};

VulkanSwapchain::VulkanSwapchain(const ast::SDLWindow& window,
                                 const ast::VulkanPhysicalDevice& physicalDevice,
                                 const ast::VulkanDevice& device,
                                 const ast::VulkanSurface& surface)
    : internal(ast::make_internal_ptr<Internal>(window, physicalDevice, device, surface)) {}

We are declaring a file private struct named VulkanSwapchainFormat to model the format for our swapchain, which will contain the colour space and surface format to use. This data could be held in a simple tuple but the struct gives it a bit more of a formal treatment:

namespace
{
    struct VulkanSwapchainFormat
    {
        vk::ColorSpaceKHR colorSpace;
        vk::Format colorFormat;
    };

The Internal struct will create and hold a VulkanSwapchainFormat object, which it derives from the getFormat free function:

struct VulkanSwapchain::Internal
{
    const VulkanSwapchainFormat format;

    Internal(const ast::SDLWindow& window,
             const ast::VulkanPhysicalDevice& physicalDevice,
             const ast::VulkanDevice& device,
             const ast::VulkanSurface& surface)
        : format(::getFormat(physicalDevice, surface)) {}
};

The getFormat function begins by querying the physical device for all of the available surface formats it can provide which meet the criteria of our surface argument. If we have no available surface formats which meet this criteria then we have to bail early:

VulkanSwapchainFormat getFormat(const ast::VulkanPhysicalDevice& physicalDevice,
                                const ast::VulkanSurface& surface)
{
    static const std::string logTag{"ast::VulkanSwapchain::getFormat"};

    // We need to make sure that there is at least one surface format compatible with our surface.
    std::vector<vk::SurfaceFormatKHR> availableSurfaceFormats{
        physicalDevice.getPhysicalDevice().getSurfaceFormatsKHR(surface.getSurface())};

    size_t availableFormatCount{availableSurfaceFormats.size()};

    if (availableFormatCount == 0)
    {
        throw std::runtime_error(logTag + ": No compatible surface formats found.");
    }

Next we grab the first surface format in the list of available formats as a default:

vk::SurfaceFormatKHR defaultFormat{availableSurfaceFormats[0]};

On the condition that there is only one available surface format and it has a format type of vk::Format::eUndefined (which aliases the VK_FORMAT_UNDEFINED typedef) then we will just choose the vk::ColorSpaceKHR::eSrgbNonlinear colour space and the vk::Format::eR8G8B8Unorm format type instead:

if (availableFormatCount == 1 && defaultFormat.format == vk::Format::eUndefined)
{
    ast::log(logTag, "Surface format is undefined: defaulting to eSrgbNonlinear + eR8G8B8Unorm.");
    return VulkanSwapchainFormat{vk::ColorSpaceKHR::eSrgbNonlinear, vk::Format::eR8G8B8Unorm};
}

If we have multiple surface formats then we will iterate the available formats looking for the one we prefer (vk::Format::eR8G8B8Unorm) and if we find it, use it as the result:

for (const auto& availableFormat : availableSurfaceFormats)
{
    if (availableFormat.format == vk::Format::eR8G8B8Unorm)
    {
        ast::log(logTag, "Found supported eR8G8B8Unorm surface format.");
        return VulkanSwapchainFormat{availableFormat.colorSpace, availableFormat.format};
    }
}

If we couldn’t find vk::Format::eR8G8B8Unorm then we’ll just go with whatever was in the default format:

ast::log(logTag, "Surface format eR8G8B8Unorm not found, using default available format.");
return VulkanSwapchainFormat{defaultFormat.colorSpace, defaultFormat.format};

We can run this code now by revisiting vulkan-context.cpp and adding the following header:

#include "vulkan-swapchain.hpp"

Then updating the Internal struct to add a swapchain member field and initialise it in the constructor:

struct VulkanContext::Internal
{
    ...
    const ast::VulkanSwapchain swapchain;

    Internal() : ...
                 swapchain(ast::VulkanSwapchain(window, physicalDevice, device, surface))

Also add the public getter function to the bottom of the swapchain implementation to expose the swapchain instance:

const vk::SwapchainKHR& VulkanSwapchain::getSwapchain() const
{
    return internal->swapchain.get();
}

Run the application and depending on your configuration you should see some logging output indicating which surface format the swapchain will use.


Determine which presentation mode to use

The presentation mode is the way in which Vulkan will send the rendered images to the display device. There are a variety of presentation modes that may be available - varying in their quality and abilities. The different modes are explained in reasonable detail in the official docs here: https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/VkPresentModeKHR.html.

If you are a PC gamer you probably know about the term vsync which is a technique where the vertical sync rate also known as the refresh rate of your display monitor can be used to govern when a frame is rendered during game play. When a game is configured to respect the refresh rate of the monitor (vysnc is enabled), it will not render more frames per second than the monitor can support - for example if the monitor runs at 60hz refresh rate, with vsync enabled only up to 60 frames per second will be rendered.

The advantage of doing this is to avoid tearing which is caused by the graphics card drawing the next frame of a game at the same time the previous frame is still being presented through the monitor. This results in flickering and visual anomalies such as the screen tearing into two different views of the scene which is quite awful to experience in fast gameplay.

In our Vulkan application we will need to choose a presentation mode that gives us the best experience available. Generally we should prefer presentation modes that accommodate vertical sync behaviour to avoid the tearing problem. Of course nothing in Vulkan is straightforward and the collection of available presentation modes on a given device might not always let us use the option we would prefer. To choose a presentation mode we will evaluate what presentation modes are available based on the order we would like them - best to worst - and choose the first one that matches.

The presentation mode we will choose will be held as a vk::PresentModeKHR field in our swapchain class and when choosing it our preference of best to worst presentation modes is:

  1. vk::PresentModeKHR::eMailbox
  2. vk::PresentModeKHR::eFifo
  3. vk::PresentModeKHR::eFifoRelaxed
  4. vk::PresentModeKHR::eImmediate

The documentation claims that eFifo is the only presentation mode that is required to be supported, however we’ll leave options 3 and 4 in anyway to be safe.

Add a new field to the swapchain internal struct and create it via the getPresentationMode free function (which we will write in a moment):

struct VulkanSwapchain::Internal
{
    const VulkanSwapchainFormat format;
    const vk::PresentModeKHR presentationMode;

    Internal(const ast::SDLWindow& window,
             const ast::VulkanPhysicalDevice& physicalDevice,
             const ast::VulkanDevice& device,
             const ast::VulkanSurface& surface)
        : format(::getFormat(physicalDevice, surface)),
          presentationMode(::getPresentationMode(physicalDevice, surface)) {}
};

Next we will author the getPresentationMode free function. Add the #include <stack> header, then author the getPresentationMode function in the anonymous namespace:

namespace
{
    ...

    vk::PresentModeKHR getPresentationMode(const ast::VulkanPhysicalDevice& physicalDevice,
                                           const ast::VulkanSurface& surface)
        {
        static const std::string logTag{"ast::VulkanSwapchain::getPresentationMode"};

        // We need to make sure that there is at least one presentation mode compatible with our surface.
        std::vector<vk::PresentModeKHR> availableModes{
            physicalDevice.getPhysicalDevice().getSurfacePresentModesKHR(surface.getSurface())};

        if (availableModes.empty())
        {
            throw std::runtime_error(logTag + ": No compatible present modes found.");
        }

        // Load up the presentation modes into a stack so they are popped
        // in our preferred order for evaluation.
        std::stack<vk::PresentModeKHR> preferredModes;
        preferredModes.push(vk::PresentModeKHR::eImmediate);
        preferredModes.push(vk::PresentModeKHR::eFifoRelaxed);
        preferredModes.push(vk::PresentModeKHR::eFifo);
        preferredModes.push(vk::PresentModeKHR::eMailbox);

        while (!preferredModes.empty())
        {
            // Take the mode at the top of the stack and see if the list of available modes contains it.
            vk::PresentModeKHR mode{preferredModes.top()};

            if (std::find(availableModes.begin(), availableModes.end(), mode) != availableModes.end())
            {
                // If we find the current preferred presentation mode, we are done.
                return mode;
            }

            // If our preferred mode is not found, pop the stack ready for the next iteration.
            preferredModes.pop();
        }

        // None of our preferred presentation modes were found, can't go further...
        throw std::runtime_error(logTag + ": No compatible presentation modes found.");
    }
}

We begin by querying our physical device for all the presentation modes it can support based on our surface criteria. If we have have no supported presentation modes then Vulkan ain’t gonna work so we’ll throw an exception …

vk::PresentModeKHR getPresentationMode(const ast::VulkanPhysicalDevice& physicalDevice,
                                       const ast::VulkanSurface& surface)
{
    static const std::string logTag{"ast::VulkanSwapchain::getPresentationMode"};

    std::vector<vk::PresentModeKHR> availableModes{
        physicalDevice.getPhysicalDevice().getSurfacePresentModesKHR(surface.getSurface())};

    if (availableModes.empty())
    {
        throw std::runtime_error(logTag + ": No compatible present modes found.");
    }

With a list of available modes we then create a stack representing the presentation modes in the order we would prefer them. I’ve used a stack here so our order of preference is actually in reverse so we can pop them one at a time:

std::stack<vk::PresentModeKHR> preferredModes;
preferredModes.push(vk::PresentModeKHR::eImmediate);
preferredModes.push(vk::PresentModeKHR::eFifoRelaxed);
preferredModes.push(vk::PresentModeKHR::eFifo);
preferredModes.push(vk::PresentModeKHR::eMailbox);

With a stack of preferred modes, we then iterate the stack while it isn’t empty, popping each element then looking for it in the available list of modes. If we find a match we return it and we are done:

while (!preferredModes.empty())
{
    vk::PresentModeKHR mode{preferredModes.top()};

    if (std::find(availableModes.begin(), availableModes.end(), mode) != availableModes.end())
    {
        return mode;
    }

    preferredModes.pop();
}

Of course, if none of the modes in our stack are available, we drop out:

throw std::runtime_error(logTag + ": No compatible presentation modes found.");

That’s it for choosing the presentation mode.


Swapchain extent

Vulkan offers a simple struct to hold a width and height to represent a dimension in the form of vk::Extent2D. Our swapchain needs to know its extent to define how big it should be. We can query SDL to find out the size of the window we are drawing to, which can become our width and height values of the extent.

Add a new member field named extent and initialise it via the getExtent free function which we’ll write in a sec:

struct VulkanSwapchain::Internal
{
    const VulkanSwapchainFormat format;
    const vk::PresentModeKHR presentationMode;
    const vk::Extent2D extent;

    Internal(const ast::SDLWindow& window,
             const ast::VulkanPhysicalDevice& physicalDevice,
             const ast::VulkanDevice& device,
             const ast::VulkanSurface& surface)
        : format(::getFormat(physicalDevice, surface)),
          presentationMode(::getPresentationMode(physicalDevice, surface)),
          extent(::getExtent(window)) {}
};

Add the getExtent free function:

namespace
{
    ...

    vk::Extent2D getExtent(const ast::SDLWindow& window)
    {
        int drawableWidth;
        int drawableHeight;
        SDL_Vulkan_GetDrawableSize(window.getWindow(), &drawableWidth, &drawableHeight);

        return vk::Extent2D{
            static_cast<uint32_t>(drawableWidth),
            static_cast<uint32_t>(drawableHeight)};
    }
}

We are calling the SDL_Vulkan_GetDrawableSize function and grabbing the resulting drawableWidth and drawableHeight values as a vk::Extent2D object.


Surface transform flags

Our swapchain needs to know what surface transform flags are applicable for the current physical device and surface so it knows how to transform the output. Although we could query the physical device along with the surface to find out what transform is available, we will be using the identity transform, meaning the output image will not be rotated or changed:

vk::SurfaceTransformFlagBitsKHR::eIdentity

If we instead used the transform from the physical device it might be a bit faster, however on platforms such as Android our entire 3D scene would be rotated 90 degrees if our phone is in landscape which is no good. Although there are ways to counter this in code, for simplicity sake identity is the one we will use so it behaves the best across all our platforms.

Learn more about the transform flags here: https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/VkSurfaceTransformFlagBitsKHR.html

Add a new vk::SurfaceTransformFlagBitsKHR transform field to hold the transform flags and initialise it with vk::SurfaceTransformFlagBitsKHR::eIdentity:

struct VulkanSwapchain::Internal
{
    const VulkanSwapchainFormat format;
    const vk::PresentModeKHR presentationMode;
    const vk::Extent2D extent;
    const vk::SurfaceTransformFlagBitsKHR transform;

    Internal(const ast::SDLWindow& window,
             const ast::VulkanPhysicalDevice& physicalDevice,
             const ast::VulkanDevice& device,
             const ast::VulkanSurface& surface)
        : format(::getFormat(physicalDevice, surface)),
          presentationMode(::getPresentationMode(physicalDevice, surface)),
          extent(::getExtent(window)),
          transform(vk::SurfaceTransformFlagBitsKHR::eIdentity) {}
};

Creating the swapchain

We now have all the components needed to create an instance of a vk::UniqueSwapchainKHR which we will hold in our swapchain class. Add a new field to hold the swapchain, initialised via the createSwapchain() function:

struct VulkanSwapchain::Internal
{
    ...

    const vk::UniqueSwapchainKHR swapchain;

    Internal(const ast::SDLWindow& window,
             const ast::VulkanPhysicalDevice& physicalDevice,
             const ast::VulkanDevice& device,
             const ast::VulkanSurface& surface)
        : ...
          swapchain(::createSwapchain(physicalDevice, device, surface, format, presentationMode, extent, transform)),

Next we will write the createSwapchain function which does take quite a few arguments:

namespace
{
    ...

    vk::UniqueSwapchainKHR createSwapchain(
        const ast::VulkanPhysicalDevice& physicalDevice,
        const ast::VulkanDevice& device,
        const ast::VulkanSurface& surface,
        const VulkanSwapchainFormat& format,
        const vk::PresentModeKHR& presentationMode,
        const vk::Extent2D& extent,
        const vk::SurfaceTransformFlagBitsKHR& transform)
    {
        // Grab the capabilities of the current physical device in relation to the surface.
        vk::SurfaceCapabilitiesKHR surfaceCapabilities{
            physicalDevice.getPhysicalDevice().getSurfaceCapabilitiesKHR(surface.getSurface())};

        // We will pick a minimum image count of +1 to the minimum supported on the device.
        uint32_t minimumImageCount{surfaceCapabilities.minImageCount + 1};
        uint32_t maxImageCount{surfaceCapabilities.maxImageCount};

        // Make sure our image count doesn't exceed any maximum if there is one.
        // Note: The Vulkan docs state that a value of 0 doesn't mean there is
        // a limit of 0, it means there there is no limit.
        if (maxImageCount > 0 && minimumImageCount > maxImageCount)
        {
            minimumImageCount = maxImageCount;
        }

        vk::SwapchainCreateInfoKHR createInfo{
            vk::SwapchainCreateFlagsKHR(),            // Flags
            surface.getSurface(),                     // Surface
            minimumImageCount,                        // Minimum image count
            format.colorFormat,                       // Image format
            format.colorSpace,                        // Image color space
            extent,                                   // Image extent
            1,                                        // Image array layers
            vk::ImageUsageFlagBits::eColorAttachment, // Image usage
            vk::SharingMode::eExclusive,              // Image sharing mode
            0,                                        // Queue family index count
            nullptr,                                  // Queue family indices
            transform,                                // Pre transform
            vk::CompositeAlphaFlagBitsKHR::eOpaque,   // Composite alpha
            presentationMode,                         // Present mode
            VK_TRUE,                                  // Clipped
            vk::SwapchainKHR()};                      // Old swapchain

        // If our device has a discrete presentation queue, we must specify
        // that swapchain images are permitted to be shared between both
        // the graphics and presentation queues.
        if (device.hasDiscretePresentationQueue())
        {
            std::array<uint32_t, 2> queueIndices{
                device.getGraphicsQueueIndex(),
                device.getPresentationQueueIndex()};

            createInfo.imageSharingMode = vk::SharingMode::eConcurrent;
            createInfo.queueFamilyIndexCount = 2;
            createInfo.pQueueFamilyIndices = queueIndices.data();
        }

        return device.getDevice().createSwapchainKHRUnique(createInfo);
    }
}

The swapchain will have a number of images which it rotates through as it cycles rendering frames. When configuring a swapchain we need to tell Vulkan the minimum number of images to use. Devices will support different minimum and maximum numbers of these images so we’ll need to write some code to try and pick a number that is suitable.

You can view the documentation about the minimum and maximum image count here: https://khronos.org/registry/vulkan/specs/1.1-extensions/man/html/VkSurfaceCapabilitiesKHR.html.

Vulkan can tell us the minimum and maximum number of images publicly supported for a given physical device through the getSurfaceCapabilitiesKHR function, which we can then query for the minImageCount and maxImageCount properties:

vk::UniqueSwapchainKHR createSwapchain(...)
{
    // Grab the capabilities of the current physical device in relation to the surface.
    vk::SurfaceCapabilitiesKHR surfaceCapabilities{
        physicalDevice.getPhysicalDevice().getSurfaceCapabilitiesKHR(surface.getSurface())};

    // We will pick a minimum image count of +1 to the minimum supported on the device.
    uint32_t minimumImageCount{surfaceCapabilities.minImageCount + 1};
    uint32_t maxImageCount{surfaceCapabilities.maxImageCount};

Very important: The maxImageCount property does not mean the swapchain will not have more than that number of swapchain images once the swapchain has been created - it just advertises to us that it is the maximum number we can request. In fact on my Samsung Galaxy S7 Edge, the maxImageCount reported by the surface capabilities was 3 however Vulkan actually generated 4 swapchain images internally when creating the swapchain. The take away from this is to never assume that the image count we come up with in the createSwapchain function represents the actual number of swapchain images Vulkan creates in the swapchain.

We will try to set the minimum number of images to minImageCount plus 1, but not exceeding maxImageCount.

vk::UniqueSwapchainKHR createSwapchain(...)
{
    ...

    // We will pick a minimum image count of +1 to the minimum supported on the device.
    uint32_t minimumImageCount{surfaceCapabilities.minImageCount + 1};
    uint32_t maxImageCount{surfaceCapabilities.maxImageCount};

The maxImageCount property can have a value of 0 which does not mean there is a limit of 0 - it means there is no maximum limit. So when evaluating our image count we must check if it exceeds the maximum image count only if the maximum image count is greater than 0.

vk::UniqueSwapchainKHR createSwapchain(...)
{
    ...

    if (maxImageCount > 0 && minimumImageCount > maxImageCount)
    {
        minimumImageCount = maxImageCount;
    }

Once we have computed the minimum image count we would like we build a vk::SwapchainCreateInfoKHR object to describe how to create our swapchain:

vk::UniqueSwapchainKHR createSwapchain(...)
{
    ...

    vk::SwapchainCreateInfoKHR createInfo{
        vk::SwapchainCreateFlagsKHR(),            // Flags
        surface.getSurface(),                     // Surface
        minimumImageCount,                        // Minimum image count
        format.colorFormat,                       // Image format
        format.colorSpace,                        // Image color space
        extent,                                   // Image extent
        1,                                        // Image array layers
        vk::ImageUsageFlagBits::eColorAttachment, // Image usage
        vk::SharingMode::eExclusive,              // Image sharing mode
        0,                                        // Queue family index count
        nullptr,                                  // Queue family indices
        transform,                                // Pre transform
        vk::CompositeAlphaFlagBitsKHR::eOpaque,   // Composite alpha
        presentationMode,                         // Present mode
        VK_TRUE,                                  // Clipped
        vk::SwapchainKHR()};                      // Old swapchain

We have to also perform a check to see if our logical device had identified that there is a discrete presentation queue - that is, different from the graphics queue. If it does have a discrete presentation queue, we need to adjust how we create the swapchain to allow it to share ownership of its images between both graphics and presentation queues and use the eConcurrent sharing mode. We also have to specify an array of the queue indices that represents who may share ownership - which are our graphics and presentation queues:

vk::UniqueSwapchainKHR createSwapchain(...)
{
    ...

    if (device.hasDiscretePresentationQueue())
    {
        std::array<uint32_t, 2> queueIndices{
            device.getGraphicsQueueIndex(),
            device.getPresentationQueueIndex()};

        createInfo.imageSharingMode = vk::SharingMode::eConcurrent;
        createInfo.queueFamilyIndexCount = 2;
        createInfo.pQueueFamilyIndices = queueIndices.data();
    }

Finally we ask our logical device to create a swapchain for us, using the createInfo object to configure it:

return device.getDevice().createSwapchainKHRUnique(createInfo);

Run your application again to ensure that everything is firing OK - you can’t see it but you will have successfully created the swapchain which is a big step forward for our Vulkan implementation.


Swapchain images

We have one final task when creating our swapchain - we must also create a collection of image views to access each of the swapchain images. We have already told the swapchain what the minimum number of images should be and the swapchain will have provisioned its images including however many additional images it deems necessary to work internally. We need to generate an image view for each image in the swapchain in order to interact with them later.

The documentation explains what an image view is: https://www.khronos.org/registry/vulkan/specs/1.1-extensions/html/vkspec.html#resources-image-views - basically we can’t directly use a vk::Image, instead we must use a complementary vk::ImageView to interact with them.

Our swapchain wrapper class will need to hold a list of these image views - one for each image in the swapchain. To represent a Vulkan image view, we will create another wrapper class. Create vulkan-image-view.hpp and vulkan-image-view.cpp in our Vulkan application source folder. Enter the following into the header:

#pragma once

#include "../../core/graphics-wrapper.hpp"
#include "../../core/internal-ptr.hpp"

namespace ast
{
    struct VulkanImageView
    {
        VulkanImageView(const vk::Device& device,
                        const vk::Image& image,
                        const vk::Format& format,
                        const vk::ImageAspectFlags& aspectFlags,
                        const uint32_t& mipLevels);

        const vk::ImageView& getImageView() const;

    private:
        struct Internal;
        ast::internal_ptr<Internal> internal;
    };
} // namespace ast

The image view will actually be generated by the device, using the other arguments to fill in its configuration info. Edit the implementation with the following:

#include "vulkan-image-view.hpp"

using ast::VulkanImageView;

namespace
{
    vk::UniqueImageView createImageView(const vk::Device& device,
                                        const vk::Image& image,
                                        const vk::Format& format,
                                        const vk::ImageAspectFlags& aspectFlags,
                                        const uint32_t& mipLevels)
    {
        vk::ImageSubresourceRange subresourceRangeInfo{
            aspectFlags, // Flags
            0,           // Base mip level
            mipLevels,   // Mip level count
            0,           // Base array layer
            1            // Layer count
        };

        vk::ImageViewCreateInfo createInfo{
            vk::ImageViewCreateFlags(), // Flags
            image,                      // Image
            vk::ImageViewType::e2D,     // View type
            format,                     // Format
            vk::ComponentMapping(),     // Components
            subresourceRangeInfo        // Subresource range
        };

        return device.createImageViewUnique(createInfo);
    }
} // namespace

struct VulkanImageView::Internal
{
    const vk::UniqueImageView imageView;

    Internal(const vk::Device& device,
             const vk::Image& image,
             const vk::Format& format,
             const vk::ImageAspectFlags& aspectFlags,
             const uint32_t& mipLevels)
        : imageView(::createImageView(device, image, format, aspectFlags, mipLevels)) {}
};

VulkanImageView::VulkanImageView(const vk::Device& device,
                                 const vk::Image& image,
                                 const vk::Format& format,
                                 const vk::ImageAspectFlags& aspectFlags,
                                 const uint32_t& mipLevels)
    : internal(ast::make_internal_ptr<Internal>(device, image, format, aspectFlags, mipLevels)) {}

const vk::ImageView& VulkanImageView::getImageView() const
{
    return internal->imageView.get();
}

There is lots of boilerplate in the implementation, the interesting part is the createImageView free function which constructs a configuration object then asks the device to generate a new unique image view object from it. You might be wondering why we are passing in the mipLevels argument since we’ve never talked about it before - in a later article we will add mip mapping and it will be handy to have integrated this now rather than refactoring a heap of code later to accommodate it.

Save the implementation and hop back to vulkan-swapchain.hpp. Add the following headers to give us access to our new class and to be able to use std::vector:

#include "vulkan-image-view.hpp"
#include <vector>

Also add a new public function definition so we can expose the list of image views (this will be important later):

struct VulkanSwapchain
{
    ...

    const std::vector<ast::VulkanImageView>& getImageViews() const;

Edit the implementation again and add the following new field, constructed with a createImageViews function which we’ll write:

struct VulkanSwapchain::Internal
{
    ...

    const std::vector<ast::VulkanImageView> imageViews;

    Internal(...)
        : ...
          imageViews(::createImageViews(device, swapchain.get(), format)) {}

Before we implement the createImageViews function, add the public getImageViews implementation to the bottom of the file:

const std::vector<ast::VulkanImageView>& VulkanSwapchain::getImageViews() const
{
    return internal->imageViews;
}

Now for the createImageViews function, add the following to the anonymous namespace:

namespace
{
    ...

    std::vector<ast::VulkanImageView> createImageViews(const ast::VulkanDevice& device,
                                                       const vk::SwapchainKHR& swapChain,
                                                       const VulkanSwapchainFormat& format)
    {
        std::vector<ast::VulkanImageView> imageViews;

        // For each of the images in the swap chain, we need to create a new 'image view'.
        for (const vk::Image& image : device.getDevice().getSwapchainImagesKHR(swapChain))
        {
            ast::VulkanImageView imageView{
                device.getDevice(),
                image,
                format.colorFormat,
                vk::ImageAspectFlagBits::eColor,
                1};

            imageViews.push_back(std::move(imageView));
        }

        return imageViews;
    }
}

We ask the device to give us the collection of images used by the swapchain instance, iterating them and creating a new ast::VulkanImageView for each one. The image views are added to the list as they are created - using std::move because under the hood they are our internal_ptr component which is only movable (not copyable). We specify a mipLevel of 1 because each frame buffer should not generate mip maps.

Run the application once again and though nothing visually has changed, we know that our swapchain is ready to rock!

Important: There is an implementation scenario I haven’t covered yet where we would need to recreate our swapchain at runtime. One of the triggers for doing this is on window size change events where the properties of the existing swapchain become invalid. We will tackle this topic later once we have a rendering loop, as it is likely to be the first point in time where it would cause us a problem.


Summary

Setting up the swapchain was a fairly chunky piece of work, it is represents what I feel is the flavour of writing Vulkan code - in that it really does expect you to micro manage many of the details that technologies like OpenGL have historically hidden away.

Our next article will continue to build upon the swapchain to add a render context to our Vulkan application.

The code for this article can be found here.

Continue to Part 22: Vulkan create render pass.

End of part 21