How to implement Uniforms in Vulkan?

In this section, we will understand the requirements and execution model for a Uniform implementation in Vulkan. We will also describe the step-by-step instructions to apply 3D transformations on the rendered object using Uniforms. Reuse the sample recipe from the previous chapter and follow the given instructions.

Prerequisites

Let's check out the requirements first.

3D Transformation: This implementation uses the glm mathematics library to achieve 3D transformation using the library's inbuilt transformation functions. GLM is a header-only C++ mathematics library for graphics software based on the GLSL specification. You can download this library from http://glm.g-truc.net. In order to use it, perform the following changes:

  • CMakeLists.txt: Add GLM support by adding the following lines to the project's CMakeLists.txt file:
             # GLM SETUP 

            set (EXTDIR "${CMAKE_SOURCE_DIR}/../../external") 
            set (GLMINCLUDES "${EXTDIR}") 
            get_filename_component(GLMINC_PREFIX "${GLMINCLUDES}" ABSOLUTE) 
            if(NOT EXISTS ${GLMINC_PREFIX}) 
                message(FATAL_ERROR "Necessary glm headers do not exist: " 
                        ${GLMINC_PREFIX}) 
            endif() 
            include_directories( ${GLMINC_PREFIX} ) 
  • Header files: Include the header files for GLM in the Headers.h file:
           /*********** GLM HEADER FILES ***********/ 
            #define GLM_FORCE_RADIANS 
            #include "glm/glm.hpp" 
            #include <glm/gtc/matrix_transform.hpp> 

Applying transformations: The transformations are executed just before the rendering happens. Introduce an update() function in the current design and call it just before the render() function is executed. Add update() to VulkanRenderer and VulkanDrawable and implement main.cpp as follows:

int main(int argc, char **argv) 
 { 
     VulkanApplication* appObj = VulkanApplication::GetInstance(); 
     appObj->initialize(); 
     appObj->prepare(); 
     bool isWindowOpen = true; 
     while (isWindowOpen) { 
     // Add the update function here..

     appObj->update(); 
     isWindowOpen = appObj->render(); 
  } 
      appObj->deInitialize(); 
} 

The descriptor class: The VulkanDrawable class inherits VulkanDescriptor, bringing all the descriptor-related helper functions and user variables together, yet keeping the code logic separate. At the same time, it allows different implementation-drawable classes to extend it as per their requirements:

Prerequisites

Execution model overview

This section will help us understand the execution model for Uniforms using the descriptor sets in Vulkan. The following are the step-by-step instructions:

  1. Initialization: When an application is initialized, it calls the renderer's initialize() function. This function creates all the descriptors associated with each drawable object. The VulkanDrawable class is inherited from VulkanDescriptor, which contains the descriptor sets and the descriptor pool along with the related helper functions. The descriptor sets are allocated from the descriptor pool.
  2. Creating the descriptor layout: Descriptor layouts define the descriptor bindings. This binding indicates the metadata about the descriptor, such as what kind of shader it is associated with, the type of the descriptor, the binding index in the shader, and the total number of descriptors of this type.
  3. The pipeline layout: Create the pipeline layout; the descriptor set is specified in the pipeline object through pipeline layouts.
  4. Creating a uniform buffer for the transformation: The transformation information is specified in a 4 x 4 transformation matrix. This is created (createUniformBuffer()) in a uniform buffer in the device memory that is used by the vertex shader to read the transformation information and apply it to the geometry vertices.
  5. Creating the descriptor pool: Next, create a descriptor pool from which the descriptor sets will be allocated.
  6. Creating the descriptor set: Allocate the descriptor set from the created descriptor pool (step 5) and associate the uniform buffer data (created in step 4) with it.
  7. Updating the transformation: The transformation is updated in each frame where the uniform buffer GPU memory is mapped and updated with new transformation data contents.

Initialization

Initialization includes vertex and fragment shader implementation, the building of the uniform buffer resource, and the creation of the descriptor set from the descriptor pool. The descriptor set creation process includes building the descriptor and pipeline layout.

Shader implementation

The transformation is applied through a vertex shader using the uniform buffer as an input interface through the uniform block (bufferVals) with the layout binding index 1, as highlighted in bold in the following code.

The transformation is calculated by the product of the model view project matrix of bufferValsmvp (layout binding = 0)—and the input vertices—pos (layout location = 0):

// Vertex shader
#version 450
layout (std140, binding = 0) uniform bufferVals {
mat4 mvp;
} myBufferVals;

layout (location = 0) in vec4 pos;
layout (location = 1) in vec4 inColor;
layout (location = 0) out vec4 outColor;
void main() {
 outColor = inColor;
 gl_Position = myBufferVals.mvp * pos;
 gl_Position.z = (gl_Position.z + gl_Position.w) / 2.0;
}

There is no change required in the fragment shader. The input color received at location 0 (color) is used as the current fragment color specified by the output, location 0 (outColor):

// Fragment shader
#version 450
layout (location = 0) in vec4 color;
layout (location = 0) out vec4 outColor;
void main() {
 outColor = color;
}

Creating descriptors

When the renderer is initialized (using the initialize() function), the descriptors are created in the helper function called createDescriptors(). This function first creates the descriptor layout for each drawable object by calling the createDescriptorSetLayout() function of VulkanDrawable. Next, the descriptor object is created inside the createDescriptor() function of VulkanDrawable. In this example, we are not programming textures; therefore, we send the parameter value as Boolean false:

// Create the descriptor sets 
void VulkanRenderer::createDescriptors() 
{ 
   for each (VulkanDrawable* drawableObj in drawableList) 
   { 
         // It is up to an application how it manages the
 
        // creation of descriptor. Descriptors can be cached
 
        // and reuse for all similar objects. 
         drawableObj->createDescriptorSetLayout(false); 
 
         // Create the descriptor set 
         drawableObj->createDescriptor(false);      
   } 
} 
 
void VulkanRenderer::initialize() 
{ 
   . . . . 
   // Create the vertex and fragment shader 
   createShaders(); 
 
   // Create descriptor set layout

   createDescriptors(); 
 
   // Manage the pipeline state objects 
   createPipelineStateManagement(); 
   . . . . 
} 

The createDescriptorSetLayout() function must be executed before you create the graphics pipeline layout. This ensures the descriptor layout is properly utilized while the pipeline layout is being created in the VulkanDrawable::createPipelineLayout() function. For more information on createPipelineLayout(), refer to the Implementing the pipeline layout creation subsection of the Pipeline layouts section in this chapter.

Descriptor set creation comprises of three steps—first, creating the uniform buffer; second, creating the descriptor pool; and finally, allocating the descriptor set and updating the descriptor set with the uniform buffer resource:

void VulkanDescriptor::createDescriptor(bool useTexture) 
{ 
   // Create the uniform buffer resource  
   createDescriptorResources(); 
   
 
   // Create the descriptor pool and

   // use it for descriptor set allocation 
   createDescriptorPool(useTexture); 
 
   // Create descriptor set with uniform buffer data in it 
   createDescriptorSet(useTexture); 
} 

For more information on the creation of the uniform resource, refer to the Creating the descriptor set resources section in this chapter. In addition, you can refer to the Creating the descriptor pool and Creating the descriptor sets sections for a detailed understanding of descriptor pools and descriptor sets' creation.

Rendering

The created descriptor set needs to be specified in the drawing object. This is done when the command buffer of the drawing object is recorded (VulkanDrawable::recordCommandBuffer()).

The descriptor set is bound with the recorded command buffer inside recordCommandBuffer() using the vkCmdBindDescriptorSets() API. This API is called after the pipeline object is bound (vkCmdBindPipeline()) with the current command buffer and before you bind the vertex buffer (vkCmdBindVertexBuffers()) API:

void VulkanDrawable::recordCommandBuffer(int currentBuffer, 
                                   VkCommandBuffer* cmdDraw) 
{ 
    // Bound the command buffer with the graphics pipeline 
    vkCmdBindPipeline(*cmdDraw, VK_PIPELINE_BIND_POINT_GRAPHICS,  
                                 *pipeline); 
    // Bind the descriptor set into the command buffer
       
    vkCmdBindDescriptorSets(*cmdDraw,VK_PIPELINE_BIND_POINT_GRAPHICS,

    pipelineLayout, 0, 1, descriptorSet.data(), 0, NULL); 
 
    const VkDeviceSize offsets[1] = { 0 }; 
    vkCmdBindVertexBuffers(*cmdDraw, 0, 1,  
               &VertexBuffer.buf, offsets); 
 
   . . . . 
} 

For more information on the vkCmdBindDescriptorSets() API specification, refer to the following subsection, Binding the descriptor set.

Binding the descriptor set

One or more created descriptor sets can be specified in the command buffer using vkCmdBindDescriptorSets():

void vkCmdBindDescriptorSets( 
    VkCommandBuffer          commandBuffer, 
    VkPipelineBindPoint      pipelineBindPoint, 
    VkPipelineLayout         layout, 
    uint32_t                 firstSet, 
    uint32_t                 descriptorSetCount, 
    const VkDescriptorSet*   pDescriptorSets, 
    uint32_t                 dynamicOffsetCount, 
    const uint32_t*          pDynamicOffsets); 

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

Parameters

Description

commandBuffer

This is the command buffer (VkCommandBuffer) to which the descriptor sets will be bound.

pipelineBindPoint

This field is a binding point to the pipeline of the type VkPipelineBindPoint, which indicates whether the descriptor will be used by the graphics pipeline or the compute pipeline. The respective binding points for the graphics and compute pipeline do not interfere with each other's work.

Layouts

This refers to the VkPipelineLayout object used to program the bindings.

firstSet

This indicates the index of the first descriptor set to be bound.

descriptorSetCount

This refers to the number of elements in the pDescriptorSets arrays.

pDescriptorSets

This is an array of handles for the VkDescriptorSet objects describing the descriptor sets to write to.

dynamicOffsetCount

This refers to the number of dynamic offsets in the pDynamicOffsets array.

pDynamicOffsets

This is a pointer to an array of uint32_t values specifying the dynamic offsets.

Update

Once the command buffer (bounded with the descriptor set) is submitted to the queue, it executes and renders the drawing object with the transformation specified in the uniform buffer. In order to update and render a continuous update transformation, the update() function can be used.

Note

Updating the descriptor set could be a performance-critical path; therefore, it is advisable to partition multiple descriptors based upon the frequency with which they are updated. It can be divided into the scene, model, and drawing levels, where the update frequency is low, medium, and high, respectively.

Updating the transformation

The transformation is updated in each frame inside the update() function of the drawable class (VulkanDrawable), which acquires the memory location of the uniform buffer and updates the transformation matrix with the new information. The uniform buffer memory location is not available directly because it is a resident of the GPU memory; therefore, the GPU memory is allocated by means of memory mapping, where a portion of the GPU memory is mapped to the CPU memory. Once the memory is updated with it, it is remapped to the GPU memory. The following code snippet implements the update() function:

void VulkanDrawable::update() 
{ 
   VulkanDevice* deviceObj = rendererObj->getDevice(); 
   uint8_t *pData; 
   glm::mat4 Projection = glm::perspective(glm::radians(45.0f), 1.0f,  
                                 0.1f, 100.0f); 
   glm::mat4 View = glm::lookAt( 
   glm::vec3(0, 0, 5),           // Camera is in World Space 
   glm::vec3(0, 0, 0),           // and looks at the origin 
   glm::vec3(0, 1, 0));          // Head is up 
    
   glm::mat4 Model = glm::mat4(1.0f); 
   static float rot = 0; 
   rot += .003; 
   Model = glm::rotate(Model, rot, glm::vec3(0.0, 1.0, 0.0)) 
         * glm::rotate(Model, rot, glm::vec3(1.0, 1.0, 1.0)); 
 
   // Compute the ModelViewProjection transformation matrix 
   glm::mat4 MVP = Projection * View * Model; 
 
   // Map the GPU memory on to local host 
   VkResult result = vkMapMemory(deviceObj->device, UniformData. 
         memory, 0, UniformData.memRqrmnt.size, 0,  
         (void **)&pData); 
   assert(result == VK_SUCCESS); 
    
   // The device memory we have kept mapped it,

   // invalidate the range of mapped buffer in order

   // to make it visible to the host. 
   VkResult res = vkInvalidateMappedMemoryRanges(deviceObj->device, 
                1, &UniformData.mappedRange[0]); 
   assert(res == VK_SUCCESS); 
 
   // Copy computed data in the mapped buffer 
   memcpy(pData, &MVP, sizeof(MVP)); 
 
   // Flush the range of mapped buffer in order to
 
  // make it visible to the device. If the memory

   // is coherent (memory property must be

   // VK_MEMORY_PROPERTY_HOST_COHERENT_BIT) then the driver

   // may take care of this, otherwise for non-coherent
 
  // mapped memory vkFlushMappedMemoryRanges() needs
 
  // to be called explicitly to flush out the pending
 
  // writes on the host side 
   res = vkFlushMappedMemoryRanges(deviceObj->device,  
               1, &UniformData.mappedRange[0]); 
   assert(res == VK_SUCCESS); 
} 

In the preceding implementation, once the uniform buffer is mapped, we do not unmap it until the application stops using the uniform buffer. In order to make the range of uniform buffer visible to the host, we invalidate the mapped range using vkInvalidateMappedMemoryRanges(). After the new data is updated in the mapped buffer, the host flushes out any pending writes using vkFlushMappedMemoryRanges() and makes the mapped memory visible to the device.

Finally, don't forget to unmap the mapped device memory when it is no longer needed using the vkUnmapMemory() API. In this present example, we unmap it before destroying the uniform buffer objects:

void VulkanDrawable::destroyUniformBuffer() 
{ 
    vkUnmapMemory(deviceObj->device, UniformData.memory); 
    vkDestroyBuffer(rendererObj->getDevice()->device,  
         UniformData.buffer, NULL); 
    vkFreeMemory(rendererObj->getDevice()->device,  
         UniformData.memory, NULL); 
} 

The following is the output showing the revolving cube:

Updating the transformation

..................Content has been hidden....................

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