Recursive AI

Shader variants with defines and includes

All oneshader recipes

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:

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