[0050] - Dynamic resources
| Status | Under 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_tdirectly from the indexing operator. This is not possible because__hlsl_resource_tis 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_infoandheap_sampler_infodefer handle creation until that conversion.