-
Notifications
You must be signed in to change notification settings - Fork 21
[0046] Add proposal for HLSL ConstantBuffer<T> #413
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
0b024db
b992b9d
accb1db
131f186
2f98a33
404764a
a786a0b
b6d80e9
d91b3ce
823163b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,348 @@ | ||
| --- | ||
| title: "[NNNN] - HLSL ConstantBuffer<T> Implementation" | ||
| params: | ||
| authors: Steven Perron | ||
| - github_username: s-perron | ||
| status: Under Consideration | ||
| sponsors: Steven Perron | ||
| - github_username: s-perron | ||
| --- | ||
|
|
||
| ## Introduction | ||
|
|
||
| This proposal details the implementation of the `ConstantBuffer<T>` 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<T>` | ||
| behaves as a standard type, supporting instantiation, arrays, function | ||
| parameters, and assignments. The `ConstantBuffer<T>` type acts more like other | ||
| resource type than it does a `cbuffer`. The unique aspect of the | ||
| `ConstantBuffer<T>` type is that it can be used as a drop in replacement for | ||
| `T`, as if ConstantBuffer<T> inherited from `T`. However, it is not really | ||
|
s-perron marked this conversation as resolved.
Outdated
|
||
| inheritance. | ||
|
|
||
| ## Motivation | ||
|
|
||
| HLSL developers require `ConstantBuffer<T>` as it is part of the language | ||
| standard, and it is used. | ||
|
|
||
| ## Proposed solution | ||
|
|
||
| We propose implementing `ConstantBuffer<T>` as a built-in template class that | ||
| provides an implicit conversion to `const hlsl_constant T &`. 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<T>` to `T` when necessary. | ||
|
|
||
| ### Frontend (Clang AST/Sema) | ||
|
|
||
| 1. **Built-in Template:** Define `ConstantBuffer<T>` 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 const hlsl_constant T &() const` within the `ConstantBuffer<T>` | ||
| template. | ||
| 3. **Sema Member Lookup Interception:** Modify `Sema::LookupMemberExpr` (in | ||
| `SemaExprMember.cpp`) to detect member accesses on `ConstantBuffer<T>`. If | ||
| detected, Sema will inject a call to the implicit conversion operator, | ||
| effectively transforming `cb.field` into | ||
| `((const 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`. | ||
|
s-perron marked this conversation as resolved.
Outdated
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about methods? Should we start enforcing those have a const
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As I wrote the implementation, your example would be rejected because the member expression would add a cast to However that is a good example that I need to add. If DXC allowed call to non-const member function, should we allow them too? At what point do we issue an error? Note that your example fails when generating DXIL. I'm not sure why it works for spir-v. We probably have a bad optimization removing the invalid store because the spir-v is invalid.
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we had a couple issues open in DXC for this weirdness,IMO the fact dxc allows this is an oversight, and we should not support this. Maybe something we can specify in hlsl 202x
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I left this as a open question. We should get more people to chime in on. With a good compiler error it might not be too hard for devs to add |
||
|
|
||
| ### CodeGen (Clang) | ||
|
|
||
| 1. **Handle Types:** Emit instances of `ConstantBuffer<T>` 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<T>` type is defined in the compiler conceptually as the | ||
| following C++ template class: | ||
|
|
||
| ```cpp | ||
| template <typename T> | ||
| 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 const reference of type T | ||
| operator const hlsl_constant T&() const { | ||
| return (const hlsl_constant T&)__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<S> cb; | ||
| float main() { | ||
| return cb.a; | ||
| } | ||
| ``` | ||
|
|
||
| **AST Structure:** | ||
|
|
||
| ```text | ||
| `-MemberExpr 'float' lvalue .a | ||
| `-CXXMemberCallExpr 'const hlsl_constant S' lvalue | ||
| `-MemberExpr '<bound member function type>' .operator const hlsl_constant S & | ||
| `-ImplicitCastExpr 'const hlsl::ConstantBuffer<S>' lvalue <NoOp> | ||
| `-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 | ||
| conversions. | ||
|
s-perron marked this conversation as resolved.
Outdated
|
||
|
|
||
| Assigning a `ConstantBuffer<T>` 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 <NoOp> | ||
| `-ImplicitCastExpr 'S' lvalue <UserDefinedConversion> | ||
| `-CXXMemberCallExpr 'const hlsl_constant S' lvalue | ||
| `-MemberExpr '<bound member function type>' .operator const hlsl_constant S & | ||
| `-ImplicitCastExpr 'const hlsl::ConstantBuffer<S>' lvalue <NoOp> | ||
| `-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 | ||
| conversions. | ||
|
|
||
| Passing a `ConstantBuffer<T>` to a function expecting `T` invokes the implicit | ||
| conversion. Passing it to a function expecting `ConstantBuffer<T>` invokes the | ||
| handle-copying constructor. | ||
|
|
||
| ```hlsl | ||
| void takes_s(S s) {} | ||
| void takes_cb(ConstantBuffer<S> c) {} | ||
|
|
||
| void test() { | ||
| takes_s(cb); // Calls operator const 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<S> cb_arr[2]; | ||
| float f = cb_arr[1].a; | ||
| ``` | ||
|
|
||
| #### 5. Template Support | ||
|
|
||
| `ConstantBuffer<T>` 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<typename Tm> | ||
| void foo(Tm t) { | ||
| float f = t.a; // Works even if Tm is ConstantBuffer<S> | ||
| } | ||
|
|
||
| void test() { | ||
| foo(cb); // Tm is deduced as ConstantBuffer<S> | ||
| } | ||
| ``` | ||
|
|
||
| In the primary template `foo`, the expression `t.a` is represented as a | ||
| `CXXDependentScopeMemberExpr` because the type of `t` is dependent: | ||
|
|
||
| ```text | ||
| `-CXXDependentScopeMemberExpr '<dependent type>' lvalue .a | ||
| `-DeclRefExpr 'Tm' lvalue ParmVar 't' 'Tm' | ||
| ``` | ||
|
|
||
| When `foo` is instantiated as `foo<ConstantBuffer<S>>`, Clang's template | ||
| instantiation mechanism rebuilds the member expression. Since the type of `t` is | ||
| now known to be `ConstantBuffer<S>`, the standard member lookup logic in | ||
| `Sema::LookupMemberExpr` is triggered. Our interception logic then identifies | ||
| `ConstantBuffer<S>` and injects the call to the implicit conversion operator | ||
| `operator const hlsl_constant S&()`, resulting in the same AST structure as | ||
| non-templated member access: | ||
|
|
||
| ```text | ||
| `-MemberExpr 'float' lvalue .a | ||
| `-CXXMemberCallExpr 'const hlsl_constant S' lvalue | ||
| `-MemberExpr '<bound member function type>' .operator const hlsl_constant S & | ||
| `-ImplicitCastExpr 'const hlsl::ConstantBuffer<S>' lvalue <NoOp> | ||
| `-DeclRefExpr 't' 'hlsl::ConstantBuffer<S>' | ||
| ``` | ||
|
|
||
| This ensures that `ConstantBuffer<T>` 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 const 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 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<T>` for several reasons: | ||
|
|
||
| - **Encapsulation:** In `ConstantBuffer<T>`, 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<T>` 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<T>` inheriting from `T` | ||
|
|
||
| A significant alternative considered was to have `ConstantBuffer<T>` 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<T>)` 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<T>` 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? | ||
|
|
||
| ## 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. | ||
Uh oh!
There was an error while loading. Please reload this page.