Vulkan Push Constants 详解:小数据高速传递机制与工程实践
一、前言
在 Vulkan 中,CPU 向 GPU Shader 传递数据的方式有很多种,例如:
1. Uniform Buffer
2. Dynamic Uniform Buffer
3. Storage Buffer
4. Texture / Sampler
5. Specialization Constants
6. Push Constants
其中 Push Constants 是一种非常特殊、非常轻量的传参方式。
它不需要创建 Buffer,也不需要 Descriptor Set,而是直接通过命令缓冲区把少量数据“推送”给 Shader 使用。
一句话概括:
Push Constants 是 Vulkan 中用于向 Shader 快速传递少量数据的一种机制,适合存放频繁变化、体积很小的参数。
例如:
1. 一个 model 矩阵
2. 一个物体 ID
3. 一个材质索引
4. 一个开关参数
5. 一个颜色参数
6. 一个绘制模式标志
相比 Uniform Buffer,Push Constants 使用更简单,CPU 侧更新也更直接。但它的容量很小,不能存放大量数据。
二、为什么需要 Push Constants?
假设我们要渲染一个物体,需要给顶点着色器传递一个模型矩阵:
glm::mat4 model;
如果使用 Uniform Buffer,我们需要:
1. 创建 VkBuffer
2. 分配 VkDeviceMemory
3. 映射内存
4. 写入数据
5. 创建 Descriptor Set Layout
6. 创建 Descriptor Pool
7. 分配 Descriptor Set
8. vkUpdateDescriptorSets
9. 绘制时绑定 Descriptor Set
对于相机矩阵、灯光参数、大量物体数据来说,这些步骤是合理的。
但如果只是传递一个很小的数据,例如:
int objectID;
float roughness;
glm::vec4 color;
每次都创建或更新 Uniform Buffer 就显得比较重。
Push Constants 的优势就在这里:
它可以绕过 Buffer 和 Descriptor Set,直接把小数据推送给 Shader。
典型调用方式如下:
vkCmdPushConstants(
commandBuffer,
pipelineLayout,
VK_SHADER_STAGE_VERTEX_BIT,
0,
sizeof(PushConstantData),
&pushData
);
这条命令会把 pushData 中的数据写入当前命令缓冲区,使 Shader 在随后的 draw call 中能够读取到它。
三、Push Constants 的核心特点
Push Constants 有几个非常重要的特点:
1. 不需要 VkBuffer;
2. 不需要 VkDeviceMemory;
3. 不需要 Descriptor Set;
4. 数据直接记录进 Command Buffer;
5. 适合少量、高频变化的数据;
6. 数据大小受 maxPushConstantsSize 限制;
7. 必须在 Pipeline Layout 中声明 Push Constant Range;
8. Shader 中需要使用 push_constant 布局声明。
它的使用路径可以理解为:
CPU 数据
↓
vkCmdPushConstants
↓
Command Buffer
↓
Pipeline Layout 中的 Push Constant Range
↓
Shader 中的 push_constant block
四、Push Constants 适合存放什么?
Push Constants 适合存放“小而频繁变化”的数据。
例如:
struct PushConstantData
{
glm::mat4 model;
};
或者:
struct PushConstantData
{
glm::vec4 baseColor;
int materialIndex;
};
也可以是:
struct PushConstantData
{
uint32_t objectID;
uint32_t textureIndex;
uint32_t renderMode;
float time;
};
常见使用场景:
1. 每个物体的 model 矩阵;
2. 每个 draw call 的对象 ID;
3. 材质索引;
4. Shader 分支开关;
5. Debug 显示模式;
6. 颜色参数;
7. 少量变换参数;
8. 时间参数;
9. 选择不同渲染路径的 flag。
如果数据非常小,并且每次绘制都要变化,Push Constants 通常是很好的选择。
五、Push Constants 不适合存放什么?
Push Constants 不适合存放大量数据。
例如:
1. 大量物体矩阵数组;
2. 大量骨骼矩阵;
3. 大量灯光数据;
4. 粒子系统数据;
5. 大型材质数组;
6. 大量实例化数据;
7. 任意长度数组;
8. 纹理数据。
这些数据应该使用:
1. Uniform Buffer;
2. Dynamic Uniform Buffer;
3. Storage Buffer;
4. Texture;
5. Descriptor Indexing;
6. Instancing Buffer。
Push Constants 的定位不是“大容量数据存储”,而是“小数据快速传递”。
六、设备限制:maxPushConstantsSize
Push Constants 的大小受设备限制。
需要通过物理设备属性获取:
VkPhysicalDeviceProperties deviceProperties{};
vkGetPhysicalDeviceProperties(physicalDevice, &deviceProperties);
uint32_t maxPushConstantsSize =
deviceProperties.limits.maxPushConstantsSize;
这个值表示当前 GPU 支持的最大 Push Constants 字节数。
Vulkan 规范要求设备至少支持 128 字节。
也就是说,你至少可以安全地使用:
128 bytes
一个 glm::mat4 通常是:
4 × 4 × sizeof(float) = 64 bytes
因此一个 model 矩阵通常可以放进 Push Constants。
但如果你想放两个矩阵:
struct PushConstantData
{
glm::mat4 model;
glm::mat4 normalMatrix;
};
大小大约是:
64 + 64 = 128 bytes
这已经接近最低保证值。
如果再加入更多数据,就可能超过部分设备限制。
所以工程中要注意:
Push Constants 不要设计得太大。
七、Shader 中如何声明 Push Constants?
在 GLSL 中,可以这样声明 Push Constants:
#version 450
layout(push_constant) uniform PushConstantData
{
mat4 model;
} pushData;
然后在 Shader 中使用:
gl_Position =
camera.proj *
camera.view *
pushData.model *
vec4(inPosition, 1.0);
完整顶点着色器示例:
#version 450
layout(location = 0) in vec3 inPosition;
layout(location = 1) in vec3 inColor;
layout(set = 0, binding = 0) uniform CameraUBO
{
mat4 view;
mat4 proj;
} camera;
layout(push_constant) uniform PushConstantData
{
mat4 model;
} pushData;
layout(location = 0) out vec3 fragColor;
void main()
{
gl_Position =
camera.proj *
camera.view *
pushData.model *
vec4(inPosition, 1.0);
fragColor = inColor;
}
这里:
CameraUBO
来自普通 Uniform Buffer,用于存放相机数据。
PushConstantData
来自 Push Constants,用于存放当前物体的 model 矩阵。
这种组合非常常见:
Camera 数据:Uniform Buffer
Object 数据:Push Constants
八、C++ 端 Push Constant 结构体
C++ 端需要定义与 Shader 中对应的数据结构:
struct PushConstantData
{
glm::mat4 model;
};
需要注意:
C++ 端结构体布局必须和 Shader 端匹配。
如果 Shader 中是:
layout(push_constant) uniform PushConstantData
{
mat4 model;
} pushData;
那么 C++ 中也应该是:
struct PushConstantData
{
glm::mat4 model;
};
如果 Shader 中还有其他字段:
layout(push_constant) uniform PushConstantData
{
mat4 model;
vec4 color;
int materialIndex;
} pushData;
那么 C++ 中也应该对应:
struct PushConstantData
{
glm::mat4 model;
glm::vec4 color;
int materialIndex;
};
不过涉及 vec3、float、int 混合时,必须特别注意内存对齐问题。
初学阶段建议尽量使用:
mat4
vec4
uint32_t
int32_t
float
并避免复杂嵌套结构。
九、创建 Push Constant Range
Push Constants 不是随便就能使用的。它必须在创建 Pipeline Layout 时声明。
声明方式是使用:
VkPushConstantRange
示例:
VkPushConstantRange pushConstantRange{};
pushConstantRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
pushConstantRange.offset = 0;
pushConstantRange.size = sizeof(PushConstantData);
字段含义如下:
stageFlags
表示哪些 Shader Stage 可以访问这段 Push Constants。
例如:
VK_SHADER_STAGE_VERTEX_BIT
表示只有顶点着色器可以访问。
VK_SHADER_STAGE_FRAGMENT_BIT
表示只有片元着色器可以访问。
如果顶点和片元都要访问,可以写:
VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT
十、Pipeline Layout 中加入 Push Constant Range
Push Constant Range 要在创建 Pipeline Layout 时传入:
VkPipelineLayoutCreateInfo pipelineLayoutInfo{};
pipelineLayoutInfo.sType =
VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
pipelineLayoutInfo.setLayoutCount = 1;
pipelineLayoutInfo.pSetLayouts = &descriptorSetLayout;
pipelineLayoutInfo.pushConstantRangeCount = 1;
pipelineLayoutInfo.pPushConstantRanges = &pushConstantRange;
VkPipelineLayout pipelineLayout;
vkCreatePipelineLayout(
device,
&pipelineLayoutInfo,
nullptr,
&pipelineLayout
);
完整逻辑是:
VkPushConstantRange pushConstantRange{};
pushConstantRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
pushConstantRange.offset = 0;
pushConstantRange.size = sizeof(PushConstantData);
VkPipelineLayoutCreateInfo pipelineLayoutInfo{};
pipelineLayoutInfo.sType =
VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
pipelineLayoutInfo.setLayoutCount = 1;
pipelineLayoutInfo.pSetLayouts = &descriptorSetLayout;
pipelineLayoutInfo.pushConstantRangeCount = 1;
pipelineLayoutInfo.pPushConstantRanges = &pushConstantRange;
vkCreatePipelineLayout(
device,
&pipelineLayoutInfo,
nullptr,
&pipelineLayout
);
没有这一步,调用 vkCmdPushConstants 时会产生验证层错误。
十一、绘制时调用 vkCmdPushConstants
Push Constants 通常在绘制前更新。
例如渲染多个物体:
for (uint32_t i = 0; i < objectCount; i++)
{
PushConstantData pushData{};
pushData.model = objects[i].modelMatrix;
vkCmdPushConstants(
commandBuffer,
pipelineLayout,
VK_SHADER_STAGE_VERTEX_BIT,
0,
sizeof(PushConstantData),
&pushData
);
vkCmdDrawIndexed(
commandBuffer,
indexCount,
1,
0,
0,
0
);
}
重点是:
vkCmdPushConstants(...)
必须在对应 draw call 之前调用。
它表示:
接下来这次绘制,Shader 使用这份 Push Constant 数据。
如果下一次绘制需要不同数据,就再次调用:
vkCmdPushConstants(...)
十二、Push Constants 的完整绘制流程
一个典型流程如下:
初始化阶段:
1. 定义 C++ PushConstantData 结构体
2. 在 Shader 中声明 layout(push_constant)
3. 创建 VkPushConstantRange
4. 创建 Pipeline Layout 时传入 Push Constant Range
5. 创建 Graphics Pipeline
命令录制阶段:
1. 绑定 Pipeline
2. 绑定 Descriptor Set
3. 绑定 Vertex Buffer
4. 绑定 Index Buffer
5. 为当前物体准备 PushConstantData
6. 调用 vkCmdPushConstants
7. 调用 vkCmdDrawIndexed
8. 绘制下一个物体时重复步骤 5 到 7
可以理解为:
每个 draw call 前:
更新一小块 Shader 参数
然后立刻绘制
十三、Push Constants 与 Uniform Buffer 的区别
1. Uniform Buffer
Uniform Buffer 的特点:
1. 需要创建 Buffer;
2. 需要分配显存;
3. 需要 Descriptor Set;
4. 适合中小规模数据;
5. 适合每帧更新或较低频率更新;
6. 可以存放 camera、light、material 等数据。
例如:
struct CameraUBO
{
glm::mat4 view;
glm::mat4 proj;
};
这类数据通常每帧更新一次,适合 Uniform Buffer。
2. Push Constants
Push Constants 的特点:
1. 不需要 Buffer;
2. 不需要 Descriptor Set;
3. 直接通过命令缓冲区传递;
4. 适合很小的数据;
5. 适合每个 draw call 更新;
6. 容量有限。
例如:
struct PushConstantData
{
glm::mat4 model;
};
这类数据每个物体都不同,且体积不大,适合 Push Constants。
3. 对比总结
Uniform Buffer:
适合相机矩阵、灯光参数、材质参数等较稳定数据。
Push Constants:
适合 model 矩阵、objectID、材质索引、渲染模式开关等小型高频数据。
可以这样搭配:
CameraUBO:Uniform Buffer
Object Transform:Push Constants
Material Index:Push Constants
Large Material Data:Storage Buffer
十四、Push Constants 与 Dynamic Uniform Buffer 的区别
上一篇文章介绍过 Dynamic Uniform Buffer。它和 Push Constants 经常被拿来比较。
1. Dynamic Uniform Buffer
Dynamic Uniform Buffer 的思想是:
一个大 Uniform Buffer 里存放多个对象的数据;
绘制每个对象时通过 dynamic offset 选择不同区域。
优点:
1. 可以存放较多对象数据;
2. 适合批量对象;
3. Descriptor Set 数量少;
4. 数据组织清晰。
缺点:
1. 需要处理 minUniformBufferOffsetAlignment;
2. 需要创建 Buffer;
3. 需要 Descriptor Set;
4. 代码复杂度比 Push Constants 高。
2. Push Constants
Push Constants 的思想是:
每次绘制前直接把少量数据推送给 Shader。
优点:
1. 简单;
2. 不需要 Buffer;
3. 不需要 Descriptor Set;
4. 适合小数据频繁更新。
缺点:
1. 容量小;
2. 不适合大量对象数据;
3. 不适合复杂数组;
4. 受 maxPushConstantsSize 限制。
3. 选择建议
只有一个 model 矩阵:
优先考虑 Push Constants。
每个对象有较多参数:
考虑 Dynamic Uniform Buffer。
对象数量极多,数据规模更大:
考虑 Storage Buffer 或 Instancing。
想做 GPU-driven rendering:
考虑 Storage Buffer + Indirect Draw。
十五、Push Constants 与 Specialization Constants 的区别
Push Constants 和 Specialization Constants 名字相似,但用途完全不同。
1. Push Constants
Push Constants 是运行时传递的数据。
它可以在每个 draw call 之前改变:
vkCmdPushConstants(...)
适合:
1. model 矩阵;
2. object ID;
3. 材质索引;
4. 当前渲染模式;
5. 小型参数。
2. Specialization Constants
Specialization Constants 是 Pipeline 创建阶段确定的常量。
它更像是 Shader 编译参数。
适合:
1. 是否开启某个 Shader 分支;
2. 固定光源数量;
3. 固定采样数量;
4. 固定算法模式。
区别总结:
Push Constants:
运行时变化,draw call 级别更新。
Specialization Constants:
Pipeline 创建时确定,不能在 draw call 中随意改变。
十六、多个 Shader Stage 使用 Push Constants
有时顶点着色器和片元着色器都需要访问 Push Constants。
例如:
顶点着色器需要:
mat4 model;
片元着色器需要:
vec4 color;
可以定义一个结构:
struct PushConstantData
{
glm::mat4 model;
glm::vec4 color;
};
Pipeline Layout 中:
VkPushConstantRange pushConstantRange{};
pushConstantRange.stageFlags =
VK_SHADER_STAGE_VERTEX_BIT |
VK_SHADER_STAGE_FRAGMENT_BIT;
pushConstantRange.offset = 0;
pushConstantRange.size = sizeof(PushConstantData);
Shader 中也可以分别使用。
顶点着色器:
layout(push_constant) uniform PushConstantData
{
mat4 model;
vec4 color;
} pushData;
片元着色器:
layout(push_constant) uniform PushConstantData
{
mat4 model;
vec4 color;
} pushData;
layout(location = 0) out vec4 outColor;
void main()
{
outColor = pushData.color;
}
这种方式简单,但有一个问题:
两个 Shader Stage 都能访问整段 Push Constants。
如果想更精细控制,也可以使用多个 Push Constant Range。
十七、多个 Push Constant Range
Vulkan 允许在 Pipeline Layout 中声明多个 Push Constant Range。
例如:
offset = 0, size = 64 -> Vertex Shader 使用 model 矩阵
offset = 64, size = 16 -> Fragment Shader 使用 color
C++ 端:
VkPushConstantRange vertexRange{};
vertexRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
vertexRange.offset = 0;
vertexRange.size = sizeof(glm::mat4);
VkPushConstantRange fragmentRange{};
fragmentRange.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
fragmentRange.offset = sizeof(glm::mat4);
fragmentRange.size = sizeof(glm::vec4);
std::array<VkPushConstantRange, 2> pushConstantRanges =
{
vertexRange,
fragmentRange
};
创建 Pipeline Layout:
VkPipelineLayoutCreateInfo pipelineLayoutInfo{};
pipelineLayoutInfo.sType =
VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
pipelineLayoutInfo.setLayoutCount = 1;
pipelineLayoutInfo.pSetLayouts = &descriptorSetLayout;
pipelineLayoutInfo.pushConstantRangeCount =
static_cast<uint32_t>(pushConstantRanges.size());
pipelineLayoutInfo.pPushConstantRanges =
pushConstantRanges.data();
更新时:
glm::mat4 model = objects[i].modelMatrix;
vkCmdPushConstants(
commandBuffer,
pipelineLayout,
VK_SHADER_STAGE_VERTEX_BIT,
0,
sizeof(glm::mat4),
&model
);
然后更新片元颜色:
glm::vec4 color = objects[i].color;
vkCmdPushConstants(
commandBuffer,
pipelineLayout,
VK_SHADER_STAGE_FRAGMENT_BIT,
sizeof(glm::mat4),
sizeof(glm::vec4),
&color
);
这样可以让顶点和片元阶段使用不同的 Push Constant 区域。
不过对于初学项目,推荐先使用一个统一结构体:
struct PushConstantData
{
glm::mat4 model;
glm::vec4 color;
};
这样更简单。
十八、Push Constants 的内存布局问题
Push Constants 和 Uniform Buffer 一样,也需要注意 CPU 端和 Shader 端的数据布局一致。
例如:
layout(push_constant) uniform PushConstantData
{
mat4 model;
vec3 color;
float roughness;
} pushData;
C++ 端如果写成:
struct PushConstantData
{
glm::mat4 model;
glm::vec3 color;
float roughness;
};
一般情况下可能能工作,但涉及不同编译器、不同对齐策略时,最好保持谨慎。
更稳妥的方式是使用 vec4:
layout(push_constant) uniform PushConstantData
{
mat4 model;
vec4 color;
} pushData;
C++ 端:
struct PushConstantData
{
glm::mat4 model;
glm::vec4 color;
};
如果需要存放多个标志位,可以使用:
struct PushConstantData
{
glm::mat4 model;
glm::vec4 color;
uint32_t materialIndex;
uint32_t objectID;
uint32_t renderMode;
uint32_t padding;
};
这里加上 padding 是为了让结构体大小更整齐。
十九、Push Constants 的 offset 和 size 要求
调用 vkCmdPushConstants 时,需要指定:
offset
size
pValues
例如:
vkCmdPushConstants(
commandBuffer,
pipelineLayout,
VK_SHADER_STAGE_VERTEX_BIT,
0,
sizeof(PushConstantData),
&pushData
);
其中:
offset = 0
表示从 Push Constant 区域的开头写入。
size = sizeof(PushConstantData)
表示写入多少字节。
需要注意:
1. offset 必须在 Pipeline Layout 声明的范围内;
2. size 必须在 Pipeline Layout 声明的范围内;
3. offset + size 不能超过对应 Push Constant Range;
4. offset 和 size 通常应满足 4 字节对齐;
5. stageFlags 必须与 Pipeline Layout 中声明的 stageFlags 兼容。
常见错误是:
VkPushConstantRange pushConstantRange{};
pushConstantRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
pushConstantRange.offset = 0;
pushConstantRange.size = sizeof(glm::mat4);
但是调用时:
vkCmdPushConstants(
commandBuffer,
pipelineLayout,
VK_SHADER_STAGE_FRAGMENT_BIT,
0,
sizeof(glm::mat4),
&model
);
这里 Shader Stage 不匹配,因为 Pipeline Layout 只声明了:
VK_SHADER_STAGE_VERTEX_BIT
却在调用时使用:
VK_SHADER_STAGE_FRAGMENT_BIT
这会导致验证层错误。
二十、Push Constants 的生命周期
Push Constants 的数据不是全局永久状态。
它记录在 Command Buffer 中,并作用于后续命令。
典型理解:
vkCmdPushConstants
↓
后续 draw call 使用这份数据
↓
再次 vkCmdPushConstants
↓
后续 draw call 使用新的数据
例如:
pushData.model = modelA;
vkCmdPushConstants(..., &pushData);
vkCmdDrawIndexed(...); // 使用 modelA
pushData.model = modelB;
vkCmdPushConstants(..., &pushData);
vkCmdDrawIndexed(...); // 使用 modelB
如果你忘记在第二次绘制前更新 Push Constants,那么第二次绘制会继续使用上一次的数据。
例如:
pushData.model = modelA;
vkCmdPushConstants(..., &pushData);
vkCmdDrawIndexed(...); // Object A 正确
vkCmdDrawIndexed(...); // Object B 可能错误,因为仍然使用 modelA
所以每个对象数据不同的时候,要在每次 draw 前重新 push。
二十一、Push Constants 与 Command Buffer
Push Constants 是命令缓冲区的一部分。
也就是说:
vkCmdPushConstants 不是立即执行;
它只是把“推送数据”这个命令记录到 Command Buffer 里。
当 Command Buffer 被提交到 GPU 队列后,GPU 才会按照记录顺序执行。
因此:
vkCmdPushConstants(...)
vkCmdDrawIndexed(...)
这两条命令的顺序非常重要。
如果顺序写反:
vkCmdDrawIndexed(...)
vkCmdPushConstants(...)
那么这次 draw call 不会使用新的 Push Constant 数据。
正确顺序必须是:
vkCmdPushConstants(...)
vkCmdDrawIndexed(...)
二十二、Push Constants 与 Pipeline Layout 兼容性
Push Constants 和 Pipeline Layout 密切相关。
创建 Pipeline 时,Pipeline 会使用一个 Pipeline Layout。
这个 Pipeline Layout 中必须声明 Push Constant Range。
如果不同 Pipeline 使用不同的 Push Constant Range,就要注意 Pipeline Layout 兼容性。
例如:
Pipeline A:
PushConstantRange size = 64, stage = vertex
Pipeline B:
PushConstantRange size = 80, stage = vertex + fragment
如果频繁切换 Pipeline,就要确保:
当前 vkCmdPushConstants 使用的 pipelineLayout 与当前绑定 Pipeline 的 layout 兼容。
对于初学者,最安全的做法是:
同一类渲染 Pipeline 使用相同的 Pipeline Layout。
例如:
基础 Mesh Pipeline
Phong Pipeline
PBR Pipeline
Shadow Pipeline
可以根据功能分别设计 Push Constant 结构,但不要随意混用。
二十三、一个完整的最小示例
1. C++ 结构体
struct PushConstantData
{
glm::mat4 model;
};
2. Shader 顶点阶段
#version 450
layout(location = 0) in vec3 inPosition;
layout(set = 0, binding = 0) uniform CameraUBO
{
mat4 view;
mat4 proj;
} camera;
layout(push_constant) uniform PushConstantData
{
mat4 model;
} pushData;
void main()
{
gl_Position =
camera.proj *
camera.view *
pushData.model *
vec4(inPosition, 1.0);
}
3. 创建 Push Constant Range
VkPushConstantRange pushConstantRange{};
pushConstantRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
pushConstantRange.offset = 0;
pushConstantRange.size = sizeof(PushConstantData);
4. 创建 Pipeline Layout
VkPipelineLayoutCreateInfo pipelineLayoutInfo{};
pipelineLayoutInfo.sType =
VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
pipelineLayoutInfo.setLayoutCount = 1;
pipelineLayoutInfo.pSetLayouts = &descriptorSetLayout;
pipelineLayoutInfo.pushConstantRangeCount = 1;
pipelineLayoutInfo.pPushConstantRanges = &pushConstantRange;
VkPipelineLayout pipelineLayout;
vkCreatePipelineLayout(
device,
&pipelineLayoutInfo,
nullptr,
&pipelineLayout
);
5. 绘制时更新 Push Constants
for (uint32_t i = 0; i < objects.size(); i++)
{
PushConstantData pushData{};
pushData.model = objects[i].modelMatrix;
vkCmdPushConstants(
commandBuffer,
pipelineLayout,
VK_SHADER_STAGE_VERTEX_BIT,
0,
sizeof(PushConstantData),
&pushData
);
vkCmdDrawIndexed(
commandBuffer,
indexCount,
1,
0,
0,
0
);
}
这就是 Push Constants 的最小使用闭环。
二十四、Push Constants 在 glTF 渲染中的使用
在 glTF 模型渲染中,一个模型通常由多个 Node 和 Mesh 组成。
每个 Node 都有自己的变换矩阵:
glTF Scene
├── Node 0 -> model matrix
├── Node 1 -> model matrix
├── Node 2 -> model matrix
└── Node 3 -> model matrix
如果每个 Node 只需要传递一个 model 矩阵,那么 Push Constants 非常适合。
渲染逻辑可以写成:
void drawNode(
VkCommandBuffer commandBuffer,
const Node& node
)
{
PushConstantData pushData{};
pushData.model = node.worldMatrix;
vkCmdPushConstants(
commandBuffer,
pipelineLayout,
VK_SHADER_STAGE_VERTEX_BIT,
0,
sizeof(PushConstantData),
&pushData
);
for (const Primitive& primitive : node.mesh.primitives)
{
vkCmdDrawIndexed(
commandBuffer,
primitive.indexCount,
1,
primitive.firstIndex,
0,
0
);
}
for (const Node& child : node.children)
{
drawNode(commandBuffer, child);
}
}
这样每个 Node 在绘制前都可以把自己的世界矩阵传给 Shader。
这种设计非常直观:
相机矩阵:Uniform Buffer
节点模型矩阵:Push Constants
材质参数:Descriptor Set / Uniform Buffer / Storage Buffer
纹理:Combined Image Sampler
二十五、Push Constants 在 PBR 渲染中的使用
在 PBR 渲染中,每个物体通常需要:
1. model 矩阵;
2. normal 矩阵;
3. 材质索引;
4. 是否使用纹理的标志位;
5. object ID。
一种常见结构:
struct PushConstantData
{
glm::mat4 model;
glm::mat4 normalMatrix;
uint32_t materialIndex;
uint32_t objectID;
uint32_t hasNormalMap;
uint32_t hasEmissiveMap;
};
不过这个结构大小大约是:
64 + 64 + 16 = 144 bytes
这可能超过某些设备最低保证的 128 字节。
所以更推荐:
struct PushConstantData
{
glm::mat4 model;
uint32_t materialIndex;
uint32_t objectID;
uint32_t flags;
uint32_t padding;
};
然后 normal matrix 可以在 Shader 中根据 model 矩阵计算,或者放到其他 Buffer 中。
材质详细数据可以放在 Storage Buffer 中:
struct MaterialData
{
glm::vec4 baseColor;
float metallic;
float roughness;
int baseColorTextureIndex;
int normalTextureIndex;
};
Push Constants 只负责告诉 Shader:
当前 draw call 使用哪个 materialIndex。
这种设计更符合 Push Constants 的定位。
二十六、性能分析
Push Constants 的性能优势主要来自:
1. 不需要 Descriptor Set 更新;
2. 不需要 Buffer 映射;
3. 不需要显存分配;
4. 命令记录简单;
5. 对少量数据非常高效。
但是不能误解为:
Push Constants 永远比 Uniform Buffer 快。
它只适合小数据。
如果你把大量对象数据都塞进 Push Constants,反而会遇到限制。
从工程经验上看:
小于 128 字节、每个 draw call 变化的数据:
Push Constants 非常合适。
大于 128 字节或需要数组访问的数据:
优先考虑 Buffer。
二十七、常见错误与解决方案
错误一:没有在 Pipeline Layout 中声明 Push Constant Range
错误表现:
调用 vkCmdPushConstants 时验证层报错。
原因:
Pipeline Layout 中 pushConstantRangeCount 为 0。
解决:
pipelineLayoutInfo.pushConstantRangeCount = 1;
pipelineLayoutInfo.pPushConstantRanges = &pushConstantRange;
错误二:Push Constant 大小超过设备限制
错误写法:
struct PushConstantData
{
glm::mat4 model;
glm::mat4 view;
glm::mat4 proj;
glm::mat4 normalMatrix;
};
这个结构大小约为:
64 × 4 = 256 bytes
可能超过某些设备的 maxPushConstantsSize。
解决:
Camera 数据放 Uniform Buffer;
Object 数据放 Push Constants;
大数组放 Storage Buffer。
错误三:Shader Stage 不匹配
Pipeline Layout 中:
pushConstantRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
但是调用时:
vkCmdPushConstants(
commandBuffer,
pipelineLayout,
VK_SHADER_STAGE_FRAGMENT_BIT,
0,
sizeof(PushConstantData),
&pushData
);
这是错误的。
解决:
pushConstantRange.stageFlags =
VK_SHADER_STAGE_VERTEX_BIT |
VK_SHADER_STAGE_FRAGMENT_BIT;
或者调用时只使用:
VK_SHADER_STAGE_VERTEX_BIT
错误四:C++ 结构体和 GLSL 结构不一致
Shader:
layout(push_constant) uniform PushConstantData
{
mat4 model;
vec4 color;
} pushData;
C++:
struct PushConstantData
{
glm::vec4 color;
glm::mat4 model;
};
字段顺序不同,结果错误。
解决:
C++ 结构体字段顺序必须与 GLSL 保持一致。
错误五:忘记在每次绘制前更新
如果多个物体使用不同 model 矩阵,但只 push 一次:
vkCmdPushConstants(...);
for (...)
{
vkCmdDrawIndexed(...);
}
那么所有物体都会使用同一个 model 矩阵。
正确写法:
for (...)
{
vkCmdPushConstants(...);
vkCmdDrawIndexed(...);
}
错误六:把 Push Constants 当成大 Buffer 使用
错误思想:
我要把所有物体的数据都放进 Push Constants。
这是不合适的。
Push Constants 不是数组存储区,也不是大容量数据通道。
正确思路:
Push Constants 只存当前 draw call 需要的少量参数。
二十八、推荐工程设计
对于一个基础 Vulkan Renderer,可以这样设计:
Descriptor Set 0:Frame 数据
binding 0:CameraUBO
binding 1:LightUBO
Descriptor Set 1:Material 数据
binding 0:MaterialUBO 或 StorageBuffer
binding 1:Texture Array
binding 2:Sampler
Push Constants:
model matrix
materialIndex
objectID
flags
C++ 结构体:
struct MeshPushConstants
{
glm::mat4 model;
uint32_t materialIndex;
uint32_t objectID;
uint32_t flags;
uint32_t padding;
};
Shader:
layout(push_constant) uniform MeshPushConstants
{
mat4 model;
uint materialIndex;
uint objectID;
uint flags;
uint padding;
} pushData;
绘制:
for (const RenderObject& object : renderObjects)
{
MeshPushConstants pushData{};
pushData.model = object.modelMatrix;
pushData.materialIndex = object.materialIndex;
pushData.objectID = object.objectID;
pushData.flags = object.flags;
vkCmdPushConstants(
commandBuffer,
pipelineLayout,
VK_SHADER_STAGE_VERTEX_BIT |
VK_SHADER_STAGE_FRAGMENT_BIT,
0,
sizeof(MeshPushConstants),
&pushData
);
vkCmdDrawIndexed(
commandBuffer,
object.indexCount,
1,
object.firstIndex,
object.vertexOffset,
0
);
}
这种结构清晰、扩展性也比较好。
二十九、Push Constants 最佳实践
可以总结为以下几点:
1. Push Constants 只存小数据;
2. 不要超过 128 字节,除非你明确检查过设备限制;
3. 常用来存 model matrix、materialIndex、objectID、flags;
4. Camera 数据不要放 Push Constants,应该放 Uniform Buffer;
5. 大数组不要放 Push Constants,应该放 Storage Buffer;
6. 每次 draw call 数据不同,就在 draw 前调用 vkCmdPushConstants;
7. Pipeline Layout 必须声明 Push Constant Range;
8. Shader Stage 必须匹配;
9. C++ 和 GLSL 结构体布局必须一致;
10. 尽量使用 mat4、vec4、uint32_t 这类对齐友好的类型。
三十、结语
Push Constants 是 Vulkan 中非常实用的小数据传递机制。
它的核心价值是:
不创建 Buffer;
不更新 Descriptor Set;
直接把少量数据推送给 Shader。
它特别适合每个 draw call 都会变化的小型参数,例如:
1. model 矩阵;
2. object ID;
3. material index;
4. 渲染模式 flags;
5. 简单颜色参数。
但是它也有明显限制:
容量很小;
不能替代 Uniform Buffer;
不能替代 Storage Buffer;
不适合大量数组数据。
在 Vulkan 工程中,比较推荐的组合是:
Camera / Light 数据:Uniform Buffer
每个物体的小参数:Push Constants
大量对象或材质数据:Storage Buffer
每个对象矩阵数组:Dynamic Uniform Buffer 或 Storage Buffer
纹理资源:Descriptor Set
如果说 Uniform Buffer 负责“稳定数据”,Storage Buffer 负责“大规模数据”,那么 Push Constants 就负责“少量、高频、立即使用的数据”。
可以这样记住:
Push Constants 是 Vulkan 中最快捷的小数据传参方式。
它不是大容量存储工具,而是 draw call 级别的轻量参数通道。
掌握 Push Constants 后,我们就可以更清晰地组织 Vulkan 中的 per-object 数据,也能更好地理解 Pipeline Layout、Shader 参数绑定和命令缓冲区之间的关系。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)