Skip to content

Commit 16b31ff

Browse files
committed
Proposal for input semantics
This is another proposal on how to implement semantic input in Clang, given DXIL & SPIR-V have drasticly different handlings, but some parts could be shared. Another proposal exists: llvm#112 which also suggest a sema change.
1 parent 139a5d2 commit 16b31ff

File tree

1 file changed

+245
-0
lines changed

1 file changed

+245
-0
lines changed

proposals/NNNN-input-semantics.md

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
# Input semantics
2+
3+
* Proposal: [NNNN](http://NNNN-input-semantics.md)
4+
* Author(s): [Nathan Gauër](https://github.com/Keenuts)
5+
* Status: **Design In Progress**
6+
7+
## Introduction
8+
9+
HLSL shaders can read form the pipeline state using [semantics](https://learn.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl-semantics).
10+
This proposal looks into how to implement input semantics in Clang.
11+
Output semantics are out of scope of this proposal, but some parts will be
12+
similar.
13+
14+
## Motivation
15+
16+
HLSL input semantics are a core part of the shading language.
17+
18+
## Behavior
19+
20+
### HLSL
21+
22+
Input semantics are used by the API to determine how to connect pipeline
23+
data to the shader.
24+
25+
Input semantic attributes can be applied to a function parameter, or a struct
26+
field's declaration. They only carry meaning when used on an entry point
27+
parameter, or a struct type used by one of the entry point parameters.
28+
All other uses are simply ignored.
29+
30+
HLSL has two kinds of semantics: System and User.
31+
System semantics are linked to specific parts of the pipeline, while
32+
user semantics are just a way for the user to link the output of a stage
33+
to the input of another stage.
34+
35+
When the semantic attribute is applied to a struct (type or value), it applies
36+
recursively to each inner fields, shadowing any other semantic.
37+
38+
Each scalar in the entrypoint must have a semantic attribute attached, either
39+
directly or inherited from the parent struct type.
40+
41+
Example:
42+
43+
```hlsl
44+
struct B {
45+
int b1 : SB1;
46+
int b2 : SB2;
47+
};
48+
49+
struct C {
50+
int c1 : SC1;
51+
int c2 : SC2;
52+
};
53+
54+
struct D {
55+
int d1;
56+
};
57+
58+
struct E {
59+
int e1 : EC;
60+
int e2 : EC;
61+
};
62+
63+
struct F {
64+
int f1 : FC;
65+
int f2 : FC;
66+
};
67+
68+
[[shader("pixel")]]
69+
void main(float a : SA, B b : SB, C c, D d, E e, F f : SF) { }
70+
```
71+
72+
In this example:
73+
- `a` is linked to the semantic `SA`.
74+
- `b.b1` and `b.b2` are linked to the semantic `SB` because `SB` shadows the
75+
semantics attached to each field.
76+
- `c.c1` has the semantic `SC1`, and `c.c2` the semantic `SC2`.
77+
- `d.d1`, hence `d`, is illegal: no semantic is attached to `d.d1`.
78+
- `e.e1` and `e.e2` are invalid: `EC` usage is duplicated without being inherited.
79+
- `f.f1` and `f.f2` semantic is `SF`, shadowing the duplicated `FC` semantic.
80+
81+
**Note**: HLSL forbids explicit **non-shadowed** semantic duplication. In this
82+
sample, the parameter `e` uses `E`, which explicitly declares two fields with
83+
the same semantic. This is illegal. \
84+
`b` has the semantic `SB` applied on the whole struct. Meaning all its fields
85+
share the same semantic `SB`. This is legal because the duplication comes
86+
from inheritance.
87+
Lastly, `f` explicitly duplicates the semantic `FC`. But because those are
88+
shadowed by the semantic `SF`, this is valid HLSL.
89+
90+
**Note**: Implicit semantic duplication is allowed for user semantics, but
91+
always forbidden for system semantics.
92+
93+
### SPIR-V
94+
95+
On the SPIR-V side, user semantics are translated into `Location`
96+
decorated `Input` variables. The `Location` decoration takes an index.
97+
System semantics are either translated to `Location` decorated `Input`
98+
variables, or `BuiltIn` decorated `Input` variables.
99+
100+
In the example above, there are no system semantics, meaning every
101+
parameter would get a `Location` decorated variable associated.
102+
Each scalar field/parameter is associated with a unique index starting at 0,
103+
from the first parameter's field to the last parameter's field.
104+
105+
In the sample above:
106+
107+
- `a` would have the `Location 0`.
108+
- `b.b1` would have the `Location 1`.
109+
- `b.b2` would have the `Location 2`.
110+
- `c.c1` would have the `Location 3`.
111+
- ...
112+
113+
It is also possible to explicitly set the index, using the
114+
`[[vk::location(/* Index */)]]` attribute. \
115+
Mixing implicit and explicit location assignment is **not legal**.
116+
Hence interaction between both mechanism is out of scope.
117+
118+
### DXIL
119+
120+
On the DXIL side, some system semantics are translated to builtin function
121+
calls. But most are visible along user semantics in the `input signature`.
122+
123+
To pass data between stages, DirectX provides a fixed list of <4 x 32bit>
124+
registers. E.g: 32 x <4 x 32 bit> for VS in D3D11.
125+
126+
The input signature assigns to each semantic a `Name`, `Index`, `Mask`,
127+
`Register`, `SysValue`, and `Format`.
128+
129+
- `Name`: the semantic attribute name.
130+
- `Index`: used to differentiate two inputs sharing the same `Name`.
131+
- `Register`: determines which 16-byte register the value shall be read from.
132+
- `Mask`:what 32-bit part is used for this value, e.g: `xyz` or `x`.
133+
- `Format`: how to interpret the data, e.g. `float` or `int`.
134+
- `SysValue`: `NONE` for user semantic, a known string for system semantics.
135+
136+
Unlike in SPIR-V, the `Register` and `Mask` cannot be simply deduced from the
137+
iteration order. Those value depends on the [packing rules](https://github.com/Microsoft/DirectXShaderCompiler/blob/main/docs/DXIL.rst#signature-packing)
138+
of the inputs.
139+
140+
## Proposal
141+
142+
SPIR-V and DXIL semantic lowering is very different. SPIR-V respects the
143+
parameters/field order, while DXIL packs by following an extensive set of
144+
rules. The System vs User semantics handling is also divergent.
145+
146+
This means we will be able to share the Sema checks, but will have to build
147+
two distinct paths during codegen.
148+
149+
### Sema
150+
151+
Type check for semantics is currently handled in `SemaDeclAttr.cpp`. \
152+
Iteration is done on each declaration, and if a semantic attribute is present,
153+
the type is checked.
154+
Each declaration being handled independently, this method does not support
155+
inherited/shadowed semantic attributes.
156+
157+
Sema checks are divided into three parts:
158+
- check for type compatibility between the variable and the semantic.
159+
- check for semantic duplication.
160+
- check for invalid system semantic usage depending on shader stage.
161+
162+
Proposition is to remove the type validation from the `SemaDeclAttr` and
163+
move it later into SemaHLSL, with the other checks.
164+
Idea is we need to have the inheritance rules to check types, so we should
165+
avoid duplicating this logic in two places.
166+
167+
The pseudo-code for this check should be:
168+
169+
```cpp
170+
void checkSemantic(std::unordered_set<HLSLAnnotationAttr> &UsedSemantics,
171+
DeclaratorDecl *Decl,
172+
HLSLAnnotationAttr *InheritedSemantic = nullptr) {
173+
174+
HLSLAnnotationAttr *Semantic = InheritedSemantic ? InheritedSemantic : Decl->get<HLSLAnnotationAttr>();
175+
RecordDecl *RD = dyn_cast<RecordDecl>(Decl);
176+
177+
// Case 1: type is a scalar, and we have a semantic. End case.
178+
if (Semantic && !RD) {
179+
if (UsedSemantics.contains(Semantic) && !InheritedSemantic)
180+
Fail("Explicit semantic duplication", Decl->getLocation())
181+
182+
UsedSemantics.insert(Semantic)
183+
diagnoseSemanticType(Decl, Semantic);
184+
diagnoseSemanticEnvironment(Decl, Context.ShaderEnv);
185+
return;
186+
}
187+
188+
// Case 2: type is scalar, but we have no semantic: error
189+
if (!RD)
190+
Fail("Missing semantic", Decl->getLocation());
191+
192+
// Case 3: it's a struct. Simply recurse, optionnally inherit semantic.
193+
if (RecordDecl *RD = dyn_cast<RecordDecl>(Decl)) {
194+
for (FieldDecl *FD : Decl->asRecordDecl()->getFields())
195+
checkSemantic(UsedSemantics, FD, Semantic);
196+
}
197+
}
198+
199+
...
200+
201+
std::unordered_set<clang::HLSLAnnotationAttr> UsedSemantics;
202+
for (ParmVarDecl Decl : entrypoint->getParams()) {
203+
checkSemantic(UsedSemantics, Decl, /* InheritedSemantic= */ nullptr);
204+
}
205+
```
206+
207+
At this point, we are guaranteed to have only valid and unique semantics, as
208+
well as valid types.
209+
210+
### CodeGen
211+
212+
DXIL and SPIR-V codegen will be very different, but the flattening/inheritance
213+
bit can be shared.
214+
215+
The proposal is to provide a sorted list in `CGHLSLRuntime`:
216+
217+
```cpp
218+
struct SemanticIO {
219+
// The active semantic for this scalar/field.
220+
HLSLAnnotationAttr *Semantic;
221+
222+
// Info about this field/scalar.
223+
DeclaratorDecl *Decl;
224+
llvm::Type *Type;
225+
226+
// The loaded value in the wrapper for this scalar/field. Each target
227+
// must provide this value.
228+
llvm::Value *Value = nullptr;
229+
};
230+
231+
Vector<SemanticIO> InputSemantics;
232+
```
233+
234+
The proposal is to let each target implement the logic in CGHLSLRuntime to
235+
load the semantics, and set the `Value` field of the `SemanticIO` struct.
236+
The order of the vector represents the flattening order.
237+
238+
Providing the full list should allow DXIL to easily implement packing rules.
239+
240+
At this stage, `CGHLSLRuntime` will have an ordered list of `SemanticIO`
241+
structs. The order is from the first parameter/field to the last
242+
parameter/field (DFS), and each will point to an `llvm::Value`.
243+
244+
The common code will then build the arguments list for the entrypoint call
245+
using the provided `llvm::Value`. This is shared across targets.

0 commit comments

Comments
 (0)