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:
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.
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; };
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:
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 |
|
This field specifies the logical device ( |
|
This field specifies the descriptor set layout metadata using the pointer to an object of the |
|
This controls host memory deallocation. Refer to the Host memory section in Chapter 5, Command Buffer and Memory Management in Vulkan. |
|
The created descriptor set layout objects are returned in the form of |
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 |
|
This is the type information of this control structure. It must be specified as |
|
This could be a valid pointer to an extension-specific structure or |
|
This field is of the |
|
This refers to the number of entries in the |
|
This is a pointer to the structure array of |
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 |
|
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. |
|
This indicates the type of the descriptor being used for binding. The type is expressed using the |
|
This indicates the number of descriptors in the shader as an array, and it refers to the shader that is contained in the binding. |
|
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 |
|
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
|
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.
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.
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 |
|
This is a logical device that destroys the descriptor set layout. |
|
This is the descriptor set layout object to be destroyed. |
|
This controls host memory deallocation. Refer to the Host memory section in Chapter 5, Command Buffer and Memory Management in Vulkan. |
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:
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 |
|
This indicates the logical device ( |
|
This field is the metadata of the pipeline layout object specified using the pointer to the |
|
This controls host memory deallocation. Refer to the Host memory section in Chapter 5, Command Buffer and Memory Management in Vulkan. |
|
This returns the |
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); }
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 |
|
This is the |
|
This indicates the pipeline layout object ( |
|
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.
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);
}
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.
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 |
|
This specifies the logical device ( |
|
This field is the metadata of the descriptor pool object, which is specified using a pointer to an object of the |
|
This controls host memory allocation. Refer to the Host memory section in Chapter 5, Command Buffer and Memory Management in Vulkan. |
|
This indicates the created descriptor pool object's handle of the type ( |
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:
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.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 vkCreateDescriptorPoo
l
()
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); }
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);
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);
}
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
:
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.VkBuffer
object to the vkGetBufferMemoryRequirements
API. This will return the required memory information in the VkMemoryRequirements
type object (memRqrmnt
).UniformData::memory
of the VkDeviceMemory
type) for the buffer resource using the vkAllocateMemory()
API.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.UniformData::memory
) to the buffer object (UniformData::buffer
) using the vkBindBufferMemory()
API.
The following diagram provides an overview of the described process:
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.
The descriptor set creation process comprises two steps:
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);
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]); }
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 VkCopyDescriptorSet
type). 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:
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.VkWriteDescriptorSet
control structure. There could be zero or more write operations possible.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 |
|
This is the type information of this control structure. It must be specified as |
|
This could be a valid pointer to an extension-specific structure or |
|
This is the destination descriptor set that will be updated. |
|
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. |
|
This field indicates the starting element index in the array of descriptors within a single binding. |
|
This is the count of the descriptors to be updated in any of these: |
|
This field indicates the type of each participating descriptor ( |
|
This is an array of |
|
This is an array of |
|
This is an array containing |
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 |
|
This is the type information of this control structure. It must be specified as |
|
This could be a valid pointer to an extension-specific structure or |
|
This specifies the source descriptor set that will be copied from. |
|
This specifies the binding index within the source descriptor set. |
|
This indicates the starting array element within the first updated binding. |
|
This specifies the destination descriptor set into which the source descriptor will be copied. |
|
This specifies the binding index within the destination descriptor set. |
|
This field indicates the starting index in the array of descriptors within a single binding. |
|
This refers to the total count of descriptors that will be copied from the source to the destination. |
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); }
18.218.212.102