Low-Latency Uniforms with GLSL Push Constants
Push constants offer the fastest path to pass frequently changing
uniform data from the CPU to GPU shaders in modern graphics APIs like
Vulkan. By bypassing standard uniform buffer allocations and descriptor
set updates, push constants embed data directly into the command buffer
stream. This article explains how push constants achieve low-latency
uniform updates through the layout(push_constant) qualifier
in GLSL, examining their hardware-level advantages, memory architecture
differences, and optimal use cases for high-frequency rendering
updates.
The Overhead of Traditional Uniform Buffers
Standard Uniform Buffer Objects (UBOs) require dedicated GPU memory allocations, staging buffers, CPU-to-GPU memory copies, and descriptor set management. Updating a traditional uniform between draw calls typically requires one of several latency-inducing paths:
- Re-binding Descriptor Sets: Calling binding commands that invalidate pipeline state caches.
- Dynamic Offsets: Using
VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMICto index into a large buffer, which still incurs an extra layer of pointer indirection during shader execution. - Pipeline Stalls: Waiting for synchronization barriers to prevent race conditions between adjacent draw calls accessing the same memory region.
These factors introduce CPU overhead and increase GPU latency because the shader must fetch data over the memory bus through multiple levels of cache hierarchy.
How Push Constants Eliminate Latency
Push constants bypass the traditional memory access path entirely.
Instead of referencing an external buffer via a memory address, push
constants write small payloads directly into the command stream using
host API calls like vkCmdPushConstants.
When the GPU processes the command stream, it routes this inline payload directly into fast, on-chip register storage or dedicated constant caches. This architectural design yields several latency benefits:
- Zero Memory Indirection: Shaders read push constants as if they were localized registers or fast on-chip memory, eliminating DRAM read latencies and potential cache misses.
- No Descriptor Allocation: Push constants do not consume descriptor pools, require descriptor sets, or need pipeline layout updates.
- Inline State Transitions: Because the data is embedded directly into the recorded Vulkan command buffer, each draw call carries its own snapshot of data, naturally eliminating CPU-GPU race conditions and synchronization barriers between draws.
GLSL Implementation
via layout(push_constant)
In GLSL, push constants are exposed via a single uniform block using
the push_constant layout qualifier. Unlike UBOs, this block
does not use set or binding qualifiers because
it does not bind to a descriptor set.
#version 450
layout(push_constant) uniform PushConstantBlock {
mat4 modelMatrix;
vec4 tintColor;
uint materialIndex;
} pushConstants;
layout(location = 0) in vec3 inPosition;
layout(location = 0) out vec4 outColor;
void main() {
gl_Position = pushConstants.modelMatrix * vec4(inPosition, 1.0);
outColor = pushConstants.tintColor;
}The fields within the block must align with the corresponding offsets configured in the pipeline's layout on the CPU. The Vulkan pipeline layout specifies which shader stages have visibility to which ranges of the push constant block, ensuring that only the stages requiring the data consume the necessary constant space.
Hardware Limitations and Best Practices
Because push constants reside in high-speed, on-chip storage, their capacity is strictly limited:
- Size Constraints: The Vulkan specification guarantees a minimum of only 128 bytes (equivalent to two standard 4x4 floating-point matrices). While many modern hardware vendors support up to 256 bytes or more, exceeding 128 bytes compromises portability across mobile and embedded GPUs.
- Best Use Cases: Push constants are ideal for highly volatile, per-draw data that changes every draw call, such as model matrices, object IDs for GPU-driven rendering, time values, and animation indices.
- Bulk Data: Bulk data, skeletal bones, or global frame state that stays static across hundreds of draw calls should remain in standard UBOs, Storage Buffers (SSBOs), or bindless resource arrays.