HLSL Working Group

[0050] - Dynamic resources

StatusUnder Consideration
Author
Sponsor

Introduction

Shader Model 6.6 introduced dynamic resources, also known as bindless resources or directly indexed resources. This feature allows shaders to create resources from descriptors by directly indexing the CBV/SRV/UAV heap or the sampler heap. The CBV/SRV/UAV heap is also called the resource descriptor heap.

HLSL exposes these heaps through two built-in global objects: ResourceDescriptorHeap and SamplerDescriptorHeap. Indexing either object produces an intermediate value that can be converted to a resource or sampler object. Resources created this way do not require binding locations or root signature descriptor-table mappings.

Motivation

Dynamic resources are part of the HLSL language and we need to support them in Clang.

Current Behavior in DXC

This section documents how DXC handles dynamic resources.

Example 1 - Assigning to local resource variable

Resource instance acquired by direct indexing is usually assigned to a local resource variable:

cbuffer CB {
    uint Index;
}

[numthreads(4, 1, 1)]
void main() {
  RWBuffer<int> Buf = ResourceDescriptorHeap[Index];
  Buf[0] = 123;
}

https://godbolt.org/z/sYfbe9GEs

The same local variable can be used for different resource instances:

cbuffer CB {
    uint Index;
}

[numthreads(4, 1, 1)]
void main() {
  RWBuffer<int> Buf;
  Buf = ResourceDescriptorHeap[Index];
  Buf[0] = 123;

  Buf = ResourceDescriptorHeap[Index + 1];
  Buf[0] = 456;
}

https://godbolt.org/z/xE3P9Yzff

Example 2 - Function argument

Directly indexed resource instance can also be used as a function argument (without first assigning it to a local variable).

cbuffer CB {
    uint Index;
}

void foo(RWBuffer<int> Buf) {
  Buf[0] = 123;
}

[numthreads(4, 1, 1)]
void main() {
  foo(ResourceDescriptorHeap[Index]);
}

https://godbolt.org/z/EjaK4rxhe

Example 3 - Using directly indexed heap as a resource class

The result of directly indexing ResourceDescriptorHeap or SamplerDescriptorHeap cannot be used as a concrete resource object. That makes sense because, at that point, the descriptor’s resource type is unknown, so resource-specific members such as Load are not available.

cbuffer CB {
    uint Index;
}

[numthreads(4, 1, 1)]
void main() {
  int i = ResourceDescriptorHeap[Index].Load(10);
}

https://godbolt.org/z/ezPf8aaeb

DXC produces the following diagnostics:

error: no member named 'Load' in '.Resource'`

Example 4 - Indexing into a wrong heap does not get diagnosed

Indexing into a SamplerDescriptorHeap to get a CBV/SRV/UAV resource, or indexing into a ResourceDescriptorHeap to get a sampler resource does not produce an error. This is something the Clang implementation could improve upon.

cbuffer CB {
    uint Index;
}

[numthreads(4, 1, 1)]
void main() {
  RWBuffer<int> Buf = SamplerDescriptorHeap[Index];
  int i = Buf[0];
}

https://godbolt.org/z/fnMhbeeEr

Example 5 - Assigning to a Global Variable

The result of heap indexing cannot be used to initialize a global resource variable because DXC ignores initializers on external globals:

RWBuffer<int> GlobalBuf = ResourceDescriptorHeap[5];

[numthreads(4, 1, 1)]
void main() {
  GlobalBuf[0] = 123;
}

https://godbolt.org/z/Td7vMz1vf

DXC produces the following diagnostics:

warning: Initializer of external global will be ignored
error: Explicit load/store type does not match pointee type of pointer operand

Proposed solution

At the point of indexing into ResourceDescriptorHeap or SamplerDescriptorHeap, the compiler does not yet know the resource type with which the descriptor will be used. That type becomes known only when the result is assigned to or used to initialize a resource object, or passed as a function argument.

Therefore, indexing a heap global will not produce a resource handle directly. Instead, it will return a small internal struct that carries the heap index. Indexing ResourceDescriptorHeap returns heap_resource_info, and indexing SamplerDescriptorHeap returns heap_sampler_info. These structs will be defined in the hlsl::__detail namespace.

Using two distinct struct types lets the compiler diagnose heap mismatches during overload resolution: a sampler type can only be constructed from heap_sampler_info, and a CBV/SRV/UAV resource type can only be constructed from heap_resource_info.

Every resource class will have an implicit constructor that accepts the corresponding heap info struct and it will use this information and Clang built-in function(s) to create a concrete handle type from the heap. Standard C++ implicit conversion rules then make the assignment, initialization, and function-argument cases work without special handling in Sema. This design also supports creating multiple handles for resources with counters.

Detailed design

Frontend (Clang AST/Sema)

Heap Info Structs

The internal structs heap_resource_info and heap_sampler_info will be defined in the hlsl::__detail namespace by HLSLExternalSemaSource before any resource classes so they can then be used as arguments of the heap resource constructors. The structs will look like this:

namespace hlsl {
namespace __detail {

struct heap_resource_info {
  unsigned int Index;
};

struct heap_sampler_info {
  unsigned int Index;
};

} // namespace
} // namespace

heap_resource_info identifies a descriptor in the CBV/SRV/UAV heap and heap_sampler_info identifies a descriptor in the sampler heap. Keeping them as separate types is what allows heap mismatches to be diagnosed at the point of conversion.

Heap Globals

The global heap variables ResourceDescriptorHeap and SamplerDescriptorHeap can be declared in a new builtin header hlsl/hlsl_resources.h, which will be included from the default header hlsl.h. The types of these globals will be resource_descriptor_heap_struct and sampler_descriptor_heap_struct, which will be defined in the hlsl::__detail namespace in hlsl/hlsl_detail.h.

It would look like this:

hlsl/hlsl_detail.h:

namespace hlsl {
namespace __detail {

struct resource_descriptor_heap_struct {
  heap_resource_info operator[](uint32_t Index) {
    return heap_resource_info{Index};
  }
};

struct sampler_descriptor_heap_struct {
  heap_sampler_info operator[](uint32_t Index) {
    return heap_sampler_info{Index};
  }
};

} // namespace
} // namespace

hlsl/hlsl_resources.h:

namespace hlsl {

#define _HLSL_AVAILABILITY(platform, version)                                  \
  __attribute__((availability(platform, introduced = version)))

_HLSL_AVAILABILITY(shadermodel, 6.6)
static __detail::resource_descriptor_heap_struct ResourceDescriptorHeap;

_HLSL_AVAILABILITY(shadermodel, 6.6)
static __detail::sampler_descriptor_heap_struct SamplerDescriptorHeap;

} // namespace hlsl

Heap Constructors on Resource Types

Each resource type will get a new constructor that takes the heap info struct for the heap it is allowed to be created from. CBV/SRV/UAV resource types get a constructor that takes heap_resource_info, and sampler types get a constructor that takes heap_sampler_info. This constructor will call a new Clang builtin function __builtin_hlsl_resource_handlefromheap, passing in the heap index. Because each resource class only accepts the heap info struct for the matching heap, indexing the wrong heap fails to compile.

For example:

namespace hlsl {

template <typename T> struct RWBuffer {
  ...
public:
  RWBuffer(__detail::heap_resource_info HeapResInfo) {
    __handle = __builtin_hlsl_resource_handlefromheap(__handle, HeapResInfo.Index);
  };
};

} // namespace

The first argument of the builtin function will be used to infer the return handle type of the builtin.

For resource types with counters the constructor will use an additional builtin __builtin_hlsl_resource_counterhandlefromheap to initialize their counter handle.

Because this constructor is implicit and takes exactly one argument, all of the DXC usage patterns in the previous section will fall out of ordinary overload resolution.

CodeGen (Clang)

Clang CodeGen will lower __builtin_hlsl_resource_handlefromheap to the target-specific intrinsic llvm.[dx|spv].resource.handlefromheap(i32 Index), overloaded on the resource’s target extension handle type.

For SPIR-V targets, CodeGen will lower __builtin_hlsl_resource_counterhandlefromheap to llvm.spv.resource.counterhandlefromheap. DirectX does not represent the counter as a separate descriptor, so CodeGen will instead return the main resource handle unchanged. This matches the existing handling of counter handles created from implicit and explicit bindings.

Backend (LLVM)

The DirectX and SPIR-V backends will gain the new intrinsic llvm.[dx|spv].resource.handlefromheap. The intrinsic will take a heap index and return a value of the concrete resource target type. Whether the descriptor comes from the sampler heap or the CBV/SRV/UAV heap is determined by the resource target type, so no separate heap-selection flag is needed.

SPIR-V will also support llvm.spv.resource.counterhandlefromheap which will take a heap index and will create a handle for the resource counter.

Passes that currently identify resource-handle creation by looking for llvm.[dx|spv].resource.handlefrombinding will need to be updated to also recognize the corresponding *.handlefromheap intrinsic calls.

Existing passes assume that every resource has a binding and use that binding to distinguish instances of the same resource type. Resources created from a descriptor heap have no binding, so resource analysis must be extended to support binding-less resources and distinguish their instances by heap index value instead.

The following LLVM passes will need to be updated:

DXILResourceAnalysis

The analysis will handle each call to llvm.dx.resource.handlefromheap by creating a binding-less ResourceInfo. The Binding member of ResourceInfo will become optional, and a new HeapIndexID field will distinguish heap resource instances. The heap-index operand of llvm.dx.resource.handlefromheap is a Value *. DXILResourceAnalysis will assign a unique HeapIndexID to each distinct heap-index value.

Heap resources do not participate in binding ID assignment, so setBindingID will be called only for resources that have a binding. To reflect this meaning, RecordID should be renamed to BindingID.

DXILRemoveUnusedResources

The pass should also remove unused resources created by llvm.dx.resource.handlefromheap.

DXILTranslateMetadata and DXILPrettyPrinter

DXILTranslateMetadata will neither create symbols for heap resources nor emit entries for them in the SRV, UAV, CBuffer, or Sampler metadata lists. DXILPrettyPrinter will omit heap resources from the resource bindings table that is printed as a comment in the LLVM assembly.

DXContainerGlobals

The pass will skip resources without bindings when emitting PSV resource entries.

SPIR-V passes

The pass SPIRVLegalizeImplicitBinding will be extended to handle heap resources (it will be renamed SPIRVLegalizeImplicitAndHeapBinding). The pass will scan the module for calls to llvm.spv.resource.handlefromheap and group them according to whether they create CBV/SRV/UAV resources or samplers. Calls to llvm.spv.resource.counterhandlefromheap will form a third group.

Unlike DXIL, SPIR-V does not natively support descriptor heaps. Instead, the backend represents them as unbounded resource arrays. After resolving other implicit bindings, the pass assigns the first available binding to each heap group in this order: CBV/SRV/UAV resources, samplers, and counters.

For each group, the pass will replace the heap intrinsic calls with their explicit handlefrombinding equivalents using the assigned binding. The pass does not need to create the unbounded resource-array globals itself. It only needs to assign a unique array name shared by all resources belonging to the same array; the existing SPIRVInstructionSelector will create the globals.

Because the CBV/SRV/UAV group can contain resources of different types, these resources must be represented by separate arrays, one for each unique resource type. All of these arrays will use the same binding and therefore overlap.

DXIL Lowering

For DirectX targets, llvm.dx.resource.handlefromheap will lower to a dx.op.createHandleFromHeap operation followed by dx.op.annotateHandle. This matches the structure of the existing createHandleFromBinding lowering.

dx.op.createHandleFromHeap takes three operands: the heap index, a flag that selects the sampler heap, and a flag that indicates whether the index is non-uniform. The sampler-heap flag is derived from the resource target type being created (true for sampler types, false otherwise). The lowering will compute the non-uniform flag using the existing hasNonUniformIndex helper.

For example, the intrinsic call

%typed = call target("dx.TypedBuffer", <4 x float>, 1, 0, 0)
    @llvm.dx.resource.handlefromheap.tdx.TypedBuffer_v4f32_1_0_0(i32 3)

will lower to

%0 = call %dx.types.Handle @dx.op.createHandleFromHeap(i32 218, i32 3, i1 false, i1 false)
%1 = call %dx.types.Handle @dx.op.annotateHandle(i32 216, %dx.types.Handle %0,
         %dx.types.ResourceProperties { i32 4106, i32 1033 })

SPIR-V Lowering

TBD

Shader Flags

Descriptor-heap usage must be recorded in the shader feature flags. The DXILShaderFlags pass will inspect each llvm.dx.resource.handlefromheap call and set SamplerDescriptorHeapIndexing when the resulting resource target type is a sampler, or ResourceDescriptorHeapIndexing otherwise.

Alternatives considered

  • Return __hlsl_resource_t directly from the indexing operator. This is not possible because __hlsl_resource_t is parameterized by the resource class, contained type, and other type attributes that are unknown at the point of heap indexing. The concrete handle type becomes known only when the indexed value is converted to a specific resource type. heap_resource_info and heap_sampler_info defer handle creation until that conversion.

Acknowledgments