Initializing Vulkan swapchain
Normally, each frame is rendered into an offscreen image. After the rendering process is finished, the offscreen image should be made visible or “presented.” A swapchain is an object that holds a collection of available offscreen images, or more specifically, a queue of rendered images waiting to be presented to the screen. In OpenGL, presenting an offscreen buffer to the visible area of a window is done using system-dependent functions, such as wglSwapBuffers()
on Windows, eglSwapBuffers()
on OpenGL ES embedded systems, glXSwapBuffers()
on Linux, or automatically on macOS. Vulkan, however, gives us much more fine-grained control. We need to select a presentation mode for swapchain images and specify various flags.
In this recipe, we will show how to create a Vulkan swapchain object using the Vulkan instance and device initialized in the previous recipe.
Getting ready
Revisit the previous recipe Initializing Vulkan instance and graphical device, which covers the initial steps necessary to initialize Vulkan. The source code discussed in this recipe is implemented in the class lvk::VulkanSwapchain
.
How to do it...
In the previous recipe, we began learning how Vulkan instances and devices are created by exploring the helper function lvk::createVulkanContextWithSwapchain()
. This led us to the function VulkanContext::initContext()
, which we discussed in detail. Let’s continue our journey by exploring VulkanContext::initSwapchain()
and the related class VulkanSwapchain
from LightweightVK.
- First, let us take a look at a function which retrieves various surface format support capabilities and stores them in the member fields of
VulkanContext
. The function also checks depth format support, but only for those depth formats that might be used by LightweightVK.void lvk::VulkanContext::querySurfaceCapabilities() { const VkFormat depthFormats[] = { VK_FORMAT_D32_SFLOAT_S8_UINT, VK_FORMAT_D24_UNORM_S8_UINT, VK_FORMAT_D16_UNORM_S8_UINT, VK_FORMAT_D32_SFLOAT, VK_FORMAT_D16_UNORM }; for (const auto& depthFormat : depthFormats) { VkFormatProperties formatProps; vkGetPhysicalDeviceFormatProperties( vkPhysicalDevice_, depthFormat, &formatProps); if (formatProps.optimalTilingFeatures) deviceDepthFormats_.push_back(depthFormat); } if (vkSurface_ == VK_NULL_HANDLE) return;
- All the surface capabilities and surface formats are retrieved and stored. First, we get the number of supported formats, then allocate the storage to hold them and read the actual properties.
vkGetPhysicalDeviceSurfaceCapabilitiesKHR( vkPhysicalDevice_, vkSurface_, &deviceSurfaceCaps_); uint32_t formatCount; vkGetPhysicalDeviceSurfaceFormatsKHR( vkPhysicalDevice_, vkSurface_, &formatCount, nullptr); if (formatCount) { deviceSurfaceFormats_.resize(formatCount); vkGetPhysicalDeviceSurfaceFormatsKHR( vkPhysicalDevice_, vkSurface_, &formatCount, deviceSurfaceFormats_.data()); }
- In a similar way, store surface present modes as well.
uint32_t presentModeCount; vkGetPhysicalDeviceSurfacePresentModesKHR( vkPhysicalDevice_, vkSurface_, &presentModeCount, nullptr); if (presentModeCount) { devicePresentModes_.resize(presentModeCount); vkGetPhysicalDeviceSurfacePresentModesKHR( vkPhysicalDevice_, vkSurface_, &presentModeCount, devicePresentModes_.data()); } }
Knowing all supported color surface formats, we can choose a suitable one for our swapchain. Let’s take a look at the chooseSwapSurfaceFormat()
helper function to see how it’s done. The function takes a list of available formats and a desired color space as input.
- First, it selects a preferred surface format based on the desired color space and the RGB/BGR native swapchain image format. RGB or BGR is determined by going through all available color formats returned by Vulkan and picking the one—RGB or BGR—that appears first in the list. If BGR is encountered earlier, it will be chosen. Once the preferred image format and color space are selected, the function goes through the list of supported formats to try to find an exact match. Here,
colorSpaceToVkSurfaceFormat()
andisNativeSwapChainBGR()
are local C++ lambdas. Check the full source code to see their implementations.VkSurfaceFormatKHR chooseSwapSurfaceFormat( const std::vector<VkSurfaceFormatKHR>& formats, lvk::ColorSpace colorSpace) { const VkSurfaceFormatKHR preferred = colorSpaceToVkSurfaceFormat( colorSpace, isNativeSwapChainBGR(formats)); for (const VkSurfaceFormatKHR& fmt : formats) { if (fmt.format == preferred.format && fmt.colorSpace == preferred.colorSpace) return fmt; }
- If we cannot find both a matching format and color space, try matching only the format. If we cannot match the format, default to the first available format. On many systems, it will be
VK_FORMAT_R8G8B8A8_UNORM
or a similar format.for (const VkSurfaceFormatKHR& fmt : formats) { if (fmt.format == preferred.format) return fmt; } return formats[0]; }
This function is called from the constructor of VulkanSwapchain
. Once the format has been selected, we need to do a few more checks before we can create an actual Vulkan swapchain.
- The first check is to ensure that the selected format supports presentation operation on the graphics queue family used to create the swapchain.
lvk::VulkanSwapchain::VulkanSwapchain( VulkanContext& ctx, uint32_t width, uint32_t height) : ctx_(ctx), device_(ctx.vkDevice_), graphicsQueue_(ctx.deviceQueues_.graphicsQueue), width_(width), height_(height) { surfaceFormat_ = chooseSwapSurfaceFormat( ctx.deviceSurfaceFormats_, ctx.config_.swapChainColorSpace); VkBool32 queueFamilySupportsPresentation = VK_FALSE; vkGetPhysicalDeviceSurfaceSupportKHR(ctx.getVkPhysicalDevice(), ctx.deviceQueues_.graphicsQueueFamilyIndex, ctx.vkSurface_, &queueFamilySupportsPresentation));
- The second check is necessary to choose usage flags for swapchain images. Usage flags define if swapchain images can be used as color attachments, in transfer operations, or as storage images to allow compute shaders to operate directly on them. Different devices have different capabilities and storage images are not always supported, especially on mobile GPUs. Here’s a C++ local lambda to do it:
auto chooseUsageFlags = [](VkPhysicalDevice pd, VkSurfaceKHR surface, VkFormat format) -> VkImageUsageFlags { VkImageUsageFlags usageFlags = VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | VK_IMAGE_USAGE_TRANSFER_SRC_BIT; VkSurfaceCapabilitiesKHR caps; vkGetPhysicalDeviceSurfaceCapabilitiesKHR(pd, surface, &caps); const bool isStorageSupported = (caps.supportedUsageFlags & VK_IMAGE_USAGE_STORAGE_BIT) > 0; VkFormatProperties props; vkGetPhysicalDeviceFormatProperties(pd, format, &props); const bool isTilingOptimalSupported = (props.optimalTilingFeatures & VK_IMAGE_USAGE_STORAGE_BIT) > 0; if (isStorageSupported && isTilingOptimalSupported) { usageFlags |= VK_IMAGE_USAGE_STORAGE_BIT; } return usageFlags; }
- Now we should select the presentation mode. The preferred presentation mode is
VK_PRESENT_MODE_MAILBOX_KHR
which specifies that the Vulkan presentation system should wait for the next vertical blanking period to update the current image. Visual tearing will not be observed in this case. However, this presentation mode is not guaranteed to be supported. In this situation, we can try pickingVK_PRESENT_MODE_IMMEDIATE_KHR
for the fastest frames-per-second without V-sync, or we can always fall back toVK_PRESENT_MODE_FIFO_KHR
. The differences between all possible presentation mode are described in the Vulkan specification https://www.khronos.org/registry/vulkan/specs/1.3-extensions/man/html/VkPresentModeKHR.htmlauto chooseSwapPresentMode = []( const std::vector<VkPresentModeKHR>& modes) -> VkPresentModeKHR { #if defined(__linux__) || defined(_M_ARM64) if (std::find(modes.cbegin(), modes.cend(), VK_PRESENT_MODE_IMMEDIATE_KHR) != modes.cend()) { return VK_PRESENT_MODE_IMMEDIATE_KHR; } #endif if (std::find(modes.cbegin(), modes.cend(), VK_PRESENT_MODE_MAILBOX_KHR) != modes.cend()) { return VK_PRESENT_MODE_MAILBOX_KHR; } return VK_PRESENT_MODE_FIFO_KHR; };
- The last helper lambda we need will choose the number of images in the swapchain object. It is based on the surface capabilities we retrieved earlier. Instead of using
minImageCount
directly, we request one additional image to make sure we are not waiting on the GPU to complete any operations.auto chooseSwapImageCount = []( const VkSurfaceCapabilitiesKHR& caps) -> uint32_t { const uint32_t desired = caps.minImageCount + 1; const bool exceeded = caps.maxImageCount > 0 && desired > caps.maxImageCount; return exceeded ? caps.maxImageCount : desired; };
- Let’s go back to the constructor
VulkanSwapchain::VulkanSwapchain()
and explore how it uses all abovementioned helper functions to create a Vulkan swapchain object. The code here becomes rather short and consists only of filling in theVkSwapchainCreateInfoKHR
structure.const VkImageUsageFlags usageFlags = chooseUsageFlags( ctx.getVkPhysicalDevice(), ctx.vkSurface_, surfaceFormat_.format); const bool isCompositeAlphaOpaqueSupported = (ctx.deviceSurfaceCaps_.supportedCompositeAlpha & VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR) != 0; const VkSwapchainCreateInfoKHR ci = { .sType = VK_STRUCTURE_TYPE_SWAPCHAIN_CREATE_INFO_KHR, .surface = ctx.vkSurface_, .minImageCount = chooseSwapImageCount(ctx.deviceSurfaceCaps_), .imageFormat = surfaceFormat_.format, .imageColorSpace = surfaceFormat_.colorSpace, .imageExtent = {.width = width, .height = height}, .imageArrayLayers = 1, .imageUsage = usageFlags, .imageSharingMode = VK_SHARING_MODE_EXCLUSIVE, .queueFamilyIndexCount = 1, .pQueueFamilyIndices = &ctx.deviceQueues_.graphicsQueueFamilyIndex, .preTransform = ctx.deviceSurfaceCaps_.currentTransform, .compositeAlpha = isCompositeAlphaOpaqueSupported ? VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR : VK_COMPOSITE_ALPHA_INHERIT_BIT_KHR, .presentMode = chooseSwapPresentMode(ctx.devicePresentModes_), .clipped = VK_TRUE, .oldSwapchain = VK_NULL_HANDLE, }; vkCreateSwapchainKHR(device_, &ci, nullptr, &swapchain_);
- After the swapchain object has been created, we can retrieve swapchain images.
VkImage swapchainImages[LVK_MAX_SWAPCHAIN_IMAGES]; vkGetSwapchainImagesKHR( device_, swapchain_, &numSwapchainImages_, nullptr); if (numSwapchainImages_ > LVK_MAX_SWAPCHAIN_IMAGES) { numSwapchainImages_ = LVK_MAX_SWAPCHAIN_IMAGES; } vkGetSwapchainImagesKHR( device_, swapchain_, &numSwapchainImages_, swapchainImages);
The retrieved VkImage
objects can be used to create VkImageView
objects for textures and attachments. This topic will be discussed in the recipe Using texture data in Vulkan in the next chapter.
With Vulkan now initialized, we can run our first application, Chapter02/01_Swapchain
, which displays an empty black window. In the next recipe, we’ll explore Vulkan’s built-in debugging capabilities to move closer to actual rendering.