一、定义
Vulkan规范中,推送常量是可通过API写入并在着色器中访问的小块数据。其优势在于允许应用程序设置着色器参数,而无需创建缓冲区或频繁修改/绑定描述符集,提升数据更新效率。 本节使用推送常量替换 UBO 传递 MVP 变换矩阵。
二、核心使用方法
1. 着色器代码实现
- 结构定义:在GLSL中通过
layout(push_constant)
声明推送常量块,类似统一缓冲区。
// 示例1
layout(push_constant) uniform PushConstants {
mat4 model;
mat4 view;
mat4 proj;
} pc;
// 示例2
layout(push_constant, std430) uniform pc {
vec4 data;
};
-
内存布局规则:
- 示例1使用默认布局规则(
std140
)- 在
std140
布局中:- 矩阵按列主序存储
- 基本类型有严格对齐要求(如vec4必须对齐到16字节边界)
- 可能存在额外填充字节
- 在
- 示例2显式指定
std430
布局规则- 而
std430
布局更宽松:- 数据按自然对齐存储
- 减少填充字节
- 更高效的内存利用
- 而
- 示例1使用默认布局规则(
-
对性能的影响:
std430
通常减少内存浪费,提高数据传输效率- 对于包含大量标量或向量的结构体,
std430
优势明显
2. 管线布局配置
通过vkCreatePipelineLayout
设置推送常量范围,示例:
VkPushConstantRange range = {
.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT, // 作用的着色器阶段
.offset = 0, // 起始偏移
.size = 16 // 数据大小(如vec4占16字节)
};
VkPipelineLayoutCreateInfo create_info = {
.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO,
.pushConstantRangeCount = 1,
.pPushConstantRanges = &range
};
vkCreatePipelineLayout(device, &create_info, NULL, &pipeline_layout);
3. 命令缓冲区更新
使用vkCmdPushConstants
在记录命令时写入数据:
float data[4] = {0.0f, 1.0f, 2.0f, 3.0f};
vkCmdPushConstants(commandBuffer, pipeline_layout,
VK_SHADER_STAGE_FRAGMENT_BIT, 0, 16, data);
三、关键特性与注意事项
1. 偏移量设置
- 着色器中指定偏移:通过
layout(offset = N)
修改字段偏移,如:layout(push_constant, std430) uniform pc { layout(offset = 32) vec4 data; // 偏移32字节 };
- 管线布局同步更新:
VkPushConstantRange.offset
需与着色器偏移一致。
2. 管线布局兼容性
- 兼容条件:若两个管线布局的推送常量范围(阶段、偏移、大小)完全相同,则可兼容。
- 无效场景:当调用
vkCmdDraw
等命令时,若当前管线布局与最后一次vkCmdPushConstants
的布局范围不匹配,则操作无效。
3. 生命周期与绑定规则
- 描述符集不影响:
vkCmdBindDescriptorSets
不影响推送常量的有效性。 - 混合绑定点:可在同一命令缓冲区中交替绑定图形管线和计算管线,但需确保推送常量范围与当前管线匹配。
- 不兼容管线绑定:绑定不兼容布局的管线不会清除推送常量值,但后续调用需重新绑定兼容管线才能有效使用。
4. 特殊场景
- 无静态推送常量的布局:即使着色器未使用推送常量,包含推送常量范围的管线布局仍有效,
vkCmdPushConstants
可正常调用。 - 增量更新:支持分阶段更新部分数据,如:
// 首次更新前4字节为0 vkCmdPushConstants(offset: 0, size: 4, value = [0, 0, 0, 0]); // 后续更新中间4字节为1 vkCmdPushConstants(offset: 4, size: 4, value = [1, 1, 1, 1]);
四、推送常量与统一缓冲区对比
核心概念对比
特性 | 统一缓冲区(Uniform Buffer) | 推送常量(Push Constant) |
---|---|---|
存储位置 | 设备内存(Device Memory)或主机可见内存(Host-Visible Memory) | 直接存储在命令缓冲区中(Command Buffer) |
传输方式 | 通过内存复制到缓冲区,再绑定到描述符集 | 作为命令参数直接嵌入命令缓冲区 |
访问范围 | 全局可见,可被多个着色器阶段访问 | 通常限制在单个渲染过程(Render Pass)或命令范围内 |
更新频率 | 适合频繁更新的数据 | 适合高频更新、小批量的数据 |
数据量限制 | 通常较大(受限于VkPhysicalDeviceLimits::maxUniformBufferRange) | 较小(通常不超过128字节,依赖于VkPhysicalDeviceLimits::maxPushConstantsSize) |
性能特点 | 存在内存访问开销,适合大数据块 | 零复制,直接从命令缓冲区读取,延迟极低 |
同步需求 | 需要适当的内存屏障确保数据可见性 | 隐式同步,随命令缓冲区提交自动同步 |
适用场景
-
统一缓冲区适用场景:
- 数据量大(如光照信息、变换矩阵数组)
- 更新频率中等(如每帧更新一次)
- 需要在多个着色器阶段共享
- 数据结构复杂(如结构体数组)
-
推送常量适用场景:
- 高频更新的数据(如每绘制调用更新)
- 数据量小(如单个变换矩阵、少量参数)
- 低延迟要求(如动态分支条件)
- 临时计算参数(如当前帧号、调试标志)
五、代码更新
- 在 VkTypes 中新增 PushConstants 结构
struct PushConstants {
glm::mat4 model;
glm::mat4 view;
glm::mat4 proj;
};
- 在 VkContext 中新增 transform PushConstants实例属性
Struct VkContext {
...
PushConstants* transform;
...
};
- 修改 HelloRect 代码使用 PushConstants 传递 MVP 矩阵
//----------HelloRect.h------------------------
#pragma once
#include <vector>
#include "renderer/VkContext.h"
using namespace renderer;
class HelloRect {
public:
HelloRect();
void update(VkContext&);
const std::vector<Vertex> vertices;
const std::vector<uint16_t> indices;
// UBO ubo;
PushConstants transform;
};
//---------HelloRect.cpp-------------------
...
void HelloRect::update(VkContext& vkcontext) {
static auto startTime = std::chrono::high_resolution_clock::now();
auto currentTime = std::chrono::high_resolution_clock::now();
float time = std::chrono::duration<float, std::chrono::seconds::period>(currentTime - startTime).count();
transform.model = glm::rotate(glm::mat4(1.0f), time * glm::radians(90.0f), glm::vec3(0.0f, 0.0f, 1.0f));
transform.view = glm::lookAt(glm::vec3(1.0f, 1.0f, 1.0f), glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 0.0f, 1.0f));
transform.proj = glm::perspective(
glm::radians(45.0f), vkcontext.swapChainExtent.width / (float) vkcontext.swapChainExtent.height, 0.1f, 10.0f);
transform.proj[1][1] *= -1;
}
...
- 修改管线配置
// 创建图形管线
{
...
// 定义推送常量范围,描述推送常量数据将如何被着色器使用
VkPushConstantRange pushConstantRange{};
// 指定推送常量数据将被顶点着色器阶段使用
// 若需要在多个阶段使用(如顶点和片段着色器),可使用按位或组合标志
pushConstantRange.stageFlags = VK_SHADER_STAGE_VERTEX_BIT;
// 推送常量数据在内存块中的起始偏移量(以字节为单位)
// 必须是4的倍数(VK_SHADER_STAGE_VERTEX_BIT的对齐要求)
pushConstantRange.offset = 0;
// 推送常量数据的大小(以字节为单位)
// 这里设置为3个mat4矩阵的大小,对应着色器中的model、view、proj矩阵
pushConstantRange.size = sizeof(glm::mat4) * 3;
VkPipelineLayoutCreateInfo pipelineLayoutInfo{};
pipelineLayoutInfo.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO;
pipelineLayoutInfo.setLayoutCount = 1;
pipelineLayoutInfo.pSetLayouts = &vkcontext->descriptorSetLayout;
// 指定管线使用的推送常量范围数量
pipelineLayoutInfo.pushConstantRangeCount = 1;
// 指向推送常量范围数组的指针,这里使用上面配置的范围
pipelineLayoutInfo.pPushConstantRanges = &pushConstantRange;
...
}
- 记录命令缓冲区每帧调用
void recordCommandBuffer(VkCommandBuffer commandBuffer, uint32_t imageIndex, VkContext* vkcontext) {
VkCommandBufferBeginInfo beginInfo{};
beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO;
if (vkBeginCommandBuffer(commandBuffer, &beginInfo) != VK_SUCCESS) {
throw std::runtime_error("录制命令缓冲失败!");
}
// 将数据写入命令缓冲区,作为推送常量传递给着色器
vkCmdPushConstants(
commandBuffer, // 目标命令缓冲区,存储推送常量数据
vkcontext->pipelineLayout, // 管线布局,定义了推送常量的访问方式
VK_SHADER_STAGE_VERTEX_BIT, // 指定接收推送常量的着色器阶段(此处为顶点着色器)
0, // 推送常量数据的起始偏移量(字节)
sizeof(PushConstants), // 推送常量数据的大小(字节)
vkcontext->transform // 指向CPU内存中推送常量数据的指针
);
....
- 主函数更新
int main() {
initWindow();
vkcontext.window = window;
HelloRect app;
...
vkcontext.transform = &app.transform; // 赋值给上下文对象
...
}
测试运行正常,当前代码分支: 11_pushconstants