Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Free Learning
Arrow right icon
Arrow up icon
GO TO TOP
The Modern Vulkan Cookbook

You're reading from   The Modern Vulkan Cookbook A practical guide to 3D graphics and advanced real-time rendering techniques in Vulkan

Arrow left icon
Product type Paperback
Published in Apr 2024
Publisher Packt
ISBN-13 9781803239989
Length 334 pages
Edition 1st Edition
Languages
Tools
Concepts
Arrow right icon
Authors (2):
Arrow left icon
Preetish Kakkar Preetish Kakkar
Author Profile Icon Preetish Kakkar
Preetish Kakkar
Mauricio Maurer Mauricio Maurer
Author Profile Icon Mauricio Maurer
Mauricio Maurer
Arrow right icon
View More author details
Toc

Table of Contents (12) Chapters Close

Preface 1. Chapter 1: Vulkan Core Concepts 2. Chapter 2: Working with Modern Vulkan FREE CHAPTER 3. Chapter 3: Implementing GPU-Driven Rendering 4. Chapter 4: Exploring Techniques for Lighting, Shading, and Shadows 5. Chapter 5: Deciphering Order-Independent Transparency 6. Chapter 6: Anti-Aliasing Techniques 7. Chapter 7: Ray Tracing and Hybrid Rendering 8. Chapter 8: Extended Reality with OpenXR 9. Chapter 9: Debugging and Performance Measurement Techniques 10. Index 11. Other Books You May Enjoy

Providing shader data

Providing data from your application that will be used in shaders is one of the most convoluted aspects of Vulkan and requires several steps that need to be accomplished in the right order (and with the right parameters). In this recipe, with many smaller recipes, you will learn how to provide data used in shaders, such as textures, buffers, and samplers.

Getting ready

Resources consumed by shaders are specified using the layout keyword, along with set and binding qualifiers:

layout(set = 0, binding=0) uniform Transforms
{
    mat4 model;
    mat4 view;
    mat4 projection;
} MVP;

Each resource is represented by a binding. A set is a collection of bindings. One binding doesn’t necessarily represent just one resource; it can also represent an array of resources of the same type.

How to do it…

Providing a resource as input to shaders is a multi-step process that involves the following:

  1. Specifying sets and their bindings using descriptor set layouts. This step doesn’t associate real resources with sets/bindings. It just specifies the number and types of bindings in a set.
  2. Building a pipeline layout, which describes which sets will be used in a pipeline.
  3. Creating a descriptor pool that will provide instances of descriptor sets. A descriptor pool contains a list of how many bindings it can provide grouped by binding type (texture, sampler, shader storage buffer (SSBO), uniform buffers).
  4. Allocate descriptor sets from the pool with vkAllocateDescriptorSets.
  5. Bind resources to bindings using vkUpdateDescriptorSets. In this step, we associate a real resource (a buffer, a texture, and so on) with a binding.
  6. Bind descriptor sets and their bindings to a pipeline during rendering using vkCmdBindDescriptorSet. This step makes resources bound to their set/bindings in the previous step available to shaders in the current pipeline.

The next recipes will show you how to perform each one of those steps.

Specifying descriptor sets with descriptor set layouts

Consider the following GLSL code, which specifies several resources:

struct Vertex {
    vec3 pos;
    vec2 uv;
    vec3 normal;
};
layout(set = 0, binding=0) uniform Transforms
{
    mat4 model;
    mat4 view;
    mat4 projection;
} MVP;
layout(set = 1, binding = 0) uniform texture2D textures[];
layout(set = 1, binding = 1) uniform sampler   samplers[];
layout(set = 2, binding = 0) readonly buffer VertexBuffer
{
    Vertex vertices[];
} vertexBuffer;

The code requires three sets (0, 1, and 2), so we need to create three descriptor set layouts. In this recipe, you will learn how to create a descriptor set layout for the preceding code.

Getting ready

Descriptor sets and bindings are created, stored, and managed by the VulkanCore::Pipeline class in the repository. A descriptor set in Vulkan acts as a container that holds resources, such as buffers, textures, and samplers, for use by shaders. Binding refers to the process of associating these descriptor sets with specific shader stages, enabling seamless interaction between shaders and resources during rendering. These descriptor sets serve as gateways through which resources are seamlessly bound to shader stages, orchestrating harmony between data and shader execution. To facilitate this synergy, the class simplifies descriptor set creation and management, complemented by methods for efficient resource binding within the Vulkan rendering pipeline.

How to do it…

A descriptor set layout states its bindings (number and types) with the vkDescriptorSetLayout structure. Each binding is described using an instance of the vkDescriptorSetLayoutBinding structure. The relationship between the Vulkan structures needed to create a descriptor set layout for the preceding code is shown in Figure 2.11:

Figure 2.11 – Illustrating the configuration of descriptor set layouts for GLSL shaders

Figure 2.11 – Illustrating the configuration of descriptor set layouts for GLSL shaders

The following code shows how to specify two bindings for set 1, which are stored in a vector of bindings:

constexpr uint32_t kMaxBindings = 1000;
const VkDescriptorSetLayoutBinding texBinding = {
    .binding = 0,
    .descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE,
    .descriptorCount = kMaxBindings,
    .stageFlags = VK_SHADER_STAGE_VERTEX_BIT,
};
const VkDescriptorSetLayoutBinding samplerBinding = {
    .binding = 1,
    .descriptorType = VK_DESCRIPTOR_TYPE_SAMPLER,
    .descriptorCount = kMaxBindings,
    .stageFlags = VK_SHADER_STAGE_VERTEX_BIT,
};
struct SetDescriptor {
  uint32_t set_;
  std::vector<VkDescriptorSetLayoutBinding> bindings_;
};
std::vector<SetDescriptor> sets(1);
sets[0].set_ = 1;
sets[0].bindings_.push_back(texBinding);
sets[0].bindings_.push_back(samplerBinding);

Since each binding describes a vector, and the VkDescriptorSetLayoutBinding structure requires the number of descriptors, we are using a large number that hopefully will accommodate all elements we need in the array. The vector of bindings is stored in a structure that describes a set with its number and all its bindings. This vector will be used to create a descriptor set layout:

constexpr VkDescriptorBindingFlags flagsToEnable =
    VK_DESCRIPTOR_BINDING_PARTIALLY_BOUND_BIT |
    VK_DESCRIPTOR_BINDING_UPDATE_UNUSED_WHILE_PENDING_BIT;
for (size_t setIndex = 0;
     const auto& set : sets) {
  std::vector<VkDescriptorBindingFlags> bindFlags(
      set.bindings_.size(), flagsToEnable);
  const VkDescriptorSetLayoutBindingFlagsCreateInfo
      extendedInfo{
          .sType =
              VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_BINDING_FLAGS_CREATE_INFO,
          .pNext = nullptr,
          .bindingCount = static_cast<uint32_t>(
              set.bindings_.size()),
          .pBindingFlags = bindFlags.data(),
      };
  const VkDescriptorSetLayoutCreateInfo dslci = {
      .sType =
          VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
      .pNext = &extendedInfo,
      .flags =
          VK_DESCRIPTOR_SET_LAYOUT_CREATE_UPDATE_AFTER_BIND_POOL_BIT_EXT,
      .bindingCount =
          static_cast<uint32_t>(set.bindings_.size()),
      .pBindings = set.bindings_.data(),
  };
  VkDescriptorSetLayout descSetLayout{VK_NULL_HANDLE};
  VK_CHECK(vkCreateDescriptorSetLayout(
      context_->device(), &dslci, nullptr,
      &descSetLayout));
}

Each set requires its own descriptor set layout, and the preceding process needs to be repeated for each one. The descriptor set layout needs to be stored so that it can be referred to in the future.

Passing data to shaders using push constants

Push constants are another way to pass data to shaders. Although a very performant and easy way to do so, push constants are very limited in size, 128 bytes being the only guaranteed amount by the Vulkan specification.

This recipe will show you how to pass a small amount of data from your application to shaders, using push constants for a simple shader.

Getting ready

Push constants are stored and managed by the VulkanCore::Pipeline class.

How to do it…

Push constants are recorded directly onto the command buffer and aren’t prone to the same synchronization issues that exist with other resources. They are declared in the shader as follows, with one maximum block per shader:

layout (push_constant) uniform Transforms {
    mat4 model;
} PushConstants;

The pushed data must be split into the shader stages. Parts of it can be assigned to different shader stages or assigned to one single stage. The important part is that the data cannot be greater than the total amount available for push constants. The limit is provided in VkPhysicalDeviceLimits::maxPushConstantsSize.

Before using push constants, we need to specify how many bytes we are using in each shader stage:

const VkPushConstantRange range = {
    .stageFlags = VK_SHADER_STAGE_VERTEX_BIT,
    .offset = 0,
    .size = 64,
};
std::vector<VkPushConstantRange> pushConsts;
pushConsts.push_back(range);

The code states that the first (offset == 0) 64 bytes of the push constant data recorded in the command buffer (the size of a 4x4 matrix of floats) will be used by the vertex shader. This structure will be used in the next recipe to create a pipeline layout object.

Creating a pipeline layout

A pipeline layout is an object in Vulkan that needs to be created and destroyed by the application. The layout is specified using structures that define the layout of bindings and sets. In this recipe, you will learn how to create a pipeline layout.

Getting ready

A VkPipelineLayoutCreateInfo instance is created automatically by the VulkanCore::Pipeline class in the repository based on information provided by the application using a vector of VulkanCore::Pipeline::SetDescriptor structures.

How to do it…

With all descriptor set layouts for all sets and the push constant information in hand, the next step consists of creating a pipeline layout:

std::vector<VkDescriptoSetLayout> descLayouts;
const VkPipelineLayoutCreateInfo pipelineLayoutInfo = {
    .sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO,
    .setLayoutCount = (uint32_t)descLayouts.size(),
    .pSetLayouts = descLayouts.data(),
    .pushConstantRangeCount =
        !pushConsts.empty()
            ? static_cast<uint32_t>(pushConsts.size())
            : 0,
    .pPushConstantRanges = !pushConsts.empty()
                               ? pushConsts.data()
                               : nullptr,
};
VkPipelineLayout pipelineLayout{VK_NULL_HANDLE};
VK_CHECK(vkCreatePipelineLayout(context_->device(),
                                &pipelineLayoutInfo,
                                nullptr,
                                &pipelineLayout));

Once you have the descriptor set layout in hand and know how to use the push constants in your application, creating a pipeline layout is straightforward.

Creating a descriptor pool

A descriptor pool contains a maximum number of descriptors it can provide (be allocated from), grouped by binding type. For instance, if two bindings of the same set require one image each, the descriptor pool would have to provide at least two descriptors. In this recipe, you will learn how to create a descriptor pool.

Getting ready

Descriptor pools are allocated in the VulkanCore::Pipeline:: initDescriptorPool() method.

How to do it…

Creating a descriptor pool is straightforward. All we need is a list of binding types and the maximum number of resources we’ll allocate for each one:

constexpr uint32_t swapchainImages = 3;
std::vector<VkDescriptorPoolSize> poolSizes;
poolSizes.emplace_back(VkDescriptorPoolSize{
    VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE,
    swapchainImages* kMaxBindings});
poolSizes.emplace_back(VkDescriptorPoolSize{
    VK_DESCRIPTOR_TYPE_SAMPLER,
    swapchainImages* kMaxBindings});

Since we duplicate the resources based on the number of swapchain images to avoid data races between the CPU and the GPU, we multiply the number of bindings we requested before (kMaxBindings = 1000) by the number of swapchain images:

const VkDescriptorPoolCreateInfo descriptorPoolInfo = {
    .sType =
        VK_STRUCTURE_TYPE_DESCRIPTOR_POOL_CREATE_INFO,
    .flags =
        VK_DESCRIPTOR_POOL_CREATE_FREE_DESCRIPTOR_SET_BIT |
        VK_DESCRIPTOR_POOL_CREATE_UPDATE_AFTER_BIND_BIT,
    .maxSets = MAX_DESCRIPTOR_SETS,
    .poolSizeCount =
        static_cast<uint32_t>(poolSizes.size()),
    .pPoolSizes = poolSizes.data(),
};
VkDescriptorPool descriptorPool{VK_NULL_HANDLE};
VK_CHECK(vkCreateDescriptorPool(context_->device(),
                                &descriptorPoolInfo,
                                nullptr,
                                &descriptorPool));

Be careful not to create pools that are too large. Achieving a high-performing application means not allocating more resources than you need.

Allocating descriptor sets

Once a descriptor layout and a descriptor pool have been created, before you can use it, you need to allocate a descriptor set, which is an instance of a set with the layout described by the descriptor layout. In this recipe, you will learn how to allocate a descriptor set.

Getting ready

Descriptor set allocations are done in the VulkanCore::Pipeline:: allocateDescriptors() method. Here, developers define the count of descriptor sets required, coupled with binding counts per set. The subsequent bindDescriptorSets() method weaves the descriptors into command buffers, preparing them for shader execution.

How to do it…

Allocating a descriptor set (or a number of them) is easy. You need to fill the VkDescriptorSetAllocateInfo structure and call vkAllocateDescriptorSets:

VkDescriptorSetAllocateInfo allocInfo = {
    .sType =
        VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO,
    .descriptorPool = descriptorPool,
    .descriptorSetCount = 1,
    .pSetLayouts = &descSetLayout,
};
VkDescriptorSet descriptorSet{VK_NULL_HANDLE};
VK_CHECK(vkAllocateDescriptorSets(context_->device(),
                                  &allocInfo,
                                  &descriptorSet));

When using multiple copies of a resource to avoid race conditions, there are two approaches:

  1. Allocate one descriptor set for each resource. In other words, call the preceding code once for each copy of the resource.
  2. Create one descriptor set and update it every time you need to render.

Updating descriptor sets during rendering

Once a descriptor set has been allocated, it is not associated with any resources. This association must happen once (if your descriptor sets are immutable) or every time you need to bind a different resource to a descriptor set. In this recipe, you will learn how to update descriptor sets during rendering and after you have set up the pipeline and its layout.

Getting ready

In the repository, VulkanCore::Pipeline provides methods to update different types of resources, as each binding can only be associated with one type of resource (image, sampler, or buffer): updateSamplersDescriptorSets(), updateTexturesDescriptorSets(), and updateBuffersDescriptorSets().

How to do it…

Associating a resource with a descriptor set is done with the vkUpdateDescriptorSets function. Each call to vkUpdateDescriptorSets can update one or more bindings of one or more sets. Before updating a descriptor set, let’s look at how to update one binding.

You can associate either a texture, a texture array, a sampler, a sampler array, a buffer, or a buffer array with one binding. To associate images or samplers, use the VkDescriptorImageInfo structure. To associate buffers, use the VkDescriptorBufferInfo structure. Once one or more of those structures have been instantiated, use the VkWriteDescriptorSet structure to bind them all with a binding. Bindings that represent an array are updated with a vector of VkDescriptor*Info.

  1. Consider the bindings declared in the shader code presented next:
    layout(set = 1, binding = 0) uniform texture2D textures[];
    layout(set = 1, binding = 1) uniform sampler   samplers[];
    layout(set = 2, binding = 0) readonly buffer VertexBuffer
    {
      Vertex vertices[];
    } vertexBuffer;
  2. To update the textures[] array, we need to create two instances of VkDescriptorImageInfo and record them in the first VkWriteDescriptorSet structure:
    VkImageView imageViews[2];  // Valid Image View objects
    VkDescriptorImageInfo texInfos[] = {
     VkDescriptorImageInfo{
      .imageView = imageViews[0],
      .imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
        },
     VkDescriptorImageInfo{
      .imageView = imageViews[1],
      .imageLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL,
     },
    };
    const VkWriteDescriptorSet texWriteDescSet = {
        .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET,
        .dstSet = 1,
        ee,
        .dstArrayElement = 0,
        .descriptorCount = 2,
        .descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE,
        .pImageInfo = &texInfos,
        .pBufferInfo = nullptr,
    };
  3. The two image views will be bound to set 1 (.dstSet = 1) and binding 0 (.dstBinding = 0) as elements 0 and 1 of the array. If you need to bind more objects to the array, all you need are more instances of VkDescriptorImageInfo. The number of objects bound to the current binding is specified by the descriptorCount member of the structure.

    The process is similar for sampler objects:

    VkSampler sampler[2];  // Valid Sampler object
    VkDescriptorImageInfo samplerInfos[] = {
        VkDescriptorImageInfo{
            .sampler = sampler[0],
        },
        VkDescriptorImageInfo{
            .sampler = sampler[1],
        },
    };
    const VkWriteDescriptorSet samplerWriteDescSet = {
        .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET,
        .dstSet = 1,
        .dstBinding = 1,
        .dstArrayElement = 0,
        .descriptorCount = 2,
        .descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE,
        .pImageInfo = &samplerInfos,
        .pBufferInfo = nullptr,
    };

    This time, we are binding the sampler objects to set 1, binding 1. Buffers are bound using the VkDescriptorBufferInfo structure:

    VkBuffer buffer;            // Valid Buffer object
    VkDeviceSize bufferLength;  // Range of the buffer
    const VkDescriptorBufferInfo bufferInfo = {
        .buffer = buffer,
        .offset = 0,
        .range = bufferLength,
    };
    const VkWriteDescriptorSet bufferWriteDescSet = {
      .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET,
      .dstSet = 2,
      .dstBinding = 0,
      .dstArrayElement = 0,
      .descriptorCount = 1,
      .descriptorType = VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE,
      .pImageInfo = nullptr,
      .pBufferInfo = &bufferInfo,
    };

    Besides storing the address of the bufferInfo variable to the .pBufferInfo member of VkWriteDescriptorSet, we are binding one buffer (.descriptorCount = 1) to set 2 (.dstSet = 2) and binding 0 (.dstBinding = 0).

  4. The last step consists of storing all VkWriteDescriptorSet instances in a vector and calling vkUpdateDescriptorSets:
    VkDevice device; // Valid Vulkan Device
    std::vector<VkWriteDescriptorSet> writeDescSets;
    writeDescSets.push_back(texWriteDescSet);
    writeDescSets.push_back(samplerWriteDescSet);
    writeDescSets.push_back(bufferWriteDescSet);
    vkUpdateDescriptorSets(device, static_cast<uint32_t>(writeDescSets.size()),
                          writeDescSets.data(), 0, nullptr);

Encapsulating this task is the best way to avoid repetition and bugs introduced by forgetting a step in the update procedure.

Passing resources to shaders (binding descriptor sets)

While rendering, we need to bind the descriptor sets we’d like to use during a draw call.

Getting ready

Binding sets is done with the VulkanCore::Pipeline:: bindDescriptorSets() method.

How to do it…

To bind a descriptor set for rendering, we need to call vkCmdBindDescriptorSets:

VkCommandBuffer commandBuffer;   // Valid Command Buffer
VkPipelineLayout pipelineLayout; // Valid Pipeline layout
uint32_t set;                    // Set number
VkDescriptorSet descSet;         // Valid Descriptor Set
vkCmdBindDescriptorSets(
    commandBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS,
    pipelineLayout, set, 1u, &descSet, 0, nullptr);

Now that we’ve successfully bound a descriptor set for rendering, let’s turn our attention to another crucial aspect of our graphics pipeline: updating push constants.

Updating push constants during rendering

Push constants are updated during rendering by recording their values directly into the command buffer being recorded.

Getting ready

Updating push constants is done with the VulkanCore::Pipeline:: udpatePushConstants() method.

How to do it…

Once rendered, updating push constants is straightforward. All you need to do is call vkCmdPushConstants:

VkCommandBuffer commandBuffer;   // Valid Command Buffer
VkPipelineLayout pipelineLayout; // Valid Pipeline Layout
glm::vec4 mat;                   // Valid matrix
vkCmdPushConstants(commandBuffer, pipelineLayout,
                   VK_SHADER_STAGE_FRAGMENT_BIT, 0,
                   sizeof(glm::vec4), &mat);

This call records the contents of mat into the command buffer, starting at offset 0 and signaling that this data will be used by the vertex shader.

You have been reading a chapter from
The Modern Vulkan Cookbook
Published in: Apr 2024
Publisher: Packt
ISBN-13: 9781803239989
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
Banner background image