Chapter 10. Descriptors and Push Constant

In the previous chapter, we rendered our first drawing object on the display output. In this chapter, we will take the previous implementation ahead and implement some 3D transformations on the rendered geometry with the help of Uniforms. Uniforms are read-only blocks of data accessible in the shader, and their value is constant for an entire draw call.

Uniforms are managed by descriptors and descriptor pools. A descriptor helps connect the resources with the shaders. But it may be expected to change frequently; therefore, the allocation is performed through a preallocated descriptor buffer called the descriptor pool.

In this chapter, we will also implement a push constant. A push constant allows you to update the constant data in the shader using an optimized high-speed path.

We will cover the following topics:

  • Understanding the concept of descriptors
  • How to implement Uniforms in Vulkan
  • Push constant updates

Understanding the concept of descriptors

A descriptor consists of descriptor set objects. These objects contain storage for a set of descriptors. A descriptor set connects a given resource—such as a uniform buffer, sampled image, stored image, and so on—to the shader helping it read and interpret the incoming resource data through the layout bindings defined using the descriptor set layout. For example, resources such as image textures, sampler and buffers are bound to the shader using descriptors.

Descriptors are opaque objects and define a protocol to communicate with the shaders; behind the curtain, it provides a silent mechanism to associate the resource memory with the shaders with the help of location binding.

VulkanDescriptor - a user-defined descriptor class

In this chapter, we will introduce a new user class called VulkanDescriptor and keep our descriptor-related member variable and function here. This will be helpful in keeping the descriptor code separate from the rest of the implementation, providing a much cleaner and easier way to understand the descriptor functionality.

The following is the header declaration of the VulkanDescriptor class in VulkanDescriptor.h /.cpp. As we proceed through the various sections, we will discuss the purpose and implementation of declared functions and variables in this class in detail. Refer to the inline comments for a quick grasp:

// A user define descriptor class implementing Vulkan descriptors  
class VulkanDescriptor 
{ 
public: 
   VulkanDescriptor();     // Constructor 
   ~VulkanDescriptor();    // Destructor
   // Creates descriptor pool and allocate descriptor set from it 
   void createDescriptor(bool useTexture); 
   // Deletes the created descriptor set object 
   void destroyDescriptor(); 
 
   // Defines the descriptor sets layout binding and
   
// create descriptor layout 
   virtual void createDescriptorLayout(bool useTexture) = 0; 
   // Destroy the valid descriptor layout object 
   void destroyDescriptorLayout(); 
 
   // Creates the descriptor pool that is used to
   
// allocate descriptor sets 
   virtual void createDescriptorPool(bool useTexture) = 0; 
 
   // Deletes the descriptor pool 
   void destroyDescriptorPool(); 
 
   // Create the descriptor set from the descriptor pool allocated
 
  // memory and update the descriptor set information into it. 
   virtual void createDescriptorSet(bool useTexture) = 0; 
   void destroyDescriptorSet(); 
 
   // Creates the pipeline layout to inject into the pipeline 
   virtual void createPipelineLayout() = 0; 
   // Destroys the create pipelineLayout 
   void destroyPipelineLayouts(); 
    
   public: 
   // Pipeline layout object 
   VkPipelineLayout pipelineLayout; 
 
   // List of all the VkDescriptorSetLayouts  
   std::vector<VkDescriptorSetLayout> descLayout; 
    
   // Decriptor pool object that will be used
   
// for allocating VkDescriptorSet object 
   VkDescriptorPool descriptorPool; 
    
   // List of all created VkDescriptorSet 
   std::vector<VkDescriptorSet> descriptorSet; 
    
   // Logical device used for creating the
   
// descriptor pool and descriptor sets 
   VulkanDevice* deviceObj; 
}; 

Descriptor set layout

A descriptor set layout is a collection of zero or more descriptor bindings. It provides an interface to read the resource in the shader at the specified location. Each descriptor binding has a special type that indicates the kind of resource it is handling, the number of descriptors in that binding, the sampler descriptor arrays, and the respective shader stages to which it is associated with. This metadata information is specified in VkDescriptorSetLayoutBinding.  Following is the image showing descriptor set layout which contains various resources layout binding in it where each resource is specified with a binding number uniquely identified in that descriptor layout:

Descriptor set layout

A descriptor set layout is created using the vkCreateDescriptorSetLayout() API. This API accepts the VkDescriptorSetLayoutCreateInfo control structure into which the preceding metadata information for zero or more descriptor sets is specified using the VkDescriptorSetLayoutBinding structure. The following is the syntax of this structure:

VkResult vkCreateDescriptorSetLayout( 
    VkDevice                                    device, 
    const VkDescriptorSetLayoutCreateInfo*      pCreateInfo, 
    const VkAllocationCallbacks*                pAllocator, 
    VkDescriptorSetLayout*                      pSetLayout); 

Here are the parameters defined in the vkCreateDescriptorSetLayout() API:

Parameters

Description

device

This field specifies the logical device (VkDevice) that is responsible for creating the descriptor set layout.

pCreateInfo

This field specifies the descriptor set layout metadata using the pointer to an object of the VkDescriptorSetLayoutCreateInfo structure.

pAllocator

This controls host memory deallocation. Refer to the Host memory section in Chapter 5, Command Buffer and Memory Management in Vulkan.

pSetLayout

The created descriptor set layout objects are returned in the form of VkDescriptorSetLayout handles.

Let's understand the VkDescriptorSetLayoutCreateInfo structure, which is given here:

typedef struct VkDescriptorSetLayoutCreateInfo { 
    VkStructureType                        sType; 
    const void*                            pNext; 
    VkDescriptorSetLayoutCreateFlags       flags; 
    uint32_t                               bindingCount; 
    const VkDescriptorSetLayoutBinding*    pBindings; 
} VkDescriptorSetLayoutCreateInfo; 

The various fields of the VkDescriptorSetLayoutCreateInfo structure are defined in this table:

Parameters

Description

sType

This is the type information of this control structure. It must be specified as VK_STRUCTURE_TYPE_DESCRIPTOR-_SET_LAYOUT_CREATE_INFO.

pNext

This could be a valid pointer to an extension-specific structure or NULL.

flags

This field is of the VkDescriptorSetLayoutCreateFlags type and is presently not in use; it is reserved for future use.

bindingCount

This refers to the number of entries in the pBindings array.

pBindings

This is a pointer to the structure array of VkDescriptorSetLayoutBinding.

The following is the syntax of the VkDescriptorSetLayoutBinding structure:

typedef struct VkDescriptorSetLayoutBinding { 
    uint32_t              binding; 
    VkDescriptorType      descriptorType; 
    uint32_t              descriptorCount; 
    VkShaderStageFlags    stageFlags; 
    const VkSampler*      pImmutableSamplers; 
} VkDescriptorSetLayoutBinding; 

The various fields of the VkDescriptorSetLayoutBinding structure are defined in the following table:

Parameters

Description

binding

This is the binding index that indicates the entry of this resource type, and this index must be equal to the binding number or index used in the corresponding shader stage.

descriptorType

This indicates the type of the descriptor being used for binding. The type is expressed using the VkDescriptorType enum.

descriptorCount

This indicates the number of descriptors in the shader as an array, and it refers to the shader that is contained in the binding.

stageFlags

This field specifies which shader stages can access the value for both the graphics and compute state. This shader stage is indicated by the bit field of VkShaderStageFlagBits. If the value is VK_SHADER_STAGE_ALL, then all the defined shader stages can access the resource via the specified binding.

pImmutableSamplers

This is a pointer to an array of sampler handles represented by the corresponding binding that will be consumed by the descriptor set layout.

This field is used for initializing a set of immutable samplers if the descriptorType specified is either VK_DESCRIPTOR_TYPE_SAMPLER or VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER. If descriptorType is not one of these descriptor types, then this field (pImmutableSamplers) is ignored. Once immutable samplers are bounded, they cannot be bounded into the set layout again. The sampler slots are dynamic when this field is NULL, and the sampler handles must be bound to the descriptor sets using this layout.

The following is the complete set of the VkDescriptorType enum signifying the various descriptor types. The enumeration name of each type is self-explanatory; each of them shows the type of resource it is associated with:

typedef enum VkDescriptorType { 
            VK_DESCRIPTOR_TYPE_SAMPLER                   = 0, 
            VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER    = 1, 
            VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE             = 2, 
            VK_DESCRIPTOR_TYPE_STORAGE_IMAGE             = 3, 
            VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER      = 4, 
            VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER      = 5, 
            VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER            = 6, 
            VK_DESCRIPTOR_TYPE_STORAGE_BUFFER            = 7, 
            VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC    = 8, 
            VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC    = 9, 
            VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT          = 10, 
} VkDescriptorType; 

Let's go ahead and implement the descriptor set in the next subsection.

Implementing the descriptor set layout

The descriptor layout is implemented in the createDescriptorLayout() function of the VulkanDrawable class. This function is a pure virtual function that is declared in the VulkanDescriptor class. The VulkanDrawable class inherits the VulkanDescriptor class. The following is the implementation of this:

void VulkanDrawable::createDescriptorLayout(bool useTexture) 
{ 
    // Define the layout binding information for the
 
   // descriptor set(before creating it), specify binding point,
 
   // shader type(like vertex shader below), count etc. 
    VkDescriptorSetLayoutBinding layoutBindings[2]; 
    layoutBindings[0].binding     = 0; // DESCRIPTOR_SET_BINDING_INDEX 
    layoutBindings[0].descriptorType =  
                           VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; 
    layoutBindings[0].descriptorCount = 1; 
    layoutBindings[0].stageFlags = VK_SHADER_STAGE_VERTEX_BIT; 
    layoutBindings[0].pImmutableSamplers = NULL; 
 
    // If texture is being used then there exists a
 
   // second binding in the fragment shader 
    if (useTexture) 
    { 
         layoutBindings[1].binding      = 1; 
         layoutBindings[1].descriptorType = 
         VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER; 
         layoutBindings[1].descriptorCount = 1; 
         layoutBindings[1].stageFlags =  
           VK_SHADER_STAGE_FRAGMENT_BIT; 
         layoutBindings[1].pImmutableSamplers      = NULL; 
    } 
 
    // Specify the layout bind into the VkDescriptorSetLayout-

    // CreateInfo and use it to create a descriptor set layout 
    VkDescriptorSetLayoutCreateInfo descriptorLayout = {}; 
    descriptorLayout.sType =  
         VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO; 
    descriptorLayout.pNext = NULL; 
    descriptorLayout.bindingCount = useTexture ? 2 : 1; 
    descriptorLayout.pBindings = layoutBindings; 
 
    VkResult  result; 
    // Allocate required number of descriptor layout objects and
  
  // create them using vkCreateDescriptorSetLayout() 
    descLayout.resize(numberOfDescriptorSet); 
    result = vkCreateDescriptorSetLayout(deviceObj->device, 
    &descriptorLayout, NULL, descLayout.data()); 
    assert(result == VK_SUCCESS); 
} 

Before you create the descriptor set object, the layout bindings need to be defined. There are two VkDescriptorSetLayoutBinding objects (an array) created in the preceding implementation.

The first layout binding, layoutBindings[0], is used to bind the uniform block with the resource index specified in the shader. In the present case, the index of our uniform block in the vertex shader is 0, which is the same value that is specified in the layoutBindings[0].binding field. The other fields of the object indicate that the binding point is attached to the vertex shader stage (stageFlags), and the number of descriptors (descriptorCount) are attached as an array in the shader that is contained within the binding.

The second array object, layoutBindings[1], indicates the layout binding for texture support in our geometry; however, this sample example only implements the uniform block to demonstrate a 3D transformation. In order to use the current implementation for texture support, the useTexture flag parameter of the createDescriptorLayout() function must be set to the boolean true. In the present example, although we are using two descriptor sets, only one is used, that is, useTexture is false. In the upcoming chapter, we will implement texture support.

Destroying the descriptor set layout

The descriptor layout can be destroyed using the vkDestroyDescriptorSetLayout() API. Here's the syntax for this:

void vkDestroyDescriptorSetLayout( 
    VkDevice                      device, 
    VkDescriptorSetLayout         descriptorSetLayout, 
    const VkAllocationCallbacks*  pAllocator); 

The vkDestroyDescriptorSetLayout() API takes the following parameters:

Parameters

Description

device

This is a logical device that destroys the descriptor set layout.

descriptorSetLayout

This is the descriptor set layout object to be destroyed.

pAllocator

This controls host memory deallocation. Refer to the Host memory section in Chapter 5, Command Buffer and Memory Management in Vulkan.

Understanding pipeline layouts

Pipeline layouts allow a pipeline (graphics or compute) to access the descriptor sets. A pipeline layout object is comprised of descriptor set layouts and push constant ranges (refer to the Push constant updates section in this chapter), and it represents the complete set of resources that can be accessed by the underlying pipeline.

The pipeline layout object information needs to be provided in the VkGraphicsPipelineCreateInfo structure before the pipeline object is created using the vkCreateGraphicsPipelines() API. This information is set in the VkGraphicsPipelineCreateInfo::layout field. This is a compulsory field. If the application does not use descriptor sets, then you must create an empty descriptor layout and specify it in the pipeline layout to suffice the pipeline object (VkPipeline) creation process. For more information on the pipeline creation process, refer to the Creating graphics pipeline subsection in Chapter 8, Pipelines and Pipeline State Management.

The pipeline layout can contain zero or more descriptor sets in sequence, with each having a specific layout. This layout defines the interfaces between the shader stages and shader resources. The following image shows pipeline layout which comprises of multiple descriptor layouts contains various layout bindings for each resource:

Understanding pipeline layouts

Creating a pipeline layout

A pipeline layout object can be created with the help of the vkCreatePipelineLayout () API. This API accepts VkPipelineLayoutCreateInfo, which contains the descriptor set's state information. This creates one pipeline layout. Let's take a look at the syntax of this API:

VkResult vkCreatePipelineLayout( 
    VkDevice                           device, 
    const VkPipelineLayoutCreateInfo*  pCreateInfo, 
    const VkAllocationCallbacks*       pAllocator, 
    VkPipelineLayout*                  pPipelineLayout); 

The various fields of the vkCreatePipelineLayout structure are defined as follows:

Parameters

Description

device

This indicates the logical device (VkDevice) that is responsible for creating the pipeline layout.

pCreateInfo

This field is the metadata of the pipeline layout object specified using the pointer to the VkPipelineLayoutCreateInfo structure.

pAllocator

This controls host memory deallocation. Refer to the Host memory section in Chapter 5, Command Buffer and Memory Management in Vulkan.

pPipelineLayout

This returns the VkPipelineLayout object handle after the API is successfully executed.

Implementing the pipeline layout creation

The VulkanDrawable class implements the createPipelineLayout() interface from VulkanDrawble, which allows a drawable class to implement its own implementation based on the drawing object resource requirements.

First, VkPipelineLayoutCreateInfo is created (pPipelineLayoutCreateInfo) and specified with the descriptor layout objects (descLayout), which was created using the vkCreateDescriptorSetLayout() API. The descriptor set binding information is accessed with the pipeline layout within the pipeline (VkPipeline).

The created pPipelineLayoutCreateInfo is set into the vkCreatePipelineLayout() API to create the pipelineLayout object. During pipeline creation, this object will be passed to VkGraphicsPipelineCreateInfo::layout in order to create the graphics pipeline object (VkPipeline):

// createPipelineLayout is a virtual function from

// VulkanDescriptor and defined in the VulkanDrawable class.

// virtual void VulkanDescriptor::createPipelineLayout() = 0;

// Creates the pipeline layout to inject into the pipeline 
void VulkanDrawable::createPipelineLayout() 
{ 
   // Create the pipeline layout using descriptor layout. 
   VkPipelineLayoutCreateInfo pPipelineLayoutCreateInfo = {}; 
   pPipelineLayoutCreateInfo.sType = VK_STRUCTURE_TYPE_PIPELINE- 
                                           _LAYOUT_CREATE_INFO; 
   pPipelineLayoutCreateInfo.pNext                 = NULL; 
   pPipelineLayoutCreateInfo.pushConstantRangeCount= 0; 
   pPipelineLayoutCreateInfo.pPushConstantRanges   = NULL; 
   pPipelineLayoutCreateInfo.setLayoutCount        = 
                                 numberOfDescriptorSet; 
   pPipelineLayoutCreateInfo.pSetLayouts  = descLayout.data(); 
 
   VkResult  result; 
   result = vkCreatePipelineLayout(deviceObj->device, 
   &pPipelineLayoutCreateInfo, NULL, &pipelineLayout); 
   assert(result == VK_SUCCESS); 
} 

Destroying the pipeline layout

The created pipeline layout can be destroyed using the vkDestroyPipelineLayout() API in Vulkan. The following is its description:

void vkDestroyPipelineLayout( 
    VkDevice                            device, 
    VkPipelineLayout                    pipelineLayout, 
    const VkAllocationCallbacks*        pAllocator); 

The various fields of the vkDestroyPipelineLayout structure are defined here:

Parameters

Description

device

This is the VkDevice logical object used to destroy the pipeline layout object.

pipelineLayout

This indicates the pipeline layout object (VkPipelineLayout) that needs to be destroyed.

pAllocator

This controls host memory allocation. Refer to the Host memory section in Chapter 5, Command Buffer and Memory Management in Vulkan.

Let's use this API and implement it in the next section.

Implementing the pipeline layout destruction process

The VulkanDescriptor class provides a high-level function to destroy the created pipeline layout: the destroyPipelineLayouts() function. The following is the code implementation:

// Destroy the create pipeline layout object 
void VulkanDescriptor::destroyPipelineLayouts() 
{ 
    vkDestroyPipelineLayout(deviceObj->device, pipelineLayout, NULL); 
} 

Descriptor pool

In Vulkan, descriptor sets cannot be created directly; instead, these are first allocated from a special pool called a descriptor pool. A descriptor pool is responsible for allocating the descriptor set objects. In other words, it is a collection of descriptors from which the descriptor set is allocated.

Note

Descriptor pools are useful in efficient memory allocation of several objects of the descriptor set without requiring global synchronization.

Creating a descriptor pool

Creating a descriptor pool is simple; use the vkCreateDescriptorPool API. The following is the API specification, followed by the implementation of this API in our sample recipe:

VkResult vkCreateDescriptorPool( 
    VkDevice                            device, 
    const VkDescriptorPoolCreateInfo*   pCreateInfo, 
    const VkAllocationCallbacks*        pAllocator, 
    VkDescriptorPool*                   pDescriptorPool); 

The various fields of the vkCreateDescriptorPool structure are defined here:

Parameters

Description

device

This specifies the logical device (VkDevice) that is responsible for creating the descriptor pool.

pCreateInfo

This field is the metadata of the descriptor pool object, which is specified using a pointer to an object of the VkDescriptorPoolCreateInfo structure.

pAllocator

This controls host memory allocation. Refer to the Host memory section in Chapter 5, Command Buffer and Memory Management in Vulkan.

pDescriptorPool

This indicates the created descriptor pool object's handle of the type (VkDescriptorPool), which is a result of the execution of this API.

Implementing the creation of the descriptor pool

The createDescriptorPool () is a pure virtual function exposed by the VulkanDescriptor class. This function is implemented in the VulkanDrawble class, which is responsible for creating the descriptor pool in our Vulkan application sample. Let's understand the working of this function:

  • First, the descriptor pool's size structure is defined indicating the number of pools that need to be created within the descriptor pool for allocating each type of descriptor set. There are two types of descriptor sets that are being used in the following implementation; thus, two VkDescriptorPoolSize objects are created. The first object indicates the descriptor pool that it needs to provide the allocation for the uniform buffer descriptor types. This pool will be used to allocate the descriptor set objects that bind to the uniform block resource types.
  • The second object indicates the descriptor pool for texture samplers. We will implement the texture in the next chapter.
  • These created objects (descriptorTypePool) are then specified in the descriptor pool's CreateInfo structure (descriptorPoolCreateInfo) to indicate the types of descriptor sets (with other state information as well) that are going to be supported by the created descriptor pool. Finally, the descriptorTypePool object is used by the vkCreateDescriptorPool() API to create the descriptor pool object descriptorPool.

The implementation of the descriptor pool is given here:

// Creates the descriptor pool, this function depends on - 

// createDescriptorSetLayout() 
void VulkanDrawable::createDescriptorPool(bool useTexture) 
{ 
   VkResult  result; 
   // Define the size of descriptor pool based on the

   // type of descriptor set being used. 
   VkDescriptorPoolSize descriptorTypePool[2]; 
 
   // The first descriptor pool object is of type Uniform buffer 
descriptorTypePool[0].type = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; 
   descriptorTypePool[0].descriptorCount     = 1; 
 
   // If texture is supported then define the second object with 

   // descriptor type to be Image sampler 
   if (useTexture){ 
             descriptorTypePool[1].type= VK_DESCRIPTOR_TYPE_- 
                                            COMBINED_IMAGE_SAMPLER; 
         descriptorTypePool[1].descriptorCount     = 1; 
   } 
 
   // Populate the descriptor pool state information

   // in the create info structure. 
   VkDescriptorPoolCreateInfo descriptorPoolCreateInfo = {}; 
   descriptorPoolCreateInfo.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_- 
                                 POOL_CREATE_INFO; 
   descriptorPoolCreateInfo.pNext            = NULL; 
   descriptorPoolCreateInfo.maxSets          = 1; 
   descriptorPoolCreateInfo.poolSizeCount= useTexture ? 2 : 1; 
   descriptorPoolCreateInfo.pPoolSizes = descriptorTypePool; 
 
   // Create the descriptor pool using the descriptor
 
  // pool create info structure 
   result = vkCreateDescriptorPool(deviceObj->device, 
         &descriptorPoolCreateInfo, NULL, &descriptorPool); 
   assert(result == VK_SUCCESS); 
} 

Destroying the descriptor pool

The descriptor pool can be destroyed using the vkDestroyDescriptorPool() API. This API accepts three parameters. The first parameter, device, specifies the logical device (VkDevice) that owns the descriptor pool and will be used to destroy descriptorPool. The second parameter, descriptorPool, is the descriptor pool object that needs to be destroyed using this API. The last parameter, pAllocator, controls host memory allocation. You can refer to the Host memory section in Chapter 5, Command Buffer and Memory Management in Vulkan, for more information:

void vkDestroyDescriptorPool( 
    VkDevice                      device, 
    VkDescriptorPool              descriptorPool, 
    const VkAllocationCallbacks*  pAllocator); 

Implementing the destruction of the descriptor pool

In the present sample application, the desctroyDescriptorPool() function from VulkanDescriptor can be used to destroy the created descriptor pool object:

// Deletes the descriptor pool 
void VulkanDescriptor::destroyDescriptorPool() 
{ 
    vkDestroyDescriptorPool(deviceObj->device, descriptorPool, NULL); 
} 

Creating the descriptor set resources

Before the descriptor sets are created, it's compulsory to create the resources in order to associate or bound them with it. In this section, we will create a uniform buffer resource and later associate it with the descriptor set we will create in the following, Creating the descriptor sets, section.

All the descriptor-related resources are created in the createDescriptorResources() interface of the VulkanDescriptor class. Based on the requirements, this interface can be implemented in the derived class.

In the present example, this interface is implemented in the VulkanDrawable class, which creates a uniform buffer and stores a 4 x 4 transformation into it. For this, we need to create buffer type resources. Remember, there are two types of resources in Vulkan: buffers and images. We created the buffer resource in the Understanding the buffer resource section in Chapter 7, Buffer resource, Render Pass, Framebuffer, and Shaders with SPIR-V. In the same chapter, we created the vertex buffer (see the Creating geometry with buffer resource section). We will reuse our learning from this chapter and implement a uniform buffer to store the uniform block information.

The following code implements createDescriptorResources(), where it calls another function, createUniformBuffer(), which creates the uniform buffer resource:

// Create the Uniform resource inside. Create Descriptor set

// associated resources before creating the descriptor set 
void VulkanDrawable::createDescriptorResources() 
{ 
   createUniformBuffer(); 
} 

The createUniformBuffer() function produces the transformation matrices information using the glm library helper functions. It computes the correct Model, View, and Project matrices as per the user specification and stores the result in the MVP matrix. MVP is stored in the host memory and needs to be transferred to the device memory using the buffer object (VkBuffer). The following are step-by-step instructions to create the buffer resource (VkBuffer) of MVP:

  1. Creating the buffer object: Create a VkBuffer object (UniformData.buffer) using the vkCreateBuffer() API. This API intakes a VkCreateBufferInfo structure object (bufInfo) that specifies the important buffer metadata used to create the buffer object. For example, it indicates the usage type in bufInfo.usage as VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT since MVP is treated as a uniform block resource in the vertex shader. The other important piece of information it needs is the size of the buffer; this will be required to hold the complete MVP buffer information. In the present case, it is equal to the size of a 4 x 4 transformation matrix. At this stage, when the buffer object is created (UniformData.buffer), no physical backing is associated with it. In order to allocate physical memory, proceed to the next steps.
  2. Allocating physical memory for the buffer resource
    • Get the memory requirements: Allocate the appropriate size of the memory required by the buffer resource. Query the essential memory by passing the VkBuffer object to the vkGetBufferMemoryRequirements API. This will return the required memory information in the VkMemoryRequirements type object (memRqrmnt).
    • Determining the memory type: Get the proper memory type from the available options and select the one that matches the user properties.
    • Allocating device memory: Allocate the physical memory (in UniformData::memory of the VkDeviceMemory type) for the buffer resource using the vkAllocateMemory() API.
    • Mapping the device memory: Map the physical device memory to the application's address space using the vkMapMemory() API. Upload the uniform buffer data to this address space. Invalidate the mapped buffer to make it visible to the host using vkInvalidateMappedMemoryRanges(). If the memory property is set with VK_MEMORY_PROPERTY_HOST_COHERENT_BIT, then the driver may take care of this; otherwise, for non-coherent mapped memory, vkInvalidateMappedMemoryRanges() needs to be called explicitly.
    • Binding the allocated memory: Bind the device memory (UniformData::memory) to the buffer object (UniformData::buffer) using the vkBindBufferMemory() API.

The following diagram provides an overview of the described process:

Creating the descriptor set resources

Once the buffer resource is created, it stores the necessary information in the local data structure for housekeeping purposes:

class VulkanDrawable : public VulkanDescriptor 
{ 
. . . .  
   // Local data structure for uniform buffer house keeping 
   struct { 
         // Buffer resource object 
         VkBuffer                      buffer; 
          
         // Buffer resourece object's allocated device memory 
         VkDeviceMemory                memory;            
 
         // Buffer info that need to supplied into

         // write descriptor set (VkWriteDescriptorSet) 
         VkDescriptorBufferInfo  bufferInfo;        
 
         // Store the queried memory requirement
 
        // of the uniform buffer 
         VkMemoryRequirements    memRqrmnt;         
 
         // Metadata of memory mapped objects 
         std::vector<VkMappedMemoryRange> mappedRange; 
         // Host pointer containing the mapped device
 
        // address which is used to write data into. 
         uint8_t*                pData;             
   } UniformData; 
 
. . . .  
}; 
 
void VulkanDrawable::createUniformBuffer() 
{ 
    VkResult  result; 
    bool  pass; 
    Projection = glm::perspective(radians(45.f), 1.f, .1f, 100.f); 
    View = glm::lookAt( 
               glm::vec3(10, 3, 10), // Camera in World Space 
               glm::vec3(0, 0, 0),   // and looks at the origin 
               glm::vec3(0, -1, 0) );// Head is up 
                                  
    Model      = glm::mat4(1.0f); 
    MVP  = Projection * View * Model; 
 
    // Create buffer resource states using VkBufferCreateInfo 
    VkBufferCreateInfo bufInfo = {}; 
    bufInfo.sType    = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; 
    bufInfo.pNext    = NULL; 
    bufInfo.usage    = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT; 
    bufInfo.size     = sizeof(MVP); 
    bufInfo.queueFamilyIndexCount = 0; 
    bufInfo.pQueueFamilyIndices    = NULL; 
    bufInfo.sharingMode    = VK_SHARING_MODE_EXCLUSIVE; 
    bufInfo.flags    = 0; 
 
    // Use create buffer info and create the buffer objects 
    result = vkCreateBuffer(deviceObj->device, &bufInfo, NULL,  
               &UniformData.buffer); 
    assert(result == VK_SUCCESS); 
 
    // Get the buffer memory requirements 
    VkMemoryRequirements memRqrmnt; 
    vkGetBufferMemoryRequirements(deviceObj->device,  
               UniformData.buffer, &memRqrmnt); 
 
    VkMemoryAllocateInfo memAllocInfo = {}; 
    memAllocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; 
    memAllocInfo.pNext                       = NULL; 
    memAllocInfo.memoryTypeIndex       = 0; 
    memAllocInfo.allocationSize = memRqrmnt.size; 
 
    // Determine the type of memory required
 
   // with the help of memory properties 
    pass = deviceObj->memoryTypeFromProperties 
         (memRqrmnt.memoryTypeBits,  
         VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT,  
         &memAllocInfo.memoryTypeIndex); 
    assert(pass); 
 
    // Allocate the memory for buffer objects 
    result = vkAllocateMemory(deviceObj->device, &memAllocInfo, 
          NULL, &(UniformData.memory)); 
    assert(result == VK_SUCCESS); 
 
    // Map the GPU memory on to local host 
    result = vkMapMemory(deviceObj->device, UniformData.memory, 
    0, memRqrmnt.size, 0, (void **)&UniformData.pData); 
    assert(result == VK_SUCCESS); 
 
    // Copy computed data in the mapped buffer 
    memcpy(UniformData.pData, &MVP, sizeof(MVP)); 
 
    // We have only one Uniform buffer object to update 
    UniformData.mappedRange.resize(1); 
 
    // Populate the VkMappedMemoryRange data structure 
    UniformData.mappedRange[0].sType   =  
               VK_STRUCTURE_TYPE_MAPPED_MEMORY_RANGE; 
    UniformData.mappedRange[0].memory  = UniformData.memory; 
    UniformData.mappedRange[0].offset  = 0; 
    UniformData.mappedRange[0].size    = sizeof(MVP); 
 
    // Invalidate the range of mapped buffer in order
 
   // to make it visible to the host. 
    vkInvalidateMappedMemoryRanges(deviceObj->device, 1,  
               &UniformData.mappedRange[0]); 
 
    // Bind the buffer device memory  
    result = vkBindBufferMemory(deviceObj->device,  
         UniformData.buffer, UniformData.memory, 0); 
    assert(result == VK_SUCCESS); 
 
    // Update the local data structure with uniform

    // buffer for house keeping 
    UniformData.bufferInfo.buffer      = UniformData.buffer; 
    UniformData.bufferInfo.offset      = 0; 
    UniformData.bufferInfo.range       = sizeof(MVP); 
    UniformData.memRqrmnt              = memRqrmnt; 
} 

Next, we will create the descriptor set and associate the created uniform buffer with it.

Creating the descriptor sets

The descriptor set creation process comprises two steps:

  1. Descriptor set allocation: This allocates the descriptor set from the descriptor pool.
  2. Resource assignment: Here, the descriptor set is associated with the created resource data.

Allocating the descriptor set object from the descriptor pool

The descriptor set is allocated from the descriptor pool using the vkAllocateDescriptorSets() API. This API intakes three parameters. The first parameter (device) specifies the logical device (of the type VkDevice) that owns the descriptor pool. The second parameter (pAllocateInfo) is a pointer to an object of the VkDescriptorSetAllocateInfo structure describing the various parameters that will be helpful in the allocation process of the descriptor pool. The last parameter (pDescriptorSets) is a pointer to an array of VkDescriptorSet; this will be filled by the API with the handles of each allocated descriptor set:

VkResult vkAllocateDescriptorSets( 
    VkDevice                           device, 
    const VkDescriptorSetAllocateInfo* pAllocateInfo, 
    VkDescriptorSet*                   pDescriptorSets); 

Destroying the allocated descriptor set objects

The allocated descriptor set objects can be freed using the vkFreeDescriptorSets() API. This API accepts four parameters. The first parameter (device) is the logical device that owns the descriptor pool. The second device is the descriptor pool (descriptorPool) that was used to allocate the descriptor sets. The third parameter (descriptorSetCount) indicates the number of elements in the last parameter. The last parameter (pDescriptorSets) is the VkDescriptorSet object's array that needs to be freed:

VkResult vkFreeDescriptorSets( 
    VkDevice                 device, 
    VkDescriptorPool         descriptorPool, 
    uint32_t                 descriptorSetCount, 
    const VkDescriptorSet*   pDescriptorSets); 

In this current sample implementation, the vkFreeDescriptorSets() API is exposed through the destroyDescriptorSet() helper function in the VulkanDescriptor class. The following is the implementation code:

void VulkanDescriptor::destroyDescriptorSet() 
{ 
   vkFreeDescriptorSets(deviceObj->device, descriptorPool,  
         numberOfDescriptorSet, &descriptorSet[0]); 
} 

Associating the resources with the descriptor sets

The descriptor sets can be associated with the resources information by updating them using the vkUpdateDescriptorSets() API. This API uses four parameters. The first parameter, device, is the logical device that will be used to update the descriptor sets. This logical device should be the one that owns the descriptor sets. The second parameter, descriptorWriteCount, specifies the element count in the pDescriptorWrites array (of the VkCopyDescriptorSettype). The third parameter, pDescriptorWrites, is a pointer to an array of VkWriteDescriptorSet in the pDescriptorCopies array. The last parameter, pDescriptorCopies, is a pointer to the array objects (the VkCopyDescriptorSet structures) describing the descriptor sets to copy between:

void vkUpdateDescriptorSets( 
    VkDevice                         device, 
    uint32_t                         descriptorWriteCount, 
    const VkWriteDescriptorSet*      pDescriptorWrites, 
    uint32_t                         descriptorCopyCount, 
    const VkCopyDescriptorSet*       pDescriptorCopies); 

The update is an amalgamation of two operations, namely write and copy:

  • Write: The allocated descriptor set is updated by filling an array of zero or more VkWriteDescriptorSet control structures with the resource information, such as buffer data, count, binding index, and more. The write operation is specified in the vkUpdateDescriptorSets() API. This API intakes the filled VkWriteDescriptorSet data structure.
  • Copy: The copy operation uses the existing descriptor sets and copies their information to the destination descriptor set. The copy operation is specified by the VkWriteDescriptorSet control structure. There could be zero or more write operations possible.

Note

The write operations are executed first, followed by the copy operations. For each operation type (write or copy), zero or more operations are represented in the form of arrays, and within these arrays, the operations are performed in the order that they appear.

The following is the specification of VkWriteDescriptorSet:

typedef struct VkWriteDescriptorSet { 
      VkStructureType                  sType; 
      const void*                      pNext; 
      VkDescriptorSet                  dstSet; 
      uint32_t                         dstBinding; 
      uint32_t                         dstArrayElement; 
      uint32_t                         descriptorCount; 
      VkDescriptorType                 descriptorType; 
      const VkDescriptorImageInfo*     pImageInfo; 
      const VkDescriptorBufferInfo*    pBufferInfo; 
      const VkBufferView*              pTexelBufferView; 
} VkWriteDescriptorSet; 

The various fields of the VkWriteDescriptorSet structure are defined as follows:

Parameters

Description

sType

This is the type information of this control structure. It must be specified as VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET.

pNext

This could be a valid pointer to an extension-specific structure or NULL.

dstSet

This is the destination descriptor set that will be updated.

dstBinding

This specifies the descriptor binding within the set. This should be the same as the binding index specified in the shader for a given shader stage.

dstArrayElement

This field indicates the starting element index in the array of descriptors within a single binding.

descriptorCount

This is the count of the descriptors to be updated in any of these: pImageInfo, pBufferInfo, or pTexelBufferView.

descriptorType

This field indicates the type of each participating descriptor (pImageInfo, pBufferInfo, or pTexelBufferView).

pImageInfo

This is an array of VkDescriptorImageInfo structures that represent the image resource. This field must be VK_NULL_HANDLE if not specified.

pBufferInfo

This is an array of VkDescriptorBufferInfo structures or it can be VK_NULL_HANDLE if not specified.

pTexelBufferView

This is an array containing VkBufferView handles, or it can be VK_NULL_HANDLE if not specified.

Let's take a look at the VkCopyDescriptorSet specification:

typedef struct VkCopyDescriptorSet { 
   VkStructureType         sType; 
   const void*             pNext; 
   VkDescriptorSet         srcSet; 
   uint32_t                srcBinding;  
   uint32_t                srcArrayElement; 
   VkDescriptorSet         dstSet; 
   uint32_t                dstBinding; 
   uint32_t                dstArrayElement; 
   uint32_t                descriptorCount; 
} VkCopyDescriptorSet; 

The various fields of the VkCopyDescriptorSet structure are defined here:

Parameters

Description

sType

This is the type information of this control structure. It must be specified as VK_STRUCTURE_TYPE_COPY_DESCRIPTOR_SET.

pNext

This could be a valid pointer to an extension-specific structure or NULL.

srcSet

This specifies the source descriptor set that will be copied from.

srcBinding

This specifies the binding index within the source descriptor set.

srcArrayElement

This indicates the starting array element within the first updated binding.

dstSet

This specifies the destination descriptor set into which the source descriptor will be copied.

dstBinding

This specifies the binding index within the destination descriptor set.

dstArrayElement

This field indicates the starting index in the array of descriptors within a single binding.

descriptorCount

This refers to the total count of descriptors that will be copied from the source to the destination.

Implementing descriptor set creation

Descriptor sets are created in the VulkanDrawable class, which inherits the createDescriptorSet() interface from the VulkanDescriptor class and implements it.

First, the VkDescriptorSetAllocateInfo control structure (dsAllocInfo) is created and specified within the descriptor pool to allocate descriptorSet from the intended descriptor pool. The second important thing that needs to be specified is the descriptor layout information that we created and stored in the descLayout object. The descriptor layout provides an interface to read the resource in the shader.

The allocated descriptor sets are empty and do not hold any valid information. They are updated using the write or copy descriptor structures (Vk<Write/Copy>DescriptorSet). In this implementation, the write descriptor write[0] is specified with the uniform data buffer (UniformData::bufferInfo) along with other state information. This information includes the destination descriptor set object, descriptorSet[0], into which this uniform buffer needs to be bound and the destination binding index to which it should be attached. The dstBinding must be equal to the index specified in the shader stage. The update operation is performed using vkUpdateDescriptorSets(), specifying the write descriptor into it:

// Creates the descriptor sets using descriptor pool.

// This function depends on the createDescriptorPool()

// and createUniformBuffer(). 
void VulkanDrawable::createDescriptorSet(bool useTexture) 
{ 
    VulkanPipeline* pipelineObj = rendererObj->getPipelineObject(); 
    VkResult result; 
    // Create the descriptor allocation structure and specify

    // the descriptor pool and descriptor layout 
    VkDescriptorSetAllocateInfo dsAllocInfo[1]; 
    dsAllocInfo[0].sType         = 
               VK_STRUCTURE_TYPE_DESCRIPTOR_SET_ALLOCATE_INFO; 
    dsAllocInfo[0].pNext               = NULL; 
    dsAllocInfo[0].descriptorPool      = descriptorPool; 
    dsAllocInfo[0].descriptorSetCount  = 1; 
    dsAllocInfo[0].pSetLayouts         = descLayout.data(); 
 
    // Allocate the number of descriptor set needs to be produced 
    descriptorSet.resize(1); 
 
    // Allocate descriptor sets  
    result = vkAllocateDescriptorSets(deviceObj->device,  
                     dsAllocInfo, descriptorSet.data()); 
    assert(result == VK_SUCCESS); 
 
    // Allocate two write descriptors for - 1. MVP and 2. Texture 
    VkWriteDescriptorSet writes[2]; 
    memset(&writes, 0, sizeof(writes)); 
 
    // Specify the uniform buffer related
 
   // information into first write descriptor 
    writes[0]        = {}; 
    writes[0].sType  = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET; 
    writes[0].pNext  = NULL; 
    writes[0].dstSet = descriptorSet[0]; 
    writes[0].descriptorCount    = 1; 
    writes[0].descriptorType     = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER; 
    writes[0].pBufferInfo        = &UniformData.bufferInfo; 
    writes[0].dstArrayElement    = 0; 
    writes[0].dstBinding         = 0;  
 
    // If texture is used then update the second
 
    // write descriptor structure. We will use this descriptor

    // set in the next chapter where textures are used. 
    if (useTexture) 
    { 
      // In this sample textures are not used 
       writes[1]                  = {}; 
       writes[1].sType            = VK_STRUCTURE_TYPE_WRITE- 
                                    _DESCRIPTOR_SET;
       writes[1].dstSet           = descriptorSet[0]; 
       writes[1].dstBinding       = 1; 
       writes[1].descriptorCount  = 1; 
       writes[1].descriptorType   = VK_DESCRIPTOR_TYPE_- 
                                    COMBINED_IMAGE_SAMPLER; 
       writes[1].pImageInfo       = NULL; 
       writes[1].dstArrayElement  = 0; 
    } 
 
    // Update the uniform buffer into the allocated descriptor set 
    vkUpdateDescriptorSets(deviceObj->device,  
               useTexture ? 2 : 1, writes, 0, NULL); 
} 
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.218.212.102