This blog post discusses how to manage resources in HLSL for Vulkan, using the SPIR-V CodeGen of DirectXShaderCompiler (DXC). It is one of the “HLSL for Vulkan” series.
Since resource is too huge a topic in graphics to be covered in a single blog post, I will mainly focus on the shader side here.
Resource & Descriptor
Resources is just a block of data on memory. They can be textures, vertex geometry data, etc. Resources need to be bound to the graphics pipeline for shaders to access them. Both DirectX and Vulkan use descriptors to refer to resources for binding. Descriptors, in a GPU-specific opaque format, are just handles/views to the resource. There are multiple types of descriptors.
In DirectX, common descriptor types are:
- Sampler - sampler (read-only) - using
s#
registers - SRV - shader resource view (read-only) - using
t#
registers - CBV - constant buffer view (read-only) - using
b#
registers - UAV - unordered access view (read-write) - using
u#
registers
Vulkan has its own terms for descriptor types:
- Sampler - read-only
- Sampled image - read-only
- Storage image - read-write
- Combined image sampler - read-only
- Uniform texel buffer - read-only
- Storage texel buffer - read-write
- Uniform buffer - read-only
- Storage buffer - read-write
- Input attachment - read-only
HLSL Resource Types
HLSL provides corresponding resource types to access the resources behind various descriptor types. The following table summerizes them and their GLSL equivalents if possible:
HLSL Type | DirectX Descriptor Type | Vulkan Descriptor Type | GLSL Type |
---|---|---|---|
SamplerState |
Sampler | Sampler | uniform sampler* |
SamplerComparisonState |
Sampler | Sampler | uniform sampler*Shadow |
Buffer |
SRV | Uniform Texel Buffer | uniform samplerBuffer |
RWBuffer |
UAV | Storage Texel Buffer | uniform imageBuffer |
Texture* |
SRV | Sampled Image | uniform texture* |
RWTexture* |
UAV | Storage Image | uniform image* |
cbuffer |
CBV | Uniform Buffer | uniform { ... } |
ConstantBuffer |
CBV | Uniform Buffer | uniform { ... } |
tbuffer |
CBV | Storage Buffer | |
TextureBuffer |
CBV | Storage Buffer | |
StructuredBuffer |
SRV | Storage Buffer | buffer { ... } |
RWStructuredBuffer |
UAV | Storage Buffer | buffer { ... } |
ByteAddressBuffer |
SRV | Storage Buffer | |
RWByteAddressBuffer |
UAV | Storage Buffer | |
AppendStructuredBuffer |
UAV | Storage Buffer | |
ConsumeStructuredBuffer |
UAV | Storage Buffer |
You can find more detailed information on how these resource types are translated into SPIR-V in the SPIR-V doc in DXC.
Subpass inputs
The above table lists native HLSL resource types. To further support HLSL for Vulkan shader programming, we added the following resource type for Vulkan subpass inputs into DXC1.
HLSL Type | Vulkan Descriptor Type | GLSL Type |
---|---|---|
SubpassInput |
Input Attachment | uniform subpassInput |
SubpassInputMS |
Input Attachment | uniform subpassInputMS |
See the SPIR-V doc in DXC for the syntax and how to use subpass inputs.
Resource Binding
Vulkan descriptors are grouped together into descriptor set objects. According to the Vulkan spec,
A descriptor set object is an opaque object that contains storage for a set of descriptors, where the types and number of descriptors is defined by a descriptor set layout. … The layout is used both for determining the resources that need to be associated with the descriptor set, and determining the interface between shader stages and shader resources.
A descriptor set layout object is defined by an array of zero or more descriptor bindings. Each individual descriptor binding is specified by a descriptor type, a count (array size) of the number of descriptors in the binding, a set of shader stages that can access the binding, …
Vulkan requires shader resource variables to have the SPIR-V DescriptorSet
and Binding
decoration, which should be “assigned and matched with the
descriptor set layout objects in the pipeline layout”. We support three
mechanisms to control DescriptorSet
number and Binding
number assignment.
Vulkan attribute assignment
You can use the [[vk::binding(X, Y)]]
attribute to specify the
descriptor set number Y
and binding number X
. The descriptor set
number Y
can be omitted; if missing, it will default to #0.
For example, the following code will assign MyTexture1
to descriptor
set #0 and binding #3, and MyTexture2
to descriptor set #1 and
binding #5.
[[vk::binding(3)]] Texture2D MyTexture1;
[[vk::binding(5, 1)]] Texture2D MyTexture2;
This way of control has the highest priority; it will overrule the other if multiple ways are used on the same variable.
The benefits of this way is explicity, but it will render the shader
incompilable with fxc.exe
, which does not support C++11 style attributes.
You may want to use #ifdef
to wrap this attribute to share the same shader
between DirectX and Vulkan. This issue will be alleviated after DXC becomes
widely adopted for DirectX. Using DXC to generate DXIL will just
ignore this attribute.
Associated counters
RW/append/consume structured buffers have associated counters. Those counters will have their own descriptors.
[[vk::counter_binding(Z)]]
can be attached to a RW/append/consume structured
buffer to specify the binding number for the associated counter to Z
.
Note that the descriptor set number of the counter is always the same as the
main buffer. If no explicit [[vk::counter_binding(Z)]]
attribute is
provided, the associated counter will just get the next unused binding number
from the main buffer’s descriptor set.
HLSL register assignment
If no Vulkan attribute is specified and the resource variable has a
:register(xX, spaceY)
annotation, the compiler will pick up information
from it and assign the resource variable to descriptor set number Y
and binding number X
.
For example, the following code will assign MyTexture1
to descriptor
set #0 and binding #3, and MyTexture2
to descriptor set #1 and
binding #5.
Texture2D MyTexture1 : register(t3);
Texture2D MyTexture2 : register(t5, space1);
This approach does not touch the shader source code, so the shader can be
shared between DirectX and Vulkan. But, we may assign the same binding
number in the same descriptor set to multiple resource variables. This is
because we have multiple register types in HLSL: s
, t
, b
, and u
.
Yet we don’t have corresponding concepts in Vulkan for register types.
Register overlap
To handle the overlap, you can use command-line options to shift the binding numbers for a particular register type:
-fvk-s-shift M N
-fvk-t-shift M N
-fvk-b-shift M N
-fvk-u-shift M N
-fvk-*-shift M N
means to shift all binding numbers gotten from register
annotations by M
in descriptor set N
. For example,
SamplerState MySampler1 : register(s5);
SamplerState MySampler2 : register(s5, space1);
Texture2D MyTexture1 : register(t5);
Texture2D MyTexture2 : register(t5, space2);
With -fvk-s-shift 10 0
and -fvk-t-shift 20 2
, we have
Variable | Descriptor Set # | Binding # |
---|---|---|
MySampler1 |
0 | 15 |
MySampler2 |
1 | 5 |
MyTexture1 |
0 | 5 |
MyTexture2 |
2 | 25 |
-fvk-*-shift
accepts a special value all
as the set number.
-fvk-*-shift M all
means shifting all sets of the given register type by M
.
The same example but with -fvk-s-shift 10 all
, we have
Variable | Descriptor Set # | Binding # |
---|---|---|
MySampler1 |
0 | 15 |
MySampler2 |
1 | 15 |
MyTexture1 |
0 | 5 |
MyTexture2 |
2 | 5 |
Default assignment
If neither Vulkan attribute nor HLSL register annotation is specified for a resource variable, the compiler will just assign the next unused binding number in descriptor set #0 to the resource.
This approach is better to be used together with SPIR-V reflection.
$Globals cbuffer
Following fxc.exe
conventions, we collect all global variables, excluding
resources, into a $Globals cbuffer. For example, for the following code:
float4 Var1;
static float3 Var2 = 1.5;
float2 Var3;
Texture2D Var4;
StructuredBuffer<float4> Var5;
We will have the following Vulkan uniform buffer:
; Name debug information
OpMemberName %type__Globals 0 "Var1"
OpMemberName %type__Globals 1 "Var3"
OpName %type__Globals "type.$Globals"
OpName %_Globals "$Globals"
; Type
%type__Globals = OpTypeStruct %v4float %v2float
%_ptr_Uniform_type__Globals = OpTypePointer Uniform %type__Globals
; Variable
%_Globals = OpVariable %_ptr_Uniform_type__Globals Uniform
This $Globals cbuffer will get the next unused binding number in descriptor set #0. There is no explicit way to control the descriptor set number and binding number assignmnet at the moment; but one may come up in the future.
Resource Memory Layout
In Vulkan, resources must be explicitly laid out. DXC right now provides three sets of memory layout rules to ease porting from other APIs.
Rules | Command-line Option | Uniform Buffer | Storage Buffer |
---|---|---|---|
Vulkan | (default) | “vector-relaxed” std140 | “vector-relaxed” std430 |
DirectX | -fvk-use-dx-layout |
fxc.exe behavior |
fxc.exe behavior |
OpenGL | -fvk-use-gl-layout |
std140 | std430 |
In the above, std140 and std430 are well defined layout rules in GLSL spec. “Vector-relaxed” std140/std430 means std140/std430 with modifications to relax packing rules for vectors: the alignment of a vector type is set to be the alignment of its element type, if not crossing 16-byte boundary. If crossing 16-byte boundary, the alignment will be set to 16 bytes. “Vector-relaxed” std140/std430 satisfies Vulkan spec Standard Uniform/Storage Buffer Layout.
DirectX layout rules enables packing data on the application side that can be shared with DirectX. Note that this is not yet officially supported by Vulkan; it just happens to work.
See the SPIR-V doc in DXC for more details.
Takeaways
- DXC supports three approaches to control resource bindings
[[vk::binding(X, Y)]]
and[[vk::counter_binding(Z)]]
:register(xX, spaceY)
with-fvk-*shift M N
- Default next unused binding number assignment
- DXC supports three sets of layout rules
- Vulkan rules: the default
- DirectX rules:
-fvk-use-dx-layout
- OpenGL rules
-fvk-use-gl-layout
-
You will need to compile DXC with
ENABLE_SPIRV_CODEGEN
to have the compiler recognize them. ↩︎