Skip to content
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

[0011] Resource element type validation #69

Merged
merged 21 commits into from
Oct 31, 2024
Merged
Changes from 2 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
7b0a53b
add intangible example, move proposal over
bob80905 Sep 13, 2024
cf382f7
include non-rawbuffer case, give examples
bob80905 Sep 16, 2024
2955655
rename, address greg
bob80905 Sep 17, 2024
55b8156
address Damyan, add some bullet lists
bob80905 Sep 18, 2024
38a0efe
use TypedBuffer instead of non-rawbuffer
bob80905 Sep 18, 2024
aeedba4
add info about textures, remove 32bit limit, dont code format rawbuffer
bob80905 Sep 19, 2024
2cd9493
introduce spir-v rules, discuss implementation of custom builtin type…
bob80905 Sep 20, 2024
46a67ae
fix typo
bob80905 Sep 23, 2024
5b9869e
address Chris and Damyan
bob80905 Sep 24, 2024
563aa4a
define is_spirv_target
bob80905 Sep 24, 2024
9f8ba5a
clarify type_trait implementation location, remove expected diagnostics
bob80905 Sep 25, 2024
2fb070c
simplify proposed solution, add eighthalves example, make type_traits…
bob80905 Sep 25, 2024
b7497e4
simplify by using __builtin_hlsl_is_line_vector_layout_compatible
bob80905 Sep 25, 2024
a2f38c1
incorporate design meeting feedback, remove is_complete_type, remove …
bob80905 Oct 3, 2024
fe417de
address Damyan
bob80905 Oct 4, 2024
6b19731
final touch of formatting
bob80905 Oct 4, 2024
5c0096c
small edits'
bob80905 Oct 21, 2024
739fe7d
insert ennum / bool constraint into builtin
bob80905 Oct 22, 2024
dd1c1bd
remove RET, remove mention of raw buffers, rename builtin
bob80905 Oct 30, 2024
f4a44ae
add back mention of raw buffers, remove references to line vector
bob80905 Oct 30, 2024
036cf48
rename builtin / concept, and rename filename
bob80905 Oct 31, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 142 additions & 0 deletions proposals/0010-validating-resource-container-elements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
* Proposal: [0010](0010-validating-resource-container-elements.md)
damyanp marked this conversation as resolved.
Show resolved Hide resolved
* Author(s): [Joshua Batista](https://github.com/bob80905)
* Sponsor: TBD
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you're the sponsor

* Status: **Under Consideration**
* Impacted Project(s): (LLVM)

*During the review process, add the following fields as needed:*
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you can remove this and the PRs line


* PRs: [#NNNN](https://github.com/microsoft/DirectXShaderCompiler/pull/NNNN)
* Issues: [#75676](https://github.com/llvm/llvm-project/issues/75676)

## Introduction
Resources are often used in HLSL, with various resource element types (RETs).
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a new acronym? Not objecting, but I don't think I've seen it before.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, completely new as far as I know. Just thought I'd use an acronym since that phrase is littered everywhere in this spec.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, when I see RET I think of this: https://www.felixcloutier.com/x86/ret

For example:
```
RWBuffer<float> rwbuf: register(u0);
```
In this code, the RET is `float`, and the resource type is `RWBuffer`. The
resource type is not a `RawBuffer` variant, and so there is a distinct set
of rules that define valid RETs for this resource type.
damyanp marked this conversation as resolved.
Show resolved Hide resolved

RETs for non-`RawBuffer` variants may include basic types (ints and uints of sizes 16
and 32, as well as half, and float). Structs that contain fields of these basic
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bool, uint64_t and double are currently allowed as well https://godbolt.org/z/eb7Yh73W6

Copy link
Collaborator Author

@bob80905 bob80905 Sep 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand correctly from my conversation with @tex3d, though the uint64_t and double cases compile, they will cause undefined behavior at runtime, and we do not want to allow any types that are larger than 32 bits.
Even if 2 64-bit types can technically fit in 4 32-bit fields, it's a hard limit that there may not be more than 4 elements, and each element may not be more than 32 bits.
We allow bool.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Support for 64-bit values seems like a totally reasonable thing to do. Is there something fundamental about them that mean we can't support them properly, or is this just that DXC is buggy?

What about in SPIR-V today? Looks like SPIR-V supports int64_t2.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's something fundamental (If I recall correctly, I think it had to do something with the hardware converter / DXGI format being capped at 32 bits in D3D, and that is not changeable).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think I said anything about them not working or causing undefined behavior. The undefined behavior case was tied to matching unorm or snorm in HLSL with the DXGI resource type for typed UAVs.

They definitely are supported on DXC (by decomposing to 32-bit components), and FXC the same way IIRC (for double).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps what was thought of was the fact that you can't use these with Sample operations (only Load/Store), since it's equivalent to a typed resource with an int32 component type, which doesn't support those operations.

In fact, if you try it, you get a validation error (we only catch it there), as it attempts to use a 64-bit overload for the sample operation, and define an invalid %dx.types.ResRet.f64 type.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since it's equivalent to a typed resource with an int32 component type, which doesn't support those operations.

Circling back to this conversation, I recall that this isn't correct anymore - there is more to add.

We added integer sampling support in SM 6.7. This relaxed checks in the front end that disallowed integer types. So integer types are actually allowed in SM 6.7 on (but not for comparison sampling).

Unfortunately, when 64-bit types are used with sample operations, those resource return types won't be translated into two i32 values per 64-bit value. That's what leads to the invalid operation type for these caught by validation. These types could be supported using integer sampling, if we had broken them up as we do for load operations.

I think this means that when using a 64-bit component type in a typed resource element, we should consider this as being implicitly translated to two 32-bit uints per 64-bit component for the actual type used. Then the rules are applied according to the translated element type.

There is one thing bugging me: double component type seemingly should trigger an error in IsValidObjectElement, but it doesn't. See the code here:
https://github.com/microsoft/DirectXShaderCompiler/blob/a023a95f73618e8ca1791147d3722a3e1ea7608f/tools/clang/lib/Sema/SemaHLSL.cpp#L6058

types (where all fields in the struct have the same type) may also be RETs.
Structs that either have structs as fields or arrays of structs as fields may also be
Copy link

@pow3clk pow3clk Sep 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In addition, vectors and matrices of 4 elements or fewer should be allowed both in and out of structs. I recognize that the matrix support is not part of this proposal, but it might be worth mentioning as part of the introduction potentially with a parenthetical or something that says it will be addressed separately.

allowed, as long as everything can fit in 4 32-bit quantities. Additionally, resource
types are not allowed within an RET, even if the underlying resource type has a
primitive RET.

RETs for `RawBuffer` variants are much less constrained, the only rule is that the RET
may not be an incomplete type (a handle type or a resource type).
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can HLSL authors interact with handle types directly?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should technically be possible.
I wrote a test in clang\test\SemaHLSL\resource_binding_attr_error.hlsl where __hlsl_resource_t, the handle type, was directly declared, so it is spellable.
But I don't think we intend users to interact with them.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically the __hlsl_resource_t type is spellable, but there is not much one can do with it. It needs to be decorated with resource type attributes to signify what kind of resource it is representing, and it will never get automatic binding unless it is embedded in a struct that looks like a resource class (has the handle as a first field named "h").


If someone writes `RWBuffer<MyCustomType>` and MyCustomType is not a
valid RET, there there should be infrastructure to reject this RET and emit a message
explaining why it was rejected as an RET.

## Motivation

Currently, in `clang\lib\CodeGen\CGHLSLRuntime.cpp`, under `calculateElementType`
there is a whitelist of valid RETs.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"allow list" or similar is a preferable term to whitelist.

Any reference to code seems more at home in detailed design. I think the same information can be provided by saying what the current clang compiler will and won't accept.

Anything that is not an int, uint, nor a floating-point type, will be rejected.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

those types within a vector are also currently supported

The whitelist isn't broad enough, because it doesn't include the case where the RET
is user-defined. Ideally, a user should be able to determine how any user-defined
structure is invalid as an RET. Some system should be in place to more completely
enforce the rules for valid and invalid RETs, as well as provide useful information
on why they are invalid.

For example, `RWBuffer<bool> b : register(u4);` will not emit any error in DXC,
but will in clang-dxc, despite the fact that `bool` is a valid RET.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per the link above, bool itself seems to work. Bool vectors do not work in any respect, but that's another matter: https://godbolt.org/z/eb7Yh73W6

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm, there must have been a fix over the past several months for there to no longer be any errors. I'll update this statment.


## Proposed solution

The proposed solution is to use some type_traits defined in the std library, create
some custom type_traits that aren't defined there, and join them together to define a
set of conceptual constraints for any RET that is used. These conceptual constraints
will be applied to every non-`RawBuffer` resource type that is defined, so that all
non-`RawBuffer` HLSL resources have the same rules about which RETs are valid.
Validation will occur upon resource type instantiation. Additionally, certain
resource types are `RawBuffer` variants, such as `StructuredBuffer`. Such resource
types will have a `[[hlsl::raw_buffer]]` attribute in the attributed type. If this is
detected, the rules for valid RETs will be loosened.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I get from this that there will be one set of type_traits for all that will be the most restrictive, so the typed buffer requirements and then the structured buffers will have some kind of exceptions? Is that simple than giving each its own set of type_traits or have I misunderstood?

From the below, it sounds like there will be two sets of type_traits, but this paragraph makes it sound like there will only be one set of type_traits that will be applied to "any RET".

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, what I intend to communicate is that that RawBuffer set of type traits will be applied to those resource types on declaration in HLSLExternalSemaSource.cpp, and the non-RawBuffer type traits will be applied to their respective resource types. I'll tweak the wording.


## Detailed design

In `clang\lib\Sema\HLSLExternalSemaSource.cpp`, `RWBuffer` is defined, along with
`RasterizerOrderedBuffer` and `StructuredBuffer`. It is at this point that the
`type_traits` should be incorporated into these resource declarations. All of the
non-`RawBuffer` `type_traits` will be applied on each non-`RawBuffer` HLSL resource
type. For every `type_trait` that is not true for the given RET, an associated error
message will be emitted.

The list of type_traits that define a valid non-`RawBuffer` RET are descsribed below:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo

Suggested change
The list of type_traits that define a valid non-`RawBuffer` RET are descsribed below:
The list of type_traits that define a valid non-`RawBuffer` RET are described below:

| type_trait | Description|
|-|-|
| `__is_complete_type` | An RET should either be a complete type, or a user defined type that has been completely defined. |
| `__is_intangible_type` | An RET should not contain any handles with unknown sizes, i.e., should not be intangible. So, we should assert this type_trait is false. |
| `__is_homogenous_aggregate` | RETs may be basic types, but if they are aggregate types, then all underlying basic types should be the same type. |
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In LLVM parlance, I think "aggregate type" applies to structs and arrays, but not vectors nor matrices. https://github.com/llvm/llvm-project/blob/a1d64626ba16f5128530ac771c6e641b1155184f/llvm/include/llvm/IR/Type.h#L291-L293

So I think we need another term for this.

| `__is_contained_in_four_groups_of_thirty_two_bits` | RETs should fit in four 32-bit quantities |
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there are two criteria here that are munged into one. That's how DXC implies it is working as well and it was only relevant when we got sub 32-bit types, but now that we have, there are two separate requirements here.

  • The total number of components must be four or fewer. That's how DXC behaves and we might consider changing it, but I don't think it makes sense to do so now.
  • The total size must be less than that of 4x32 bits. I think this wording is more confusing than clarifying. I would prefer to state it as 128 bits or if you prefer, 16 bytes.

The reason for separating these is that 8 uint16s will fit in the size, but is more than 4 components and is rejected by DXC.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To your second point, I think the constraint is slightly stronger, each element may not exceed 32 bits. I alluded to the reason earlier.
Yes, the wording here can be made clearer, I'll adjust.


Only `__is_complete_type` and `__is_intangible_type` are needed for `RawBuffer` RETs.

* Examples:
```
struct x {
int i;
};
struct a {
int aa;
int ab;
};
struct b {
x bx;
int i;
};
struct c;
struct d {
a ca;
float4 cb;
};
struct e {
int a;
int b;
};
struct f {
e x[2];
e y[2];
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be a bit easier to follow if, instead of arbitrary struct names, they described what they contained. So struct x could be struct oneInt and struct a could be struct twoInt etc

RWBuffer<int> r1; // valid
RWBuffer<float> r2; // valid
RWBuffer<float4> r3; // valid
RWBuffer<x> r4; // valid
RWBuffer<a> r5; // valid - all fields are valid primitive types
RWBuffer<b> r6; // valid - all fields (the struct) has valid primitive types for all its fields

// diagnostic: "resource element type 'c' has incomplete definition"
RWBuffer<c> r7;// invalid - the RET isn't complete, the definition is missing.
// the type_trait that would catch this is `__is_complete_type`

// diagnostic: "resource element type 'd' has non-homogenous aggregate type"
RWBuffer<d> r8; // invalid - struct `a` has int types, and this is not homogenous with the float4 contained in `c`.
// the type_trait that would catch this is `__is_homogenous_aggregate`
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It also has 4 floats and 2 ints, which is more than 4 components and also won't fit in 16 bytes. Might be better to keep the examples at one violation each.


StructuredBuffer<d> r8Structured; // valid

// diagnostic: "resource element type 'f' cannot be grouped into 4 32-bit quantities"
RWBuffer<f> r9; // invalid - the struct f cannot be grouped into 4 32-bit quantities.
// the type_trait that would catch this is `__is_contained_in_four_groups_of_thirty_two_bits`

StructuredBuffer<f> r9Structured; // valid

// diagnostic: "resource element type 'RWBuffer<int>' has intangible type"
RWBuffer<RWBuffer<int> > r10; // invalid - the RET has a handle with unknown size, thus it is an intangible RET.
// the type trait that would catch this is `__is_intangible_type`
```
## Alternatives considered (Optional)
We could instead implement a diagnostic function that checks each of these conceptual constraints in
one place, either in Sema or CodeGen, but this would prevent us from defining a single header where
all resource information is localized.

## Acknowledgments (Optional)

<!-- {% endraw %} -->