A HLSL compiler for .NET, written in C# with no dependency beyond .NET itself.
Modern HLSL (Shader Model 6 style) for vertex, pixel and compute shaders goes in;
SPIR-V for Vulkan, Metal Shading Language and WGSL come out, each with
reflection. An MSBuild task and a source generator compile your *.hlsl files
during dotnet build, report errors on the HLSL line, and generate C# structs
and binding code that match the compiled shader.
How many times faster or slower OneShader compiles a shader than DXC (the released binary, SPIR-V output), one process per entry point over 68 corpus entry points, geometric mean, log scale. A bar to the right is a win for OneShader, a bar to the left is a loss:
Full results, with allocations per compile, output size, the losses and the method →
Why OneShader
- Shader errors are build errors. Put
*.hlslin a project and referenceRecursiveAI.OneShader.Build: a mistake shows up asShaders/Sprite.hlsl(24,45): error OSH0300: 'Atlass' is not declared. Did you mean 'Atlas'?on the HLSL line, with a stable code, not as a black screen at run time. - C# that matches the shader. Constant buffers, vertex inputs and resource bindings become blittable structs and constants whose offsets equal the compiled shader's, so a CPU/GPU layout mismatch fails the build (generated bindings). One layout serves every target.
- Three targets from one front end. SPIR-V (Vulkan 1.2+) is the default;
Metal Shading Language and WGSL are opt-in per project
(
<OneShaderTargets>SpirV;Msl;Wgsl</OneShaderTargets>). A feature a target cannot express is a diagnostic naming the target, never a silent miscompile (MSL, WGSL). - Checked against DXC and real GPUs. SPIR-V is validated with
spirv-valand every corpus shader is run on the GPU next to the same shader compiled by DXC, with the Vulkan validation layers on; WGSL goes throughnagaand a browser WebGPU run (WebGPU parity); MSL goes through Apple's compiler and a Metal run. - Safe on text you did not write. The compiler runs inside your build, so
bad input yields diagnostics, never an exception or a stack overflow, and
the front end is fuzzed (fuzzing). It contains no
unsafecode, uses no reflection, and is NativeAOT and trim clean. - Fast, with the losses shown. As a NativeAOT binary it compiles the corpus about 2.4 times faster than DXC, warm, and every one of the 68 entry points is faster. Run as an ordinary .NET process it is slower than DXC on every one of them (about 6 times with ReadyToRun, 14 times with the JIT), because starting the runtime and the compiler's code dominates a compile that takes 0.34 ms at the median once the process is running. Its SPIR-V is 5.7% larger than DXC's by default because it keeps debug names, and 8.8% smaller with them off. Memory use was not measured (every result).
Install
For building shaders into your project:
dotnet add package RecursiveAI.OneShader.BuildFor the compiler itself, for hot reload and tools:
dotnet add package RecursiveAI.OneShaderFrom a command line or another build system (CMake, Make, scripts), with a DXC-style interface:
dotnet tool install -g RecursiveAI.OneShader.Tool # then: oneshader -T ps_6_0 -E PSMain -spirv shader.hlsl -Fo shader.spvTarget framework net10.0. Build with the .NET SDK (dotnet build); Visual
Studio's .NET Framework MSBuild cannot load the build task. Package guides:
RecursiveAI.OneShader.Build,
RecursiveAI.OneShader and
RecursiveAI.OneShader.Tool.
Quick start
Shaders/Sprite.hlsl:
cbuffer Frame : register(b0) { float4x4 ViewProjection; }; [[vk::binding(1)]] Texture2D<float4> Atlas; [[vk::binding(2)]] SamplerState AtlasSampler; struct VSIn { float2 Position : POSITION; float2 Uv : TEXCOORD0; float4 Color : COLOR0; }; struct VSOut { float4 Position : SV_Position; float2 Uv : TEXCOORD0; float4 Color : COLOR0; }; [shader("vertex")] VSOut VSMain(VSIn i) { VSOut o; o.Position = mul(ViewProjection, float4(i.Position, 0, 1)); o.Uv = i.Uv; o.Color = i.Color; return o; } [shader("pixel")] float4 PSMain(VSOut i) : SV_Target { return Atlas.Sample(AtlasSampler, i.Uv) * i.Color; }
After dotnet build, with no reflection and no file I/O at run time:
using MyGame.Shaders; // <RootNamespace>.Shaders ReadOnlySpan<byte> vs = Sprite.VSMain.SpirV; // embedded SPIR-V var frame = new Sprite.Frame { ViewProjection = camera }; // explicit layout, Size = 64 uint set = Sprite.Bindings.Atlas.Set, binding = Sprite.Bindings.Atlas.Binding;
To compile at run time instead (tools, hot reload):
using OneShader; CompileResult result = ShaderCompiler.CompileFile("Shaders/Sprite.hlsl"); if (!result.Success) { foreach (var d in result.Diagnostics) Console.WriteLine(d); return; } byte[] spirv = result.Find("PSMain", CompileTarget.SpirV)!.ToArray();
Status
Version 1.2. Vertex, pixel and compute shaders compile to
optimized SPIR-V, Metal Shading Language and WGSL with reflection; the build
package embeds each selected target next to generated C# bindings (the
OneShaderTargets switch), and the runtime API supports hot reload with a
layout-compatibility check. Not supported: geometry, tessellation, mesh and
ray-tracing stages, unbounded resource arrays and the SM 6.5 partitioned wave operations (WaveMatch, WaveMultiPrefix*), and some
features on some targets (the backend pages list them). The public API and the
generated code follow Semantic Versioning; the
changelog lists what each release changes.
Where to go
- Using it: the package guides (build, compiler), the generated bindings, and the reflection format.
- Backends: SPIR-V, MSL, WGSL, the shared IR, and browser WebGPU parity.
- Evidence: performance and its losses (the
numbers behind the chart; reproduce with
bench/run.sh), fuzzing, and the corpus incorpus/. - What changed: CHANGELOG.md.
Building
dotnet build dotnet test
Build output goes to the directory named by ONESHADER_ARTIFACTS (or
.artifacts in the repository when it is unset). Tests that compare against
reference compilers use released binaries fetched by tools/fetch-oracles.sh;
versions are pinned in tools/oracles.env.
License
Licensed under either of MIT or Apache-2.0 at your option.
Copyright (c) Recursive AI, LLC.