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.
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
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} )
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:
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:
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.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.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.
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 bufferVals
—mvp
(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;
}
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.
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.
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 |
|
This is the command buffer ( |
|
This field is a binding point to the pipeline of the type |
|
This refers to the |
|
This indicates the index of the first descriptor set to be bound. |
|
This refers to the number of elements in the |
|
This is an array of handles for the |
|
This refers to the number of dynamic offsets in the |
|
This is a pointer to an array of |
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.
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.
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:
3.142.173.238