将数据映射到着色器
所有 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;
剩下要做的就是在绘制调用之前绑定顶点缓冲区和可选的索引缓冲区。
在创建 |
vkBeginCommandBuffer();
// ...
vkCmdBindVertexBuffer();
vkCmdDraw();
// ...
vkCmdBindVertexBuffer();
vkCmdBindIndexBuffer();
vkCmdDrawIndexed();
// ...
vkEndCommandBuffer();
更多信息可以在顶点输入数据处理章节中找到 |
描述符
资源描述符是将统一缓冲区、存储缓冲区、采样器等数据映射到 Vulkan 中任何着色器阶段的核心方式。可以这样概念化描述符:将其视为着色器可以使用的内存指针。
描述符分组在描述符集中,描述符集绑定到着色器。即使描述符集中只有一个描述符,在绑定到着色器时也会使用整个 VkDescriptorSet
。
示例
在此示例中,有以下 3 个描述符集

着色器的 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();
以下结果如下所示

描述符类型
Vulkan 规范有一个着色器资源和存储类对应关系表,其中描述了每种描述符类型需要在 SPIR-V 中如何映射。
以下显示了 GLSL 和 SPIR-V 映射到每个描述符类型的示例。
有关 GLSL 的更多信息,请参见GLSL 规范 - 12.2.4. 仅限 Vulkan:采样器、图像、纹理和缓冲区
存储图像
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_SAMPLER
和 VK_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;
重要提示
|
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
。
物理存储缓冲区
推广到 Vulkan 1.2 的 VK_KHR_buffer_device_address 扩展添加了在着色器中拥有“指针”的功能。在 SPIR-V 中使用 PhysicalStorageBuffer
存储类,应用程序可以调用 vkGetBufferDeviceAddress
,这将返回内存的 VkDeviceAddress
。
虽然这是一种将数据映射到着色器的方式,但它不是与着色器交互的方式。例如,如果应用程序想将其与统一缓冲区一起使用,则必须创建一个同时具有 VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT
和 VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT
的 VkBuffer
。从本例来看,Vulkan 将使用描述符与着色器交互,但随后可以使用物理存储缓冲区来更新该值。
限制
对于以上所有示例,重要的是要注意,Vulkan 中存在限制,这些限制公开了可以在单个时间绑定多少数据。
-
输入属性
-
maxVertexInputAttributes
-
maxVertexInputAttributeOffset
-
-
描述符
-
maxBoundDescriptorSets
-
每个阶段的限制
-
maxPerStageDescriptorSamplers
-
maxPerStageDescriptorUniformBuffers
-
maxPerStageDescriptorStorageBuffers
-
maxPerStageDescriptorSampledImages
-
maxPerStageDescriptorStorageImages
-
maxPerStageDescriptorInputAttachments
-
每个类型的限制
-
maxPerStageResources
-
maxDescriptorSetSamplers
-
maxDescriptorSetUniformBuffers
-
maxDescriptorSetUniformBuffersDynamic
-
maxDescriptorSetStorageBuffers
-
maxDescriptorSetStorageBuffersDynamic
-
maxDescriptorSetSampledImages
-
maxDescriptorSetStorageImages
-
maxDescriptorSetInputAttachments
-
如果使用描述符索引,则为
VkPhysicalDeviceDescriptorIndexingProperties
-
如果使用内联统一块,则为
VkPhysicalDeviceInlineUniformBlockPropertiesEXT
-
-
推送常量
-
maxPushConstantsSize
- 保证所有设备上至少为128
字节
-