Vulkan学习笔记13—推送常量

一、定义

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布局更宽松:
        • 数据按自然对齐存储
        • 减少填充字节
        • 更高效的内存利用
  • 对性能的影响

    • 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)
性能特点存在内存访问开销,适合大数据块零复制,直接从命令缓冲区读取,延迟极低
同步需求需要适当的内存屏障确保数据可见性隐式同步,随命令缓冲区提交自动同步

适用场景

  • 统一缓冲区适用场景

    • 数据量大(如光照信息、变换矩阵数组)
    • 更新频率中等(如每帧更新一次)
    • 需要在多个着色器阶段共享
    • 数据结构复杂(如结构体数组)
  • 推送常量适用场景

    • 高频更新的数据(如每绘制调用更新)
    • 数据量小(如单个变换矩阵、少量参数)
    • 低延迟要求(如动态分支条件)
    • 临时计算参数(如当前帧号、调试标志)

五、代码更新

  1. 在 VkTypes 中新增 PushConstants 结构
 struct PushConstants {
     glm::mat4 model;
     glm::mat4 view;
     glm::mat4 proj;
 };

  1. 在 VkContext 中新增 transform PushConstants实例属性
Struct VkContext {
	...
	PushConstants* transform;
	...
};

  1. 修改 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;

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

  1. 记录命令缓冲区每帧调用
 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内存中推送常量数据的指针
	);

  ....

  1. 主函数更新
int main() {
    initWindow();
    vkcontext.window = window;

    HelloRect app;

    ...

    vkcontext.transform = &app.transform; // 赋值给上下文对象

    ...
}

测试运行正常,当前代码分支: 11_pushconstants

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值