---
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 structure 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, cryptic 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 allows 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](08-compiling.md) 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 of 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.
```c++
// 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:

```c++
// 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:

```c++

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:

```csharp
// 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; }
}
```

```csharp
// 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 of how they will be specialized.
To specialize the shader, we can author a third module that provides a definition for the `extern Sampler` type:

```csharp
// sampler.slang
import common;
export struct Sampler : ISampler = FooSampler;
```

The `=` syntax defines a type alias 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 module 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:
```c++
// 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 Precompiled Modules with the API

In addition to using `slangc` for precompiling Slang modules, the `IModule` class provides a method to serialize itself to disk:

```C++
/// 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 imports. 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:

```c++
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:

```c++
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 set up 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 `import`s 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 up-to-date, and will recompile the module
from source if it is not up-to-date.

> #### Standalone precompiled modules
>
> When a `.slang-module` file is distributed without its original `.slang` source on the search paths, `UseUpToDateBinaryModule` will
> still allow the binary to load: if the module's primary source file cannot be found, the freshness check accepts the binary as
> up-to-date instead of treating it as stale. A missing *secondary* dependency (a transitively included file) still marks the module
> stale, since the source-backed cache layout in that case is incomplete. Note that with the primary source absent, the compiler
> cannot validate that the binary was produced with a matching Slang version and option set — the stored content hash is not
> compared in that path. Callers who need that validation should keep the original sources reachable via the configured search paths.


## 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.