---
title: "Compiling HLSL shaders in a C# project"
url: https://recursiveai.org/oneshader/articles/csharp-shader-compiler/
description: "Four ways to compile HLSL in a .NET project: dxc from MSBuild, Vortice.Dxc, OneShader’s build package or its runtime compiler, with verified projects."
date: 2026-10-26
author: OneShader team
---

# Compiling HLSL shaders in a C# project

Four ways to compile HLSL in a .NET project: dxc from MSBuild, Vortice.Dxc, OneShader’s build package or its runtime compiler, with verified projects.

October 26, 2026 · OneShader team

A C# project can compile HLSL shaders in four ways: run Microsoft’s `dxc` from an MSBuild target, call DXC at run time through a binding such as Vortice.Dxc, or use a shader compiler written in .NET, OneShader, either as a build package or as a library. The first two need DXC’s native binaries on every machine that builds or runs the code. OneShader is managed code that runs inside `dotnet build`, and it also generates the C# structs that have to match the shader.

Each project below was built and run with the .NET 10 SDK on Linux, against DXC from the LunarG Vulkan SDK 1.4.363.0 and the [OneShader 1.2.0](https://www.nuget.org/packages/RecursiveAI.OneShader.Build/1.2.0) packages. All of them compile the same compute shader, a particle update:

*Shaders/Particles.hlsl · HLSL*

```
struct Particle { float3 Position; float Age; float3 Velocity; float Radius; };
 
cbuffer SimParams : register(b0)
{
    float  DeltaTime;
    float3 Gravity;
    uint   Count;
};
 
RWStructuredBuffer<Particle> Particles : register(u1);
 
[shader("compute")]
[numthreads(256, 1, 1)]
void Simulate(uint3 id : SV_DispatchThreadID)
{
    if (id.x >= Count)
        return;
    Particle p = Particles[id.x];
    p.Velocity += Gravity * DeltaTime;
    p.Position += p.Velocity * DeltaTime;
    p.Age += DeltaTime;
    Particles[id.x] = p;
}
```

## The options at a glance

| Approach | When it compiles | Native code needed | Outputs | C# for buffers and bindings |
|---|---|---|---|---|
| dxc from an MSBuild `Exec` target | Build | The `dxc` binary on every build machine ([DXC releases](https://github.com/microsoft/DirectXShaderCompiler/releases/tag/v1.9.2609) ship Windows and Linux) | DXIL, SPIR-V | Written by hand |
| [Vortice.Dxc](https://www.nuget.org/packages/Vortice.Dxc/3.8.3) 3.8.3 (MIT) | Run time | `dxcompiler` and `dxil` through [Vortice.Dxc.Native](https://www.nuget.org/packages/Vortice.Dxc.Native/1.0.6) 1.0.6, for linux-x64, win-x64 and win-arm64 | DXC’s; whether the bundled build includes SPIR-V output is not documented | Written by hand |
| [RecursiveAI.OneShader.Build](https://www.nuget.org/packages/RecursiveAI.OneShader.Build/1.2.0) 1.2.0 (MIT OR Apache-2.0) | Build | None | SPIR-V, Metal Shading Language, WGSL | Generated from the compiled shader |
| [RecursiveAI.OneShader](https://www.nuget.org/packages/RecursiveAI.OneShader/1.2.0) 1.2.0 (MIT OR Apache-2.0) | Run time | None | SPIR-V, Metal Shading Language, WGSL | Reflection at run time |

The native files are from each package’s file list on NuGet. Vortice.Dxc describes itself as “DirectXShaderCompiler (DXC) bindings”: the compiling is done by DXC’s own native library, shipped in Vortice.Dxc.Native. OneShader compiles vertex, pixel and compute shaders, and doesn’t write DXIL.

Two packages often found in the same search do other jobs. [ComputeSharp](https://www.nuget.org/packages/ComputeSharp/3.2.0) 3.2.0 runs “C# code in parallel on the GPU through DX12 and dynamically generated HLSL compute shaders”, so it goes from C# to HLSL, the other direction. [Veldrid.SPIRV](https://www.nuget.org/packages/Veldrid.SPIRV/1.0.15) 1.0.15, last published in June 2022, wraps SPIRV-Cross to “cross-compile SPIR-V bytecode into HLSL, GLSL, ESSL, and MSL”. It starts from SPIR-V, not from HLSL.

## Run dxc from MSBuild

The lowest-dependency way to use DXC is a target that runs it on each shader before the build, with `Inputs` and `Outputs` so that unchanged shaders are skipped:

*DxcExec.csproj · .NET 10 SDK · dxc on PATH*

```
<Project Sdk="Microsoft.NET.Sdk">
 
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
 
  <ItemGroup>
    <Shader Include="Shaders/Particles.hlsl" Profile="cs_6_0" Entry="Simulate" />
  </ItemGroup>
 
  <!-- Compile each shader with dxc when it changed, and copy the .spv next to the app. -->
  <Target Name="CompileShaders" BeforeTargets="BeforeBuild" Inputs="@(Shader)" Outputs="@(Shader->'$(IntermediateOutputPath)%(Filename).spv')">
    <MakeDir Directories="$(IntermediateOutputPath)" />
    <Exec Command="dxc -spirv -T %(Shader.Profile) -E %(Shader.Entry) &quot;%(Shader.FullPath)&quot; -Fo &quot;$(IntermediateOutputPath)%(Shader.Filename).spv&quot;" />
    <ItemGroup>
      <None Include="$(IntermediateOutputPath)%(Shader.Filename).spv" CopyToOutputDirectory="PreserveNewest" Link="%(Shader.Filename).spv" />
    </ItemGroup>
  </Target>
 
</Project>
```

*Program.cs · .NET 10*

```
byte[] spirv = File.ReadAllBytes(Path.Combine(AppContext.BaseDirectory, "Particles.spv"));
Console.WriteLine($"Particles.spv: {spirv.Length} bytes, magic 0x{BitConverter.ToUInt32(spirv, 0):x8}");
```

*Output*

```
Particles.spv: 1732 bytes, magic 0x07230203
```

It works, and the output passes `spirv-val`. MSBuild even recognizes DXC’s `file(line,col): error` lines, so a typo shows up on the HLSL line, with a generated code and a second error for the failed command:

*dotnet build · a typo in Particles.hlsl*

```
/tmp/DxcExec/DxcExec.csproj(17,5): error MSB3075: The command "dxc -spirv -T cs_6_0 -E Simulate "/tmp/DxcExec/Shaders/Particles.hlsl" -Fo "obj/Debug/net10.0/Particles.spv"" exited with code 5. Please verify that you have sufficient rights to run this command.
/tmp/DxcExec/Shaders/Particles.hlsl(19,19): error GF494D608: use of undeclared identifier 'Gravty'; did you mean 'Gravity'? [/tmp/DxcExec/DxcExec.csproj]
```

What it leaves to you: a `dxc` binary on every developer machine and CI machine (there is no macOS build in the DXC release), a convention for profiles and entry points per file, loading the `.spv` files at run time, and a C# struct for `SimParams` that matches the shader’s [constant buffer packing](https://recursiveai.org/oneshader/articles/hlsl-constant-buffer-packing-rules/).

## Compile at run time with Vortice.Dxc

Vortice.Dxc exposes DXC’s compiler API to C#, so an app can compile shaders while it runs, for an editor or hot reload. Its package depends on [Vortice.Dxc.Native](https://www.nuget.org/packages/Vortice.Dxc.Native/1.0.6), which carries `libdxcompiler.so` and `libdxil.so` for linux-x64 and `dxcompiler.dll` and `dxil.dll` for win-x64 and win-arm64. Those binaries ship with your app, and the app runs on those platforms. The DXC API itself is described in the Vulkan Guide’s [runtime compilation section](https://docs.vulkan.org/guide/latest/hlsl.html). Vortice.Dxc’s README and NuGet page don’t include a usage example, so none is shown here.

## Compile HLSL during dotnet build with OneShader

`RecursiveAI.OneShader.Build` compiles every `*.hlsl` file in the project during `dotnet build`. The compiler is a .NET task, so there is nothing to install and nothing native to ship. The project file only needs the package reference:

*Fountain.csproj · .NET 10*

```
﻿<Project Sdk="Microsoft.NET.Sdk">
 
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>
 
  <ItemGroup>
    <PackageReference Include="RecursiveAI.OneShader.Build" Version="1.2.0" />
  </ItemGroup>
 
</Project>
```

For each shader file, the build generates a static class in `<RootNamespace>.Shaders`. It holds the SPIR-V embedded in the assembly, a struct for each constant buffer and structured-buffer element with the offsets of the compiled shader, the thread-group size, and the descriptor bindings as constants:

*Program.cs · RecursiveAI.OneShader.Build 1.2.0*

```
using System.Numerics;
using System.Runtime.InteropServices;
using Fountain.Shaders;
 
// The SPIR-V was compiled during dotnet build and is embedded in the assembly.
ReadOnlySpan<byte> spirv = Particles.Simulate.SpirV;
Console.WriteLine($"Simulate: {spirv.Length} bytes of SPIR-V");
 
// The constant buffer as a struct with the shader's offsets: upload its bytes as they are.
var sim = new Particles.SimParams { DeltaTime = 1f / 60, Gravity = new Vector3(0, -9.81f, 0), Count = 10_000 };
Console.WriteLine($"SimParams: {Marshal.SizeOf<Particles.SimParams>()} bytes, Gravity at {Marshal.OffsetOf<Particles.SimParams>("Gravity")}, Count at {Marshal.OffsetOf<Particles.SimParams>("Count")}");
 
// The dispatch size and the descriptor bindings, as constants.
uint groups = (sim.Count + Particles.Simulate.ThreadGroupSizeX - 1) / Particles.Simulate.ThreadGroupSizeX;
Console.WriteLine($"Dispatch({groups}, 1, 1) for {sim.Count} particles");
Console.WriteLine($"SimParams at set {Particles.Bindings.SimParams.Set}, binding {Particles.Bindings.SimParams.Binding}");
Console.WriteLine($"Particles at set {Particles.Bindings.Particles.Set}, binding {Particles.Bindings.Particles.Binding}");
Console.WriteLine($"Particle: {Marshal.SizeOf<Particles.Particle>()} bytes per element");
```

*Output*

```
Simulate: 2116 bytes of SPIR-V
SimParams: 32 bytes, Gravity at 4, Count at 16
Dispatch(40, 1, 1) for 10000 particles
SimParams at set 0, binding 0
Particles at set 0, binding 1
Particle: 32 bytes per element
```

`Gravity` is at offset 4, not 16. HLSL packs a `float3` into the rest of the 16-byte row after `DeltaTime`, and `Count` starts the next row. The generated struct follows the shader, so the bytes you upload are the bytes the shader reads. If the shader changes, the struct changes with it on the next build. A typo is a build error on the HLSL line, with a stable code:

*dotnet build · a typo in Particles.hlsl*

```
/tmp/Fountain/Shaders/Particles.hlsl(19,19): error OSH0300: 'Gravty' is not declared. Did you mean 'Gravity'? [/tmp/Fountain/Fountain.csproj]
```

To also get Metal Shading Language and WGSL, set `<OneShaderTargets>SpirV;Msl;Wgsl</OneShaderTargets>`, and each entry point gains `Msl` and `Wgsl` members. The [compile HLSL in a C# project](https://recursiveai.org/oneshader/cookbook/compile-hlsl-csharp/) recipe covers includes, defines, namespaces and per-file targets, and [generated bindings](https://recursiveai.org/oneshader/docs/generated-bindings/) lists everything the build writes. The build task needs the .NET SDK’s `dotnet build`: Visual Studio’s .NET Framework MSBuild can’t load it.

## Compile HLSL at run time with OneShader

For tools, editors and hot reload, `RecursiveAI.OneShader` is the same compiler as a library:

*Program.cs · RecursiveAI.OneShader 1.2.0*

```
using OneShader;
 
CompileResult result = ShaderCompiler.CompileFile("Shaders/Particles.hlsl", new CompileOptions
{
    Targets = [CompileTarget.SpirV, CompileTarget.Wgsl],
});
foreach (var d in result.Diagnostics)
    Console.WriteLine(d);   // file(line,col): error OSHnnnn: message
if (!result.Success)
    return 1;
 
foreach (ShaderBlob blob in result.Blobs)
    Console.WriteLine($"{blob.EntryPoint} {blob.Target}: {blob.Data.Length} bytes");
return 0;
```

*Output*

```
Simulate SpirV: 2116 bytes
Simulate Wgsl: 1888 bytes
```

A failed compile returns diagnostics in the same `file(line,col): error OSHnnnn` form rather than throwing. `result.Reflection` describes every resource, buffer layout and entry point, and `InterfaceCompatibility.Compare` tells a hot-reload loop whether the new shader still fits the C# that was generated for the old one ([hot reload recipe](https://recursiveai.org/oneshader/cookbook/hot-reload-shaders/)).

## Which one to use

- **Keep DXC** (from MSBuild or through Vortice.Dxc) if you need DXIL for Direct3D 12, or geometry, tessellation, mesh or ray-tracing shaders.
- **Use OneShader’s build package** for vertex, pixel and compute shaders that should compile with the rest of the project on Windows, Linux and macOS, with C# structs and bindings that can’t drift from the shader, and Vulkan, Metal and WebGPU output from one source.
- **Use OneShader’s runtime compiler** for editors, asset pipelines and hot reload, where shipping a native DXC per platform is the cost you want to avoid.

## Common questions

### Can I compile HLSL without Windows?

Yes. DXC has Linux release builds, and OneShader runs anywhere the .NET 10 SDK or runtime runs, including macOS. FXC, the older `fxc.exe`, is for Shader Models 2 through 5.1: “For Shader Model 6, you use DXC.EXE instead” ([Microsoft Learn](https://learn.microsoft.com/en-us/windows/win32/direct3dhlsl/dx-graphics-hlsl-part1)).

### How do I share a struct between HLSL and C#?

Give the C# struct an explicit layout with the shader’s offsets, or let the build generate it. The rules are in [HLSL constant buffer packing rules](https://recursiveai.org/oneshader/articles/hlsl-constant-buffer-packing-rules/), and the generated version is in the [constant buffer structs recipe](https://recursiveai.org/oneshader/cookbook/cbuffer-packing-csharp-structs/).

To start, run `dotnet add package RecursiveAI.OneShader.Build`, add a `Shaders/*.hlsl` file and build. [Getting started](https://recursiveai.org/oneshader/docs/getting-started/) walks through a first sprite shader, and the [OneShader overview](https://recursiveai.org/oneshader/) shows everything the compiler does.
