yum-mirror/slang
Making it easier to work with shaders
git clone https://git.yummers.dev/yum-mirror/slang
8e6af6259
master
layout: user-guide
Interoperation with Target-Specific Code
Slang provides low-level interoperation mechanisms to allow developers to use target-specific features or invoke code written in the target language from Slang code. These mechanisms are:
__intrinsic_asmconstruct to map a function invocation to specific textual target code.__require_preludeconstruct to inject arbitrary text to the generated textual target code.__target_switchconstruct to use different implementations for different targets.spirv_asmconstruct to define inline SPIRV assembly blocks.
Note
The language mechanisms described in this chapter are considered internal compiler features. The compiler does not provide comprehensive checks around their uses. These mechanisms are also subject to breaking changes in future releases.
Defining Intrinsic Functions for Textual Targets
When using Slang to generate code for a textual target, e.g. HLSL, GLSL, CUDA or C++, you can use __intrinsic_asm to define what code to generate for an invocation to an intrinsic function. For example, the following Slang code defines an intrinsic function myPrint, that when called, will produce a call to printf in the target code:
void myPrint (float v ) {__intrinsic_asm R"(printf("v is %f", $0))" ; }void test () {myPrint (1.0f ); }
Compiling the above code to CUDA or C++ will yield the following output:
// ... void test_0 () {printf ("v is %f" ,1.0f ); }
The __intrinsic_asm statement in myPrint serves as the definition for the function. When a function body contains __intrinsic_asm, the function is treated by the compiler as an intrinsic and it must not contain other ordinary statements. Calls to an intrinsic function will be translated using the definition string of the intrinsic. In this example, the intrinsic is defined by the string literal R"(printf("v is %f", $0))", which is used to translate the call from test(). The "$0" in the literal is replaced with the first argument. Besides "$<index>", you may also use the following macros in an intrinsic definition:
| Macro | Expands to |
|---|---|
$<index> | Argument <index>, starting from 0 |
$T<index> | Type of argument <index> |
$TR | The return type. |
$N<index> | The element count of argument <index>, if the argument is a vector. |
$S<index> | The scalar type of argument <index>, if the argument is a matrix or vector. |
$*<index> | Emit all arguments starting from <index> as comma separated list |
Defining Intrinsic Types
You can use __target_intrinsic modifier on a struct type to cause the type being emitted as a specific string for a given target. For example:
__target_intrinsic(cpp, "std::string")
struct CppString
{
uint size()
{
__intrinsic_asm "static_cast<uint32_t>(($0).size())";
}
}
When compiling the above code to C++, the CppString struct will not be emitted as a C++ struct. Instead, all uses of CppString will be emitted as std::string.
Injecting Preludes
If you have code written in the target language that you want to include in the generated code, you can use __requirePrelude.
For example:
int getMyEnvVariable () {__requirePrelude (R"(#include <stdlib.h>)" );__requirePrelude (R"(#include <string>)" );__requirePrelude (R"( int getEnvVarImpl() { char* var = getenv("MY_ENVIRONMENT_VAR"); return std::stoi(var); } )" );__intrinsic_asm "getEnvVarImpl()" ; }void test () {if (getMyEnvVariable ()== 0 )return ; }
In this code, getMyEnvVariable() is defined as an intrinsic Slang function that will translate to a call to getEnvVarImpl() in the target code. The first two __requirePrelude calls causes include directives being emitted in the resulting code, and the third __requirePrelude call causes a definition of getEnvVarImpl(), written in C++, being emitted before other Slang functions are emitted. The above code will translate to the following output:
// ... #include <stdlib.h> #include <string> int getEnvVarImpl () {char * var = getenv ("MY_ENVIRONMENT_VAR" );return std::stoi (var ); }void test_0 () {if (getEnvVarImpl ()== 0 )return ; }
The strings in __requirePrelude are deduplicated: the same prelude string will only be emitted once no matter how many times an intrinsic function is invoked. Therefore, it is good practice to put #include lines as separate __requirePrelude statements to prevent duplicate #includes being generated in the output code.
Managing Cross-Platform Code
If you are defining an intrinsic function that maps to multiple targets in different ways, you can use __target_switch construct to manage the target-specific definitions. For example, here is a snippet from the Slang core module that defines getRealtimeClock:
[ __requiresNVAPI ] __glsl_extension ( GL_EXT_shader_realtime_clock ) uint2 getRealtimeClock () { __target_switch { case hlsl : __intrinsic_asm "uint2(NvGetSpecial(NV_SPECIALOP_GLOBAL_TIMER_LO), NvGetSpecial( NV_SPECIALOP_GLOBAL_TIMER_HI))" ; case glsl : __intrinsic_asm "clockRealtime2x32EXT()" ; case spirv : return spirv_asm { OpCapability ShaderClockKHR ; OpExtension "SPV_KHR_shader_clock" ; result : $$uint2 = OpReadClockKHR Device }; default : return uint2 ( 0 , 0 ); } }
This definition causes getRealtimeClock() to translate to a call to NVAPI when targeting HLSL, to clockRealtime2x32EXT() when targeting
GLSL, and to the OpReadClockKHR instruction when compiling directly to SPIRV through the inline SPIRV assembly block. The default case is
used for target not specified in the __target_switch statement.
Currently, the following target names are supported in a case statement: cpp, cuda, glsl, hlsl, and spirv.
Inline SPIRV Assembly
When targeting SPIRV, Slang allows you to directly write a SPIRV assembly block and use it as part of an expression. For example:
int test () {int localVar = 5 ;return 1 + spirv_asm { %temp :$$int = OpIMul $localVar $ (2 );result :$$int = OpIAdd %temp %temp };// returns 21 }
A SPIRV assembly block contains one or more SPIRV instructions, separated by semicolons. Each SPIRV instruction has the form:
%identifier : <type> = <opcode> <operand> ... ;
where <opcode> defines a value named identifier of <type>, or simply:
<opcode> <operand> ... ;
When <opcode> does not define a return value.
When used as part of an expression, the Slang type of the spirv_asm construct is defined by the last instruction, which must be in the form of:
result: <type> = ...
You can use the $ prefix to begin an anti-quote of a Slang expression inside a spirv_asm block. This is commonly used to refer to a Slang variable, such as localVar in the example, as an operand. Additionally, the $$ prefix is used to reference a Slang type, such as the $$uint references in the example.
You can also use the & prefix to refer to an l-value as a pointer-typed value in SPIRV, for example:
float modf (float x ,out float ip ) {return spirv_asm {result :$$float = OpExtInst glsl450 Modf $x & ip }; }
Opcodes such as OpCapability, OpExtension and type definitions are allowed inside a spirv_asm block. These instructions will be deduplicated and inserted into the correct sections defined by the SPIRV specification, for example:
uint4 WaveMatch (T value ) {return spirv_asm {OpCapability GroupNonUniformPartitionedNV ;OpExtension "SPV_NV_shader_subgroup_partitioned" ;OpGroupNonUniformPartitionNV $$uint4 result $value }; }
You may use SPIRV enum values directly as operands, for example:
void memoryBarrierImage () {spirv_asm {OpMemoryBarrier Device AcquireRelease |ImageMemory }; }
To access SPIRV builtin variables, you can use the builtin(VarName:type) syntax as an operand:
uint InstanceIndex () {return spirv_asm {result :$$uint = OpLoad builtin (InstanceId :uint ); }; }
1--- 2layout : user-guide 3--- 4 5Interoperation with Target-Specific Code 6=========== 7 8Slang provides low-level interoperation mechanisms to allow developers to use target-specific features or invoke code written in the target language from Slang code. These mechanisms are: 9- `__intrinsic_asm` construct to map a function invocation to specific textual target code. 10- `__require_prelude` construct to inject arbitrary text to the generated textual target code. 11- `__target_switch` construct to use different implementations for different targets. 12- `spirv_asm` construct to define inline SPIRV assembly blocks. 13 14> #### Note 15> The language mechanisms described in this chapter are considered internal compiler features. 16> The compiler does not provide comprehensive checks around their uses. These mechanisms are also subject 17> to breaking changes in future releases. 18 19## Defining Intrinsic Functions for Textual Targets 20 21When using Slang to generate code for a textual target, e.g. HLSL, GLSL, CUDA or C++, you can use `__intrinsic_asm` to define what code to generate for an invocation to an intrinsic function. For example, the following Slang code defines an intrinsic function `myPrint`, that when called, will produce a call to `printf` in the target code: 22``` cpp 23void myPrint ( float v ) 24{ 25__intrinsic_asm R"(printf("v is %f", $0))" ; 26} 27 28void test () 29{ 30myPrint ( 1.0f ); 31} 32``` 33Compiling the above code to CUDA or C++ will yield the following output: 34 35``` cpp 36// ... 37void test_0 () 38{ 39printf ( "v is %f" , 1.0f ); 40} 41``` 42 43The `__intrinsic_asm` statement in `myPrint` serves as the definition for the function. When a function body contains `__intrinsic_asm`, the function is treated by the compiler as an intrinsic and it must not contain other ordinary statements. Calls to an intrinsic function will be translated using the definition string of the intrinsic. In this example, the intrinsic is defined by the string literal `R"(printf("v is %f", $0))"`, which is used to translate the call from `test()`. The `"$0"` in the literal is replaced with the first argument. Besides `"$<index>"`, you may also use the following macros in an intrinsic definition: 44 45| Macro | Expands to | 46|-----------|-------------| 47| `$<index>` | Argument `<index>`, starting from 0 | 48| `$T<index>` | Type of argument `<index>` | 49| `$TR` | The return type. | 50| `$N<index>` | The element count of argument `<index>`, if the argument is a vector. | 51| `$S<index>` | The scalar type of argument `<index>`, if the argument is a matrix or vector. | 52| `$*<index>` | Emit all arguments starting from `<index>` as comma separated list | 53 54## Defining Intrinsic Types 55 56You can use `__target_intrinsic` modifier on a `struct` type to cause the type being emitted as a specific string for a given target. For example: 57``` 58__target_intrinsic(cpp, "std::string") 59struct CppString 60{ 61uint size() 62{ 63__intrinsic_asm "static_cast<uint32_t>(($0).size())"; 64} 65} 66``` 67When compiling the above code to C++, the `CppString` struct will not be emitted as a C++ struct. Instead, all uses of `CppString` will be emitted as `std::string`. 68 69## Injecting Preludes 70 71If you have code written in the target language that you want to include in the generated code, you can use `__requirePrelude`. 72For example: 73``` cpp 74int getMyEnvVariable () 75{ 76__requirePrelude ( R"(#include <stdlib.h>)" ); 77__requirePrelude ( R"(#include <string>)" ); 78__requirePrelude ( R"( 79int getEnvVarImpl() 80{ 81char* var = getenv("MY_ENVIRONMENT_VAR"); 82return std::stoi(var); 83} 84)"); 85__intrinsic_asm "getEnvVarImpl()" ; 86} 87void test () 88{ 89if ( getMyEnvVariable () == 0 ) 90return ; 91} 92``` 93In this code, `getMyEnvVariable()` is defined as an intrinsic Slang function that will translate to a call to `getEnvVarImpl()` in the target code. The first two `__requirePrelude` calls causes include directives being emitted in the resulting code, and the third `__requirePrelude` call causes a definition of `getEnvVarImpl()`, written in C++, being emitted before other Slang functions are emitted. The above code will translate to the following output: 94``` cpp 95// ... 96#include <stdlib.h> 97#include <string> 98int getEnvVarImpl () 99{ 100char * var = getenv ( "MY_ENVIRONMENT_VAR" ); 101return std:: stoi ( var ); 102} 103void test_0 () 104{ 105if ( getEnvVarImpl () == 0 ) 106return ; 107} 108``` 109 110The strings in `__requirePrelude` are deduplicated: the same prelude string will only be emitted once no matter how many times an intrinsic function is invoked. Therefore, it is good practice to put `#include` lines as separate `__requirePrelude` statements to prevent duplicate `#include`s being generated in the output code. 111 112## Managing Cross-Platform Code 113If you are defining an intrinsic function that maps to multiple targets in different ways, you can use `__target_switch` construct to manage the target-specific definitions. For example, here is a snippet from the Slang core module that defines `getRealtimeClock`: 114``` hlsl 115[ __requiresNVAPI ] 116__glsl_extension( GL_EXT_shader_realtime_clock ) 117uint2 getRealtimeClock () 118{ 119__target_switch 120{ 121case hlsl : 122__intrinsic_asm "uint2(NvGetSpecial(NV_SPECIALOP_GLOBAL_TIMER_LO), NvGetSpecial( NV_SPECIALOP_GLOBAL_TIMER_HI))" ; 123case glsl : 124__intrinsic_asm "clockRealtime2x32EXT()" ; 125case spirv : 126return spirv_asm 127{ 128OpCapability ShaderClockKHR ; 129OpExtension "SPV_KHR_shader_clock" ; 130result : $$uint2 = OpReadClockKHR Device 131}; 132default : 133return uint2 ( 0 , 0 ); 134} 135} 136``` 137This definition causes `getRealtimeClock()` to translate to a call to NVAPI when targeting HLSL, to `clockRealtime2x32EXT()` when targeting 138GLSL, and to the `OpReadClockKHR` instruction when compiling directly to SPIRV through the inline SPIRV assembly block. The `default` case is 139used for target not specified in the `__target_switch` statement. 140 141Currently, the following target names are supported in a `case` statement: `cpp`, `cuda`, `glsl`, `hlsl`, and `spirv`. 142 143## Inline SPIRV Assembly 144 145When targeting SPIRV, Slang allows you to directly write a SPIRV assembly block and use it as part of an expression. For example: 146``` cpp 147int test () 148{ 149int localVar = 5 ; 150return 1 + spirv_asm { 151% temp : $$int = OpIMul $localVar $ ( 2 ); 152result : $$int = OpIAdd % temp % temp 153}; 154// returns 21 155} 156``` 157A SPIRV assembly block contains one or more SPIRV instructions, separated by semicolons. Each SPIRV instruction has the form: 158``` 159%identifier : <type> = <opcode> <operand> ... ; 160``` 161where `<opcode>` defines a value named `identifier` of `<type>`, or simply: 162``` 163<opcode> <operand> ... ; 164``` 165When `<opcode>` does not define a return value. 166 167When used as part of an expression, the Slang type of the `spirv_asm` construct is defined by the last instruction, which must be in the form of: 168``` 169result: <type> = ... 170``` 171 172You can use the `$` prefix to begin an anti-quote of a Slang expression inside a `spirv_asm` block. This is commonly used to refer to a Slang variable, such as `localVar` in the example, as an operand. Additionally, the `$$` prefix is used to reference a Slang type, such as the `$$uint` references in the example. 173 174You can also use the `&` prefix to refer to an l-value as a pointer-typed value in SPIRV, for example: 175``` cpp 176float modf ( float x , out float ip ) 177{ 178return spirv_asm 179{ 180result : $$float = OpExtInst glsl450 Modf $x & ip 181}; 182} 183``` 184 185Opcodes such as `OpCapability`, `OpExtension` and type definitions are allowed inside a `spirv_asm` block. These instructions will be deduplicated and inserted into the correct sections defined by the SPIRV specification, for example: 186``` cpp 187uint4 WaveMatch ( T value ) 188{ 189return spirv_asm 190{ 191OpCapability GroupNonUniformPartitionedNV ; 192OpExtension "SPV_NV_shader_subgroup_partitioned" ; 193OpGroupNonUniformPartitionNV $$uint4 result $value 194}; 195} 196``` 197 198You may use SPIRV enum values directly as operands, for example: 199``` cpp 200void memoryBarrierImage () 201{ 202spirv_asm 203{ 204OpMemoryBarrier Device AcquireRelease | ImageMemory 205}; 206} 207``` 208 209To access SPIRV builtin variables, you can use the `builtin(VarName:type)` syntax as an operand: 210``` cpp 211uint InstanceIndex () 212{ 213return spirv_asm { 214result : $$uint = OpLoad builtin ( InstanceId : uint ); 215}; 216} 217``` 218