Skip to content

Commit 183eae0

Browse files
authored
[HLSL][Docs] Add documentation for HLSL functions (#75397)
This adds a new document that covers the HLSL approach to function calls and parameter semantics. At time of writing this document is a proposal for the implementation.
1 parent 0a1b066 commit 183eae0

File tree

2 files changed

+322
-0
lines changed

2 files changed

+322
-0
lines changed

clang/docs/HLSL/FunctionCalls.rst

Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
===================
2+
HLSL Function Calls
3+
===================
4+
5+
.. contents::
6+
:local:
7+
8+
Introduction
9+
============
10+
11+
This document describes the design and implementation of HLSL's function call
12+
semantics in Clang. This includes details related to argument conversion and
13+
parameter lifetimes.
14+
15+
This document does not seek to serve as official documentation for HLSL's
16+
call semantics, but does provide an overview to assist a reader. The
17+
authoritative documentation for HLSL's language semantics is the `draft language
18+
specification <https://microsoft.github.io/hlsl-specs/specs/hlsl.pdf>`_.
19+
20+
Argument Semantics
21+
==================
22+
23+
In HLSL, all function arguments are passed by value in and out of functions.
24+
HLSL has 3 keywords which denote the parameter semantics (``in``, ``out`` and
25+
``inout``). In a function declaration a parameter may be annotated any of the
26+
following ways:
27+
28+
#. <no parameter annotation> - denotes input
29+
#. ``in`` - denotes input
30+
#. ``out`` - denotes output
31+
#. ``in out`` - denotes input and output
32+
#. ``out in`` - denotes input and output
33+
#. ``inout`` - denotes input and output
34+
35+
Parameters that are exclusively input behave like C/C++ parameters that are
36+
passed by value.
37+
38+
For parameters that are output (or input and output), a temporary value is
39+
created in the caller. The temporary value is then passed by-address. For
40+
output-only parameters, the temporary is uninitialized when passed (if the
41+
parameter is not explicitly initialized inside the function an undefined value
42+
is stored back to the argument expression). For parameters that are both input
43+
and output, the temporary is initialized from the lvalue argument expression
44+
through implicit or explicit casting from the lvalue argument type to the
45+
parameter type.
46+
47+
On return of the function, the values of any parameter temporaries are written
48+
back to the argument expression through an inverted conversion sequence (if an
49+
``out`` parameter was not initialized in the function, the uninitialized value
50+
may be written back).
51+
52+
Parameters of constant-sized array type are also passed with value semantics.
53+
This requires input parameters of arrays to construct temporaries and the
54+
temporaries go through array-to-pointer decay when initializing parameters.
55+
56+
Implementations are allowed to avoid unnecessary temporaries, and HLSL's strict
57+
no-alias rules can enable some trivial optimizations.
58+
59+
Array Temporaries
60+
-----------------
61+
62+
Given the following example:
63+
64+
.. code-block:: c++
65+
66+
void fn(float a[4]) {
67+
a[0] = a[1] + a[2] + a[3];
68+
}
69+
70+
float4 main() : SV_Target {
71+
float arr[4] = {1, 1, 1, 1};
72+
fn(arr);
73+
return float4(arr[0], arr[1], arr[2], arr[3]);
74+
}
75+
76+
In C or C++, the array parameter decays to a pointer, so after the call to
77+
``fn``, the value of ``arr[0]`` is ``3``. In HLSL, the array is passed by value,
78+
so modifications inside ``fn`` do not propagate out.
79+
80+
.. note::
81+
82+
DXC may pass unsized arrays directly as decayed pointers, which is an
83+
unfortunate behavior divergence.
84+
85+
Out Parameter Temporaries
86+
-------------------------
87+
88+
.. code-block:: c++
89+
90+
void Init(inout int X, inout int Y) {
91+
Y = 2;
92+
X = 1;
93+
}
94+
95+
void main() {
96+
int V;
97+
Init(V, V); // MSVC (or clang-cl) V == 2, Clang V == 1
98+
}
99+
100+
In the above example the ``Init`` function's behavior depends on the C++
101+
implementation. C++ does not define the order in which parameters are
102+
initialized or destroyed. In MSVC and Clang's MSVC compatibility mode, arguments
103+
are emitted right-to-left and destroyed left-to-right. This means that the
104+
parameter initialization and destruction occurs in the order: {``Y``, ``X``,
105+
``~X``, ``~Y``}. This causes the write-back of the value of ``Y`` to occur last,
106+
so the resulting value of ``V`` is ``2``. In the Itanium C++ ABI, the parameter
107+
ordering is reversed, so the initialization and destruction occurs in the order:
108+
{``X``, ``Y``, ``~Y``, ``X``}. This causes the write-back of the value ``X`` to
109+
occur last, resulting in the value of ``V`` being set to ``1``.
110+
111+
.. code-block:: c++
112+
113+
void Trunc(inout int3 V) { }
114+
115+
116+
void main() {
117+
float3 F = {1.5, 2.6, 3.3};
118+
Trunc(F); // F == {1.0, 2.0, 3.0}
119+
}
120+
121+
In the above example, the argument expression ``F`` undergoes element-wise
122+
conversion from a float vector to an integer vector to create a temporary
123+
``int3``. On expiration the temporary undergoes elementwise conversion back to
124+
the floating point vector type ``float3``. This results in an implicit
125+
element-wise conversion of the vector even if the value is unused in the
126+
function (effectively truncating the floating point values).
127+
128+
129+
.. code-block:: c++
130+
131+
void UB(out int X) {}
132+
133+
void main() {
134+
int X = 7;
135+
UB(X); // X is undefined!
136+
}
137+
138+
In this example an initialized value is passed to an ``out`` parameter.
139+
Parameters marked ``out`` are not initialized by the argument expression or
140+
implicitly by the function. They must be explicitly initialized. In this case
141+
the argument is not initialized in the function so the temporary is still
142+
uninitialized when it is copied back to the argument expression. This is
143+
undefined behavior in HLSL, and any use of the argument after the call is a use
144+
of an undefined value which may be illegal in the target (DXIL programs with
145+
used or potentially used ``undef`` or ``poison`` values fail validation).
146+
147+
Clang Implementation
148+
====================
149+
150+
.. note::
151+
152+
The implementation described here is a proposal. It has not yet been fully
153+
implemented, so the current state of Clang's sources may not reflect this
154+
design. A prototype implementation was built on DXC which is Clang-3.7 based.
155+
The prototype can be found
156+
`here <https://github.com/microsoft/DirectXShaderCompiler/pull/5249>`_. A lot
157+
of the changes in the prototype implementation are restoring Clang-3.7 code
158+
that was previously modified to its original state.
159+
160+
The implementation in clang depends on two new AST nodes and minor extensions to
161+
Clang's existing support for Objective-C write-back arguments. The goal of this
162+
design is to capture the semantic details of HLSL function calls in the AST, and
163+
minimize the amount of magic that needs to occur during IR generation.
164+
165+
The two new AST nodes are ``HLSLArrayTemporaryExpr`` and ``HLSLOutParamExpr``,
166+
which respectively represent the temporaries used for passing arrays by value
167+
and the temporaries created for function outputs.
168+
169+
Array Temporaries
170+
-----------------
171+
172+
The ``HLSLArrayTemporaryExpr`` represents temporary values for input
173+
constant-sized array arguments. This applies for all constant-sized array
174+
arguments regardless of whether or not the parameter is constant-sized or
175+
unsized.
176+
177+
.. code-block:: c++
178+
179+
void SizedArray(float a[4]);
180+
void UnsizedArray(float a[]);
181+
182+
void main() {
183+
float arr[4] = {1, 1, 1, 1};
184+
SizedArray(arr);
185+
UnsizedArray(arr);
186+
}
187+
188+
In the example above, the following AST is generated for the call to
189+
``SizedArray``:
190+
191+
.. code-block:: text
192+
193+
CallExpr 'void'
194+
|-ImplicitCastExpr 'void (*)(float [4])' <FunctionToPointerDecay>
195+
| `-DeclRefExpr 'void (float [4])' lvalue Function 'SizedArray' 'void (float [4])'
196+
`-HLSLArrayTemporaryExpr 'float [4]'
197+
`-DeclRefExpr 'float [4]' lvalue Var 'arr' 'float [4]'
198+
199+
In the example above, the following AST is generated for the call to
200+
``UnsizedArray``:
201+
202+
.. code-block:: text
203+
204+
CallExpr 'void'
205+
|-ImplicitCastExpr 'void (*)(float [])' <FunctionToPointerDecay>
206+
| `-DeclRefExpr 'void (float [])' lvalue Function 'UnsizedArray' 'void (float [])'
207+
`-HLSLArrayTemporaryExpr 'float [4]'
208+
`-DeclRefExpr 'float [4]' lvalue Var 'arr' 'float [4]'
209+
210+
In both of these cases the argument expression is of known array size so we can
211+
initialize an appropriately sized temporary.
212+
213+
It is illegal in HLSL to convert an unsized array to a sized array:
214+
215+
.. code-block:: c++
216+
217+
void SizedArray(float a[4]);
218+
void UnsizedArray(float a[]) {
219+
SizedArray(a); // Cannot convert float[] to float[4]
220+
}
221+
222+
When converting a sized array to an unsized array, an array temporary can also
223+
be inserted. Given the following code:
224+
225+
.. code-block:: c++
226+
227+
void UnsizedArray(float a[]);
228+
void SizedArray(float a[4]) {
229+
UnsizedArray(a);
230+
}
231+
232+
An expected AST should be something like:
233+
234+
.. code-block:: text
235+
236+
CallExpr 'void'
237+
|-ImplicitCastExpr 'void (*)(float [])' <FunctionToPointerDecay>
238+
| `-DeclRefExpr 'void (float [])' lvalue Function 'UnsizedArray' 'void (float [])'
239+
`-HLSLArrayTemporaryExpr 'float [4]'
240+
`-DeclRefExpr 'float [4]' lvalue Var 'arr' 'float [4]'
241+
242+
Out Parameter Temporaries
243+
-------------------------
244+
245+
Output parameters are defined in HLSL as *casting expiring values* (cx-values),
246+
which is a term made up for HLSL. A cx-value is a temporary value which may be
247+
the result of a cast, and stores its value back to an lvalue when the value
248+
expires.
249+
250+
To represent this concept in Clang we introduce a new ``HLSLOutParamExpr``. An
251+
``HLSLOutParamExpr`` has two forms, one with a single sub-expression and one
252+
with two sub-expressions.
253+
254+
The single sub-expression form is used when the argument expression and the
255+
function parameter are the same type, so no cast is required. As in this
256+
example:
257+
258+
.. code-block:: c++
259+
260+
void Init(inout int X) {
261+
X = 1;
262+
}
263+
264+
void main() {
265+
int V;
266+
Init(V);
267+
}
268+
269+
The expected AST formulation for this code would be something like:
270+
271+
.. code-block:: text
272+
273+
CallExpr 'void'
274+
|-ImplicitCastExpr 'void (*)(int &)' <FunctionToPointerDecay>
275+
| `-DeclRefExpr 'void (int &)' lvalue Function 'Init' 'void (int &)'
276+
|-HLSLOutParamExpr 'int' lvalue inout
277+
`-DeclRefExpr 'int' lvalue Var 'V' 'int'
278+
279+
The ``HLSLOutParamExpr`` captures that the value is ``inout`` vs ``out`` to
280+
denote whether or not the temporary is initialized from the sub-expression. If
281+
no casting is required the sub-expression denotes the lvalue expression that the
282+
cx-value will be copied to when the value expires.
283+
284+
The two sub-expression form of the AST node is required when the argument type
285+
is not the same as the parameter type. Given this example:
286+
287+
.. code-block:: c++
288+
289+
void Trunc(inout int3 V) { }
290+
291+
292+
void main() {
293+
float3 F = {1.5, 2.6, 3.3};
294+
Trunc(F);
295+
}
296+
297+
For this case the ``HLSLOutParamExpr`` will have sub-expressions to record both
298+
casting expression sequences for the initialization and write back:
299+
300+
.. code-block:: text
301+
302+
-CallExpr 'void'
303+
|-ImplicitCastExpr 'void (*)(int3 &)' <FunctionToPointerDecay>
304+
| `-DeclRefExpr 'void (int3 &)' lvalue Function 'inc_i32' 'void (int3 &)'
305+
`-HLSLOutParamExpr 'int3' lvalue inout
306+
|-ImplicitCastExpr 'float3' <IntegralToFloating>
307+
| `-ImplicitCastExpr 'int3' <LValueToRValue>
308+
| `-OpaqueValueExpr 'int3' lvalue
309+
`-ImplicitCastExpr 'int3' <FloatingToIntegral>
310+
`-ImplicitCastExpr 'float3' <LValueToRValue>
311+
`-DeclRefExpr 'float3' lvalue 'F' 'float3'
312+
313+
In this formation the write-back casts are captured as the first sub-expression
314+
and they cast from an ``OpaqueValueExpr``. In IR generation we can use the
315+
``OpaqueValueExpr`` as a placeholder for the ``HLSLOutParamExpr``'s temporary
316+
value on function return.
317+
318+
In code generation this can be implemented with some targeted extensions to the
319+
Objective-C write-back support. Specifically extending CGCall.cpp's
320+
``EmitWriteback`` function to support casting expressions and emission of
321+
aggregate lvalues.

clang/docs/HLSL/HLSLDocs.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ HLSL Design and Implementation
1414
HLSLIRReference
1515
ResourceTypes
1616
EntryFunctions
17+
FunctionCalls

0 commit comments

Comments
 (0)