Write Stride shaders in C#: typed, checked by the C# compiler, reloaded
live. The engine's own SDSL shaders are available as C# classes to inherit, call and modify, and any
.sdsl converts to C# and back.
Experimental. This project was written by Claude (Anthropic's AI) under my direction; I could not have built it on my own, so take it as an experiment rather than a finished tool. It is tested though: 469 of the engine's shaders go SDSL → C# → SDSL and compile to identical SPIR-V, and the CPU run of the demo is compared with the GPU's pixel by pixel.
[Shader, Mixin(typeof(Global))]
public partial class DemoRings : ImageEffectShader
{
[Stage]
public override float4 Shading()
{
float2 p = streams.TexCoord - 0.5f;
float d = length(p);
float wave = 0.5f + 0.5f * sin(d * 60.0f - Time * 5.0f);
float fade = saturate(1.0f - d * 1.6f);
float3 color = lerp(new float3(0.05f, 0.1f, 0.3f), new float3(1.0f, 0.8f, 0.3f), wave) * fade;
return new float4(color, 1.0f);
}
}is translated at build time, by a Roslyn source generator, to the SDSL the engine compiles:
shader DemoRings : ImageEffectShader, Global
{
stage override float4 Shading()
{
float2 p = streams.TexCoord - 0.5;
float d = length(p);
float wave = 0.5 + 0.5 * sin(d * 60.0 - Time * 5.0);
float fade = saturate(1.0 - d * 1.6);
float3 color = lerp(float3(0.05, 0.1, 0.3), float3(1.0, 0.8, 0.3), wave) * fade;
return float4(color, 1.0);
}
};- Shaders as C# classes. A
partial classmarked[Shader], written with HLSL's types (float3,float4x4,Texture2D<T>, swizzles, intrinsics): IntelliSense, go to definition, refactoring, and compile errors before the shader compiler sees anything. - The whole engine, typed. 476 of Stride's 479 shaders as C# classes (
Csl.Engine), so a shader inheritsImageEffectShader, mixes inColorUtilityor callsLuminanceUtils.Lumawith checked types. - Compute wrappers. Each compute shader gets a generated
…Effectclass: one typed property per parameter, resource checks,Dispatch(width, height). - SDSL → C#.
csl convertturns any.sdsl, the engine's included, into such a class, to read or to modify; the modified shader replaces the engine's under the same name. - Checked on the whole engine. Every engine shader is converted SDSL → C# → SDSL and both versions are compiled by the engine; the SPIR-V is compared instruction for instruction.
- Live reload in the demo: save a shader, see it redrawn.
- Run and debug on the CPU. The same C# runs on the CPU (
Csl.Cpu), engine code included, with the GPU's arithmetic as measured: Ctrl+click a pixel in the demo and step through its shader in the debugger.
Windows, the .NET 10 SDK and a Direct3D 11 GPU (feature level 11_0). Stride 4.4.0-beta8 comes from
NuGet on restore (StrideVersion in Directory.Build.props; -p:StrideUseDevPackages=true picks
the packages a local Stride checkout packs).
git clone https://github.com/Nicogo1705/StrideCSL
cd StrideCSL
dotnet run --project demo/Csl.Demo
(or Csl.Demo, the first project of StrideCSL.slnx, from Visual Studio). A window shows the shaders of
demo/Csl.Demo/Shaders/, all written in C#. Edit one and save while it runs: it is redrawn from the
new C#, no restart. C# errors are printed in the console the way the build prints them, and the
previous version stays on screen until the file compiles again.
| Shader | Kind | Shows |
|---|---|---|
DemoGradient |
image effect | The simplest one: start here. Colour from the coordinates, Time from the engine's Global. |
DemoRings |
image effect | length, sin, lerp. |
DemoMandelbrot |
image effect | A loop with a break. |
DemoWobble |
image effect | Sampling Texture0 (a checkerboard the demo makes). |
DemoLuma |
image effect | Calling an engine shader's static function: LuminanceUtils.Luma. |
DemoPlasma |
image effect, on DemoTile |
Shared code: inherits DemoTile, calls DemoCommon.Palette. |
DemoClouds |
image effect, on DemoTile |
Shared code: DemoCommon's fractal noise, warped by itself. |
DemoSphere |
image effect, on DemoTile |
Engine shaders both ways: Math.RayIntersectsSphere and Math.PI called by name (static), Utilities.FresnelSchlick through a mixin. |
DemoOverlay |
image effect, on DemoTile |
An engine shader mixed in: BlendUtils.Overlay of the checkerboard over a gradient. |
DemoBlur |
compute | A gaussian blur over the whole window, one thread per pixel, run through its generated DemoBlurEffect. |
DemoTile |
shared, inherited | The base of the tiles above: Shading written once, calling Color(p) that each tile overrides; Global mixed in for Time; Aspect, set by the app. |
DemoCommon |
shared, called | Static functions any shader calls by name: Hash, Noise, Fbm, Palette. |
Keys: 1-9 one shader alone, 0 or space all of them, B the blur on and off, + and - its radius; Ctrl+click a tile to run that pixel on the CPU, stopping in the debugger when one is attached (see Running shaders on the CPU).
The demo's launch profiles (Visual Studio's start button list, or dotnet run --project demo/Csl.Demo --launch-profile "..."):
| Profile | Does |
|---|---|
| Gallery (GPU) | The window above. |
| Gallery computed on the CPU | The same gallery, every shader run by the CPU, animated at what the CPU manages (fps in the title). |
| Meshes (GPU), Ctrl+click a pixel to debug it | A cube and a sphere drawn with a C# PBR shader (MeshShaders/DemoLitMesh: the engine's GGX terms mixed in, a sky for the ambient light, ACES); Ctrl+click runs that pixel on the CPU, vertex shader, rasterizer and pixel shader, from what the GPU had. |
| Compare meshes CPU and GPU | A frame of the meshes drawn by the GPU and rasterized by the CPU, compared; images and report in mesh-check/. |
| Benchmark the CPU frames | 10 CPU frames one after the other, their cost and the median rate, window hidden. |
| Debug a pixel on the CPU | One pixel of DemoClouds on the CPU, no GPU: under the debugger it stops before it, F11 steps into the shader. |
| Compare CPU and GPU | Every demo drawn by both, compared; images and report in cpu-check/. |
| Screenshot, GPU / CPU | One frame saved as shot-gpu.png / shot-cpu.png, window hidden. |
The profiles turn Visual Studio's Hot Reload off: the demo reloads Shaders/ itself on save, and the SDSL
constant the generator writes for each shader is a change Hot Reload cannot apply.
Each image effect is an ImageEffectShader with Shading() overridden, directly or through
DemoTile; the tiles draw into a texture that DemoBlur blurs into the back buffer. A new
non-abstract [Shader] class in the folder gets a new tile; an abstract one is shared code.
The same three ways as in SDSL, the engine's shaders and yours alike:
// Inherit: DemoTile writes Shading once and calls Color, which each tile overrides.
[Shader]
public partial class DemoPlasma : DemoTile
{
public override float3 Color(float2 p)
{
float v = sin(p.x * 8.0f + Time) + sin(length(p * 8.0f) - Time * 1.5f);
return DemoCommon.Palette(v * 0.25f); // call: a static function, by name
}
}
// Mix in: the mixed-in shader's methods become the class's own.
[Shader, Mixin(typeof(BlendUtils))]
public partial class DemoOverlay : DemoTile
{
public override float3 Color(float2 p)
{
float4 checker = Texture0.Sample(LinearRepeatSampler, streams.TexCoord * 2.0f);
float4 gradient = new float4(DemoCommon.Palette(p.x * 0.4f + Time * 0.2f), 1.0f);
return Overlay(gradient, checker).rgb; // BlendUtils.Overlay, from the engine
}
}A shader whose only role is to be shared is an abstract partial class: DemoCommon holds static
functions, DemoTile a base class with virtual methods.
Saving recompiles the folder the way the build does (Roslyn, the Csl generator,
the translator: LiveCompiler), and each shader whose SDSL changed is registered again under a new
name (DemoRings_2), so the effect compiler has nothing cached for it. The shaders that use a changed
one follow it: editing DemoCommon redraws the four tiles that call it, their SDSL pointing to
DemoCommon_2. The keys of each new name (DemoBlur_2.Radius, DemoTile_2.Aspect) are registered as
aliases of the ones the build generated, so what the app sets keeps reaching the shaders; a parameter
added while the demo runs needs a rebuild.
Csl.Demo --shot FILE.png [--time T] [--blur R] compiles the shaders from their files, draws once, saves the
image and exits, the window hidden.
| Project | Target | Role |
|---|---|---|
src/Csl.Generators |
netstandard2.0 | The code generation, both ways. As a Roslyn generator: C# [Shader] classes → SDSL, *Keys classes and compute wrappers; the stubs every shader class gets. As a library: the SDSL parser (whole files, bodies, preprocessor structure), the SDSL → C# converter and its compiler-guided fixes. |
src/Csl.Types |
net10.0 | What shader code is written with: Csl.Types (every HLSL scalar, vector and matrix type with HLSL's conversions and swizzles, the resources, the intrinsics), the attributes for what SDSL declares and C# has no keyword for, the Sdsl markers; and Csl.Cpu, which runs that code on the CPU. No Stride dependency. |
src/Csl.Engine |
net10.0 | The engine's shaders (476 of 479) as [Shader(External = true)] classes: what C# shaders inherit and call. 474 carry their bodies, for the CPU; the GPU compiles the engine's .sdsl. Written by csl engine. |
src/Csl.Runtime |
net10.0 | Running C# compute shaders: ComputeEffect wrappers, ShaderContext, allocation helpers, ShaderSourceRegistry (hands the generated SDSL to the effect compiler). |
src/Csl.Tool |
net10.0, exe csl |
csl convert: .sdsl files, or engine shaders by name, to C#. csl engine: regenerates Csl.Engine. |
demo/Csl.Demo |
net10.0, exe | The demo: C# shaders in a window, reloaded on save; also run on the CPU, pixel by pixel under the debugger, and compared with the GPU. |
tests/Csl.TestApp |
net10.0, exe | The test bench, for working on StrideCSL itself: the whole-engine round trip checked by the engine compiler, C# shaders run on the GPU and checked, the GPU's arithmetic measured, the reserved names probed. |
tests/Csl.Tests |
net10.0, xunit | Unit tests of the generator, the wrappers, the analyzer, the runtime and the CPU run, without a GPU. |
A project that writes shaders in C# references:
<ProjectReference Include="..\StrideCSL\src\Csl.Types\Csl.Types.csproj" />
<ProjectReference Include="..\StrideCSL\src\Csl.Engine\Csl.Engine.csproj" />
<ProjectReference Include="..\StrideCSL\src\Csl.Runtime\Csl.Runtime.csproj" />
<ProjectReference Include="..\StrideCSL\src\Csl.Generators\Csl.Generators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />using Csl; using Csl.Engine; using Csl.Types; using static Csl.Types.Intrinsics;
/// <summary>Output[i] = ToLinear(i / 63): an engine shader mixed in, its method called with types.</summary>
[Shader, NumThreads(64), Mixin(typeof(ColorUtility))]
public partial class CslColorLinear : ComputeShaderBase
{
[Stage] public RWBuffer<float> Output;
public override void Compute()
{
uint i = streams.DispatchThreadId.x;
Output[i] = ToLinear(i / 63.0f);
}
}The generator writes, next to the class, the SDSL (CslColorLinear.SdslSource, registered at
start-up in ShaderSourceRegistry), CslColorLinearKeys in the shape the engine gives a .sdsl,
and for a compute shader CslColorLinearEffect, a typed wrapper:
using var output = Csl.Buffers.NewTyped<float>(GraphicsDevice, 64, CslColorLinearEffect.Slots.Output);
using var effect = new CslColorLinearEffect(Services) { Output = output };
effect.Dispatch(64);A shader of any other kind (a material feature, an image effect) is used by name like any engine
shader: new ImageEffectShader(nameof(CslInvert)).
C# has one base class where SDSL has many: the first SDSL base is the C# base, the others are
[Mixin(...)]. The generated half of every shader class declares the mixins' members as stubs (so
they are in scope, typed) and streams, typed as the class itself, so streams.Position reads as in
SDSL and is checked.
Csl.Engine has the engine's shaders as C# classes; inherit one, override its methods:
[Shader]
public partial class CslInvert : ImageEffectShader
{
[Stage]
public override float4 Shading()
{
float4 color = Texture0.Sample(PointSampler, streams.TexCoord); // Texturing's, through SpriteBase
return new float4(1.0f - color.rgb, color.a);
}
}A method of a mixin (not of the C# base class) is overridden with [Override]; a mixin's method
reached through base is Sdsl.Base(this).Method().
csl convert --engine LuminanceUtils --namespace My.Shaders --out Shaders writes the engine's
LuminanceUtils as a full C# class, bodies included. Edit it; it keeps the engine's SDSL name, so
once registered (at the first ComputeEffect, or ShaderSourceRegistry.InstallInto(effectSystem))
it replaces the engine's shader in every effect that uses it. tests/Csl.TestApp/Modified does this
and the gpu tests check the result.
csl convert Shaders/MyShader.sdsl --out Shaders/Cs # a project's shaders, bodies included
csl convert --engine ComputeColorTexture --out Shaders/Cs # an engine shader, to modify
csl convert Shaders/MyShader.sdsl --declarations --out External # external classes, to extend .sdsl shaders from C#
The converter parses whole files (bodies, the #if structure, comments and doc comments), writes C#
from the tree, then compiles it with the generator and makes explicit, error after error, what HLSL
does implicitly and C# does not accept. Each fix is a marker the translator leaves out, so the SDSL
the C# translates back to is the original: Sdsl.Implicit<float3>(v) is v (a narrowing, an int
used as a condition), = default on a local is no initializer, Sdsl.Undefined(out x) is nothing.
Converting ShadingBase gives, for instance:
[Shader]
[Define("STRIDE_RENDER_TARGET_COUNT", "1", If = "!defined(STRIDE_RENDER_TARGET_COUNT)")]
public abstract partial class ShadingBase : ShaderBase
{
[Compose] public ComputeColor ShadingColor0;
[If("STRIDE_RENDER_TARGET_COUNT > 1"), Compose] public ComputeColor ShadingColor1;
...
[Stage]
public override void PSMain()
{
base.PSMain();
streams.ColorTarget = this.Shading();
if (Sdsl.If("STRIDE_RENDER_TARGET_COUNT > 1"))
{
streams.ColorTarget1 = ShadingColor1.Compute();
}
...and back exactly the SDSL it came from.
What C# says for what SDSL says
Scan report · 2026-10-10
- ✓ Prohibited terms or links
- ✓ Repository eligibility
- ✓ slopscore.md paperwork
- ✓ Content policy
- ✓ Risk review
From the balcony · 1 of 3 clapped
- Crusoeclapped
No vulnerable dependencies, clear experimental shader tooling purpose, no telemetry or credential requests, and honest about AI-assisted development with testing validation.
Cap'm Slop and Princess read it and passed. Their reasons are on the balcony, with every other verdict.
Critics are accounts on this site with no GitHub account behind them. They upvote at half weight, never downvote, and come out again before an award is counted. Who they are.

0 comments
log in to comment.