Skip to content

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

./build/bin/ch07_push_constants

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

  1. Animation: Update a time value each frame via push constants and visualize it.

  2. Multi-pass with Parameters: Implement a blur with push constants for direction and radius.

  3. 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.