Implementing MDI and PVP
MDI and PVP are features of modern graphics APIs that allow for greater flexibility and efficiency in vertex processing.
MDI allows issuing multiple draw calls with a single command, each of which derives its parameters from a buffer stored in the device (hence the indirect term). This is particularly useful because those parameters can be modified in the GPU itself.
With PVP, each shader instance retrieves its vertex data based on its index and instance IDs instead of being initialized with the vertex’s attributes. This allows for flexibility because the vertex attributes and their format are not baked into the pipeline and can be changed solely based on the shader code.
In the first sub-recipe, we will focus on the implementation of MDI, demonstrating how this powerful tool can streamline your graphics operations by allowing multiple draw calls to be issued from a single command, with parameters that can be modified directly in the GPU. In the following sub-recipe, we will guide you through the process of setting up PVP, highlighting how the flexibility of this feature can enhance your shader code by enabling changes to vertex attributes without modifying the pipeline.
Implementing MDI
For using MDI, we store all mesh data belonging to the scene in one big buffer for all the meshes’ vertices and another one for the meshes’ indices, with the data for each mesh stored sequentially, as depicted in Figure 2.12.
The drawing parameters are stored in an extra buffer. They must be stored sequentially, one for each mesh, although they don’t have to be provided in the same order as the meshes:
Figure 2.12 – MDI data layout
We will now learn how to implement MDI using the Vulkan API.
Getting ready
In the repository, we provide a utility function to decompose an EngineCore::Model
object into multiple buffers suitable for an MDI implementation, called EngineCore::convertModel2OneBuffer()
, located in GLBLoader.cpp
.
How to do it…
Let’s begin by looking at the indirect draw parameters’ buffer.
The commands are stored following the same layout as the VkDrawIndexedIndirectCommand
structure:
typedef struct VkDrawIndexedIndirectCommand { uint32_t indexCount; uint32_t instanceCount; uint32_t firstIndex; int32_t vertexOffset; uint32_t firstInstance; } VkDrawIndexedIndirectCommand;
indexCount
specifies how many indices are part of this command and, in our case, is the number of indices for a mesh. One command reflects one mesh, so its instanceCount
value is one. The firstVertex
member is the index of the first index element in the buffer to use for this mesh, while vertexOffset
points to the first vertex element in the buffer to use. An example with the correct offsets is shown in Figure 2.12.
Once the vertex, index, and indirect commands buffers are bound, calling vkCmdDrawIndexedIndirect
consists of providing the buffer with the indirect commands and an offset into the buffer. The rest is done by the device:
VkCommandBuffer commandBuffer; // Valid Command Bufer VkBuffer indirectCmdBuffer; // Valid buffer w/ // indirect commands uint32_t meshCount; // Number of indirect commands in // the buffer uint32_t offset = 0; // Offset into the indirect commands // buffer vkCmdDrawIndexedIndirect( commandBuffer, indirectCmdBuffer, offset, meshCount, sizeof(VkDrawIndexedIndirectDrawCommand));
In this recipe, we learned how to utilize vkCmdDrawIndexedIndirect
, a key function in Vulkan that allows for high-efficiency drawing.
Using PVP
The PVP technique allows vertex data and their attributes to be extracted from buffers with custom code instead of relying on the pipeline to provide them to vertex shaders.
Getting ready
We will use the following structures to perform the extraction of vertex data – the Vertex
structure, which encodes the vertex’s position (pos
), normal
, UV coordinates (uv
), and its material index (material
):
struct Vertex { vec3 pos; vec3 normal; vec2 uv; int material; };
We will also use a buffer object, referred to in the shader as VertexBuffer
:
layout(set = 2, binding = 0) readonly buffer VertexBuffer { Vertex vertices[]; } vertexBuffer;
Next, we will learn how to use the vertexBuffer
object to access vertex data.
How to do it…
The shader code used to access the vertex data looks like this:
void main() { Vertex vertex = vertexBuffer.vertices[gl_VertexIndex]; }
Note that the vertex and its attributes are not declared as inputs to the shader. gl_VertexIndex
is automatically computed and provided to the shader based on the draw call and the parameters recorded in the indirect command retrieved from the indirect command buffer.
Index and vertex buffers
Note that both the index and vertex buffers are still provided and bound to the pipeline before the draw call is issued. The index buffer must have the VK_BUFFER_USAGE_INDEX_BUFFER_BIT
flag enabled for the technique to work.