Creating a Swapchain#

A swapchain is the collection of framebuffers used to render to the screen. The application will acquire these image buffers and render to them. The configuration of the swapchain then controls how these images are presented to the surface.

Functionality for a swapchain is found in the extension VK_KHR_swapchain rather than in the core API. You already set this up in Initial Vulkan Setup.

The swapchain and its images are created together, so there are several characteristics of the images and the swapchain that need to be set before creation. The available options for these characteristics are closely related to its corresponding surface and must be compatible with it. The application can query the physical device for the surface capabilities to determine this.

In this specific example, the following characteristics need to be set for swapchain creation:

  • Presentation mode.

  • Minimum and maximum number of images in the swapchain.

  • Image pixel format and colour space.

  • Swapchain colour space.

  • Image extent.

Presentation mode defines how the swapchain presents images to the surface. It can have a variety of different settings, as outlined in VkPresentModeKHR.

Image format describes how the image buffers store pixel colour data. An example of an image format in Vulkan is VK_FORMAT_R8G8B8A8_UNORM. This represents an RGBA format which assigns an 8-bit value for each component. Each value is an unsigned, normalised integer. All of the formats supported by Vulkan are found in VkFormat.

Note

This also determines the colour space of the pixels – for example, VK_FORMAT_R8G8B8A8_SRGB. It also interacts with the colour space below.

The colour space describes the range of colours that can be represented on the screen. Most commonly, this is the sRGB colour space, but may be different in some applications, such as HDR monitors. For a list of all colour spaces in Vulkan, see VkColorSpaceKHR.

The image pixel’s colour space and image format are specified together in the surface format struct, VkSurfaceFormatKHR.

Image extent is the width and height of the image. If the extent changes due to the surface size changing, then the swapchain needs to be recreated.

In the code example, initSwapChain() relies on two helper methods: getCompatiblePresentMode() and getCorrectExtent(). getCompatiblePresentMode() finds a presentation mode compatible with the display; by default, VK_PRESENT_MODE_FIFO_KHR is selected as it is guaranteed to be supported on all swapchains. getCorrectExtent() gets the surface extents based on its queried capabilities and checks whether the extents are valid and equal to those in initSurface(). View in context.

void VulkanHelloAPI::initSwapChain()
{
    // These variables are used to store the surface formats that have been retrieved from the physical device.
    uint32_t formatsCount;
    std::vector<VkSurfaceFormatKHR> formats;

    // Get the number of surface formats supported by the physical device.
    debugAssertFunctionResult(vk::GetPhysicalDeviceSurfaceFormatsKHR(appManager.physicalDevice, appManager.surface, &formatsCount, nullptr), "Swap Chain Format - Get Count");

    // Resize formats vector to the number of supported surface formats.
    formats.resize(formatsCount);

    // Populate the vector list with the surface formats.
    debugAssertFunctionResult(vk::GetPhysicalDeviceSurfaceFormatsKHR(appManager.physicalDevice, appManager.surface, &formatsCount, formats.data()), "Swap Chain Format - Allocate Data");

    // If the first format is undefined then pick a default format. VK_FORMAT_B8G8R8A8_UNORM is a very common image format.
    // Otherwise if the first format is defined choose that one.
    if (formatsCount == 1 && formats[0].format == VK_FORMAT_UNDEFINED)
    {
       appManager.surfaceFormat.format = VK_FORMAT_B8G8R8A8_UNORM; // unsigned normalised BGRA with 8-bit in each component.
    }
    else
    {
        appManager.surfaceFormat = formats[0];
    }

    // Get the surface capabilities from the surface and the physical device.
    VkSurfaceCapabilitiesKHR surface_capabilities;
    debugAssertFunctionResult(vk::GetPhysicalDeviceSurfaceCapabilitiesKHR(appManager.physicalDevice, appManager.surface, &surface_capabilities), "Fetch Surface Capabilities");

    // These variables are used to store the presentation modes that have been retrieved from the physical device.
    uint32_t presentModesCount;
    std::vector<VkPresentModeKHR> presentModes;

    // Get the number of supported present modes.
    debugAssertFunctionResult(
    vk::GetPhysicalDeviceSurfacePresentModesKHR(appManager.physicalDevice, appManager.surface, &presentModesCount, nullptr), "Surface Present Modes - Get Count");

    // Resize the vector and retrieve the supported present modes.
    presentModes.resize(presentModesCount);
    debugAssertFunctionResult(vk::GetPhysicalDeviceSurfacePresentModesKHR(appManager.physicalDevice, appManager.surface, &presentModesCount, presentModes.data()),
    "Surface Present Modes - Allocate Data");

    // Choose a compatible present mode and check if the identified present mode is compatible with the device using a helper function.
    appManager.presentMode = getCompatiblePresentMode(VK_PRESENT_MODE_IMMEDIATE_KHR, presentModes);

    // Get the correct extent (dimensions) of the surface using a helper function.
    appManager.swapchainExtent = getCorrectExtent(surface_capabilities);

    // Get the minimum number of images supported on this surface.
    uint32_t surfaceImageCount = std::max<uint32_t>(3, surface_capabilities.minImageCount);

    // Populate a swapchain creation info struct with the information specified above.
    // The additional parameters specified here include what transformations to apply to the image before
    // presentation, how this surface will be composited with other surfaces, whether the implementation
    // can discard rendering operations that affect regions of the surface that are not visible, and the intended
    // usage of the swapchain images.
    VkSwapchainCreateInfoKHR swapchainInfo = {};
    swapchainInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
    swapchainInfo.flags = 0;
    swapchainInfo.pNext = nullptr;
    swapchainInfo.surface = appManager.surface;
    swapchainInfo.imageFormat = appManager.surfaceFormat.format;
    swapchainInfo.preTransform = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR;
    if ((surface_capabilities.supportedTransforms & VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR) == 0)
    {
        Log(true, "Surface does not support VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR transformation");
        assert(surface_capabilities.supportedTransforms & VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR);
    }
    swapchainInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
    swapchainInfo.presentMode = appManager.presentMode;
    swapchainInfo.minImageCount = surfaceImageCount;
    swapchainInfo.oldSwapchain = VK_NULL_HANDLE;
    swapchainInfo.clipped = VK_TRUE;
    swapchainInfo.imageExtent.width = appManager.swapchainExtent.width;
    swapchainInfo.imageExtent.height = appManager.swapchainExtent.height;
    swapchainInfo.imageArrayLayers = 1;
    swapchainInfo.imageColorSpace = appManager.surfaceFormat.colorSpace;
    swapchainInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;

    // Fix the height and width of the surface in case they are not defined.
    if (surfaceData.width == 0 || surfaceData.height == 0)
    {
        surfaceData.width = static_cast<float>(swapchainInfo.imageExtent.width);
        surfaceData.height = static_cast<float>(swapchainInfo.imageExtent.height);
    }

    // Check if the present queue and the graphics queue are the same.
    // If they are, images do not need to be shared between multiple queues, so exclusive mode is selected.
    // If not, sharing mode concurrent is selected to allow these images to be accessed from multiple queue families simultaneously.
    if (appManager.graphicsQueueFamilyIndex == appManager.presentQueueFamilyIndex)
    {
        swapchainInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
        swapchainInfo.queueFamilyIndexCount = 0;
        swapchainInfo.pQueueFamilyIndices = nullptr;
    }
    else
    {
        uint32_t queueFamilyIndices[] = { appManager.graphicsQueueFamilyIndex, appManager.presentQueueFamilyIndex };

        swapchainInfo.imageSharingMode = VK_SHARING_MODE_CONCURRENT;
        swapchainInfo.queueFamilyIndexCount = 2;
        swapchainInfo.pQueueFamilyIndices = queueFamilyIndices;
    }

    // Finally, create the swapchain.
    debugAssertFunctionResult(vk::CreateSwapchainKHR(appManager.device, &swapchainInfo, nullptr, &appManager.swapchain), "SwapChain Creation");
}

VkPresentModeKHR VulkanHelloAPI::getCompatiblePresentMode(const VkPresentModeKHR& inReqMode, const std::vector<VkPresentModeKHR>& inModes)
{
    // Check if the modes supported are compatible with the one requested.
    for (const auto& mode : inModes)
    {
        if (mode == inReqMode) { return mode; }
    }

    Log(false, "Defaulting to VK_PRESENT_MODE_FIFO_KHR");

    // If not, default to VK_PRESENT_FIFO_KHR which is the most supported format.
    return VK_PRESENT_MODE_FIFO_KHR;
}

VkExtent2D VulkanHelloAPI::getCorrectExtent(const VkSurfaceCapabilitiesKHR& inSurfCap)
{
    // The width and height of the swapchain are either both 0xFFFFFFFF (max value for uint_32t) or they are both NOT 0xFFFFFFFF.
    if (inSurfCap.currentExtent.width == std::numeric_limits<uint32_t>::max() || inSurfCap.currentExtent.height == std::numeric_limits<uint32_t>::max())
    {
        // Pass the width and height from the surface.
        appManager.swapchainExtent.width = static_cast<uint32_t>(surfaceData.width);
        appManager.swapchainExtent.height = static_cast<uint32_t>(surfaceData.height);
        VkExtent2D currentExtent = appManager.swapchainExtent;

        // The swapchain extent width and height cannot be less than the minimum surface capability or greater than
        // the maximum surface capability.
        if (appManager.swapchainExtent.width < inSurfCap.minImageExtent.width) { currentExtent.width = inSurfCap.minImageExtent.width; }
        else if (appManager.swapchainExtent.width > inSurfCap.maxImageExtent.width)
        {
            currentExtent.width = inSurfCap.maxImageExtent.width;
        }

        if (appManager.swapchainExtent.height < inSurfCap.minImageExtent.height) { currentExtent.height = inSurfCap.minImageExtent.height; }
        else if (appManager.swapchainExtent.height > inSurfCap.maxImageExtent.height)
        {
            currentExtent.height = inSurfCap.maxImageExtent.height;
        }

        // If the extents are zero, use the values picked from the surface data.
        if (currentExtent.width == 0 && currentExtent.height == 0)
        {
            currentExtent.width = static_cast<uint32_t>(surfaceData.width);
            currentExtent.height = static_cast<uint32_t>(surfaceData.height);
        }

        return currentExtent;
    }

    // In the case where the width and height are both not 0xFFFFFFFF, make sure the extents are not zero.
    // As before, if they are zero then use values picked from the surface data.
    if (inSurfCap.currentExtent.width == 0 && inSurfCap.currentExtent.height == 0)
    {
        VkExtent2D currentExtent;
        currentExtent.width = static_cast<uint32_t>(surfaceData.width);
        currentExtent.height = static_cast<uint32_t>(surfaceData.height);
        return currentExtent;
    }

    return inSurfCap.currentExtent;
}