---
title: "Getting started with OneShader"
url: https://recursiveai.org/oneshader/docs/getting-started/
description: "Compile HLSL shaders during dotnet build with errors on the HLSL line, use the C# structs and bindings generated from them, emit Metal and WGSL next to…"
version: "1.2.0"
---

# Getting started with OneShader

Compile HLSL shaders during `dotnet build` with errors on the HLSL line, use the C# structs and bindings generated from them, emit Metal and WGSL next to SPIR-V, build pipeline layouts, and compile at run time or from the command line.

OneShader is an HLSL compiler written in C# with no dependency beyond .NET itself. Modern HLSL (Shader Model 6 style, HLSL 2021) for vertex, pixel and compute shaders goes in. SPIR-V for Vulkan 1.2 and newer, Metal Shading Language and WGSL come out, each with reflection. Every sample on this page was compiled and run against the 1.2.0 packages from NuGet.

## Install

OneShader targets .NET 10 (`net10.0`), so you need the .NET 10 SDK. For shaders built into a project, reference the build package:

```sh
dotnet new console -o Start
cd Start
dotnet add package RecursiveAI.OneShader.Build --version 1.2.0
```

Build with the .NET SDK (`dotnet build` or `dotnet run`). Visual Studio’s .NET Framework MSBuild can’t load the build task. The build package brings the compiler package, `RecursiveAI.OneShader`, with it. The license is MIT or Apache-2.0, at your choice.

## Compile a shader with the project

Every `*.hlsl` file in the project is compiled on build. Save this as `Shaders/Sprite.hlsl`:

```hlsl
cbuffer Frame : register(b0) { float4x4 ViewProjection; float4 Tint; };
[[vk::binding(1)]] Texture2D<float4> Atlas;
[[vk::binding(2)]] SamplerState AtlasSampler;

struct VSIn
{
    float2 Position : POSITION;
    float2 Uv : TEXCOORD0;
    [[oneshader::format("unorm8x4")]] 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 * Tint;
    return o;
}

[shader("pixel")]
float4 PSMain(VSOut i) : SV_Target { return Atlas.Sample(AtlasSampler, i.Uv) * i.Color; }
```

The build generates a static class named after the file, `Sprite`, in the namespace `<RootNamespace>.Shaders`. `Program.cs`:

```csharp
using System.Numerics;
using System.Runtime.InteropServices;
using Start.Shaders;   // <RootNamespace>.Shaders

// The SPIR-V of each entry point is embedded in the assembly.
ReadOnlySpan<byte> vs = Sprite.VSMain.SpirV;
ReadOnlySpan<byte> ps = Sprite.PSMain.SpirV;
Console.WriteLine($"VSMain: {vs.Length} bytes of SPIR-V, PSMain: {ps.Length} bytes");

// The constant buffer as a C# struct, laid out like the shader's.
var frame = new Sprite.Frame { ViewProjection = Matrix4x4.Identity, Tint = Vector4.One };
ReadOnlySpan<byte> upload = MemoryMarshal.AsBytes(new ReadOnlySpan<Sprite.Frame>(in frame));
Console.WriteLine($"Frame: {Sprite.Frame.Size} bytes, Tint at offset {Marshal.OffsetOf<Sprite.Frame>("Tint")}, {upload.Length} bytes to upload");

// Vertex input: one interleaved buffer.
Console.WriteLine($"Vertex stride: {Sprite.VSMain.Stride}");
foreach (var a in Sprite.VSMain.Inputs)
    Console.WriteLine($"  location {a.Location}: {a.Format} at offset {a.Offset}");

// Where each resource is bound.
foreach (var b in Sprite.Bindings.All)
    Console.WriteLine($"{b.Name}: set {b.Set}, binding {b.Binding}, {b.Kind}, stages {b.Stages}");
```

`dotnet run` prints:

```text
VSMain: 1272 bytes of SPIR-V, PSMain: 856 bytes
Frame: 80 bytes, Tint at offset 64, 80 bytes to upload
Vertex stride: 20
  location 0: Float32x2 at offset 0
  location 1: Float32x2 at offset 8
  location 2: Unorm8x4 at offset 16
Frame: set 0, binding 0, ConstantBuffer, stages Vertex
Atlas: set 0, binding 1, SampledTexture, stages Pixel
AtlasSampler: set 0, binding 2, Sampler, stages Pixel
```

- Entry points come from the `[shader("vertex")]`, `[shader("pixel")]` and `[shader("compute")]` attributes. Each becomes a nested class with `Stage`, `EntryPoint` and `SpirV`, a span over bytes embedded in the assembly. Nothing is read from disk at run time.
- `Sprite.Frame` is a blittable struct with an explicit layout whose field offsets equal the compiled shader’s, so its bytes go to the GPU as they are. Vectors and matrices are `System.Numerics` types. A `Matrix4x4` member’s element (i, j) is HLSL’s `M[i][j]`.
- `[[oneshader::format("unorm8x4")]]` declares a packed vertex format: the buffer holds 4 bytes, the shader sees a `float4`. That’s why the stride is 20, not 32. DXC compiles the same file with a warning that it ignores the attribute.
- Without `[[vk::binding]]` or a register, resources are numbered in declaration order. Layouts and bindings follow the rules DXC uses for Vulkan, so existing shaders keep theirs.

All the generated members are listed in [Generated bindings](https://recursiveai.org/oneshader/docs/generated-bindings/).

## Shader errors are build errors

Misspell `Atlas` in the pixel shader and build again:

```text
/tmp/oneshader-docs/Start/Shaders/Sprite.hlsl(24,45): error OSH0300: 'Atlass' is not declared. Did you mean 'Atlas'? [/tmp/oneshader-docs/Start/Start.csproj]
```

The error is on the HLSL line and column, with a stable `OSHnnnn` code, so the IDE jumps to it like a C# error. A shader is compiled again only when it, a file it includes, or the options change.

## Add Metal and WebGPU

SPIR-V is the default. Select more targets in the project file:

```xml
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <OneShaderTargets>SpirV;Msl;Wgsl</OneShaderTargets>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="RecursiveAI.OneShader.Build" Version="1.2.0" />
  </ItemGroup>

</Project>
```

With the same `Shaders/Sprite.hlsl`:

```csharp
using System.Text;
using Targets.Shaders;

// WGSL for WebGPU and MSL for Metal, next to the SPIR-V.
string wgsl = Encoding.UTF8.GetString(Sprite.PSMain.Wgsl);
string msl = Encoding.UTF8.GetString(Sprite.PSMain.Msl);
Console.WriteLine($"WGSL entry: {Sprite.PSMain.WgslEntryPoint}, MSL function: {Sprite.PSMain.MslFunctionName}");
Console.WriteLine($"Metal vertex buffer index: {Sprite.VSMain.MslVertexBufferIndex}");
Console.WriteLine($"Atlas: @group({Sprite.Bindings.Atlas.Set}) @binding({Sprite.Bindings.Atlas.Binding}), Metal texture({Sprite.Bindings.Atlas.MslIndex})");
Console.WriteLine();
Console.WriteLine(wgsl);
File.WriteAllText("sprite.ps.metal", msl);
```

The start of the output:

```text
WGSL entry: PSMain, MSL function: PSMain
Metal vertex buffer index: 30
Atlas: @group(0) @binding(1), Metal texture(0)

diagnostic(off, derivative_uniformity);

struct PSMain_Output {
  @location(0) PSMain_result: vec4<f32>,
}

@group(0) @binding(1) var Atlas: texture_2d<f32>;
@group(0) @binding(2) var AtlasSampler: sampler;
...
@fragment
fn PSMain(@builtin(position) in_PSMain_i_Position: vec4<f32>, @location(0) in_PSMain_i_Uv: vec2<f32>, @location(1) in_PSMain_i_Color: vec4<f32>) -> PSMain_Output {
...
```

- WGSL uses the Vulkan numbers as `@group` and `@binding`. Metal counts buffers, textures and samplers separately, so `Atlas` is texture 0. The interleaved vertex buffer is Metal buffer 30.
- The constant-buffer and vertex structs are the same for every target: all three backends use one byte layout.
- A shader that a target can’t express fails the build with an error that names the target, never a silent miscompile. Clip distances on WGSL, for example, are error OSH5202. That’s why the default is SPIR-V only. A target you don’t select costs no build time.
- Clip-space Y points up in Metal and WebGPU and down in Vulkan. The shader isn’t adjusted, so flip it with the viewport.

## Build pipeline layouts

`OneShader.Layouts` turns a shader into the layouts a renderer creates, as plain data with no graphics API behind it. In the project with all three targets:

```csharp
using OneShader.Layouts;
using OneShader.Layouts.Metal;
using OneShader.Layouts.Vulkan;
using OneShader.Layouts.WebGpu;
using Layouts.Shaders;

// The vertex and pixel entry points of Sprite.hlsl, merged.
PipelineLayout pipeline = Sprite.Pipeline;

// Vulkan: descriptor set layouts and vertex input, with the numeric Vk* enum values.
PipelineLayoutInfo vulkan = pipeline.ToVulkan();
foreach (DescriptorSetLayoutInfo set in vulkan.SetLayouts)
    foreach (DescriptorSetLayoutBindingInfo b in set.Bindings)
        Console.WriteLine($"set {set.Set} binding {b.Binding}: {b.DescriptorType} ({(uint)b.DescriptorType}) x{b.DescriptorCount}, stages {b.StageFlags}");

VertexInputInfo input = Sprite.VSMain.Layout.Vertex!.ToVulkan();
foreach (VertexInputAttributeInfo a in input.Attributes)
    Console.WriteLine($"location {a.Location}: {a.Format} ({(uint)a.Format}) at {a.Offset}, stride {input.Bindings[0].Stride}");

// Metal: the argument index of each resource, per stage.
if (pipeline.TryToMetal(out MetalPipelineLayout? metal, out var metalError))
    foreach (MetalStageLayout stage in metal!.Stages)
        foreach (MetalArgument arg in stage.Arguments)
            Console.WriteLine($"Metal {stage.Stage}: {arg.Name} -> {arg.Kind}({arg.Index})");
else
    Console.WriteLine(metalError);

// WebGPU: bind group layout entries, with the API's strings.
if (pipeline.TryToWebGpu(out WebGpuPipelineLayout? webgpu, out var webgpuError))
    foreach (BindGroupLayoutInfo group in webgpu!.Groups)
        foreach (BindGroupLayoutEntryInfo e in group.Entries)
            Console.WriteLine($"WebGPU group {group.Group} binding {e.Binding}: {e.Name}, {e.Kind}, visibility {e.Visibility}");
else
    Console.WriteLine(webgpuError);
```

```text
set 0 binding 0: UniformBuffer (6) x1, stages Vertex
set 0 binding 1: SampledImage (2) x1, stages Fragment
set 0 binding 2: Sampler (0) x1, stages Fragment
location 0: R32G32Sfloat (103) at 0, stride 20
location 1: R32G32Sfloat (103) at 8, stride 20
location 2: R8G8B8A8Unorm (37) at 16, stride 20
Metal Vertex: Frame -> Buffer(0)
Metal Pixel: Atlas -> Texture(0)
Metal Pixel: AtlasSampler -> Sampler(0)
WebGPU group 0 binding 0: Frame, Buffer, visibility Vertex
WebGPU group 0 binding 1: Atlas, Texture, visibility Fragment
WebGPU group 0 binding 2: AtlasSampler, Sampler, visibility Fragment
```

- The numbers in parentheses are the values of `VkDescriptorType` and `VkFormat`, ready to cast into whichever Vulkan binding you use.
- `Sprite.Pipeline` is the file’s vertex and pixel entry points (or its one compute entry point). For other combinations, `PipelineLayout.TryCreate` merges any entry layouts and returns a conflict as a `LayoutError` value, never an exception.
- `TryToMetal` fails with `MissingTarget` when `Msl` wasn’t built. `TryToWebGpu` reports what WebGPU can’t express, such as resource arrays, with what to change.
- Creating layouts allocates, so do it when a shader loads, not every frame.

## Compile at run time

For tools, editors and hot reload, call the compiler directly. It’s the `RecursiveAI.OneShader` package (`dotnet add package RecursiveAI.OneShader --version 1.2.0`), namespace `OneShader`:

```csharp
using OneShader;
using OneShader.Reflection;

CompileResult result = ShaderCompiler.CompileFile("Shaders/Sprite.hlsl", new CompileOptions
{
    Targets = [CompileTarget.SpirV, CompileTarget.Wgsl],
});

foreach (var d in result.Diagnostics)
    Console.WriteLine(d);   // Shaders/Sprite.hlsl(24,45): error OSH0300: ...
if (!result.Success)
    return 1;

foreach (ShaderBlob blob in result.Blobs)
    Console.WriteLine($"{blob.EntryPoint} ({blob.Stage}) -> {blob.Target}: {blob.Data.Length} bytes");

File.WriteAllBytes("sprite.ps.spv", result.Find("PSMain", CompileTarget.SpirV)!.ToArray());
File.WriteAllText("sprite.ps.wgsl", result.Find("PSMain", CompileTarget.Wgsl)!.AsText());

// Reflection: what the shader binds, where, and in which stages.
foreach (ResourceInfo r in result.Reflection!.Resources)
    Console.WriteLine($"{r.Name}: {r.Kind}, set {r.Slots[0].Set} binding {r.Slots[0].Binding}, {r.Stages}");
return 0;
```

```text
VSMain (Vertex) -> SpirV: 1272 bytes
VSMain (Vertex) -> Wgsl: 1640 bytes
PSMain (Pixel) -> SpirV: 856 bytes
PSMain (Pixel) -> Wgsl: 841 bytes
Frame: ConstantBuffer, set 0 binding 0, Vertex
Atlas: SampledTexture, set 0 binding 1, Pixel
AtlasSampler: Sampler, set 0 binding 2, Pixel
```

- Errors come back as diagnostics with file, line and column, never as exceptions: bad input gives diagnostics, not an exception or a stack overflow. `ShaderCompiler.Compile(source, path, options)` takes the text instead of a path.
- Output is deterministic: the same source and options give the same bytes. `Compile` can be called from several threads at once.
- `sprite.ps.spv` passes `spirv-val` for Vulkan 1.2, and `sprite.ps.wgsl` passes naga.
- `result.Files` lists every file the compile read, including `#include`s, for a file watcher. See [Reload shaders while the app runs](https://recursiveai.org/oneshader/cookbook/hot-reload-shaders/).

## Compile from the command line

`RecursiveAI.OneShader.Tool` is the `oneshader` command, with DXC’s core flags:

```sh
dotnet tool install -g RecursiveAI.OneShader.Tool --version 1.2.0
oneshader -T ps_6_0 -E PSMain -spirv Sprite.hlsl -Fo sprite.ps.spv
oneshader --all-entries --target spirv --target msl --target wgsl --reflect sprite.json Sprite.hlsl -Fo out/sprite
```

The second command writes `sprite.VSMain.spv`, `.metal` and `.wgsl` and the same three for `PSMain`, plus the reflection as JSON. The tool needs the .NET 10 runtime. Exit code 0 is success, 1 a shader that didn’t compile, 2 a wrong command line. See [Switch from the DXC command line](https://recursiveai.org/oneshader/cookbook/migrate-from-dxc/).

## New in 1.2.0

- **Shader variants in the build.** `Axes` metadata on an item (such as `ALPHA_TEST` and `LIGHTING=NONE|LAMBERT|PBR`) compiles every combination, and the generated `Key` picks one at run time. See [Shader variants with Axes](https://recursiveai.org/oneshader/cookbook/shader-variants-axes/).
- **Specialization constants.** `[[vk::constant_id(N)]] const` values set when the pipeline is created, on Vulkan, Metal and WebGPU, with a generated `Specialization` struct. See [Specialization constants](https://recursiveai.org/oneshader/cookbook/specialization-constants/).
- **16-bit and 64-bit types.** `half`, `int16_t`, `double`, `uint64_t` and 64-bit atomics, as DXC compiles them, with the device features each entry point needs. See [16-bit and 64-bit types](https://recursiveai.org/oneshader/cookbook/hlsl-16-bit-64-bit-types/).
- **A compile cache** for run-time compiles. See [Cache shader compiles](https://recursiveai.org/oneshader/cookbook/shader-compile-cache/).

The full list is in the [changelog](https://recursiveai.org/oneshader/docs/changelog/).

## The packages

All three packages share one version number.

| Package | What it is | License |
|---|---|---|
| `RecursiveAI.OneShader.Build` | MSBuild task and source generator: compiles `*.hlsl` during `dotnet build`, reports errors on the HLSL line, embeds the selected targets, builds shader variants and generates C# structs, bindings and layouts. Guide: [the build package](https://recursiveai.org/oneshader/docs/build-package/). Brings `RecursiveAI.OneShader` with it. | MIT or Apache-2.0 |
| `RecursiveAI.OneShader` | The compiler: `ShaderCompiler`, reflection, `InterfaceCompatibility` for hot reload, `ShaderCompileCache`, pipeline layouts and specialization values (`OneShader.Layouts`), and the support types the generated code uses. Namespace `OneShader`. Guide: [the compiler package](https://recursiveai.org/oneshader/docs/compiler/). No `unsafe` code, NativeAOT and trimming supported. | MIT or Apache-2.0 |
| `RecursiveAI.OneShader.Tool` | The `oneshader` command-line compiler, a .NET tool with DXC-style flags, for CMake, Make and scripts. Guide: [the command line](https://recursiveai.org/oneshader/docs/command-line/). | MIT or Apache-2.0 |

## What OneShader 1.2.0 doesn’t do

- **Other shader stages.** Vertex, pixel and compute only: no geometry, tessellation, mesh or ray-tracing shaders.
- **Direct3D bytecode.** There’s no DXIL output. For Direct3D 12, keep DXC.
- **GLSL.** The input is HLSL and the outputs are SPIR-V, MSL and WGSL: GLSL is neither read nor written.
- **Unbounded resource arrays** (`Texture2D Tex[]`) and the partitioned wave operations `WaveMatch` and `WaveMultiPrefix*` are errors (OSH0350). Bounded arrays and the other wave and quad intrinsics work.
- **Every feature on every target.** Some constructs exist on one target and not another: `double`, for example, is SPIR-V only. The [MSL](https://recursiveai.org/oneshader/docs/msl-backend/) and [WGSL](https://recursiveai.org/oneshader/docs/wgsl-backend/) pages list them, and each one is a compile error naming the target.

## Where next

Cookbook recipes, each with a complete, run sample:

- [Compile HLSL to SPIR-V for Vulkan](https://recursiveai.org/oneshader/cookbook/hlsl-to-spirv/), [convert HLSL to WGSL](https://recursiveai.org/oneshader/cookbook/hlsl-to-wgsl/), [compile HLSL to Metal Shading Language](https://recursiveai.org/oneshader/cookbook/hlsl-to-metal/)
- [Compile HLSL in a C# project with dotnet build](https://recursiveai.org/oneshader/cookbook/compile-hlsl-csharp/), [match constant buffers with C# structs](https://recursiveai.org/oneshader/cookbook/cbuffer-packing-csharp-structs/), [push constants on every target](https://recursiveai.org/oneshader/cookbook/push-constants/)
- [Read shader reflection in C#](https://recursiveai.org/oneshader/cookbook/spirv-reflection-csharp/), [reload shaders while the app runs](https://recursiveai.org/oneshader/cookbook/hot-reload-shaders/), [shader variants with defines and includes](https://recursiveai.org/oneshader/cookbook/shader-variants-defines-includes/)
- [Write a compute shader for Vulkan and WebGPU](https://recursiveai.org/oneshader/cookbook/hlsl-compute-shader/), [wave intrinsics on Vulkan, Metal and WebGPU](https://recursiveai.org/oneshader/cookbook/hlsl-wave-intrinsics/), [InterlockedAdd and float atomics](https://recursiveai.org/oneshader/cookbook/hlsl-interlockedadd/), [the precise keyword](https://recursiveai.org/oneshader/cookbook/hlsl-precise-keyword/)
- [Shader variants with Axes](https://recursiveai.org/oneshader/cookbook/shader-variants-axes/), [specialization constants](https://recursiveai.org/oneshader/cookbook/specialization-constants/), [16-bit and 64-bit types](https://recursiveai.org/oneshader/cookbook/hlsl-16-bit-64-bit-types/), [cache shader compiles](https://recursiveai.org/oneshader/cookbook/shader-compile-cache/)
- [Switch from the DXC command line](https://recursiveai.org/oneshader/cookbook/migrate-from-dxc/)

The reference docs:

- The [overview](https://recursiveai.org/oneshader/docs/overview/) and the package guides: [build](https://recursiveai.org/oneshader/docs/build-package/), [compiler](https://recursiveai.org/oneshader/docs/compiler/), [command line](https://recursiveai.org/oneshader/docs/command-line/)
- [Generated bindings](https://recursiveai.org/oneshader/docs/generated-bindings/) and the [reflection file format](https://recursiveai.org/oneshader/docs/reflection-format/)
- The backends: [SPIR-V](https://recursiveai.org/oneshader/docs/spirv-backend/), [Metal Shading Language](https://recursiveai.org/oneshader/docs/msl-backend/), [WGSL](https://recursiveai.org/oneshader/docs/wgsl-backend/), and the [intermediate representation](https://recursiveai.org/oneshader/docs/ir/) they share
- How it’s checked and how fast it is: [WebGPU execution parity](https://recursiveai.org/oneshader/docs/webgpu-parity/), [performance against DXC](https://recursiveai.org/oneshader/docs/performance/), and the [changelog](https://recursiveai.org/oneshader/docs/changelog/)

Guides: [HLSL vs GLSL](https://recursiveai.org/oneshader/articles/hlsl-vs-glsl/) and [HLSL to SPIR-V compiler compared](https://recursiveai.org/oneshader/articles/hlsl-to-spirv-compiler/).
