一、前言

在 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;
};

不过涉及 vec3floatint 混合时,必须特别注意内存对齐问题。

初学阶段建议尽量使用:

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 参数绑定和命令缓冲区之间的关系。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐