Given the following shader:
void fn(inout int A, inout int B) {
A = 1;
B += 2;
}
export int call() {
int V = 0;
fn(V,V);
return V;
}
What is the return value of call
?
A. 1
B. 2
C. 3
D. Undefined!
Answer!
D!
HLSL doesn’t expose references as a concept, instead we have the inout
and
out
keywords, which work slightly different.
The HLSL language specification says:
Arguments to output and input/output parameters must be lvalues. … The cxvalue created from an argument to an input/output parameter is initialized through copy-initialization from the lvalue argument expression. Overload resolution shall occur on argument initialization as if the expression T Param = Arg were evaluated. … On expiration of the cxvalue, the value is assigned back to the argument lvalue expression using a resolved assignment expression as if the expression Arg = Param were written.
This is a very long way of saying that when you pass a value (in our shader
V
), to a function it gets copied to a temporary of the parameter type, and
copied back to the original variable after the function is executed.
Mirroring the C and C++ specification HLSL does not define the order in which
argument objects are initialized or destroyed. DXC targeting DXIL creates
arguments right-to-left and destroys them left-to-right, so the value will
consistently be 2
. Meanwhile DXC’s SPIR-V generator treats inout
as passing
by address which currently results in 3
.
In Clang, the DXIL code generator uses the MSVC C++ compatibility aligning with
DXC to return 2
, but the SPIRV code generator aligns with Clang and GCC’s
behavior and will return 1
.
See it here in Compiler Explorer.