diff --git a/proposals/0046-constantbuffer-t.md b/proposals/0046-constantbuffer-t.md new file mode 100644 index 0000000..f748445 --- /dev/null +++ b/proposals/0046-constantbuffer-t.md @@ -0,0 +1,412 @@ +--- +title: "[0046] - HLSL ConstantBuffer Implementation" +params: + authors: + - s-perron: Steven Perron + status: Under Consideration + sponsors: + - s-perron: Steven Perron +--- + +* PRs: [#193237](https://github.com/llvm/llvm-project/pull/193237) + +## Introduction + +This proposal details the implementation of the `ConstantBuffer` resource +type in Clang and LLVM for HLSL. Unlike the legacy `cbuffer` keyword, where each +member of the `cbuffer` becomes its own global variable, `ConstantBuffer` +behaves as a standard type, supporting instantiation, arrays, function +parameters, and assignments. The `ConstantBuffer` type acts more like other +resource type than it does a `cbuffer`. The unique aspect of the +`ConstantBuffer` type is that it can be used as a drop in replacement for +`T`, as if `ConstantBuffer` inherited from `T`. However, it is not really +inheritance. + +## Motivation + +HLSL developers require `ConstantBuffer` as it is part of the language +standard, and it is used. + +## Proposed solution + +We propose implementing `ConstantBuffer` as a built-in template class that +provides an implicit conversion to a reference of type `T` in the +`hlsl_constant` address space. To maintain compatibility with DXC, which allowed +calling non-const member functions on `ConstantBuffer` objects, the conversion +operator will return a non-const reference (`hlsl_constant T &`) in HLSL 202x. +Starting with HLSL 202y, the operator will return a const reference +(`const hlsl_constant T &`) to enforce proper const-correctness. Developers can +use `clang-tidy` to identify and mark member functions as `const` to prepare for +this transition. To ensure a seamless developer experience, `Sema` will be +modified to automatically inject this conversion when accessing members of a +`ConstantBuffer`. + +Places with HLSL specific handling for aggregates will also be updated to +convert the `ConstantBuffer` to `T` when necessary. + +### Frontend (Clang AST/Sema) + +1. **Built-in Template:** Define `ConstantBuffer` in + `HLSLExternalSemaSource.cpp` as a template class containing a single + `__hlsl_resource_t` member (the handle). The handle's contained type is + exactly `T`. +2. **Implicit Conversion Operator:** Define an implicit conversion operator + `operator hlsl_constant T &() const` within the `ConstantBuffer` + template. +3. **Sema Member Lookup Interception:** Modify `Sema::LookupMemberExpr` (in + `SemaExprMember.cpp`) to detect member accesses on `ConstantBuffer`. If + detected, Sema will inject a call to the implicit conversion operator, + effectively transforming `cb.field` into `((hlsl_constant T &)cb).field`. +4. **Sema Constraints:** Enforce that `T` must be a user-defined struct or + class, and reject primitive types, vectors, arrays, or matrices as `T`. This + is implemented using a C++20 concept constraint named + `__is_constant_buffer_element_compatible` applied to the `ConstantBuffer` + template declaration in `HLSLExternalSemaSource.cpp`. This concept evaluates + a new built-in type trait + `__builtin_hlsl_is_constant_buffer_element_compatible` to verify that `T` is + a complete struct/class (not a union) and does not contain any intangible + types. + +### CodeGen (Clang) + +1. **Handle Types:** Emit instances of `ConstantBuffer` using the + appropriate target types. +2. **Conversion Operator Implementation:** The conversion operator is + implemented using a new overload of `__builtin_hlsl_resource_getpointer` + that does not take an index. +3. **Intrinsic Redirection:** In CodeGen, this builtin lowers to a new target + intrinsic `llvm.[dx|spv].resource.getbasepointer` intrinsic. Unlike the + version used for buffers, this overload does not take an offset index, as + `ConstantBuffer` always represents a single instance of `T` at the start of + the buffer. +4. **Pointer Address Space:** The resulting pointer targets the appropriate + constant/uniform address space (`addrspace(2)` for DXIL, `addrspace(12)` for + SPIR-V). + +### Backend (LLVM) + +**New Intrinsic:** The new intrinsic `llvm.[dx|spv].resource.getbasepointer` +will have to be implemented in the DirectX and SPIR-V backends. This intrinsic +will be the same as `llvm.[dx|spv].resource.getpointer` except it does not have +to index into the struct contained in the resource. For SPIR-V, this will simply +return the result of the `OpVariable` instruction that defines the resource. For +DirectX, the backend will translate this new `getbasepointer` intrinsic and the +subsequent `getelementptr` and `load` operations into the appropriate +`dx.op.cbufferLoadLegacy` or `dx.op.cbufferLoad` operations. + +## Detailed design + +### Built-in Class Definition + +The `ConstantBuffer` type is defined in the compiler conceptually as the +following C++ template class: + +```cpp +template +class ConstantBuffer { + // Underlying handle with resource class CBuffer and contained type T + __hlsl_resource_t [[hlsl::resource_class(CBuffer)]] [[hlsl::contained_type(T)]] __handle; + +public: + // Implicit conversion to reference of type T (non-const for HLSL 202x) + operator hlsl_constant T&() const { + return *__builtin_hlsl_resource_getpointer(__handle); + } + + // Copy operations copy the handle, not the underlying data + ConstantBuffer(const ConstantBuffer& other) { + __handle = other.__handle; + } + + ConstantBuffer& operator=(const ConstantBuffer& other) { + __handle = other.__handle; + return *this; + } +}; +``` + +### Usage Examples and AST Generation + +#### 1. Member Access + +When an HLSL developer accesses a member directly from the `ConstantBuffer`, +`Sema` intercepts the member lookup and injects a call to the implicit +conversion operator. + +```hlsl +struct S { float a; float b; }; +ConstantBuffer cb; +float main() { + return cb.a; +} +``` + +**AST Structure:** + +```text +`-MemberExpr 'float' lvalue .a + `-CXXMemberCallExpr 'hlsl_constant S' lvalue + `-MemberExpr '' .operator hlsl_constant S & + `-ImplicitCastExpr 'const hlsl::ConstantBuffer' lvalue + `-DeclRefExpr 'cb' +``` + +#### 2. Local Assignment + +TODO: This needs to be updated based on the implementation in +https://github.com/llvm/llvm-project/pull/190089. That PR will disable implicit +copy constructors. + +Assigning a `ConstantBuffer` to a local variable of type `T` triggers the +implicit conversion operator, followed by `T`'s standard copy constructor. + +```hlsl +S local = cb; +``` + +**AST Structure:** + +```text +`-CXXConstructExpr 'S' 'void (const S &)' + `-ImplicitCastExpr 'const S' lvalue + `-ImplicitCastExpr 'S' lvalue + `-CXXMemberCallExpr 'hlsl_constant S' lvalue + `-MemberExpr '' .operator hlsl_constant S & + `-ImplicitCastExpr 'const hlsl::ConstantBuffer' lvalue + `-DeclRefExpr 'cb' +``` + +#### 3. Function Parameters + +TODO: This needs to be updated based on the implementation in +https://github.com/llvm/llvm-project/pull/190089. That PR will disable implicit +copy constructors. + +Passing a `ConstantBuffer` to a function expecting `T` invokes the implicit +conversion. Passing it to a function expecting `ConstantBuffer` invokes the +handle-copying constructor. + +```hlsl +void takes_s(S s) {} +void takes_cb(ConstantBuffer c) {} + +void test() { + takes_s(cb); // Calls operator hlsl_constant S&() and copies data into argument + takes_cb(cb); // Calls ConstantBuffer(const ConstantBuffer&) and copies handle +} +``` + +#### 4. Array Indexing + +For arrays of `ConstantBuffer`, the subscript operator first resolves the +handle, and then the implicit conversion occurs on the indexed element. This +code will be handled the same way that other resource arrays are handled. No +specific code changes are necessary. + +```hlsl +ConstantBuffer cb_arr[2]; +float f = cb_arr[1].a; +``` + +#### 5. Template Support + +`ConstantBuffer` can be passed to templates. Because `Sema` intercepts member +access on the `ConstantBuffer` type itself, template functions that perform +member access on deduced `ConstantBuffer` types will work correctly. + +```hlsl +template +void foo(Tm t) { + float f = t.a; // Works even if Tm is ConstantBuffer +} + +void test() { + foo(cb); // Tm is deduced as ConstantBuffer +} +``` + +In the primary template `foo`, the expression `t.a` is represented as a +`CXXDependentScopeMemberExpr` because the type of `t` is dependent: + +```text +`-CXXDependentScopeMemberExpr '' lvalue .a + `-DeclRefExpr 'Tm' lvalue ParmVar 't' 'Tm' +``` + +When `foo` is instantiated as `foo>`, Clang's template +instantiation mechanism rebuilds the member expression. Since the type of `t` is +now known to be `ConstantBuffer`, the standard member lookup logic in +`Sema::LookupMemberExpr` is triggered. Our interception logic then identifies +`ConstantBuffer` and injects the call to the implicit conversion operator +`operator hlsl_constant S&()`, resulting in the same AST structure as +non-templated member access: + +```text +`-MemberExpr 'float' lvalue .a + `-CXXMemberCallExpr 'hlsl_constant S' lvalue + `-MemberExpr '' .operator hlsl_constant S & + `-ImplicitCastExpr 'const hlsl::ConstantBuffer' lvalue + `-DeclRefExpr 't' 'hlsl::ConstantBuffer' +``` + +#### 6. Unsupported Types + +`ConstantBuffer` enforces the `__is_constant_buffer_element_compatible` +constraint. This constraint ensures that `T` is a valid struct or class and does +not contain any intangible types, such as other resource handles. Attempting to +instantiate a `ConstantBuffer` with a type that contains a resource (e.g., +`RWBuffer`, `StructuredBuffer`) will result in a compile-time error. + +```hlsl +struct S { RWBuffer buf; }; +// Error: constraints not satisfied for class template 'ConstantBuffer' +ConstantBuffer cb; +``` + +This ensures that `ConstantBuffer` remains a "drop-in" replacement for `T` +even in generic code, as the transformation happens seamlessly during template +instantiation. + +### CodeGen and LLVM IR + +When Clang emits LLVM IR for the `operator hlsl_constant T&()` conversion, it +utilizes the `llvm.dx.resource.getbasepointer` (or +`llvm.spv.resource.getbasepointer`) intrinsic to retrieve an address space +qualified pointer. + +#### DXIL Example + +For the member access `cb.a`, the target pointer is in `addrspace(2)` (the DXIL +constant address space). + +```llvm +; The handle type +%"class.hlsl::ConstantBuffer" = type { target("dx.CBuffer", %struct.S) } + +; 1. The implicit conversion resolves to the getbasepointer intrinsic +%handle = load target("dx.CBuffer", %struct.S), ptr %cb, align 4 +%base_ptr = call ptr addrspace(2) @llvm.dx.resource.getbasepointer.p2.tdx.CBuffer_s_Sst(target("dx.CBuffer", %struct.S) %handle) + +; 2. The MemberExpr applies a GEP +%gep = getelementptr inbounds %struct.S, ptr addrspace(2) %base_ptr, i32 0, i32 0 + +; 3. Finally, the value is loaded +%val = load float, ptr addrspace(2) %gep, align 4 +``` + +#### SPIR-V Example + +For SPIR-V (Vulkan), the handle is a `spirv.VulkanBuffer`, and the uniform +pointer is in `addrspace(12)`. + +```llvm +; The handle type +%"class.hlsl::ConstantBuffer" = type { target("spirv.VulkanBuffer", %struct.S, 2, 0) } + +; 1. Pointer acquisition +%handle = load target("spirv.VulkanBuffer", %struct.S, 2, 0), ptr %cb, align 8 +%base_ptr = call ptr addrspace(12) @llvm.spv.resource.getbasepointer.p12.tspirv.VulkanBuffer_s_Sst(target("spirv.VulkanBuffer", %struct.S, 2, 0) %handle) + +; 2. GEP and load +%gep = getelementptr inbounds %struct.S, ptr addrspace(12) %base_ptr, i32 0, i32 0 +%val = load float, ptr addrspace(12) %gep, align 4 +``` + +## Alternatives considered + +### Reusing `getpointer` with index 0 + +We considered reusing the existing `llvm.[dx|spv].resource.getpointer` intrinsic +with an index of `0`. However, for this to work semantically in SPIR-V, we would +have to wrap `T` in another struct when defining the `Vulkan.buffer` type +because the `0` has a meaning to index into the type. We believe this cannot be +cleanly implemented. Attempting to wrap T when adding the handle to the +`ConstantBuffer` quickly turned into having many special cases. It couldn't be +done when lowering the type of the handle to the SPIR-V target extension type +because we cannot distinguish between a `cbuffer` and `ConstantBuffer` at that +point, and we should be wrapping only one of them. The cleanest solution is to +avoid using an index entirely. + +### Reusing the Legacy `cbuffer` Model + +In the legacy model, members of a `cbuffer` are emitted as individual global +variables in the `hlsl_constant` address space. These globals are then linked to +a resource handle via metadata (`!hlsl.cbs`). + +While this works for flat, global `cbuffer` declarations, it is fundamentally +incompatible with `ConstantBuffer` for several reasons: + +- **Encapsulation:** In `ConstantBuffer`, members are scoped within the + template instance. Treating them as global variables would violate this + scoping and require complex name mangling and metadata schemes. +- **Dynamic Handles:** Metadata-based linking is static. In modern HLSL, + `ConstantBuffer` can be indexed (arrays) or passed as parameters, meaning + the handle is often dynamic and cannot be linked to a global variable at + compile time via static metadata. +- **Complexity:** Reusing the legacy model for a modern resource type would + require significant "backward" modifications to Clang's resource handling, + whereas the proposed pointer-based model aligns with how other modern + resources (like `RWBuffer`) are implemented. + +### `ConstantBuffer` inheriting from `T` + +A significant alternative considered was to have `ConstantBuffer` inherit +from `T`. This would allow standard C++ member lookup and implicit conversions +(via slicing) to work "out of the box" in Sema. + +However, this approach was rejected for several technical and architectural +reasons: + +- **AST Inaccuracy:** The AST would imply that a `ConstantBuffer` _is_ a `T`, + which is not physically true. `ConstantBuffer` is a small wrapper around a + resource handle; it does not contain the data of `T` inline. +- **Memory Layout:** Inheritance would force the AST's + `sizeof(ConstantBuffer)` to be at least `sizeof(T)`. This bloat is + misleading and could lead to bugs in parts of the compiler that rely on + accurate record sizes (e.g., alignment, padding, or future features). +- **Special Case Overload:** To achieve correct CodeGen, we would still need to + intercept `DerivedToBase` casts to prevent the compiler from attempting to + access the (non-existent) base class data via standard pointer arithmetic. + This effectively trades one type of Sema/CodeGen hack for another, more + confusing one. +- **Maintainability:** Creating a "fake" inheritance relationship introduces a + fundamental lie into the AST that every future compiler developer would have + to be aware of and handle as a special case. The proposed implicit conversion + model is more honest and follows standard C++ patterns for wrapper types. + +## Open questions + +1. **Layout Consistency:** What needs to be done to ensure that the struct `T` + used in `ConstantBuffer` is laid out correctly? It must match the layout + rules of the legacy `cbuffer`. +2. **Address Space Conversions:** How will different address spaces affect the + implicit conversions? Will we need multiple conversion operators to handle + different target address spaces or cv-qualifiers? + +**Solution:** All HLSL address spaces, including `hlsl_constant`, will be made a +subspace of the `hlsl_generic` address space. This is the same mechanism used to +allow member functions to be called on objects in any address space. See +[0021 - Allowing multiple address spaces for the `this` pointer](0021-this-address-space.md). + +3. **Const correctness:** Should the conversion operator return a `const` + reference for HLSL 202x? Returning a `const` reference would provide earlier, + clearer error messages when attempting to write to a `ConstantBuffer`. + However, this could require significant code refactoring for existing + projects. If we delay this change until HLSL 202y, developers can leverage + `clang-tidy` to identify and mark member functions as `const`, facilitating a + smoother transition. We should discuss whether the benefits of enforcing + const-correctness earlier justify the potential for increased migration + effort. + +4. **ConstantBuffer where T contains a resource:** Should we support nesting + `ConstantBuffer` types or other resources? DXC has limited support for + defining a resource in a type `T` and using it in a `ConstantBuffer`, but it + fails when the `ConstantBuffer` is in an array and has never been supported + for SPIR-V. The current proposal does not allow this, as discussed in + [Unsupported Types](#6-unsupported-types). + +## Acknowledgments + +Special thanks to the HLSL working group for examining the limitations of the +legacy cbuffer design and refining the inheritance and pointer-based codegen +model.