HLSL for Vulkan: Resources

2018-04-24
7 min read

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

  1. You will need to compile DXC with ENABLE_SPIRV_CODEGEN to have the compiler recognize them. ↩︎