Push Constants in Vulkan: Fast Per‑Draw Uniform Updates Without Descriptor Sets
Push constants let you update up to 128 bytes of shader data directly inside a Vulkan command buffer, eliminating descriptor sets for small, per‑draw uniforms. This article explains how to declare, use, and verify push constants, with a concrete example and practical checklist.
25 Mar 2026, 07:40 UTC

The Problem
In many real‑time applications you need to change a handful of values every draw call – a model matrix, a material color, a light position. Traditional Vulkan uniform buffers require a descriptor set and a buffer allocation for each distinct set of values, which can become a bookkeeping nightmare and add latency when the data changes frequently. How can you update a few bytes of uniform data quickly and without extra memory traffic?
What Push Constants Are
Push constants are a Vulkan feature that lets you write up to 128 bytes of data directly into a command buffer. The data is then available to shaders in the same pipeline layout that declared the push constant range. Because the data lives inside the command buffer, it is automatically synchronized with GPU execution – no explicit memory barriers are needed.
Key rules from the spec:
- The push constant range must be declared in the
VkPipelineLayoutCreateInfobefore pipeline creation. - Offset must be a multiple of 4 bytes; the range must be contiguous.
- All shader stages that use the range must agree on the size and layout; mismatches cause pipeline creation failures.
- On most devices the limit is 128 bytes; some support 256 bytes with
VK_KHR_push_descriptor.
Declaring and Using Them
Below is a minimal C++ snippet that shows how to declare a 64‑byte push constant range and use it in a command buffer. The example assumes you already have a Vulkan instance, device, and a graphics pipeline.
// 1. Define the push constant range
VkPushConstantRange pushRange = {};
pushRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT;
pushRange.offset = 0; // must be 4‑byte aligned
pushRange.size = 64; // 64 bytes is well within the 128‑byte limit
// 2. Create pipeline layout with the range
VkPipelineLayoutCreateInfo layoutInfo = {};
layoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
layoutInfo.pushConstantRangeCount = 1;
layoutInfo.pPushConstantRanges = &pushRange;
// descriptorSetLayoutCount and pSetLayouts omitted for brevity
VkPipelineLayout pipelineLayout;
vkCreatePipelineLayout(device, &layoutInfo, nullptr, &pipelineLayout);
// 3. Record command buffer
vkBeginCommandBuffer(cmdBuffer, &cmdBufInfo);
// Bind the pipeline first – the layout must match
vkCmdBindPipeline(cmdBuffer, VK_PIPELINE_BIND_POINT_GRAPHICS, graphicsPipeline);
// Prepare data to push. The struct must match the shader layout.
struct PushData {
float model[16]; // 64 bytes
};
PushData data = {/* fill with identity matrix or your transform */};
// Push the data into the command buffer
vkCmdPushConstants(cmdBuffer, pipelineLayout, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, 0, sizeof(PushData), &data);
// Issue the draw call
vkCmdDraw(cmdBuffer, 3, 1, 0, 0);
vkEndCommandBuffer(cmdBuffer);
In the shader you would declare a matching layout:
layout(push_constant) uniform PushConstants {
mat4 model;
} pc;
void main() {
gl_Position = pc.model * vec4(position, 1.0);
}
Concrete Example
Suppose you render a rotating cube. For each frame you update the cube’s model matrix. Using push constants keeps the per‑draw overhead minimal:
- Build a 64‑byte struct containing the 4×4 matrix.
- Call
vkCmdPushConstantsbefore eachvkCmdDraw. - Because the data is in the command buffer, the GPU reads it directly when the draw command executes.
Verification steps:
- Run the application on a Vulkan‑capable GPU.
- Render the cube and capture a frame.
- Compare the pixel output against a reference image (e.g., a rendered cube with known rotation). A mismatch indicates the push constant data was not applied.
Attempting to push 200 bytes will result in vkCmdPushConstants returning VK_ERROR_INVALID_EXTERNAL_HANDLE or the driver will log an error – this confirms the 128‑byte enforcement.
Trade‑offs & Limitations
| Aspect | Pros | Cons |
|---|---|---|
| Latency | Zero‑copy, no staging buffer. | Only works for < 128 bytes. |
| Synchronization | Implicit with command buffer. | Must match shader layout exactly. |
| Pipeline Binding | Same layout can be reused. | Changing push constant ranges requires a new pipeline layout. |
| Scalability | Ideal for per‑object small data. | Large data forces uniform buffers. |
Practical Checklist
- Confirm the device supports
VK_KHR_push_descriptorif you need >128 bytes. - Align offset to 4 bytes; size must be a multiple of 4.
- Ensure all shader stages that reference the push constant use the same layout and size.
- Keep the push constant data small (ideally < 64 bytes) to reduce command buffer size.
- Use a single pipeline layout for multiple pipelines if the push constant ranges are identical.
Takeaway
Push constants are a lightweight, fast way to feed small, frequently changing uniform data to shaders without the overhead of descriptor sets or uniform buffers. They shine in per‑draw scenarios like model transforms or material overrides. Just remember the 128‑byte limit, alignment rules, and the need for shader‑layout consistency. When those constraints fit your use case, push constants can simplify your code and give you a measurable performance boost.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.