将数据映射到着色器

所有 SPIR-V 汇编代码均由 glslangValidator 生成

本章介绍如何将 Vulkan 与 SPIR-V 接口,以便映射数据。使用从 vkAllocateMemory 分配的 VkDeviceMemory 对象,应用程序负责正确映射来自 Vulkan 的数据,以便 SPIR-V 着色器理解如何正确使用它。

在核心 Vulkan 中,有 5 种基本方法可以将数据从你的 Vulkan 应用程序映射到 SPIR-V 接口

输入属性

核心 Vulkan 中唯一一个具有 Vulkan 控制的输入属性的着色器阶段是顶点着色器阶段 (VK_SHADER_STAGE_VERTEX_BIT)。这包括在创建 VkPipeline 时声明接口槽,然后在绘制之前绑定 VkBuffer 并映射数据。其他着色器阶段(例如片段着色器阶段)也具有输入属性,但这些值由之前运行的阶段的输出确定。

在调用 vkCreateGraphicsPipelines 之前,需要使用映射到着色器的 VkVertexInputAttributeDescription 列表填充 VkPipelineVertexInputStateCreateInfo 结构。

GLSL 顶点着色器示例 (在线尝试)

#version 450
layout(location = 0) in vec3 inPosition;

void main() {
    gl_Position = vec4(inPosition, 1.0);
}

在位置 0 处只有一个输入属性。这也可以在生成的 SPIR-V 汇编代码中看到

               OpDecorate %inPosition Location 0

        %ptr = OpTypePointer Input %v3float
 %inPosition = OpVariable %ptr Input
         %20 = OpLoad %v3float %inPosition

在此示例中,以下内容可用于 VkVertexInputAttributeDescription

VkVertexInputAttributeDescription input = {};
input.location = 0;
input.binding  = 0;
input.format   = VK_FORMAT_R32G32B32_SFLOAT; // maps to vec3
input.offset   = 0;

剩下要做的就是在绘制调用之前绑定顶点缓冲区和可选的索引缓冲区。

在创建 VkBuffer 时使用 VK_BUFFER_USAGE_VERTEX_BUFFER_BIT 使其成为“顶点缓冲区”

vkBeginCommandBuffer();
// ...
vkCmdBindVertexBuffer();
vkCmdDraw();
// ...
vkCmdBindVertexBuffer();
vkCmdBindIndexBuffer();
vkCmdDrawIndexed();
// ...
vkEndCommandBuffer();

更多信息可以在顶点输入数据处理章节中找到

描述符

资源描述符是将统一缓冲区、存储缓冲区、采样器等数据映射到 Vulkan 中任何着色器阶段的核心方式。可以这样概念化描述符:将其视为着色器可以使用的内存指针。

Vulkan 中有各种描述符类型,每种类型都有自己的详细描述,说明它们允许什么。

描述符分组在描述符集中,描述符集绑定到着色器。即使描述符集中只有一个描述符,在绑定到着色器时也会使用整个 VkDescriptorSet

示例

在此示例中,有以下 3 个描述符集

mapping_data_to_shaders_descriptor_1.png

着色器的 GLSL (在线尝试)

// Note - only set 0 and 2 are used in this shader

layout(set = 0, binding = 0) uniform sampler2D myTextureSampler;

layout(set = 0, binding = 2) uniform uniformBuffer0 {
    float someData;
} ubo_0;

layout(set = 0, binding = 3) uniform uniformBuffer1 {
    float moreData;
} ubo_1;

layout(set = 2, binding = 0) buffer storageBuffer {
    float myResults;
} ssbo;

相应的 SPIR-V 汇编代码

OpDecorate %myTextureSampler DescriptorSet 0
OpDecorate %myTextureSampler Binding 0

OpMemberDecorate %uniformBuffer0 0 Offset 0
OpDecorate %uniformBuffer0 Block
OpDecorate %ubo_0 DescriptorSet 0
OpDecorate %ubo_0 Binding 2

OpMemberDecorate %uniformBuffer1 0 Offset 0
OpDecorate %uniformBuffer1 Block
OpDecorate %ubo_1 DescriptorSet 0
OpDecorate %ubo_1 Binding 3

OpMemberDecorate %storageBuffer 0 Offset 0
OpDecorate %storageBuffer BufferBlock
OpDecorate %ssbo DescriptorSet 2
OpDecorate %ssbo Binding 0

描述符的绑定在记录命令缓冲区时完成。描述符必须在绘制/调度调用时绑定。以下是一些伪代码,可以更好地表示这一点

vkBeginCommandBuffer();
// ...
vkCmdBindPipeline(); // Binds shader

// One possible way of binding the two sets
vkCmdBindDescriptorSets(firstSet = 0, pDescriptorSets = &descriptor_set_c);
vkCmdBindDescriptorSets(firstSet = 2, pDescriptorSets = &descriptor_set_b);

vkCmdDraw(); // or dispatch
// ...
vkEndCommandBuffer();

以下结果如下所示

mapping_data_to_shaders_descriptor_2.png

描述符类型

Vulkan 规范有一个着色器资源和存储类对应关系表,其中描述了每种描述符类型需要在 SPIR-V 中如何映射。

以下显示了 GLSL 和 SPIR-V 映射到每个描述符类型的示例。

存储图像

VK_DESCRIPTOR_TYPE_STORAGE_IMAGE

// VK_FORMAT_R32_UINT
layout(set = 0, binding = 0, r32ui) uniform uimage2D storageImage;

// example usage for reading and writing in GLSL
const uvec4 texel = imageLoad(storageImage, ivec2(0, 0));
imageStore(storageImage, ivec2(1, 1), texel);
OpDecorate %storageImage DescriptorSet 0
OpDecorate %storageImage Binding 0

%r32ui        = OpTypeImage %uint 2D 0 0 0 2 R32ui
%ptr          = OpTypePointer UniformConstant %r32ui
%storageImage = OpVariable %ptr UniformConstant

采样器和采样图像

VK_DESCRIPTOR_TYPE_SAMPLERVK_DESCRIPTOR_TYPE_SAMPLED_IMAGE

layout(set = 0, binding = 0) uniform sampler samplerDescriptor;
layout(set = 0, binding = 1) uniform texture2D sampledImage;

// example usage of using texture() in GLSL
vec4 data = texture(sampler2D(sampledImage,  samplerDescriptor), vec2(0.0, 0.0));
OpDecorate %sampledImage DescriptorSet 0
OpDecorate %sampledImage Binding 1
OpDecorate %samplerDescriptor DescriptorSet 0
OpDecorate %samplerDescriptor Binding 0

%image        = OpTypeImage %float 2D 0 0 0 1 Unknown
%imagePtr     = OpTypePointer UniformConstant %image
%sampledImage = OpVariable %imagePtr UniformConstant

%sampler           = OpTypeSampler
%samplerPtr        = OpTypePointer UniformConstant %sampler
%samplerDescriptor = OpVariable %samplerPtr UniformConstant

%imageLoad       = OpLoad %image %sampledImage
%samplerLoad     = OpLoad %sampler %samplerDescriptor

%sampleImageType = OpTypeSampledImage %image
%1               = OpSampledImage %sampleImageType %imageLoad %samplerLoad

%textureSampled = OpImageSampleExplicitLod %v4float %1 %coordinate Lod %float_0

组合图像采样器

VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER

在某些实现中,使用组合描述符中存储在一起的采样器和采样图像的组合从图像中进行采样可能更有效率。

layout(set = 0, binding = 0) uniform sampler2D combinedImageSampler;

// example usage of using texture() in GLSL
vec4 data = texture(combinedImageSampler, vec2(0.0, 0.0));
OpDecorate %combinedImageSampler DescriptorSet 0
OpDecorate %combinedImageSampler Binding 0

%imageType            = OpTypeImage %float 2D 0 0 0 1 Unknown
%sampleImageType      = OpTypeSampledImage imageType
%ptr                  = OpTypePointer UniformConstant %sampleImageType
%combinedImageSampler = OpVariable %ptr UniformConstant

%load           = OpLoad %sampleImageType %combinedImageSampler
%textureSampled = OpImageSampleExplicitLod %v4float %load %coordinate Lod %float_0

统一缓冲区

VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER

统一缓冲区还可以在绑定时使用动态偏移(VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC)

layout(set = 0, binding = 0) uniform uniformBuffer {
    float a;
    int b;
} ubo;

// example of reading from UBO in GLSL
int x = ubo.b + 1;
vec3 y = vec3(ubo.a);
OpMemberDecorate %uniformBuffer 0 Offset 0
OpMemberDecorate %uniformBuffer 1 Offset 4
OpDecorate %uniformBuffer Block
OpDecorate %ubo DescriptorSet 0
OpDecorate %ubo Binding 0

%uniformBuffer = OpTypeStruct %float %int
%ptr           = OpTypePointer Uniform %uniformBuffer
%ubo           = OpVariable %ptr Uniform

存储缓冲区

VK_DESCRIPTOR_TYPE_STORAGE_BUFFER

存储缓冲区还可以在绑定时使用动态偏移(VK_DESCRIPTOR_TYPE_STORAGE_BUFFER_DYNAMIC)

layout(set = 0, binding = 0) buffer storageBuffer {
    float a;
    int b;
} ssbo;

// example of reading and writing SSBO in GLSL
ssbo.a = ssbo.a + 1.0;
ssbo.b = ssbo.b + 1;
重要提示

BufferBlockUniformVK_KHR_storage_buffer_storage_class 之前已经出现过。

OpMemberDecorate %storageBuffer 0 Offset 0
OpMemberDecorate %storageBuffer 1 Offset 4
OpDecorate %storageBuffer Block
OpDecorate %ssbo DescriptorSet 0
OpDecorate %ssbo Binding 0

%storageBuffer = OpTypeStruct %float %int
%ptr           = OpTypePointer StorageBuffer %storageBuffer
%ssbo          = OpVariable %ptr StorageBuffer

统一纹理元素缓冲区

VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER

layout(set = 0, binding = 0) uniform textureBuffer uniformTexelBuffer;

// example of reading texel buffer in GLSL
vec4 data = texelFetch(uniformTexelBuffer, 0);
OpDecorate %uniformTexelBuffer DescriptorSet 0
OpDecorate %uniformTexelBuffer Binding 0

%texelBuffer        = OpTypeImage %float Buffer 0 0 0 1 Unknown
%ptr                = OpTypePointer UniformConstant %texelBuffer
%uniformTexelBuffer = OpVariable %ptr UniformConstant

存储纹理元素缓冲区

VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER

// VK_FORMAT_R8G8B8A8_UINT
layout(set = 0, binding = 0, rgba8ui) uniform uimageBuffer storageTexelBuffer;

// example of reading and writing texel buffer in GLSL
int offset = int(gl_GlobalInvocationID.x);
vec4 data = imageLoad(storageTexelBuffer, offset);
imageStore(storageTexelBuffer, offset, uvec4(0));
OpDecorate %storageTexelBuffer DescriptorSet 0
OpDecorate %storageTexelBuffer Binding 0

%rgba8ui            = OpTypeImage %uint Buffer 0 0 0 2 Rgba8ui
%ptr                = OpTypePointer UniformConstant %rgba8ui
%storageTexelBuffer = OpVariable %ptr UniformConstant

输入附件

VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT

layout (input_attachment_index = 0, set = 0, binding = 0) uniform subpassInput inputAttachment;

// example loading the attachment data in GLSL
vec4 data = subpassLoad(inputAttachment);
OpDecorate %inputAttachment DescriptorSet 0
OpDecorate %inputAttachment Binding 0
OpDecorate %inputAttachment InputAttachmentIndex 0

%subpass         = OpTypeImage %float SubpassData 0 0 0 2 Unknown
%ptr             = OpTypePointer UniformConstant %subpass
%inputAttachment = OpVariable %ptr UniformConstant

推送常量

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

这些是为每次命令缓冲区记录更新的少量(几个双字)高频率数据而设计的。

更多信息可以在推送常量章节中找到。

特化常量

特化常量是一种机制,允许在创建 VkPipeline 时指定 SPIR-V 中的常量值。这非常强大,因为它取代了在高阶着色语言(GLSL、HLSL 等)中进行预处理器宏的想法。

示例

如果应用程序想要创建多个 VkPipeline,每个 VkPipeline 的颜色值都不同,一种简单的方法是创建两个着色器。

// shader_a.frag
#version 450
layout(location = 0) out vec4 outColor;

void main() {
    outColor = vec4(0.0);
}
// shader_b.frag
#version 450
layout(location = 0) out vec4 outColor;

void main() {
    outColor = vec4(1.0);
}

使用特化常量,可以在调用 vkCreateGraphicsPipelines 来编译着色器时做出决定。这意味着只需要一个着色器。

#version 450
layout (constant_id = 0) const float myColor = 1.0;
layout(location = 0) out vec4 outColor;

void main() {
    outColor = vec4(myColor);
}

生成的 SPIR-V 汇编代码

           OpDecorate %outColor Location 0
           OpDecorate %myColor SpecId 0

// 0x3f800000 as decimal which is 1.0 for a 32 bit float
%myColor = OpSpecConstant %float 1065353216

使用特化常量,该值在着色器内部仍然是一个常量,但是例如,如果另一个 VkPipeline 使用相同的着色器,但想将 myColor 值设置为 0.5f,则可以在运行时这样做。

struct myData {
    float myColor = 1.0f;
} myData;

VkSpecializationMapEntry mapEntry = {};
mapEntry.constantID = 0; // matches constant_id in GLSL and SpecId in SPIR-V
mapEntry.offset     = 0;
mapEntry.size       = sizeof(float);

VkSpecializationInfo specializationInfo = {};
specializationInfo.mapEntryCount = 1;
specializationInfo.pMapEntries   = &mapEntry;
specializationInfo.dataSize      = sizeof(myData);
specializationInfo.pData         = &myData;

VkGraphicsPipelineCreateInfo pipelineInfo = {};
pipelineInfo.pStages[fragIndex].pSpecializationInfo = &specializationInfo;

// Create first pipeline with myColor as 1.0
vkCreateGraphicsPipelines(&pipelineInfo);

// Create second pipeline with same shader, but sets different value
myData.myColor = 0.5f;
vkCreateGraphicsPipelines(&pipelineInfo);

第二个 VkPipeline 着色器反汇编后,myColor 的新常量值为 0.5f

3 种特化常量的用法

特化常量的典型用例可以最好地分为三种不同的用法。

  • 切换特性

    • 直到运行时才知道 Vulkan 中是否支持某个特性。这种特化常量的用法是为了避免编写两个单独的着色器,而是嵌入一个常量运行时决策。

  • 改进后端优化

    • 这里的“后端”指的是实现编译器的过程,它获取生成的 SPIR-V 并将其降级为在设备上运行的某个 ISA。

    • 常量值允许进行一系列优化,例如常量折叠死代码消除等。

  • 影响类型和内存大小

    • 可以通过特化常量来设置数组的长度或使用的变量类型。

    • 重要的是要注意,编译器将需要根据这些类型和大小来分配寄存器。这意味着如果分配的寄存器差异很大,则管道缓存可能会失败。

物理存储缓冲区

推广到 Vulkan 1.2 的 VK_KHR_buffer_device_address 扩展添加了在着色器中拥有“指针”的功能。在 SPIR-V 中使用 PhysicalStorageBuffer 存储类,应用程序可以调用 vkGetBufferDeviceAddress,这将返回内存的 VkDeviceAddress

虽然这是一种将数据映射到着色器的方式,但它不是与着色器交互的方式。例如,如果应用程序想将其与统一缓冲区一起使用,则必须创建一个同时具有 VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BITVK_BUFFER_USAGE_UNIFORM_BUFFER_BITVkBuffer。从本例来看,Vulkan 将使用描述符与着色器交互,但随后可以使用物理存储缓冲区来更新该值。

限制

对于以上所有示例,重要的是要注意,Vulkan 中存在限制,这些限制公开了可以在单个时间绑定多少数据。

  • 输入属性

    • maxVertexInputAttributes

    • maxVertexInputAttributeOffset

  • 描述符

    • maxBoundDescriptorSets

    • 每个阶段的限制

    • maxPerStageDescriptorSamplers

    • maxPerStageDescriptorUniformBuffers

    • maxPerStageDescriptorStorageBuffers

    • maxPerStageDescriptorSampledImages

    • maxPerStageDescriptorStorageImages

    • maxPerStageDescriptorInputAttachments

    • 每个类型的限制

    • maxPerStageResources

    • maxDescriptorSetSamplers

    • maxDescriptorSetUniformBuffers

    • maxDescriptorSetUniformBuffersDynamic

    • maxDescriptorSetStorageBuffers

    • maxDescriptorSetStorageBuffersDynamic

    • maxDescriptorSetSampledImages

    • maxDescriptorSetStorageImages

    • maxDescriptorSetInputAttachments

    • 如果使用描述符索引,则为 VkPhysicalDeviceDescriptorIndexingProperties

    • 如果使用内联统一块,则为 VkPhysicalDeviceInlineUniformBlockPropertiesEXT

  • 推送常量

    • maxPushConstantsSize - 保证所有设备上至少为 128 字节