[0023] - Representing counter variables for typed buffers
| Status | Design In Progress |
|---|---|
| Author |
Introduction
In HLSL, RWStructuredBuffer, AppendStructuredBuffer, and ConsumeStructuredBuffer buffer types have two associated buffers: primary storage (an array of T) and a 32-bit integer counter that can be atomically incremented or decremented.
In DirectX, the counter and main storage share a binding and are closely tied. However, SPIR-V lacks the flexibility to represent both with a single binding. Consequently, DXC represents the counter resource as a separate resource with its own binding, allowing flexible counter allocation at the cost of a separate set and binding.
We propose that Clang represent typed buffers that may have a counter as a class with two handles: one for main storage and one for the counter. This makes it explicit that they are separate resources.
Motivation
The counter variables are a core feature in HLSL and must be represented.
Proposed solution
Access to resources occurs through handles stored in a static global variable. In SPIR-V, a resource and its counter are represented by distinct handles, while in DXIL they share the same handle. We propose that the IR representation adopt the more explicit SPIR-V model, representing the main buffer and its counter with two separate handles. The DXIL backend will then be responsible for merging these into a single handle.
We propose that RWStructuredBuffer objects, and others with a counter, be
modified to include two separate handles: one for main storage (bufferHandle)
and one for the counter (counterHandle). The bufferHandle is initialized as
usual via a call to llvm.dx.resource.handlefrom*binding or
llvm.spv.resource.handlefrom*binding, depending on the target.
To establish an explicit link between the resource and its counter, we propose introducing two new SPIR-V specific intrinsics:
llvm.spv.resource.counterhandlefromimplicitbinding(ResourceHandle, order_id, space_id)llvm.spv.resource.counterhandlefrombinding(ResourceHandle, binding_id, space_id)
For DirectX, the counter handle is an alias for the main resource handle. Clang’s code generation will handle this by simply copying the main resource handle.
The counterHandle will be initialized by calling one of these intrinsics,
passing the bufferHandle of the main storage as the first argument. This makes
the relationship explicit in the IR, which simplifies SPIR-V code generation for
the CounterBuffer decoration. For the DXIL backend, this allows it to
recognize that the counter handle is an alias for the main buffer handle and
merge them accordingly. For cases where the counter binding needs to be
explicitly specified, a [[vk::counter_binding]] attribute will be available.
Detailed design
The implementation will introduce a two-handle model directly into the resource class definition. This will be achieved through a combination of new AST nodes, Sema actions, and code generation logic.
AST and Type Representation
Two-Member Struct: Resource types that support counters (e.g.,
RWStructuredBuffer) will be defined as structs containing two handle members:__handlefor the primary data and__counter_handlefor the counter.IsCounterAttribute: To differentiate the types of the two handles, a new attribute,[[hlsl::is_counter]], will be introduced. The type of the__counter_handlemember will be the same as the type for__handleexcept it will be annotated with this attribute. This will be tracked in the AST by adding anIsCounterflag toHLSLAttributedResourceType::Attributes. By making the counter handle’s type identical to the main resource handle’s type, distinguished only by this attribute, we enable flexible and correct code generation. Clang’s code generation for each target (clang/lib/CodeGen/Targets/) will be responsible for interpreting this attribute. The implementation for the DirectX target can ignore this attribute, resulting in the same target type for both handles, which aligns with its single-handle model. Conversely, the implementation for the SPIR-V target can detect this attribute and generate a distinct and appropriate target type for the counter (e.g., a buffer ofi32), fitting its separate-resource model.Sema and Builtin Construction:
- In
HLSLExternalSemaSource.cpp, thesetupBufferTypefunction will be modified. For UAVs that can have counters, it will calladdCounterHandleMemberin theBuiltinTypeDeclBuilder. addCounterHandleMemberwill create the__counter_handlefield and apply the necessary attributes, includingHLSLIsCounterAttr, to its type.
- In
Counter Operations: Methods like
IncrementCounterandDecrementCounterwill be modified inHLSLBuiltinTypeDeclBuilder.cppto operate on the__counter_handlemember instead of the__handlemember, directing the atomic operations to the correct resource.
Counter Binding with [[vk::counter_binding]]
To allow for explicit control over counter bindings, a new attribute,
[[vk::counter_binding(binding)]], will be introduced. This attribute will be
represented internally by HLSLVkCounterBindingAttr, which is modeled after
HLSLVkBindingAttr and is used for explicit bindings only.
Implicit bindings for both the resource and its counter will be stored in the
HLSLResourceBindingAttr. This attribute will be extended to hold an optional
implicit counter_order_id for the counter, in addition to the resource’s own
order_id. If a resource has a counter but no explicit
[[vk::counter_binding]] attribute, Sema will assign an implicit binding
counter_order_id for the counter and store it in the
HLSLResourceBindingAttr. If a [[vk::counter_binding]] is present, it
signifies an explicit binding, and HLSLResourceBindingAttr will not store a
counter_order_id. This approach centralizes all implicit binding information
in one place and maintains a clear distinction between implicit and explicit
bindings, mirroring the existing behavior for resource bindings.
Initialization and Binding
The core of this design lies in how these two handles are initialized. In Sema, new static methods will be added to the resource class to initialize them.
New Static Methods: Four new static methods will be added to counter-enabled resource classes to handle all combinations of implicit and explicit bindings for the main resource and its counter:
__createFromBindingWithImplicitCounter(unsigned registerNo, unsigned spaceNo, int range, unsigned index, const char *name, unsigned counterOrderId):Creates a resource with an explicit binding for the main buffer and an implicit binding for its counter.
__createFromImplicitBindingWithImplicitCounter(unsigned orderId, unsigned spaceNo, int range, unsigned index, const char *name, unsigned counterOrderId):Creates a resource with implicit bindings for both the main buffer and its counter.
__createFromBindingWithCounter(unsigned registerNo, unsigned spaceNo, int range, unsigned index, const char *name, unsigned counterRegisterNo):Creates a resource with an explicit binding for the main buffer and an explicit binding for its counter. The counter will be in the same space as the main buffer.
__createFromImplicitBindingWithCounter(unsigned orderId, unsigned spaceNo, int range, unsigned index, const char *name, unsigned counterRegisterNo):Creates a resource with an implicit binding for the main buffer and an explicit binding for its counter. The counter will be in the same space as the main buffer.
Sema Logic: In
SemaHLSL.cpp, theinitGlobalResourceDeclfunction will check if a resource type has a second field with a handle type that has[[hlsl::is_counter]]attribute. If it does, it will emit a call to the appropriate...WithCounterstatic method instead of the regular creation methods.Handle Creation: The
...With*Countermethods, defined inclang/lib/Sema/HLSLBuiltinTypeDeclBuilder.cpp, will initialize the two handles using a combination of existing and new built-in functions defined inclang/include/clang/Basic/Builtins.td.- The
__handle(main data) will be initialized using the existing__builtin_hlsl_resource_handlefrombindingor__builtin_hlsl_resource_handlefromimplicitbindingbuilt-ins. - To initialize the
__counter_handle, new built-in functions will be introduced:__builtin_hlsl_resource_counterhandlefromimplicitbindingand__builtin_hlsl_resource_counterhandlefrombinding. These built-ins will take the main resource handle (__handle) as an argument, along with the counter’s binding information. - During Clang’s code generation, these new built-ins will be lowered to
their corresponding target-specific LLVM intrinsics for SPIR-V
(
llvm.spv.resource.counterhandle...). For DirectX, since the counter shares the same handle as the main resource, the built-in will be replaced by a simple copy of the main resource handle. This approach correctly models the relationship between the main resource and its counter directly in the IR, as described in the “Proposed solution”.
- The
Array Handling
For arrays of resources, the counter binding information is stored on the array
declaration itself. If an explicit [[vk::counter_binding]] is used, it will be
stored as an HLSLVkCounterBindingAttr. If the counter binding is implicit, its
counter_order_id will be stored in the HLSLResourceBindingAttr of the array
declaration. When Sema acts on the variable declaration, if the resource type
has a counter and no explicit [[vk::counter_binding]] attribute is present, it
will add an implicit counter counter_order_id to the
HLSLResourceBindingAttr.
When an array element is initialized with a call to a __createHandle...
function during CodeGen, the appropriate ...With*Counter version of the create
function will be called for resources that have a counter.
When the SPIR-V backend encounters a llvm.spv.resource.counterhandlefrom...
intrinsic, it will use the main resource handle to access the array size, index,
and name. This information is then used to construct the counter resource. This
approach avoids duplicating information and ensures that the counter resource is
correctly associated with its main resource.
LLVM IR Generation and Backend Handling
SPIR-V Target Type Generation: In
clang/lib/CodeGen/Targets/SPIR.cpp, thegetHLSLTypefunction will check for theIsCounterflag on theHLSLAttributedResourceType. If the flag is present, it will generate atarget("spirv.VulkanBuffer", ...)in theStorageBufferstorage class with ani32element type, correctly representing the counter as a 32-bit integer buffer in SPIR-V.SPIR-V Backend Intrinsic Handling: The SPIR-V backend in LLVM will be updated to recognize the
llvm.spv.resource.counterhandlefrom...intrinsics. When it encounters one, it will generate a new, distinctOpVariablefor the counter buffer. It will then use the information from the intrinsic (linking the main handle to the counter handle) to emit anOpDecorateinstruction with theCounterBufferdecoration, pointing from the main buffer’sOpVariableto the newly created counter buffer’sOpVariablewhen necessary. The backend will use the main resource handle to access the array size, index, and name for the counter resource. The name of the counter variable will be generated by appending “_counter” to the name of the main resource.
Alternatives considered
Independent llvm.spv.resource.handlefrombinding calls
The initial proposal was to initialize both the main buffer handle and the
counter handle with separate calls to llvm.spv.resource.handlefrombinding (or
the spv equivalent). This would have treated them as two entirely independent
resources from the point of view of the frontend.
However, this approach is problematic for both DXIL and SPIR-V: 1. Impossible
to Unify Calls for DXIL: The original idea suggested the two
llvm.spv.resource.handlefrombinding calls could be identical. This is not
feasible. A resource like RWStructuredBuffer<T> MyBuffer : register(u0) has an
explicit binding and the name “MyBuffer”. Its counter, however, has an implicit
binding assigned by the compiler and a different name (e.g.,
“MyBuffer_counter”). Because the binding points and names are different, the
arguments to their respective llvm.spv.resource.handlefrombinding calls must
also be different, making them impossible to common. 2. Lost Connection:
Treating the counter as a completely separate resource severs the explicit link
between the main buffer and its counter in the IR. For DXIL, this makes it
difficult to know that the two resources are related and share the same
underlying handle. For SPIR-V, this link is required to emit the CounterBuffer
decoration on the main buffer, pointing to the counter. Reconstructing this
relationship in the backend would be complex and rely on fragile naming
conventions.
Multiple binding numbers on llvm.spv.resource.handlefrombinding
One possible solution is to incorporate multiple binding locations into
llvm.spv.resource.handlefrombinding. This would allow the SPIR-V backend to
expand the call into multiple resources. However, this solution is not ideal
because it would necessitate the creation of an ad hoc target type within the
SPIR-V backend.
In contrast, the proposed solution only requires a single target type to represent a generic Vulkan buffer. This type could potentially be reused by any language targeting Vulkan.
Including the binding for the counter in the handle type.
The inclusion of the counter’s binding number in the type returned by
llvm.spv.resource.handlefrombinding was another potential solution. However,
this solution is not feasible due to resource aliases. As an example, it would
be impossible to determine the type for a RWStructuredBuffer function parameter
because it lacks a single binding location.