---
title: "Compile HLSL to SPIR-V for Vulkan"
url: https://recursiveai.org/oneshader/cookbook/hlsl-to-spirv/
description: "Compile an HLSL vertex or pixel shader to SPIR-V for Vulkan with the oneshader command and its DXC-style flags, or from C# with ShaderCompiler, and check…"
version: "1.2.0"
---

# Compile HLSL to SPIR-V for Vulkan

Compile an HLSL vertex or pixel shader to SPIR-V for Vulkan with the `oneshader` command and its DXC-style flags, or from C# with `ShaderCompiler`, and check the result with `spirv-val`.

OneShader writes SPIR-V 1.5 for the Vulkan 1.2 environment, with buffer layouts and descriptor bindings that follow DXC’s Vulkan rules. The sample shader is `Sprite.hlsl` from [Getting started](https://recursiveai.org/oneshader/docs/getting-started/): a constant buffer at `b0`, a texture at `[[vk::binding(1)]]` and a sampler at `[[vk::binding(2)]]`.

## From the command line

```sh
dotnet tool install -g RecursiveAI.OneShader.Tool --version 1.2.0

oneshader -T vs_6_0 -E VSMain -spirv Sprite.hlsl -Fo sprite.vs.spv
oneshader -T ps_6_0 -E PSMain -spirv Sprite.hlsl -Fo sprite.ps.spv

spirv-val --target-env vulkan1.2 sprite.vs.spv
spirv-val --target-env vulkan1.2 sprite.ps.spv
spirv-dis sprite.ps.spv | grep -E "Binding|DescriptorSet"
```

Both files pass `spirv-val` (from the Vulkan SDK) silently, and the bindings are where the shader put them:

```text
               OpDecorate %Atlas DescriptorSet 0
               OpDecorate %Atlas Binding 1
               OpDecorate %AtlasSampler DescriptorSet 0
               OpDecorate %AtlasSampler Binding 2
```

The same command line with `dxc` instead of `oneshader` gives the same set and binding numbers.

## From C#

With the `RecursiveAI.OneShader` package (`dotnet add package RecursiveAI.OneShader --version 1.2.0`):

```csharp
using OneShader;

string source = File.ReadAllText("Sprite.hlsl");
CompileResult result = ShaderCompiler.Compile(source, "Sprite.hlsl", new CompileOptions
{
    // The C# form of -T ps_6_0 -E PSMain; leave EntryPoints empty to compile every [shader("...")] function.
    EntryPoints = [new EntryPointRequest("PSMain", ShaderStage.Pixel)],
});

foreach (var d in result.Diagnostics)
    Console.WriteLine(d);
if (!result.Success)
    return 1;

ShaderBlob ps = result.Find("PSMain", CompileTarget.SpirV)!;
File.WriteAllBytes("sprite.ps.spv", ps.ToArray());
uint magic = BitConverter.ToUInt32(ps.Data[..4]);
Console.WriteLine($"{ps.EntryPoint}: {ps.Data.Length} bytes, magic 0x{magic:x8}");
return 0;
```

```text
PSMain: 856 bytes, magic 0x07230203
```

`0x07230203` is the SPIR-V magic number. The file written here is byte for byte what the command line wrote, and it passes `spirv-val` too.

## Good to know

- **Bindings.** Use `[[vk::binding(n)]]` or `[[vk::binding(n, set)]]`, or HLSL registers, which map to bindings as DXC maps them by default. Unannotated resources are numbered in declaration order. `-fvk-*` remapping options aren’t supported: put the numbers in the shader.
- **Debug names.** By default the SPIR-V keeps source names (`%Atlas` above). `--no-names` (or `KeepNames = false`) leaves them out: here the pixel shader goes from 856 to 692 bytes.
- **Optimization** is on by default (`-O3` and `-O0` work as in DXC; `Optimize = false` in C#).
- **Validation.** The repository validates every corpus shader with `spirv-val` and runs it on GPUs next to the same shader compiled by DXC, with the Vulkan validation layers on.
- **Not in 1.2.0:** DXIL (Direct3D 12) output, and geometry, tessellation, mesh and ray-tracing stages. `-T gs_6_0` is a command-line error naming the supported profiles.

The full SPIR-V mapping is in [SPIR-V (Vulkan)](https://recursiveai.org/oneshader/docs/spirv-backend/). To compile the same shader during `dotnet build`, see [Compile HLSL in a C# project](https://recursiveai.org/oneshader/cookbook/compile-hlsl-csharp/).

Choosing a compiler: [HLSL to SPIR-V compiler: DXC, glslang and OneShader compared](https://recursiveai.org/oneshader/articles/hlsl-to-spirv-compiler/).
