Create swap chain

The swap chain in Vulkan is responsible for handling backbuffers for rendering purposes.

The first thing to do is to create the VkSurface. This is the object in Vulkan responsible for turning a platform-specific window handle to a Vulkan-specific object. For instance, on Windows, the VkSurface object is used to abstract the HWND object away for use in further API functions.

result = vkCreateWin32SurfaceKHR (
    renderer->instance,
    &(VkWin32SurfaceCreateInfoKHR){
        .sType     = VK_STRUCTURE_TYPE_WIN32_SURFACE_CREATE_INFO_KHR,
        .pNext     = NULL,
        .flags     = 0,
        .hinstance = DllInstance,
        .hwnd      = window,
    },
    NULL,
    &renderer->surface
);

if ( result != VK_SUCCESS )
    RETURN_ERROR(-1,"vkCreateWin32SurfaceKHR failed (0x%08X)", (uint32_t)result);

Depending upon your target platform you will need to find the function that matches the platform you are targeting.

In order to create a Vulkan swapchain, the device needs to be created with the swapchain extension enabled. The name of this extension is defined in VK_KHR_SWAPCHAIN_EXTENSION_NAME.

uint32_t surfaceFormatCount = 0;
VkFormat surfaceFormat = VK_FORMAT_UNDEFINED;
VkColorSpaceKHR colorSpace = VK_COLORSPACE_MAX_ENUM;
VkSurfaceFormatKHR* surfaceFormats = 0;
result = renderer->vkInstanceVtbl.vkGetPhysicalDeviceSurfaceFormatsKHR (
    renderer->physicalDevice,
    renderer->surface,
    &surfaceFormatCount,
    NULL
);
if ( result != VK_SUCCESS )
    RETURN_ERROR(-1,"vkGetPhysicalDeviceSurfaceFormatsKHR failed (0x%08X)", (uint32_t)result);

surfaceFormats = _alloca ( surfaceFormatCount * sizeof ( VkSurfaceFormatKHR ) );

result = renderer->vkInstanceVtbl.vkGetPhysicalDeviceSurfaceFormatsKHR (
    renderer->physicalDevice,
    renderer->surface,
    &surfaceFormatCount,
    surfaceFormats
);
if ( result != VK_SUCCESS )
    RETURN_ERROR(-1,"vkGetPhysicalDeviceSurfaceFormatsKHR failed (0x%08X)", (uint32_t)result);

if ( surfaceFormatCount == 0 )
    RETURN_ERROR(-1,"No valid surface formats found");

surfaceFormat = surfaceFormats[0].format;
colorSpace    = surfaceFormats[0].colorSpace;

vkGetPhysicalDeviceSurfaceSupportKHR will specify all formats the physical device and VkSurface object support. In usual fashion, we call the function twice in order to get the count and allocate only as needed.

We are not all that interested in picking an optimal output format for our demo application, so we pick the format and colorspace of the very first surface format that appears in the list. In a production scenario it can be worth ensuring the best available option is picked rather than the first one available.

VkSurfaceCapabilitiesKHR surfaceCapabilities;
uint32_t presentModeCount = 0;
VkPresentModeKHR* presentModes = NULL;

result = renderer->vkInstanceVtbl.vkGetPhysicalDeviceSurfaceCapabilitiesKHR (
    renderer->physicalDevice,
    renderer->surface,
    &surfaceCapabilities
);
if ( result != VK_SUCCESS )
    RETURN_ERROR(-1,"vkGetPhysicalDeviceSurfaceCapabilitiesKHR failed (0x%08X)", (uint32_t)result);

VkExtent2D swapChainExtent;
if ( surfaceCapabilities.currentExtent.width == (uint32_t)-1 )
{
    swapChainExtent.width  = newWidth;
    swapChainExtent.height = newHeight;
}
else
    swapChainExtent = surfaceCapabilities.currentExtent;


if ( surfaceCapabilities.minImageCount > FRAME_BUFFER_COUNT
    || surfaceCapabilities.maxImageCount < FRAME_BUFFER_COUNT )
    RETURN_ERROR(-1, "Invalid frame buffer capability count (wanted: %u, capable: %u > %u)", FRAME_BUFFER_COUNT, surfaceCapabilities.minImageCount, surfaceCapabilities.maxImageCount );

if ( !(surfaceCapabilities.supportedTransforms & VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR) )
    RETURN_ERROR(-1, "No identity transform" );

In the preceding code, we query the surface and device combined capabilities and ensure the swapchain's extent fits. vkGetPhysicalDeviceSurfaceCapabilitiesKHR will normally return the size of the surface in surfaceCapabilities.currentExtent, but will sometimes also return 0xFFFFFFFF for both the width and the height. This indices the size of the surface will be determined by the size specified in the swap chain creation function. If this occurs in our case, we use our window width and height we get from the main application to determine the swap chain size, automatically adapting the surface to this size in the process.

We also check whether the amount of backbuffers we are asking for is within the range the implementation allows. The amount of backbuffer will need to be between minImageCount and maxImageCount.

Now we need to determine the VkPresentModeKHR we would like to use. We first determine the supported set of present modes using vkGetPhysicalDeviceSurfacePresentModesKHR, after which we go through our preferred set of methods and check support in order of our preferences.

result = renderer->vkInstanceVtbl.vkGetPhysicalDeviceSurfacePresentModesKHR (
    renderer->physicalDevice,
    renderer->surface,
    &presentModeCount,
    NULL
);
if ( result != VK_SUCCESS )
    RETURN_ERROR(-1,"vkGetPhysicalDeviceSurfacePresentModesKHR failed (0x%08X)", (uint32_t)result);

presentModes = _alloca ( presentModeCount * sizeof ( VkPresentModeKHR ) );

result = renderer->vkInstanceVtbl.vkGetPhysicalDeviceSurfacePresentModesKHR (
    renderer->physicalDevice,
    renderer->surface,
    &presentModeCount,
    presentModes
);
if ( result != VK_SUCCESS )
    RETURN_ERROR(-1,"vkGetPhysicalDeviceSurfacePresentModesKHR failed (0x%08X)", (uint32_t)result);

VkPresentModeKHR desiredPresentModes[] = {
    VK_PRESENT_MODE_MAILBOX_KHR,
    VK_PRESENT_MODE_IMMEDIATE_KHR,
    VK_PRESENT_MODE_FIFO_KHR
};
VkPresentModeKHR presentMode = VK_PRESENT_MODE_MAX_ENUM;

for ( uint32_t i = 0; i < STATIC_ARRAY_SIZE(desiredPresentModes); i++ )
{
    for ( uint32_t j = 0; j < presentModeCount; j++ )
    {
        if ( presentModes[j] == desiredPresentModes[i] )
        {
            presentMode = desiredPresentModes[i];
            break;
        }
    }
}
if ( presentMode == VK_PRESENT_MODE_MAX_ENUM )
    RETURN_ERROR(-1, "No suitable present mode found");

A full list of present modes and their differences can be found in the Vulkan specification.

With that information we can now create the swapchain itself.

VkSwapchainKHR oldSwapchain = renderer->swapChain;
result = renderer->vkDeviceVtbl.vkCreateSwapchainKHR (
    renderer->device,
    &(VkSwapchainCreateInfoKHR){
        .sType                 = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR,
        .pNext                 = NULL,
        .flags                 = 0,
        .surface               = renderer->surface,
        .minImageCount         = FRAME_BUFFER_COUNT,
        .imageFormat           = surfaceFormat,
        .imageColorSpace       = colorSpace,
        .imageExtent           = { .width = swapChainExtent.width, .height = swapChainExtent.height },
        .imageArrayLayers      = 1,
        .imageUsage            = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT,
        .imageSharingMode      = VK_SHARING_MODE_EXCLUSIVE,
        .queueFamilyIndexCount = 0,
        .pQueueFamilyIndices   = NULL,
        .preTransform          = VK_SURFACE_TRANSFORM_IDENTITY_BIT_KHR,
        .compositeAlpha        = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR,
        .presentMode           = presentMode,
        .clipped               = 1,
        .oldSwapchain          = oldSwapchain,
    },
    NULL,
    &renderer->swapChain
);
if ( result != VK_SUCCESS )
    RETURN_ERROR(-1, "vkCreateSwapchainKHR failed (0x%08X)", (uint32_t)result);

if ( oldSwapchain != VK_NULL_HANDLE )
{
    renderer->vkDeviceVtbl.vkDestroySwapchainKHR (
        renderer->device,
        oldSwapchain,
        NULL
    );
}

In the above code, note we are passing oldSwapchain. This is not required, and can simply be set to VK_NULL_HANDLE. The reason why this paramter may be useful is for resize operations, where the swap chain will have to be replaced in order to accomodate the new window size. Specifying the old swap chain will not automatically destroy the swap chain, but will smooth the transition between the two swapchains.

Last but not least we are to process the backbuffer images and make sure we can render to them. This is done using vkGetSwapchainImagesKHR followed by creating image views for them. These image views are later used to create framebuffers for rendering.

uint32_t swapChainImageCount = 0;
VkImage* swapChainImages = NULL;
result = renderer->vkDeviceVtbl.vkGetSwapchainImagesKHR (
    renderer->device,
    renderer->swapChain,
    &swapChainImageCount,
    NULL
);
if ( result != VK_SUCCESS )
    RETURN_ERROR(-1, "vkGetSwapchainImagesKHR failed (0x%08X)", (uint32_t)result);

if ( swapChainImageCount != FRAME_BUFFER_COUNT )
    RETURN_ERROR(-1, "Swap chain image count mismatch (%u / %u)", swapChainImageCount, FRAME_BUFFER_COUNT);
swapChainImages = _alloca ( swapChainImageCount * sizeof ( VkImage ) );
result = renderer->vkDeviceVtbl.vkGetSwapchainImagesKHR (
    renderer->device,
    renderer->swapChain,
    &swapChainImageCount,
    swapChainImages
);
if ( result != VK_SUCCESS )
    RETURN_ERROR(-1, "vkGetSwapchainImagesKHR failed (0x%08X)", (uint32_t)result);

////////////////////////////////////////
// Initialize swap chain image views

for ( uint32_t i = 0; i < FRAME_BUFFER_COUNT; i++ )
{
    renderer->frameBuffers[i].image = swapChainImages[i];
    result = vkCreateImageView (
        renderer->device,
        &(VkImageViewCreateInfo){
            .sType    = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
            .pNext    = NULL,
            .flags    = 0,
            .image    = swapChainImages[i],
            .viewType = VK_IMAGE_VIEW_TYPE_2D,
            .format   = surfaceFormat,
            .components = {
                .r = VK_COMPONENT_SWIZZLE_R, .g = VK_COMPONENT_SWIZZLE_G,
                .b = VK_COMPONENT_SWIZZLE_B, .a = VK_COMPONENT_SWIZZLE_A,
            },
            .subresourceRange = {
                .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
                .levelCount = 1,
                .layerCount = 1,
            },
        },
        NULL,
        &renderer->frameBuffers[i].imageView
    );
    if ( result != VK_SUCCESS )
        RETURN_ERROR(-1, "vkCreateImageView failed (0x%08X)", (uint32_t)result);
}

Note in the above code the components section: Might colors require swapping, this property can be used to do so. There are, however, 3 other properties that can be of use:

  • VK_COMPONENT_SWIZZLE_IDENTITY (0): Takes the default option for this channel. (red will map to the red channel, green to green etc) Particularly useful when zero-initializing your struct, allowing you to not worry about mistakes in swizzling the component channels
  • VK_COMPONENT_SWIZZLE_ZERO: This channel will always return 0 when sampled
  • VK_COMPONENT_SWIZZLE_ONE: This channel will always return 1 when sampled

for ( uint32_t i = 0; i < FRAME_BUFFER_COUNT; i++ )
{
    vkCmdPipelineBarrier (
        preloaderCommandBuffer,
        VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
        VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT,
        0,
        0, NULL,
        0, NULL,
        1, (VkImageMemoryBarrier[1]){
            [0] = {
                .sType               = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
                .pNext               = NULL,
                .srcAccessMask       = 0,
                .dstAccessMask       = 0,
                .oldLayout           = VK_IMAGE_LAYOUT_UNDEFINED,
                .newLayout           = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR,
                .srcQueueFamilyIndex = 0,
                .dstQueueFamilyIndex = 0,
                .image               = swapChainImages[i],
                .subresourceRange    = { .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, .levelCount = 1, .layerCount = 1 },
            },
        }
    );
}

Images created for the swapchain are created in an undefined image layout. In order to guarantee the possibility to properly display, we need to transition to a layout in an image layout that is compatible with presentation.

The pipeline stage flags specified are not that relevant here, but allow to specify the output of a set of source stages to be guaranteed to be visible for a set of destination stages.