Push Constants
The Vulkan spec defines Push Constants
as:
A small bank of values writable via the API and accessible in shaders. Push constants allow the application to set values used in shaders without creating buffers or modifying and binding descriptor sets for each update. |
How to use
Shader Code
From a shader perspective, push constant are similar to a uniform buffer. The spec provides details for the push constant interface between Vulkan and SPIR-V.
A simple GLSL fragment shader example (Try Online):
layout(push_constant, std430) uniform pc {
vec4 data;
};
layout(location = 0) out vec4 outColor;
void main() {
outColor = data;
}
Which when looking at parts of the disassembled SPIR-V
OpMemberDecorate %pc 0 Offset 0
OpDecorate %pc Block
%float = OpTypeFloat 32
%v4float = OpTypeVector %float 4
%pc = OpTypeStruct %v4float
%pc_ptr = OpTypePointer PushConstant %pc
%pc_var = OpVariable %pc_ptr PushConstant
%pc_v4float_ptr = OpTypePointer PushConstant %v4float
%access_chain = OpAccessChain %pc_v4float_ptr %pc_var %int_0
it matches the Vulkan spec description of being an OpTypeStruct
type with a Block
decoration.
Pipeline layout
When calling vkCreatePipelineLayout
the push constant ranges needs to be set in VkPipelineLayoutCreateInfo.
An example using the previous shader above:
VkPushConstantRange range = {};
range.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
range.offset = 0;
range.size = 16; // %v4float (vec4) is defined as 16 bytes
VkPipelineLayoutCreateInfo create_info = {};
create_info.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
create_info.pNext = NULL;
create_info.flags = 0;
create_info.setLayoutCount = 0;
create_info.pushConstantRangeCount = 1;
create_info.pPushConstantRanges = ⦥
VkPipelineLayout pipeline_layout;
vkCreatePipelineLayout(device, &create_info, NULL, &pipeline_layout);
Updating at record time
Lastly, the value for the push constants needs to be updated to the desired value using vkCmdPushConstants.
float data[4] = {0.0f, 1.0f, 2.0f, 3.0f}; // where sizeof(float) == 4 bytes
// vkBeginCommandBuffer()
uint32_t offset = 0;
uint32_t size = 16;
vkCmdPushConstants(commandBuffer, pipeline_layout, VK_SHADER_STAGE_FRAGMENT_BIT, offset, size, data);
// draw / dispatch / trace rays / etc
// vkEndCommandBuffer()
Offsets
Taking the above shader, a developer can add an offset to the push constant block
layout(push_constant, std430) uniform pc {
- vec4 data;
+ layout(offset = 32) vec4 data;
};
layout(location = 0) out vec4 outColor;
void main() {
outColor = data;
}
The difference from the above disassembled SPIR-V is only the member decoration
- OpMemberDecorate %pc 0 Offset 0
+ OpMemberDecorate %pc 0 Offset 32
From here the offset of 32
needs to be also specified in VkPushConstantRange
for each shader stage that uses it
VkPushConstantRange range = {};
range.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
-range.offset = 0;
+range.offset = 32;
range.size = 16;
The following diagram provides a visualization of how push constant offsets work.
Pipeline layout compatibility
The Vulkan spec defines what Compatibility for push constants as
if they were created with identical push constant ranges |
This means before a bound pipeline command is issued (vkCmdDraw
, vkCmdDispatch
, etc) the VkPipelineLayout
used in the last vkCmdPushConstants
and vkCmdBindPipeline
(for the appropriate VkPipelineBindPoint
) must have had identical VkPushConstantRange
.
Lifetime of push constants
The lifetime of push constants can open room for some edge cases and the following is designed to give some common examples of what is and is not valid with push constants.
There are some CTS under |
Binding descriptor sets has no effect
Because push constants are not tied to descriptors, the use of vkCmdBindDescriptorSets
has no effect on the lifetime or pipeline layout compatibility of push constants.
Mixing bind points
It is possible to use two different VkPipelineBindPoint
that each have different uses of push constants in their shader
// different ranges and therefore not compatible layouts
VkPipelineLayout layout_graphics; // VK_SHADER_STAGE_FRAGMENT_BIT
VkPipelineLayout layout_compute; // VK_SHADER_STAGE_COMPUTE_BIT
// vkBeginCommandBuffer()
vkCmdBindPipeline(pipeline_graphics); // layout_graphics
vkCmdBindPipeline(pipeline_compute); // layout_compute
vkCmdPushConstants(layout_graphics); // VK_SHADER_STAGE_FRAGMENT_BIT
// Still valid as the last pipeline and push constant for graphics are compatible
vkCmdDraw();
vkCmdPushConstants(layout_compute); // VK_SHADER_STAGE_COMPUTE_BIT
vkCmdDispatch(); // valid
// vkEndCommandBuffer()
Binding non-compatible pipelines
The spec say:
Binding a pipeline with a layout that is not compatible with the push constant layout does not disturb the push constant values. |
The following examples helps illustrate this:
// vkBeginCommandBuffer()
vkCmdPushConstants(layout_0);
vkCmdBindPipeline(pipeline_b); // non-compatible with layout_0
vkCmdBindPipeline(pipeline_a); // compatible with layout_0
vkCmdDraw(); // valid
// vkEndCommandBuffer()
// vkBeginCommandBuffer()
vkCmdBindPipeline(pipeline_b); // non-compatible with layout_0
vkCmdPushConstants(layout_0);
vkCmdBindPipeline(pipeline_a); // compatible with layout_0
vkCmdDraw(); // valid
// vkEndCommandBuffer()
// vkBeginCommandBuffer()
vkCmdPushConstants(layout_0);
vkCmdBindPipeline(pipeline_a); // compatible with layout_0
vkCmdBindPipeline(pipeline_b); // non-compatible with layout_0
vkCmdDraw(); // INVALID
// vkEndCommandBuffer()
Layouts with no static push constants
It is also valid to have a VkPushConstantRange
in the pipeline layout but no push constants in the shader, for example:
VkPushConstantRange range = {VK_SHADER_STAGE_VERTEX_BIT, 0, 4};
VkPipelineLayoutCreateInfo pipeline_layout_info = {VK_SHADER_STAGE_VERTEX_BIT. 1, &range};
void main() {
gl_Position = vec4(1.0);
}
If a VkPipeline
is created with the above shader and pipeline layout, it is still valid to call vkCmdPushConstants
on it.
The mental model can be thought of as vkCmdPushConstants
is tied to the VkPipelineLayout
usage and therefore why they must match before a call to a command such as vkCmdDraw()
.
The same way it is possible to bind descriptor sets that are never used by the shader, the same is true for push constants.
Updated incrementally
Push constants can be incrementally updated over the course of a command buffer.
The following shows an example of the values of a vec4
push constant
// vkBeginCommandBuffer()
vkCmdBindPipeline();
vkCmdPushConstants(offset: 0, size: 16, value = [0, 0, 0, 0]);
vkCmdDraw(); // values = [0, 0, 0, 0]
vkCmdPushConstants(offset: 4, size: 8, value = [1 ,1]);
vkCmdDraw(); // values = [0, 1, 1, 0]
vkCmdPushConstants(offset: 8, size: 8, value = [2, 2]);
vkCmdDraw(); // values = [0, 1, 2, 2]
// vkEndCommandBuffer()