Vulkan. Developer's guide. Swap chain

I continue to publish translations of the Vulkan API manual (the link to the original is vulkan-tutorial.com ), and today I want to share the translation of a new chapter - Swap chain from the Drawing a triangle section, Presentation subsection.



Content
1.



2.



3.



4.







  1. (pipeline)


5.



  1. Staging


6. Uniform-



  1. layout
  2. sets


7.



  1. Image view image sampler
  2. image sampler


8.



9.



10. -



11. Multisampling



FAQ









Swap chain





Vulkan has no such thing as a default framebuffer, so it needs an infrastructure with buffers to render images to before being displayed. This infrastructure is called a swap chain and must be explicitly created in Vulkan. Swap chain is a queue of images waiting to be displayed on the screen. The program first asks for an object image(VkImage)



to draw into, and after rendering, sends it back to the queue. Exactly how the queue works depends on the settings, but the main task of the swap chain is to synchronize the display of images with the screen refresh rate.



Checking swap chain support



Some specialized video cards do not have display outputs and therefore cannot display images on the screen. In addition, screen mapping is tied to the window system and is not part of the Vulkan core. Therefore, we need to connect the extension VK_KHR_swapchain



.



First isDeviceSuitable



, let's change the function to check if the extension is supported. We have already worked with the list of supported extensions before, so there shouldn't be any difficulties. Note that the Vulkan header file provides a handy macro VK_KHR_SWAPCHAIN_EXTENSION_NAME



that is defined as " VK_KHR_swapchain



". The advantage of this macro is that if you make a spelling mistake, the compiler will warn you about it.



Let's start by declaring a list of required extensions.



const std::vector<const char*> deviceExtensions = {
    VK_KHR_SWAPCHAIN_EXTENSION_NAME
};
      
      





For additional verification, let's create a new function checkDeviceExtensionSupport



called from isDeviceSuitable



:



bool isDeviceSuitable(VkPhysicalDevice device) {
    QueueFamilyIndices indices = findQueueFamilies(device);

    bool extensionsSupported = checkDeviceExtensionSupport(device);

    return indices.isComplete() && extensionsSupported;
}

bool checkDeviceExtensionSupport(VkPhysicalDevice device) {
    return true;
}
      
      





Let's change the body of the function to check if all the extensions we need are in the list of supported ones.



bool checkDeviceExtensionSupport(VkPhysicalDevice device) {
    uint32_t extensionCount;
    vkEnumerateDeviceExtensionProperties(device, nullptr, &extensionCount, nullptr);

    std::vector<VkExtensionProperties> availableExtensions(extensionCount);
    vkEnumerateDeviceExtensionProperties(device, nullptr, &extensionCount, availableExtensions.data());

    std::set<std::string> requiredExtensions(deviceExtensions.begin(), deviceExtensions.end());

    for (const auto& extension : availableExtensions) {
        requiredExtensions.erase(extension.extensionName);
    }

    return requiredExtensions.empty();
}
      
      





Here I used std::set<std::string>



to store the names of the required but not yet confirmed extensions. You can also use a nested loop like in a function checkValidationLayerSupport



. The performance difference is not significant.



Now let's run the program and make sure that our video card is suitable for creating a swap chain. Note that the presence of a display queue already implies support for the swap chain extension. However, it is best to make sure of this explicitly.



Connecting extensions



To use the swap chain, you first need to enable the extension VK_KHR_swapchain



. To do this, let's slightly change the padding VkDeviceCreateInfo



when creating the logical device:



createInfo.enabledExtensionCount = static_cast<uint32_t>(deviceExtensions.size());
createInfo.ppEnabledExtensionNames = deviceExtensions.data();
      
      





Swap chain support information request



Checking alone to see if the swap chain is available is not enough. The creation of the swap chain involves a lot more configuration, so we need to ask for more information.



In total, you need to check 3 types of properties:



  • Basic capabilities of the surface, such as min / max number of images in the swap chain, min / max width and height of images
  • Surface format (pixel format, color space)
  • Available operating modes


To work with this data, we will use the structure:



struct SwapChainSupportDetails {
    VkSurfaceCapabilitiesKHR capabilities;
    std::vector<VkSurfaceFormatKHR> formats;
    std::vector<VkPresentModeKHR> presentModes;
};
      
      





Now let's create a function querySwapChainSupport



that fills this structure.



SwapChainSupportDetails querySwapChainSupport(VkPhysicalDevice device) {
    SwapChainSupportDetails details;

    return details;
}
      
      





Let's start with surface capabilities. They are easy to query and return to the structure VkSurfaceCapabilitiesKHR



.



vkGetPhysicalDeviceSurfaceCapabilitiesKHR(device, surface, &details.capabilities);
      
      





This function accepts the previously created VkPhysicalDevice



and VkSurfaceKHR



. Each time we ask for supported functionality, these two parameters will be the first, since they are key components of the swap chain.



The next step is to query the supported surface formats. To do this, let's perform the already familiar ritual with a double function call:



uint32_t formatCount;
vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount, nullptr);

if (formatCount != 0) {
    details.formats.resize(formatCount);
    vkGetPhysicalDeviceSurfaceFormatsKHR(device, surface, &formatCount, details.formats.data());
}
      
      





Make sure you allocate enough space in the vector to get all the available formats.



In the same way, we request the supported modes of operation using the function vkGetPhysicalDeviceSurfacePresentModesKHR



:



uint32_t presentModeCount;
vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &presentModeCount, nullptr);

if (presentModeCount != 0) {
    details.presentModes.resize(presentModeCount);
    vkGetPhysicalDeviceSurfacePresentModesKHR(device, surface, &presentModeCount, details.presentModes.data());
}
      
      





When all the necessary information is in the structure, add the function isDeviceSuitable



to check if the swap chain is supported. For the purposes of this tutorial, we will assume that if there is at least one supported image format and one supported mode for the window surface, then the swap chain is supported.



bool swapChainAdequate = false;
if (extensionsSupported) {
    SwapChainSupportDetails swapChainSupport = querySwapChainSupport(device);
    swapChainAdequate = !swapChainSupport.formats.empty() && !swapChainSupport.presentModes.empty();
}
      
      





You only need to request swap chain support after you have verified that the extension is available.



The last line of the function changes to:



return indices.isComplete() && extensionsSupported && swapChainAdequate;
      
      





Choosing settings for swap chain



If swapChainAdequate



true, the swap chain is supported. But the swap chain can have several modes. Let's write a few functions to find the appropriate settings for creating the most efficient swap chain.



In total, let's highlight 3 types of settings:

  • surface format (color depth)
  • operating mode (conditions for changing frames on the screen)
  • swap extent (resolution of images in the swap chain)


For each setting, we will look for some "ideal" value, and if it is not available, we will use some logic to choose from what is.



Surface format



Let's add a function to select a format:



VkSurfaceFormatKHR chooseSwapSurfaceFormat(const std::vector<VkSurfaceFormatKHR>& availableFormats) {

}
      
      





Later, we will pass a member formats



from the structure SwapChainSupportDetails



as an argument.



Each element availableFormats



contains members format



and colorSpace



. The field format



defines the number and types of channels. For example, it VK_FORMAT_B8G8R8A8_SRGB



means that we have B, G, R and alpha channels of 8 bits each, for a total of 32 bits per pixel. A flag VK_COLOR_SPACE_SRGB_NONLINEAR_KHR



in the field colorSpace



indicates whether the SRGB color space is supported. Note that in an earlier version of the specification this flag was called VK_COLORSPACE_SRGB_NONLINEAR_KHR



.



We will use SRGB as the color space. SRGB is a standard for representing colors in images, and it better reproduces perceived colors. That is why we will also use one of the SRGB formats as a color format - VK_FORMAT_B8G8R8A8_SRGB



.



Let's go through the list and check if the combination we need is available:



for (const auto& availableFormat : availableFormats) {
    if (availableFormat.format == VK_FORMAT_B8G8R8A8_SRGB && availableFormat.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) {
        return availableFormat;
    }
}
      
      





If not, we can sort the available formats from more suitable to less suitable, but in most cases we can simply take the first one from the list.



VkSurfaceFormatKHR chooseSwapSurfaceFormat(const std::vector<VkSurfaceFormatKHR>& availableFormats) {
    for (const auto& availableFormat : availableFormats) {
        if (availableFormat.format == VK_FORMAT_B8G8R8A8_SRGB && availableFormat.colorSpace == VK_COLOR_SPACE_SRGB_NONLINEAR_KHR) {
            return availableFormat;
        }
    }

    return availableFormats[0];
}

      
      





Working hours



The mode of operation is perhaps the most important setting for the swap chain, since it determines the conditions for changing frames on the screen.



There are four modes available in Vulkan:



  • VK_PRESENT_MODE_IMMEDIATE_KHR



    : , , , .
  • VK_PRESENT_MODE_FIFO_KHR



    : . , . , . , .
  • VK_PRESENT_MODE_FIFO_RELAXED_KHR



    : , . . .
  • VK_PRESENT_MODE_MAILBOX_KHR



    : this is another variation of the second mode. Instead of blocking the program when the queue is full, images in the queue are replaced with new ones. This mode is suitable for implementing triple buffering. With it, you can avoid the appearance of artifacts with low latency.


Only the mode is guaranteed to be available VK_PRESENT_MODE_FIFO_KHR



, so again we will have to write a function to find the best available mode:



VkPresentModeKHR chooseSwapPresentMode(const std::vector<VkPresentModeKHR>& availablePresentModes) {
    return VK_PRESENT_MODE_FIFO_KHR;
}
      
      





Personally, I find it best to use triple buffering. It avoids artifacts with low latency.



So let's go through the list to check the available modes:



VkPresentModeKHR chooseSwapPresentMode(const std::vector<VkPresentModeKHR>& availablePresentModes) {
    for (const auto& availablePresentMode : availablePresentModes) {
        if (availablePresentMode == VK_PRESENT_MODE_MAILBOX_KHR) {
            return availablePresentMode;
        }
    }

    return VK_PRESENT_MODE_FIFO_KHR;
}
      
      





Swap extent



It remains to configure the last property. To do this, add a function:



VkExtent2D chooseSwapExtent(const VkSurfaceCapabilitiesKHR& capabilities) {

}
      
      





Swap extent is the resolution of the images in the swap chain, which almost always matches the resolution of the window (in pixels) where the images are rendered. We got the allowed range in the structure VkSurfaceCapabilitiesKHR



. Vulkan tells us what resolution we should set using a field currentExtent



(matches the size of the window). However, some window managers allow for different resolutions. For this, a special value for the width and height currentExtent



is specified - the maximum value of the type uint32_t



. In this case, from the interval between minImageExtent



and, maxImageExtent



we will choose the resolution that best matches the window resolution. The main thing is to specify the units of measurement correctly.



GLFW uses two units of measurement: pixels and screen coordinates . So, the resolution {WIDTH, HEIGHT}



that we specified when creating the window is measured in screen coordinates. But since Vulkan works with pixels, the swap chain resolution must also be specified in pixels. If you are using a high-resolution display (such as Apple's Retina display), screen coordinates do not match pixels: Due to the higher pixel density, the window resolution is higher in pixels than in screen coordinates. Since Vulkan won't fix the swap chain permission for us, we can't use the original permission {WIDTH, HEIGHT}



. Instead, we should use glfwGetFramebufferSize



to query the window's resolution in pixels before mapping it to the minimum and maximum image resolutions.



#include <cstdint> // Necessary for UINT32_MAX

...

VkExtent2D chooseSwapExtent(const VkSurfaceCapabilitiesKHR& capabilities) {
    if (capabilities.currentExtent.width != UINT32_MAX) {
        return capabilities.currentExtent;
    } else {
        int width, height;
        glfwGetFramebufferSize(window, &width, &height);

        VkExtent2D actualExtent = {
            static_cast<uint32_t>(width),
            static_cast<uint32_t>(height)
        };

        actualExtent.width = std::max(capabilities.minImageExtent.width, std::min(capabilities.maxImageExtent.width, actualExtent.width));
        actualExtent.height = std::max(capabilities.minImageExtent.height, std::min(capabilities.maxImageExtent.height, actualExtent.height));

        return actualExtent;
    }
}
      
      





Function max



and min



is used to limit the values width



and height



within the available resolutions. Don't forget to include the header file <algorithm>



to use the functions.



Swap chain creation



We now have all the information we need to create a suitable swap chain.



Let's create a function createSwapChain



and call it from initVulkan



after creating the logical device.



void initVulkan() {
    createInstance();
    setupDebugMessenger();
    createSurface();
    pickPhysicalDevice();
    createLogicalDevice();
    createSwapChain();
}

void createSwapChain() {
    SwapChainSupportDetails swapChainSupport = querySwapChainSupport(physicalDevice);

    VkSurfaceFormatKHR surfaceFormat = chooseSwapSurfaceFormat(swapChainSupport.formats);
    VkPresentModeKHR presentMode = chooseSwapPresentMode(swapChainSupport.presentModes);
    VkExtent2D extent = chooseSwapExtent(swapChainSupport.capabilities);
}
      
      





Now you need to decide how many image objects should be in the swap chain. The implementation specifies the minimum amount required for work:



uint32_t imageCount = swapChainSupport.capabilities.minImageCount;
      
      





However, if you only use this minimum, you sometimes have to wait for the driver to finish internal operations to get the next image. Therefore, it is better to request at least one more than the specified minimum:



uint32_t imageCount = swapChainSupport.capabilities.minImageCount + 1;
      
      





It is important not to exceed the maximum amount. A value 0



indicates that no maximum is specified.



if (swapChainSupport.capabilities.maxImageCount > 0 && imageCount > swapChainSupport.capabilities.maxImageCount) {
    imageCount = swapChainSupport.capabilities.maxImageCount;
}
      
      





The swap chain is a Vulkan object, so you need to populate the structure to create it. The beginning of the structure is already familiar to us:



VkSwapchainCreateInfoKHR createInfo{};
createInfo.sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR;
createInfo.surface = surface;
      
      





First, the surface is specified, to which the swap chain is attached, then - information for creating image objects:



createInfo.minImageCount = imageCount;
createInfo.imageFormat = surfaceFormat.format;
createInfo.imageColorSpace = surfaceFormat.colorSpace;
createInfo.imageExtent = extent;
createInfo.imageArrayLayers = 1;
createInfo.imageUsage = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT;
      
      





In imageArrayLayers



specifies the number of layers that each image consists of. There will always be value here 1



, unless, of course, these are stereo images. The bit field imageUsage



indicates what operations the images obtained from the swap chain will be used for. In the tutorial, we'll be rendering directly to them, but you can render to a separate image first, for example for post-processing. In this case, use the value VK_IMAGE_USAGE_TRANSFER_DST_BIT



, and for the transfer, use the memory operation.



QueueFamilyIndices indices = findQueueFamilies(physicalDevice);
uint32_t queueFamilyIndices[] = {indices.graphicsFamily.value(), indices.presentFamily.value()};

if (indices.graphicsFamily != indices.presentFamily) {
    createInfo.imageSharingMode = VK_SHARING_MODE_CONCURRENT;
    createInfo.queueFamilyIndexCount = 2;
    createInfo.pQueueFamilyIndices = queueFamilyIndices;
} else {
    createInfo.imageSharingMode = VK_SHARING_MODE_EXCLUSIVE;
    createInfo.queueFamilyIndexCount = 0; // Optional
    createInfo.pQueueFamilyIndices = nullptr; // Optional
}
      
      





Next, you need to specify how to handle images objects that are used across multiple queue families. This is true for cases where the graphics family and the display family are different families. We will render to images in the graphics queue and then send them to the display queue.



There are two ways to process images with access from multiple queues:



  • VK_SHARING_MODE_EXCLUSIVE



    : The object belongs to one queue family and ownership must be transferred explicitly before using it in another queue family. This method provides the highest performance.

  • VK_SHARING_MODE_CONCURRENT



    : Objects can be used across multiple queue families without explicitly transferring ownership.



If we have multiple queues, we will use VK_SHARING_MODE_CONCURRENT



. This method requires you to specify in advance between which queue families the ownership will be shared. This can be done using the parameters queueFamilyIndexCount



and pQueueFamilyIndices



. If the graphics queue family and display queue family are the same, which is more common, use VK_SHARING_MODE_EXCLUSIVE



.



createInfo.preTransform = swapChainSupport.capabilities.currentTransform;
      
      





You can specify that images in the swap chain are applied with any of the supported transformations ( supportedTransforms



in capabilities



), for example, rotate 90 degrees clockwise or flip horizontally. To not apply any transformations, just leave currentTransform



.



createInfo.compositeAlpha = VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR;
      
      





The field compositeAlpha



indicates whether to use the alpha channel for blending with other windows in the windowing system. You probably won't need an alpha channel, so leave it VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR



.



createInfo.presentMode = presentMode;
createInfo.clipped = VK_TRUE;
      
      





The field presentMode



speaks for itself. If we put it VK_TRUE



in the field clipped



, then we are not interested in hidden pixels (for example, if part of our window is covered by another window). You can always turn clipping off if you need to read the pixels, but for now let's leave clipping on.



createInfo.oldSwapchain = VK_NULL_HANDLE;
      
      





The last field remains - oldSwapChain



. If the swap chain becomes invalid, for example, due to the resizing of the window, it will need to be recreated from scratch and in the field oldSwapChain



specify a link to the old swap chain. This is a complex topic that we will cover in a later chapter. For now, let's say we only have one swap chain.



Let's add a class member to store the object VkSwapchainKHR



:



VkSwapchainKHR swapChain;
      
      





Now you just need to call vkCreateSwapchainKHR



to create the swap chain:



if (vkCreateSwapchainKHR(device, &createInfo, nullptr, &swapChain) != VK_SUCCESS) {
    throw std::runtime_error("failed to create swap chain!");
}
      
      





The following parameters are passed to the function: logical device, swap chain information, an optional custom allocator and a pointer to write the result. No surprises. The swap chain must be destroyed using vkDestroySwapchainKHR



before the device is destroyed:



void cleanup() {
    vkDestroySwapchainKHR(device, swapChain, nullptr);
    ...
}
      
      





Now let's run the program to make sure the swap chain was created successfully. If you receive an error message or a message like ยซ vkGetInstanceProcAddress SteamOverlayVulkanLayer.dllยป



, go to the FAQ section .



Let's try to remove the line createInfo.imageExtent = extent;



with the validation layers enabled. One of the validation levels will immediately detect the error and notify us:



image



Getting an image from a swap chain



Now that the swap chain has been created, it remains to get the VkImages descriptors . Let's add a class member for storing descriptors:



std::vector<VkImage> swapChainImages;
      
      





Image objects from the swap chain will be destroyed automatically after the swap chain itself is destroyed, so there is no need to add any cleanup code.



Immediately after the call, vkCreateSwapchainKHR



add the code to get the descriptors. Remember that we have specified only the minimum number of images in the swap chain, which means that there may be more of them. Therefore, we first request the real number of images using the function vkGetSwapchainImagesKHR



, then allocate the necessary space in the container and call it again vkGetSwapchainImagesKHR



to get the descriptors.



vkGetSwapchainImagesKHR(device, swapChain, &imageCount, nullptr);
swapChainImages.resize(imageCount);
vkGetSwapchainImagesKHR(device, swapChain, &imageCount, swapChainImages.data());
      
      





And the last thing - save the format and resolution of the swap chain images into class variables. We will need them in the future.



VkSwapchainKHR swapChain;
std::vector<VkImage> swapChainImages;
VkFormat swapChainImageFormat;
VkExtent2D swapChainExtent;

...

swapChainImageFormat = surfaceFormat.format;
swapChainExtent = extent;
      
      





We now have an image to draw and display. In the next chapter, we'll show you how to set up an image to be used as render targets, and get started with the graphics pipeline and drawing commands!



C ++



All Articles