Using Vulkan Push Constants for Low‑Overhead Per‑Draw Data
Learn how Vulkan push constants let you update small per‑draw data like matrices without costly descriptor set changes or CPU‑GPU stalls.
21 Sept 2025, 08:27 UTC

Problem: frequent uniform updates
When a draw call needs a small piece of data that changes every frame – such as a model‑view‑projection matrix or a material colour – updating a uniform buffer or descriptor set forces the CPU to wait for the GPU to finish using the previous data. This synchronization can become a noticeable overhead, especially in scenes with many objects.
Thesis: push constants give a shader‑visible memory block that can be updated with a single command
Vulkan push constants are a small, fast memory region that lives inside the command buffer. The application writes to it with vkCmdPushConstants and the shader reads it directly via a layout(push_constant) uniform. Because the data is part of the command buffer, no descriptor set binding or buffer barrier is required, eliminating the per‑draw CPU‑GPU sync.
API overview
- Size limit – the maximum is given by
VkPhysicalDeviceLimits::maxPushConstantsSize; most implementations expose 128 bytes. - Contiguity – a push constant range must be a single contiguous block; you cannot have gaps.
- Stage flags – each range declares which shader stages may read it (vertex, fragment, etc.).
Setting up the pipeline layout
Define one or more VkPushConstantRange structures and include them in the VkPipelineLayoutCreateInfo.
VkPushConstantRange pushConstantRange{};
pushConstantRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
pushConstantRange.offset = 0;
pushConstantRange.size = 64; // enough for a 4×4 float matrix
VkPipelineLayoutCreateInfo pipelineLayoutInfo{};
pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
pipelineLayoutInfo.pushConstantRangeCount = 1;
pipelineLayoutInfo.pPushConstantRanges = &pushConstantRange;
// descriptor set layouts go here if needed
VkPipelineLayout pipelineLayout;
vkCreatePipelineLayout(device, &pipelineLayoutInfo, nullptr, &pipelineLayout);
After the layout is created, any pipeline bound with it inherits the push constant range.
Recording commands – updating before each draw
In the command buffer, update the push constants right before the draw that needs them.
// Assume we have a glm::mat4 modelViewProj computed each frame
glm::mat4 mvp = camera.getProjection() * camera.getView() * modelMatrix;
// Update the push constant block (offset 0, size 64)
vkCmdPushConstants(
commandBuffer,
pipelineLayout,
VK_SHADER_STAGE_VERTEX_BIT,
0,
sizeof(mvp),
&mvp[0][0]);
vkCmdDraw(commandBuffer, vertexCount, 1, 0, 0);
Because the data lives in the command buffer, the GPU can read it immediately; no extra barrier is needed.
Worked example – rotating a quad
The following minimal example shows a quad whose model matrix rotates over time. The matrix is passed as a push constant.
Vertex shader (GLSL)
#version 450
layout(location = 0) in vec2 inPos;
layout(push_constant) uniform mvpMatrix {
mat4 mvp;
} mvp;
void main() {
gl_Position = mvp.mvp * vec4(inPos, 0.0, 1.0);
}
C++ update loop
while (!glfwWindowShouldClose(window)) {
float time = glfwGetTime();
glm::mat4 model = glm::rotate(glm::mat4(1.0f), time, glm::vec3(0.0f, 0.0f, 1.0f));
glm::mat4 mvp = proj * view * model;
vkCmdBeginCommandBuffer(cmdBuf, &beginInfo);
vkCmdPushConstants(cmdBuf, pipelineLayout,
VK_SHADER_STAGE_VERTEX_BIT, 0, sizeof(mvp), &mvp[0][0]);
vkCmdDraw(cmdBuf, 4, 1, 0, 0);
vkCmdEndCommandBuffer(cmdBuf);
// submit and present …
}
Running this code should show a smoothly rotating square. Verify with the Vulkan validation layers that no VK_VALIDATION_ERROR_INVALID_OVERFLOW or similar messages appear, confirming the 64‑byte update stays within the limit.
Trade‑off / limitation
- Size – the total push constant budget is small (often 128 bytes). If you need more data per draw, you must fall back to uniform or storage buffers.
- Contiguity – you cannot have two separate ranges with a gap; to change the layout you must recreate the pipeline layout and any pipelines that use it.
- Stage specificity – a range can only be read by the stages declared in its
stageFlags. Mixing vertex and fragment data requires either two ranges (if the implementation allows multiple ranges) or a larger combined range that both stages read.
Actionable closing
- Identify per‑draw data that fits within the push constant limit (matrices, material colours, light parameters).
- Add a
VkPushConstantRangeto your pipeline layout and recordvkCmdPushConstantsbefore each draw. - Run the application with the Vulkan validation layers enabled (
VK_LAYER_KHRONOS_validation) and confirm there are no overflow warnings. - Optionally capture a frame with RenderDoc; inspect the push constant values in the shader debugger to ensure they match your CPU‑side data.
- If you hit the size limit, migrate the excess data to a uniform buffer or descriptor set while keeping the hot‑path data in push constants.
By moving the frequently changing, small‑size data to push constants you remove a per‑draw synchronization point and often see a measurable drop in CPU time spent preparing draw calls.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.