Push Constants

We can render arbitrary meshes now, but there is not a lot we can do with them. We still don’t know how to send data from the CPU to shaders outside of vertex buffers. While the next chapter will explain the various methods in detail, there is a simple method we can start using immediately: push constants.

Push constants let us send a small amount of data (it has a limited size) to the shader, in a very simple and performant way. Push constants can send data to any shader stage (both vertex and fragment shaders), and are stored in the command buffer itself.

To use push constants, you first need to set their size (in each stage) when you create a VkPipelineLayout. Then, using the command vkCmdPushConstants, the data will be embedded into the command buffer, and will be accessible from the shader. We are going to use them for transformation matrices, to be able to move objects around and do proper 3d rendering. To start, we are going to use it to move the triangle we are rendering.

New PipelineLayout

We are going to need a new PipelineLayout to hold the push constant sizes. For that, we are going to add a new member to vulkanEngine.

class VulkanEngine {
	///other code...
	VkPipelineLayout _meshPipelineLayout;
	//other code ...


We are also going to create a struct to hold it in vk_engine.h

//add the include for glm to get matrices
#include <glm/glm.hpp>

struct MeshPushConstants {
	glm::vec4 data;
	glm::mat4 render_matrix;

For now, we will just store a single glm::vec4 into it (4 floats) and a glm::mat4 (16 floats). We will then use the matrix to transform the triangle. When you create push constant structs, alignment rules apply. You can find the exact rules for them by reading about the GLSL std430 layout. These can be very complicated, so for simplicity, we are going to restrict ourselves to glm::vec4 and glm::mat4, which have simple alignment rules. Push constants have a minimum size of 128 bytes, which is enough for common things like a 4x4 matrix and a few extra parameters.

To make space for the push constants in the pipeline, we create it inside the init_pipelines() function

	//we start from just the default empty pipeline layout info
	VkPipelineLayoutCreateInfo mesh_pipeline_layout_info = vkinit::pipeline_layout_create_info();
	//setup push constants
	VkPushConstantRange push_constant;
	//this push constant range starts at the beginning
	push_constant.offset = 0;
	//this push constant range takes up the size of a MeshPushConstants struct
	push_constant.size = sizeof(MeshPushConstants);
	//this push constant range is accessible only in the vertex shader
	push_constant.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;

	mesh_pipeline_layout_info.pPushConstantRanges = &push_constant;
	mesh_pipeline_layout_info.pushConstantRangeCount = 1;

	VK_CHECK(vkCreatePipelineLayout(_device, &mesh_pipeline_layout_info, nullptr, &_meshPipelineLayout));

	//later ....
	//remember to destroy the pipeline layout
	_mainDeletionQueue.push_function([=]() {
        	//other deletions

		vkDestroyPipelineLayout(_device, _meshPipelineLayout, nullptr);

Push constants are written in ranges. A important reason for that, is that you can have different push constants, at different ranges, in different stages. For example, you can reserve 64 bytes (1 glm::mat4) size on the vertex shader, and then start the pixel shader push constant from offset 64. This way you would have different push constants on different stages. Given that getting the ranges right is tricky, in this tutorial we are going to use a more monolithic approach of using the same push constants on both vertex and fragment shader.

Now that we have the layout, we also need to modify the shaders. Let’s add the push constant to the tri_mesh.vert shader.

#version 450

layout (location = 0) in vec3 vPosition;
layout (location = 1) in vec3 vNormal;
layout (location = 2) in vec3 vColor;

layout (location = 0) out vec3 outColor;

//push constants block
layout( push_constant ) uniform constants
	vec4 data;
	mat4 render_matrix;
} PushConstants;

void main() 
	gl_Position = PushConstants.render_matrix * vec4(vPosition, 1.0f);
	outColor = vColor;

The push_constant block has to match to the struct we have on C++ side 1 to 1, otherwise the GPU will read our data incorrectly.

Now we can modify the way we create the triangle pipeline to hook the new pipeline layout.

in init_pipelines, right before creating the mesh pipeline, we are going to hook the new layout

    pipelineBuilder._pipelineLayout = _meshPipelineLayout;

    _meshPipeline = pipelineBuilder.build_pipeline(_device, _renderPass);

Now our mesh pipeline has the space for the pushconstants, so we can now execute the command to use them.

in draw() function, right before the vkCmdDraw call, we are going to compute and write the push constant.

    //make a model view matrix for rendering the object
    //camera position
    glm::vec3 camPos = { 0.f,0.f,-2.f };

    glm::mat4 view = glm::translate(glm::mat4(1.f), camPos);
    //camera projection
    glm::mat4 projection = glm::perspective(glm::radians(70.f), 1700.f / 900.f, 0.1f, 200.0f);
    projection[1][1] *= -1;
    //model rotation
    glm::mat4 model = glm::rotate(glm::mat4{ 1.0f }, glm::radians(_frameNumber * 0.4f), glm::vec3(0, 1, 0));

    //calculate final mesh matrix
    glm::mat4 mesh_matrix = projection * view * model;

    MeshPushConstants constants;
    constants.render_matrix = mesh_matrix;

    //upload the matrix to the GPU via pushconstants
    vkCmdPushConstants(cmd, _meshPipelineLayout, VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(MeshPushConstants), &constants);

    //we can now draw
    vkCmdDraw(cmd, _triangleMesh._vertices.size(), 1, 0, 0);

You will also need to add

#include <glm/gtx/transform.hpp>

To the vk_engine.cpp file so that you can use the glm transformation matrix functions.

In the push constant call, we need to set the pointer to the data and its size (similar to memcpy), and also VK_PIPELINE_STAGE_VERTEX_BIT, which matches the stage(s) where our data should be accessible. If we have the same push constant on both vertex and fragment shader, we would have both of those flags.

If you now run the program, you will see the triangle spinning in the window, upside down. By modifying camPos and the view matrix in general, you can now create a 3d camera.


Next: Loading OBJ Meshes