---
title: "Shader variants with defines and includes"
url: https://recursiveai.org/oneshader/cookbook/shader-variants-defines-includes/
description: "Compile several variants of one HLSL source at run time with preprocessor defines, and resolve #include from memory instead of the file system, with an…"
version: "1.2.0"
---

# Shader variants with defines and includes

Compile several variants of one HLSL source at run time with preprocessor defines, and resolve `#include` from memory instead of the file system, with an asset pack, a database or an editor buffer as the source.

`CompileOptions.Defines` is the C# form of `-D`, and `CompileOptions.IncludeResolver` is a hook the compiler calls for every `#include`. Without a resolver, `ShaderCompiler.Compile` treats `#include` as an error. `CompileFile` reads includes from disk, searching the including file’s folder and then `IncludeDirectories`.

With `RecursiveAI.OneShader` 1.2.0:

```csharp
using System.Security.Cryptography;
using OneShader;

// Shader sources held in memory: an asset pack, a database, an editor buffer.
var files = new Dictionary<string, string>
{
    ["blur.hlsl"] = """
        #include "common/sampling.hlsli"
        Texture2D<float4> Source : register(t0);
        SamplerState Linear : register(s1);

        [shader("pixel")]
        float4 PSMain(float2 uv : TEXCOORD0) : SV_Target
        {
            float4 sum = 0;
            [unroll] for (int i = -RADIUS; i <= RADIUS; i++)
                sum += Source.Sample(Linear, uv + TexelOffset(i));
            return sum / (2 * RADIUS + 1);
        }
        """,
    ["common/sampling.hlsli"] = """
        #ifndef HORIZONTAL
        #define HORIZONTAL 1
        #endif
        float2 TexelOffset(int i) { return HORIZONTAL ? float2(i / 1920.0, 0) : float2(0, i / 1080.0); }
        """,
};

ResolvedInclude? Resolve(string requested, string includingFile, bool isSystemInclude) =>
    files.TryGetValue(requested, out string? text) ? new ResolvedInclude(requested, text) : null;

// One source, four variants.
foreach (string radius in new[] { "2", "6" })
    foreach (string horizontal in new[] { "1", "0" })
    {
        CompileResult result = ShaderCompiler.Compile(files["blur.hlsl"], "blur.hlsl", new CompileOptions
        {
            Defines = [new("RADIUS", radius), new("HORIZONTAL", horizontal)],
            IncludeResolver = Resolve,
        });
        string status = result.Success
            ? "SHA-256 " + Convert.ToHexStringLower(SHA256.HashData(result.Find("PSMain", CompileTarget.SpirV)!.Data))[..12]
            : string.Join("; ", result.Diagnostics);
        Console.WriteLine($"RADIUS={radius} HORIZONTAL={horizontal}: {status}, read {string.Join(", ", result.Files)}");
    }

// A missing include is a diagnostic on the #include line.
files["blur.hlsl"] = files["blur.hlsl"].Replace("common/sampling.hlsli", "common/sample.hlsli");
CompileResult missing = ShaderCompiler.Compile(files["blur.hlsl"], "blur.hlsl", new CompileOptions
{
    Defines = [new("RADIUS", "2")],
    IncludeResolver = Resolve,
});
Console.WriteLine(missing.Diagnostics[0]);
```

```text
RADIUS=2 HORIZONTAL=1: SHA-256 605dd910624c, read blur.hlsl, common/sampling.hlsli
RADIUS=2 HORIZONTAL=0: SHA-256 2fcade900707, read blur.hlsl, common/sampling.hlsli
RADIUS=6 HORIZONTAL=1: SHA-256 0479e8957f19, read blur.hlsl, common/sampling.hlsli
RADIUS=6 HORIZONTAL=0: SHA-256 bd1d621c19f2, read blur.hlsl, common/sampling.hlsli
blur.hlsl(1,10): error OSH0120: cannot find include file 'common/sample.hlsli'; searched: 'the include resolver'. Check the spelling, or add the directory that contains it to the include paths
```

## Good to know

- **Defines.** Each entry is a name and a value. A `null` value defines the name as 1, like `-D NAME`. Defines apply before the first line of the source, so `#ifndef` defaults in an include work as above.
- **The resolver** gets the requested name, the including file and whether it was `<...>` (system) or `"..."`. Return a `ResolvedInclude(path, text)`, or `null` for “not found”. The path you return is what diagnostics and `result.Files` show, and it’s the `includingFile` the resolver gets for includes nested inside that file, so relative includes can be resolved from it.
- **Variants are deterministic.** The same source, defines and options always give the same bytes, so a hash of the inputs is a safe cache key.
- **Errors in an include** point at the include’s own line, under the path the resolver returned.
- **The same options** exist on the command line (`-D`, `-I`) and in the build package (`Defines` and `IncludeDirectories` metadata, or `OneShaderDefines` and `OneShaderIncludeDirectories`). See [Compile HLSL in a C# project](https://recursiveai.org/oneshader/cookbook/compile-hlsl-csharp/).
