---
title: "Reload shaders while the app runs"
url: https://recursiveai.org/oneshader/cookbook/hot-reload-shaders/
description: "Recompile an HLSL shader when its file changes, swap in the new code while the app keeps running, and find out when an edit changed the interface the…"
version: "1.2.0"
---

# Reload shaders while the app runs

Recompile an HLSL shader when its file changes, swap in the new code while the app keeps running, and find out when an edit changed the interface the generated C# was built against, such as a constant-buffer offset.

A project built with `RecursiveAI.OneShader.Build` embeds its shaders and generates C# against their layout. During development you can recompile at run time with `ShaderCompiler.CompileFile`. An edit to a function body can be swapped in. An edit that moves a constant-buffer member can’t, because the generated `Frame` struct would write the wrong bytes. `InterfaceCompatibility.Compare` tells the two apart.

The project references `RecursiveAI.OneShader.Build` 1.2.0 and contains the `Shaders/Sprite.hlsl` of [Getting started](https://recursiveai.org/oneshader/docs/getting-started/). `Program.cs` runs four edits through the same reload function:

```csharp
using OneShader;
using OneShader.Reflection;
using HotReload.Shaders;

// What the C# was built against: the reflection embedded by the build.
ShaderReflection built = Sprite.Reflection;

// 1. Recompile the shader as it is on disk (in a real loop: when the file watcher fires).
Reload("Shaders/Sprite.hlsl");

// 2. An edit to a function body: still compatible.
string source = File.ReadAllText("Shaders/Sprite.hlsl");
string edited = Path.Combine(Path.GetTempPath(), "Sprite.hlsl");   // outside the project, or the build compiles it too
File.WriteAllText(edited, source.Replace("* i.Color;", "* i.Color * 0.5;"));
Reload(edited);

// 3. A new constant-buffer member before Tint: Tint moves, so the generated Frame struct is wrong.
File.WriteAllText(edited, source.Replace("float4x4 ViewProjection;", "float4x4 ViewProjection; float4 Fog;"));
Reload(edited);

// 4. A typo: diagnostics, not an exception.
File.WriteAllText(edited, source.Replace("AtlasSampler, i.Uv", "AtlasSampler, i.UV"));
Reload(edited);

void Reload(string path)
{
    CompileResult result = ShaderCompiler.CompileFile(path);
    Console.WriteLine($"{path}: watch {string.Join(", ", result.Files.Select(Path.GetFileName))}");
    if (!result.Success)
    {
        foreach (var d in result.Diagnostics) Console.WriteLine(d);   // keep running the last good shader
        return;
    }
    CompatibilityReport report = InterfaceCompatibility.Compare(built, result.Reflection!);
    if (report.IsCompatible)
        Console.WriteLine($"  compatible: swap in {result.Find("PSMain", CompileTarget.SpirV)!.Data.Length} bytes of SPIR-V");
    else
        Console.WriteLine($"  not compatible:\n{report}");
}
```

```text
Shaders/Sprite.hlsl: watch Sprite.hlsl
  compatible: swap in 856 bytes of SPIR-V
/tmp/Sprite.hlsl: watch Sprite.hlsl
  compatible: swap in 920 bytes of SPIR-V
/tmp/Sprite.hlsl: watch Sprite.hlsl
  not compatible:
incompatible with the compiled-in C# bindings:
  Frame.Tint moved from offset 64 to 80
  Frame.Fog was added at offset 64
Rebuild the project to update the generated C# bindings.
/tmp/Sprite.hlsl: watch Sprite.hlsl
/tmp/Sprite.hlsl(24,74): error OSH0308: The struct 'VSOut' has no member 'UV'. Did you mean 'Uv'?
```

## Good to know

- **What to watch.** `result.Files` lists the file and every `#include` it read, even when the compile fails, so the watcher keeps following a shader that’s broken mid-edit.
- **Compatible** means everything the generated C# relies on is unchanged: constant- and structured-buffer layouts (offsets, types, strides, sizes), vertex input locations, formats and offsets, each resource’s set, binding, kind, count and stages, the entry points and their stages, and compute thread-group sizes. Edits to function bodies, new entry points, and resources that become visible to fewer stages are compatible.
- **Errors never throw.** A failed compile returns diagnostics, so the loop keeps the last good shader running.
- **Threads.** `ShaderCompiler.Compile` can be called from several threads at once, and gives the same bytes as a serial compile.
- **Keep scratch copies out of the project.** Every `*.hlsl` under the project folder is compiled by the build, edited copies included.
- **`Sprite.Reflection`** is stored as text in the assembly (about 1 to 4 KB per shader) and parsed on first read. A program that never reads it carries no parser after trimming.

The repository’s `samples/HotReload` is a complete console loop with a file watcher. For reading the reflection itself, see [Read shader reflection in C#](https://recursiveai.org/oneshader/cookbook/spirv-reflection-csharp/).
