推送常量

Vulkan 规范将 推送常量 定义为

一小块值,可通过 API 写入并在着色器中访问。推送常量允许应用程序设置着色器中使用的值,而无需创建缓冲区或修改和绑定每个更新的描述符集。

如何使用

着色器代码

从着色器的角度来看,推送常量类似于统一缓冲区。规范提供了 Vulkan 和 SPIR-V 之间的推送常量接口的详细信息。

一个简单的 GLSL 片段着色器示例 (在线尝试)

layout(push_constant, std430) uniform pc {
    vec4 data;
};

layout(location = 0) out vec4 outColor;

void main() {
   outColor = data;
}

当查看反汇编的 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

它与 Vulkan 规范的描述相匹配,即具有 Block 装饰的 OpTypeStruct 类型。

管线布局

当调用 vkCreatePipelineLayout 时,需要在 VkPipelineLayoutCreateInfo 中设置推送常量范围

一个使用上述着色器的示例

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);

在记录时更新

最后,需要使用 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()

偏移量

采用上面的着色器,开发人员可以向推送常量块添加偏移量

layout(push_constant, std430) uniform pc {
-   vec4 data;
+   layout(offset = 32) vec4 data;
};

layout(location = 0) out vec4 outColor;

void main() {
   outColor = data;
}

与上述反汇编的 SPIR-V 的区别仅在于成员装饰

- OpMemberDecorate %pc 0 Offset 0
+ OpMemberDecorate %pc 0 Offset 32

从这里开始,还需要在每个使用它的着色器阶段的 VkPushConstantRange 中指定 32 的偏移量

VkPushConstantRange range = {};
range.stageFlags = VK_SHADER_STAGE_FRAGMENT_BIT;
-range.offset = 0;
+range.offset = 32;
range.size = 16;

下图提供了推送常量偏移量如何工作的可视化表示。

push_constant_offset

管线布局兼容性

Vulkan 规范定义了 推送常量的兼容性

如果它们是用相同的推送常量范围创建的

这意味着在发出绑定管线命令 (vkCmdDrawvkCmdDispatch 等) 之前,最后一个 vkCmdPushConstantsvkCmdBindPipeline 中使用的 VkPipelineLayout(对于相应的 VkPipelineBindPoint)必须具有相同VkPushConstantRange

推送常量的生命周期

推送常量的生命周期可能会为某些边缘情况打开空间,以下内容旨在提供一些关于推送常量的有效和无效的常见示例。

dEQP-VK.pipeline.push_constant.lifetime.* 下有一些 CTS

绑定描述符集无效

因为推送常量未与描述符绑定,所以使用 vkCmdBindDescriptorSets 对推送常量的生命周期或 管线布局兼容性 没有影响。

混合绑定点

可以在其着色器中使用两个不同的 VkPipelineBindPoint,每个绑定点对推送常量有不同的用法

// 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()

绑定不兼容的管线

规范说

绑定具有与推送常量布局不兼容的布局的管线不会干扰推送常量值。

以下示例有助于说明这一点

// 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()

没有静态推送常量的布局

即使管线布局中存在 VkPushConstantRange,但着色器中没有推送常量也是有效的,例如

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

如果使用上述着色器和管线布局创建 VkPipeline,则对其调用 vkCmdPushConstants 仍然有效

可以这样理解,vkCmdPushConstantsVkPipelineLayout 的使用相关联,因此它们必须匹配才能调用诸如 vkCmdDraw() 之类的命令。

正如可以绑定着色器从未使用过的描述符集一样,推送常量也是如此。

增量更新

推送常量可以在命令缓冲区的执行过程中进行增量更新。

以下示例展示了一个 vec4 推送常量的值

// 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()