yum-mirror/slang
Making it easier to work with shaders
git clone https://git.yummers.dev/yum-mirror/slang
9f9d28c1f
master
layout: user-guide permalink: /user-guide/link-time-specialization
Link-time Specialization and Module Precompilation
Traditionally, graphics developers have been relying on preprocessor defines to specialize their shader code for high-performance GPU execution. While functioning systems can be built around preprocessor macros, overusing them leads to many problems:
- Long compilation time. With preprocessor defines, specialization happens before parsing, which is a very early stage in the compilation flow. This means that the compiler must redo almost all work from scratch with every specialized variant, including parsing, type checking, IR generation and optimization, even when two specialized variants only differ in one constant value. The lack of reuse of compiler front-end work between different shader specializations contributes a significant portion to long shader compile times.
- Reduced code readability and maintainability. The compiler cannot enforce any structures on preprocessor macros and cannot offer static checks to guarantee that the preprocessor macros are used in an intended way. Macros don't blend well with the native language syntax, which leads to less readable code, mystic diagnostic messages when things go wrong, and suboptimal intellisense experience.
- Locked in with early specialization. Once the code is written using preprocessor macros for specialization, the application that uses the shader code has no choice but to provide the macro values during shader compilation and always opt-in to static specialization. If the developer changes their mind to move away from specialization, a lot of code needs to be rewritten. As a result, the application is locked out of opportunities to take advantage of different design decisions or future hardware features that allow more efficient execution of non-specialized code.
Slang approaches the problem of shader specialization by supporting generics as a first class feature that allow most specializable code to be written in strongly typed code, and by allowing specialization to be triggered through link-time constants or types.
As discussed in the Compiling code with Slang chapter, Slang provides a three-step compilation model: precompiling, linking and target code generation.
Assuming the user shader is implemented as three Slang modules: a.slang, b.slang, and c.slang, the user can precompile all three modules to binary IR and store
them as a.slang-module, b.slang-module, and c.slang-module in a complete offline process that is independent to any specialization arguments.
Next, these three IR modules are linked together to form a self-contained program that will then go through a set of compiler optimizations for target code generation.
Slang's compilation model allows specialization arguments, in the form of constants or types to be provided during linking. This means that specialization happens at
a much later stage of compilation, reusing all the work done during module precompilation.
Link-time Constants
The simplest form of link time specialization is done through link-time constants. See the following code for an example.
// main.slang // Define a constant whose value will be provided in another module at link time. extern static const int kSampleCount ;float sample (int index ) {...}RWStructuredBuffer < float > output ;void main (uint tid :SV_DispatchThreadID ) { [ForceUnroll ]for (int i = 0 ;i < kSampleCount ;i ++ )output [tid ]+= sample (i ); }
This code defines a compute shader that can be specialized with different constant values of kSampleCount. The extern modifier means that
kSampleCount is a constant whose value is not provided within the current module, but will be resolved during the linking step.
The main.slang file can be compiled offline into a binary IR module with the slangc tool:
slangc main.slang -o main.slang-module
To specialize the code with a value of kSampleCount, the user can create another module that defines it:
// sample-count.slang export static const int kSampleCount = 2 ;
This file can also be compiled separately:
slangc sample-count.slang -o sample-count.slang-module
With these two modules precompiled, we can link them together to get our specialized code:
slangc sample-count.slang-module main.slang-module -target hlsl -entry main -profile cs_6_0 -o main.hlsl
This process can also be done with Slang's compilation API as in the following code snippet:
ComPtr < slang::ISession > slangSession = ...; ComPtr < slang:: IBlob > diagnosticsBlob ; // Load the main module from file. slang:: IModule * mainModule = slangSession -> loadModule ( "main.slang" , diagnosticsBlob . writeRef ()); // Load the specialization constant module from string. const char * sampleCountSrc = R"(export static const int kSampleCount = 2;)" ; slang:: IModule * sampleCountModule = slangSession -> loadModuleFromSourceString ( "sample-count" , // module name "sample-count.slang" , // synthetic module path sampleCountSrc , // module source content diagnosticsBlob . writeRef ()); // Compose the modules and entry points. ComPtr < slang:: IEntryPoint > computeEntryPoint ; SLANG_RETURN_ON_FAIL ( mainModule -> findEntryPointByName ( entryPointName , computeEntryPoint . writeRef ())); std:: vector < slang:: IComponentType *> componentTypes ; componentTypes . push_back ( mainModule ); componentTypes . push_back ( computeEntryPoint ); componentTypes . push_back ( sampleCountModule ); ComPtr < slang:: IComponentType > composedProgram ; SlangResult result = slangSession -> createCompositeComponentType ( componentTypes . data (), componentTypes . size (), composedProgram . writeRef (), diagnosticsBlob . writeRef ()); // Link. ComPtr < slang:: IComponentType > linkedProgram ; composedProgram -> link ( linkedProgram . writeRef (), diagnosticsBlob . writeRef ()); // Get compiled code. ComPtr < slang:: IBlob > compiledCode ; linkedProgram -> getEntryPointCode ( 0 , 0 , compiledCode . writeRef (), diagnosticsBlob . writeRef ());
Link-time Types
In addition to constants, you can also define types that are specified at link-time. For example, given the following modules:
// common.slang interface ISampler { int getSampleCount (); float sample ( int index ); } struct FooSampler : ISampler { int getSampleCount () { return 1 ; } float sample ( int index ) { return 0.0 ; } } struct BarSampler : ISampler { int getSampleCount () { return 2 ; } float sample ( int index ) { return index * 0.5 ; } }
// main.slang import common ; extern struct Sampler : ISampler ; RWStructuredBuffer < float > output ; void main ( uint tid : SV_DispatchThreadID) { Sampler sampler ; for ( int i = 0 ; i < sampler . getSampleCount (); i ++ ) output [ tid ] += sampler . sample ( i ); }
Again, we can separately compile these modules into binary forms independently from how they will be specialized.
To specialize the shader, we can author a third module that provides a definition for the extern Sampler type:
// sampler.slang import common ; export struct Sampler : ISampler = FooSampler ;
The = syntax defines a typealias that allows Sampler to resolve to FooSampler at link-time.
Note that both the name and type conformance clauses must match exactly between an export and an extern declaration
for link-time types to resolve correctly. Link-time types can also be generic, and may conform to generic interfaces.
When all these three modules are linked, we will produce a specialized shader that uses the FooSampler.
Providing Default Settings
When defining an extern symbol as a link-time constant or type, it is allowed to provide a default value for that constant or type.
When no other modules exists to export the same-named symbol, the default value will be used in the linked program.
For example, the following code is considered complete at linking and can proceed to code generation without any issues:
// main.slang // Provide a default value when no other modules are exporting the symbol. extern static const int kSampleCount = 2 ;// ... void main (uint tid :SV_DispatchThreadID ) { [ForceUnroll ]for (int i = 0 ;i < kSampleCount ;i ++ )output [tid ]+= sample (i ); }
Using Precompiling Modules with the API
In addition to using slangc for precompiling Slang modules, the IModule class provides a method to serialize itself to disk:
/// Get a serialized representation of the checked module. SlangResult IModule ::serialize (ISlangBlob ** outSerializedBlob );/// Write the serialized representation of this module to a file. SlangResult IModule ::writeToFile (char const * fileName );
These functions will write only the module itself to a file, which excludes the modules that it includes. To write all imported
modules, you can use methods from the ISession class to enumerate all currently loaded modules (including transitively imported modules)
in the session:
SlangInt ISession ::getLoadedModuleCount ();IModule * ISession ::getLoadedModule (SlangInt index );
Additionally, the ISession class also provides a function to query if a previously compiled module is still up-to-date with the current
Slang version, the compiler options in the session and the current content of the source files used to compile the module:
bool ISession ::isBinaryModuleUpToDate (const char * modulePath , slang::IBlob * binaryModuleBlob );
If the compiler options or source files have been changed since the module was last compiled, the isBinaryModuleUpToDate will return false.
The compiler can be setup to automatically use the precompiled modules when they exist and up-to-date. When loading a module,
either triggered via the ISession::loadModule call or via transitive imports in the modules being loaded, the compiler will look in the
search paths for a .slang-module file first. If it exists, it will load the precompiled module instead of compiling from the source.
If you wish the compiler to verify whether the .slang-module file is up-to-date before loading it, you can specify the CompilerOptionName::UseUpToDateBinaryModule to 1
when creating the session. When this option is set, the compiler will verify the precompiled module is still update, and will recompile the module
from source if it is not up-to-date.
Additional Remarks
Link-time specialization is Slang's answer to compile-time performance and modularity issues associated with preprocessor based shader specialization. By representing specializable settings as link-time constants or link-time types, we are able to defer shader specialization to link time, allowing reuse of all the front-end compilation work that includes tokenization, parsing, type checking, IR generation and validation. As Slang evolves to support more language features and as the user code is growing to be more complex, the cost of front-end compilation will only increase over time. By using link-time specialization on precompiled modules, an application can be completely isolated from any front-end compilation cost.
1--- 2layout : user-guide 3permalink : /user-guide/link-time-specialization 4--- 5 6# Link-time Specialization and Module Precompilation 7 8Traditionally, graphics developers have been relying on preprocessor defines to specialize their shader code for high-performance GPU execution. 9While functioning systems can be built around preprocessor macros, overusing them leads to many problems: 10- Long compilation time. With preprocessor defines, specialization happens before parsing, which is a very early stage in the compilation flow. 11This means that the compiler must redo almost all work from scratch with every specialized variant, including parsing, type checking, IR generation 12 and optimization, even when two specialized variants only differ in one constant value. The lack of reuse of compiler front-end work between 13 different shader specializations contributes a significant portion to long shader compile times. 14 - Reduced code readability and maintainability. The compiler cannot enforce any structures on preprocessor macros and cannot offer static checks to 15guarantee that the preprocessor macros are used in an intended way. Macros don't blend well with the native language syntax, which leads to less 16 readable code, mystic diagnostic messages when things go wrong, and suboptimal intellisense experience. 17 - Locked in with early specialization. Once the code is written using preprocessor macros for specialization, the application that uses the shader 18code has no choice but to provide the macro values during shader compilation and always opt-in to static specialization. If the developer changes 19 their mind to move away from specialization, a lot of code needs to be rewritten. As a result, the application is locked out of opportunities to 20 take advantage of different design decisions or future hardware features that allow more efficient execution of non-specialized code. 21 22 Slang approaches the problem of shader specialization by supporting generics as a first class feature that allow most specializable code to be 23written in strongly typed code, and by allowing specialization to be triggered through link-time constants or types. 24 25As discussed in the [Compiling code with Slang](08-compiling.md) chapter, Slang provides a three-step compilation model: precompiling, linking and target code generation. 26Assuming the user shader is implemented as three Slang modules: `a.slang`, `b.slang`, and `c.slang`, the user can precompile all three modules to binary IR and store 27them as `a.slang-module`, `b.slang-module`, and `c.slang-module` in a complete offline process that is independent to any specialization arguments. 28Next, these three IR modules are linked together to form a self-contained program that will then go through a set of compiler optimizations for target code generation. 29Slang's compilation model allows specialization arguments, in the form of constants or types to be provided during linking. This means that specialization happens at 30a much later stage of compilation, reusing all the work done during module precompilation. 31 32## Link-time Constants 33 34The simplest form of link time specialization is done through link-time constants. See the following code for an example. 35``` c++ 36// main.slang 37 38// Define a constant whose value will be provided in another module at link time. 39extern static const int kSampleCount; 40 41float sample(int index) {...} 42 43RWStructuredBuffer<float> output; 44void main(uint tid : SV_DispatchThreadID) 45{ 46[ForceUnroll] 47for (int i = 0; i < kSampleCount; i++) 48output[tid] += sample(i); 49} 50``` 51This code defines a compute shader that can be specialized with different constant values of `kSampleCount`. The `extern` modifier means that 52`kSampleCount` is a constant whose value is not provided within the current module, but will be resolved during the linking step. 53The `main.slang` file can be compiled offline into a binary IR module with the `slangc` tool: 54``` 55slangc main.slang -o main.slang-module 56``` 57 58To specialize the code with a value of `kSampleCount`, the user can create another module that defines it: 59 60``` c++ 61// sample-count.slang 62export static const int kSampleCount = 2; 63``` 64 65This file can also be compiled separately: 66``` 67slangc sample-count.slang -o sample-count.slang-module 68``` 69 70With these two modules precompiled, we can link them together to get our specialized code: 71``` 72slangc sample-count.slang-module main.slang-module -target hlsl -entry main -profile cs_6_0 -o main.hlsl 73``` 74 75This process can also be done with Slang's compilation API as in the following code snippet: 76 77``` c++ 78 79ComPtr<slang::ISession> slangSession = ...; 80ComPtr<slang::IBlob> diagnosticsBlob; 81 82// Load the main module from file. 83slang::IModule* mainModule = slangSession->loadModule("main.slang", diagnosticsBlob.writeRef()); 84 85// Load the specialization constant module from string. 86const char* sampleCountSrc = R"(export static const int kSampleCount = 2;)"; 87slang::IModule* sampleCountModule = slangSession->loadModuleFromSourceString( 88"sample-count", // module name 89"sample-count.slang", // synthetic module path 90sampleCountSrc, // module source content 91diagnosticsBlob.writeRef()); 92 93// Compose the modules and entry points. 94ComPtr<slang::IEntryPoint> computeEntryPoint; 95SLANG_RETURN_ON_FAIL( 96mainModule->findEntryPointByName(entryPointName, computeEntryPoint.writeRef())); 97 98std::vector<slang::IComponentType*> componentTypes; 99componentTypes.push_back(mainModule); 100componentTypes.push_back(computeEntryPoint); 101componentTypes.push_back(sampleCountModule); 102 103ComPtr<slang::IComponentType> composedProgram; 104SlangResult result = slangSession->createCompositeComponentType( 105componentTypes.data(), 106componentTypes.size(), 107composedProgram.writeRef(), 108diagnosticsBlob.writeRef()); 109 110// Link. 111ComPtr<slang::IComponentType> linkedProgram; 112composedProgram->link(linkedProgram.writeRef(), diagnosticsBlob.writeRef()); 113 114// Get compiled code. 115ComPtr<slang::IBlob> compiledCode; 116linkedProgram->getEntryPointCode(0, 0, compiledCode.writeRef(), diagnosticsBlob.writeRef()); 117 118``` 119 120## Link-time Types 121 122In addition to constants, you can also define types that are specified at link-time. For example, given the following modules: 123 124``` csharp 125// common.slang 126interface ISampler 127{ 128int getSampleCount(); 129float sample(int index); 130} 131struct FooSampler : ISampler 132{ 133int getSampleCount() { return 1; } 134float sample(int index) { return 0.0; } 135} 136struct BarSampler : ISampler 137{ 138int getSampleCount() { return 2; } 139float sample(int index) { return index * 0.5; } 140} 141``` 142 143``` csharp 144// main.slang 145import common; 146extern struct Sampler : ISampler; 147 148RWStructuredBuffer<float> output; 149void main(uint tid : SV_DispatchThreadID) 150{ 151Sampler sampler; 152for (int i = 0; i < sampler.getSampleCount(); i++) 153output[tid] += sampler.sample(i); 154} 155``` 156 157Again, we can separately compile these modules into binary forms independently from how they will be specialized. 158To specialize the shader, we can author a third module that provides a definition for the `extern Sampler` type: 159 160``` csharp 161// sampler.slang 162import common; 163export struct Sampler : ISampler = FooSampler; 164``` 165 166The `=` syntax defines a typealias that allows `Sampler` to resolve to `FooSampler` at link-time. 167Note that both the name and type conformance clauses must match exactly between an `export` and an `extern` declaration 168for link-time types to resolve correctly. Link-time types can also be generic, and may conform to generic interfaces. 169 170When all these three modules are linked, we will produce a specialized shader that uses the `FooSampler`. 171 172## Providing Default Settings 173 174When defining an `extern` symbol as a link-time constant or type, it is allowed to provide a default value for that constant or type. 175When no other modules exists to `export` the same-named symbol, the default value will be used in the linked program. 176 177For example, the following code is considered complete at linking and can proceed to code generation without any issues: 178``` c++ 179// main.slang 180 181// Provide a default value when no other modules are exporting the symbol. 182extern static const int kSampleCount = 2; 183// ... 184void main(uint tid : SV_DispatchThreadID) 185{ 186[ForceUnroll] 187for (int i = 0; i < kSampleCount; i++) 188output[tid] += sample(i); 189} 190``` 191 192## Using Precompiling Modules with the API 193 194In addition to using `slangc` for precompiling Slang modules, the `IModule` class provides a method to serialize itself to disk: 195 196``` C++ 197/// Get a serialized representation of the checked module. 198SlangResult IModule::serialize(ISlangBlob** outSerializedBlob); 199 200/// Write the serialized representation of this module to a file. 201SlangResult IModule::writeToFile(char const* fileName); 202``` 203 204These functions will write only the module itself to a file, which excludes the modules that it includes. To write all imported 205modules, you can use methods from the `ISession` class to enumerate all currently loaded modules (including transitively imported modules) 206in the session: 207 208``` c++ 209SlangInt ISession::getLoadedModuleCount(); 210IModule* ISession::getLoadedModule(SlangInt index); 211``` 212 213Additionally, the `ISession` class also provides a function to query if a previously compiled module is still up-to-date with the current 214Slang version, the compiler options in the session and the current content of the source files used to compile the module: 215 216``` c++ 217bool ISession::isBinaryModuleUpToDate( 218const char* modulePath, 219slang::IBlob* binaryModuleBlob); 220``` 221 222If the compiler options or source files have been changed since the module was last compiled, the `isBinaryModuleUpToDate` will return false. 223 224The compiler can be setup to automatically use the precompiled modules when they exist and up-to-date. When loading a module, 225either triggered via the `ISession::loadModule` call or via transitive `import`s in the modules being loaded, the compiler will look in the 226search paths for a `.slang-module` file first. If it exists, it will load the precompiled module instead of compiling from the source. 227If you wish the compiler to verify whether the `.slang-module` file is up-to-date before loading it, you can specify the `CompilerOptionName::UseUpToDateBinaryModule` to `1` 228when creating the session. When this option is set, the compiler will verify the precompiled module is still update, and will recompile the module 229from source if it is not up-to-date. 230 231 232## Additional Remarks 233 234Link-time specialization is Slang's answer to compile-time performance and modularity issues associated with preprocessor 235based shader specialization. By representing specializable settings as link-time constants or link-time types, we are able 236to defer shader specialization to link time, allowing reuse of all the front-end compilation work that includes tokenization, 237parsing, type checking, IR generation and validation. As Slang evolves to support more language features and as the user code 238is growing to be more complex, the cost of front-end compilation will only increase over time. By using link-time specialization 239on precompiled modules, an application can be completely isolated from any front-end compilation cost.