Chapter 07: Push Constants¶
Overview¶
Push constants are small pieces of data sent directly with draw/dispatch commands. This chapter covers:
- When to use push constants vs descriptors
- Size limitations
- Performance benefits
What you'll learn:
- Declaring push constants in shaders
- Setting up push constant ranges
- Updating push constants efficiently
Push Constants vs Descriptors¶
| Aspect | Push Constants | Descriptors |
|---|---|---|
| Size | Small (128-256 bytes typical) | Unlimited |
| Update cost | Very low | Moderate |
| Setup | Simple | Complex |
| Use case | Per-dispatch params | Buffers, images |
How Push Constants Work¶
┌─────────────────────────────────────────────────────────┐
│ Command Buffer │
├─────────────────────────────────────────────────────────┤
│ vkCmdPushConstants(cmd, layout, stages, 0, 16, &data) │
│ vkCmdDispatch(cmd, 64, 1, 1) │
│ │
│ vkCmdPushConstants(cmd, layout, stages, 0, 16, &data2) │
│ vkCmdDispatch(cmd, 64, 1, 1) │
└─────────────────────────────────────────────────────────┘
Data travels with the command — no buffer allocation, no descriptor updates.
Size Limits¶
Query your device's limit:
VkPhysicalDeviceProperties props;
vkGetPhysicalDeviceProperties(physical_device, &props);
printf("Max push constant size: %u bytes\n",
props.limits.maxPushConstantsSize);
| GPU | Typical Limit |
|---|---|
| NVIDIA | 256 bytes |
| AMD | 128 bytes |
| Intel | 128 bytes |
| Apple | 4096 bytes |
Portable Code
Assume 128 bytes for maximum compatibility.
Shader Declaration¶
#version 450
layout(local_size_x = 256) in;
layout(push_constant) uniform PushConstants {
float scale;
float offset;
uint count;
uint pad; // Align to 16 bytes
} pc;
layout(set = 0, binding = 0) buffer Data { float v[]; } data;
void main() {
uint idx = gl_GlobalInvocationID.x;
if (idx < pc.count) {
data.v[idx] = data.v[idx] * pc.scale + pc.offset;
}
}
Pipeline Layout Setup¶
VkPushConstantRange push_range = {
.stageFlags = VK_SHADER_STAGE_COMPUTE_BIT,
.offset = 0,
.size = sizeof(PushConstants) // 16 bytes
};
VkPipelineLayoutCreateInfo layout_info = {
.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO,
.setLayoutCount = 1,
.pSetLayouts = &desc_layout,
.pushConstantRangeCount = 1,
.pPushConstantRanges = &push_range
};
VkPipelineLayout pipeline_layout;
vkCreatePipelineLayout(device, &layout_info, NULL, &pipeline_layout);
Updating Push Constants¶
typedef struct {
float scale;
float offset;
uint32_t count;
uint32_t pad;
} PushConstants;
PushConstants pc = {
.scale = 2.0f,
.offset = 10.0f,
.count = 1024,
.pad = 0
};
vkCmdPushConstants(cmd, pipeline_layout,
VK_SHADER_STAGE_COMPUTE_BIT,
0, sizeof(pc), &pc);
vkCmdDispatch(cmd, 4, 1, 1);
Partial Updates¶
Update only part of the push constant block:
// Update just scale (offset 0, size 4)
float new_scale = 3.0f;
vkCmdPushConstants(cmd, layout, VK_SHADER_STAGE_COMPUTE_BIT,
0, sizeof(float), &new_scale);
// Update just offset (offset 4, size 4)
float new_offset = 20.0f;
vkCmdPushConstants(cmd, layout, VK_SHADER_STAGE_COMPUTE_BIT,
sizeof(float), sizeof(float), &new_offset);
Multiple Dispatches with Different Constants¶
for (int i = 0; i < 100; i++) {
PushConstants pc = {
.scale = (float)i,
.offset = (float)(i * 10),
.count = ARRAY_SIZE
};
vkCmdPushConstants(cmd, layout, VK_SHADER_STAGE_COMPUTE_BIT,
0, sizeof(pc), &pc);
vkCmdDispatch(cmd, workgroups, 1, 1);
}
This is much faster than updating uniform buffers 100 times!
Running the Example¶
Output from an Apple M3 Pro, abridged. Note the 4096-byte limit — MoltenVK is generous here; the Vulkan guaranteed minimum is only 128 bytes:
=== Push Constant Limits ===
Max Push Constants Size: 4096 bytes
Our struct size: 16 bytes
Pipeline layout created with 16 bytes of push constants
=== Testing Push Constant Operations ===
--- Linear (2x + 0.5) ---
[ 0] 0.0000 -> 0.5000
[ 256] 0.2500 -> 1.0000
[ 512] 0.5000 -> 1.5000
[ 768] 0.7500 -> 2.0000
[1023] 0.9990 -> 2.4980
--- Polynomial (x^2) ---
[ 0] 0.0000 -> 0.0000
[ 256] 0.2500 -> 0.0625
...
=== Chained Dispatches with Different Push Constants ===
Chain: input * 2, then square
[0] 0.0000 -> (0.0000)^2 = 0.0000
[512] 0.5000 -> (1.0000)^2 = 0.2500
=== Push Constant Update Cost ===
Running 1000 iterations...
Push constants: 183.65 ms total (0.1837 ms/iter)
Chapter 07 completed!
What this number measures
Each iteration re-records and submits a command buffer and waits on a fence, so the per-iteration time is dominated by submission and synchronization, not by the push-constant update itself. The sample measures the push-constant path only — it does not benchmark a uniform-buffer alternative to compare against.
Chained Operations¶
Use push constants to parameterize pipeline chains:
// First: multiply
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, multiply_pipeline);
float multiplier = 2.0f;
vkCmdPushConstants(cmd, layout, stages, 0, 4, &multiplier);
vkCmdDispatch(cmd, workgroups, 1, 1);
// Barrier
vkCmdPipelineBarrier(cmd, ...);
// Second: square
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_COMPUTE, square_pipeline);
vkCmdDispatch(cmd, workgroups, 1, 1);
Alignment Rules¶
Push constants follow std430 layout rules:
| Type | Size | Alignment |
|---|---|---|
float |
4 | 4 |
vec2 |
8 | 8 |
vec3 |
12 | 16 |
vec4 |
16 | 16 |
mat4 |
64 | 16 |
int |
4 | 4 |
uint |
4 | 4 |
// C struct must match shader layout!
typedef struct {
float scale; // offset 0
float offset; // offset 4
float padding[2]; // offset 8-15 (vec3 would need this)
float vector[4]; // offset 16 (vec4)
} PushConstants;
When to Use Push Constants¶
Good Use Cases¶
- ✅ Transform matrices (if size allows)
- ✅ Frame/time uniforms
- ✅ Per-dispatch parameters
- ✅ Array indices
- ✅ Flags/modes
Bad Use Cases¶
- ❌ Large arrays
- ❌ Texture/buffer references (use descriptors)
- ❌ Data larger than limit
Exercises¶
-
Animation: Update a time value each frame via push constants and visualize it.
-
Multi-pass with Parameters: Implement a blur with push constants for direction and radius.
-
Benchmark: Compare push constants vs uniform buffers for various update frequencies.
Common Errors¶
Offset/Size Mismatch¶
// Wrong: offset + size exceeds declared range
vkCmdPushConstants(cmd, layout, stages, 64, 128, &data);
// If range was declared as 128 bytes starting at 0, this fails
Alignment Issues¶
C struct doesn't match shader layout:
// Wrong
struct { float x; vec3 v; }; // v at offset 4
// Shader expects v at offset 16 (vec3 has 16-byte alignment)
Stage Mismatch¶
Push constants to wrong stage:
// Declared for COMPUTE_BIT but using VERTEX_BIT
vkCmdPushConstants(cmd, layout, VK_SHADER_STAGE_VERTEX_BIT, ...);
What's Next?¶
Push constants let us parameterize shaders at runtime. But what about compile-time parameters? In Chapter 08, we'll learn about specialization constants for optimizing shader variants.