Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Save more on your purchases! discount-offer-chevron-icon
Savings automatically calculated. No voucher code required.
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Newsletter Hub
Free Learning
Arrow right icon
timer SALE ENDS IN
0 Days
:
00 Hours
:
00 Minutes
:
00 Seconds
Arrow up icon
GO TO TOP
Vulkan 3D Graphics Rendering Cookbook

You're reading from   Vulkan 3D Graphics Rendering Cookbook Implement expert-level techniques for high-performance graphics with Vulkan

Arrow left icon
Product type Paperback
Published in Feb 2025
Publisher Packt
ISBN-13 9781803248110
Length 714 pages
Edition 2nd Edition
Languages
Tools
Arrow right icon
Authors (3):
Arrow left icon
Sergey Kosarevsky Sergey Kosarevsky
Author Profile Icon Sergey Kosarevsky
Sergey Kosarevsky
Alexey Medvedev Alexey Medvedev
Author Profile Icon Alexey Medvedev
Alexey Medvedev
Viktor Latypov Viktor Latypov
Author Profile Icon Viktor Latypov
Viktor Latypov
Arrow right icon
View More author details
Toc

Table of Contents (14) Chapters Close

Preface 1. Establishing a Build Environment 2. Getting Started with Vulkan FREE CHAPTER 3. Working with Vulkan Objects 4. Adding User Interaction and Productivity Tools 5. Working with Geometry Data 6. Physically Based Rendering Using the glTF 2.0 Shading Model 7. Advanced PBR Extensions 8. Graphics Rendering Pipeline 9. glTF Animations 10. Image-Based Techniques 11. Advanced Rendering Techniques and Optimizations 12. Other Books You May Enjoy
13. Index

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.

  1. 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;
    
  2. 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());
      }
    
  3. 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.

  1. 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() and isNativeSwapChainBGR() 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;
      }
    
  2. 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.

  1. 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));
    
  2. 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;
    }
    
  3. 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 picking VK_PRESENT_MODE_IMMEDIATE_KHR for the fastest frames-per-second without V-sync, or we can always fall back to VK_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.html
    auto 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;
      };
    
  4. 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;
    };
    
  5. 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 the VkSwapchainCreateInfoKHR 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_);
    
  6. 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.

You have been reading a chapter from
Vulkan 3D Graphics Rendering Cookbook - Second Edition
Published in: Feb 2025
Publisher: Packt
ISBN-13: 9781803248110
Register for a free Packt account to unlock a world of extra content!
A free Packt account unlocks extra newsletters, articles, discounted offers, and much more. Start advancing your knowledge today.
Unlock this book and the full library FREE for 7 days
Get unlimited access to 7000+ expert-authored eBooks and videos courses covering every tech area you can think of
Renews at $19.99/month. Cancel anytime
Visually different images