---
title: "Compiling HLSL to SPIR-V for Vulkan"
url: https://recursiveai.org/oneshader/articles/hlsl-to-spirv-compiler/
description: "Compile HLSL to SPIR-V with DXC, glslang or OneShader: the commands, how registers become Vulkan bindings, the flags that bite, and how to validate the output."
date: 2026-10-26
author: OneShader team
---

# Compiling HLSL to SPIR-V for Vulkan

Compile HLSL to SPIR-V with DXC, glslang or OneShader: the commands, how registers become Vulkan bindings, the flags that bite, and how to validate the output.

October 26, 2026 · OneShader team

To compile HLSL to SPIR-V for Vulkan, run an HLSL compiler with a SPIR-V back end: `dxc -spirv -T ps_6_0 -E main shader.hlsl -Fo shader.spv` with Microsoft’s DirectX Shader Compiler (DXC), or the same flags with `oneshader`, a .NET HLSL compiler that also writes Metal and WGSL. Then check the result with `spirv-val`. The command is the easy part. Most of the problems come from how HLSL registers turn into Vulkan descriptor bindings, and which target environment the compiler assumes.

This page compares the three compilers you’ll find for HLSL to SPIR-V and shows each one on the same shaders. Every command below was run with DXC from the LunarG Vulkan SDK 1.4.363.0 (dxcompiler 1.9.0.5512), glslang 16.6.0, SPIRV-Tools v2026.4 and [OneShader 1.2.0](https://www.nuget.org/packages/RecursiveAI.OneShader.Tool/1.2.0), and the output shown is what they printed.

## HLSL to SPIR-V compilers at a glance

| Compiler, version read | HLSL it accepts | Outputs | Runs as | License |
|---|---|---|---|---|
| [DXC](https://github.com/microsoft/DirectXShaderCompiler/releases/tag/v1.9.2609) v1.9.2609, released 29 September 2026 | Shader Model 6.x, every stage, including ray tracing and mesh shaders on Vulkan ([Vulkan Guide](https://docs.vulkan.org/guide/latest/hlsl.html)) | DXIL (Direct3D 12) by default, SPIR-V with `-spirv` | Native binary and library; the release ships Windows and Linux builds | University of Illinois/NCSA ([LICENSE.TXT](https://github.com/microsoft/DirectXShaderCompiler/blob/main/LICENSE.TXT)) |
| [glslang](https://github.com/KhronosGroup/glslang) 16.6.0, released 11 September 2026 | “An approximation of HLSL”; the HLSL front end is deprecated as of April 2026 ([README](https://github.com/KhronosGroup/glslang/blob/main/README.md)) | SPIR-V (its main job is GLSL) | Native binary and library | Several licenses, listed in its [LICENSE.txt](https://github.com/KhronosGroup/glslang/blob/main/LICENSE.txt) |
| [OneShader](https://www.nuget.org/packages/RecursiveAI.OneShader.Tool/1.2.0) 1.2.0 | Modern HLSL (Shader Model 6 style, HLSL 2021) for vertex, pixel and compute shaders | SPIR-V 1.5 for Vulkan 1.2, Metal Shading Language, WGSL, reflection JSON | .NET 10: a `dotnet` tool, an MSBuild package and a library | MIT OR Apache-2.0 |

DXC is the reference. The Vulkan Guide calls it “the reference HLSL to SPIR-V compiler” and says it “has the most complete and up-to-date support and is the recommended way of generating SPIR-V from HLSL” ([HLSL in Vulkan](https://docs.vulkan.org/guide/latest/hlsl.html)). glslang is the reference GLSL compiler. Its HLSL support no longer takes bug reports, and it will be removed at glslang’s next major version, with at least 18 months’ notice ([issue #4210](https://github.com/KhronosGroup/glslang/issues/4210)). OneShader covers a narrower set of stages than DXC. In return it writes three targets from one front end and runs anywhere .NET 10 runs. It doesn’t write DXIL, and it has no geometry, tessellation, mesh or ray-tracing stages.

SPIR-V matters on Direct3D too. Microsoft announced in September 2024 that “once Shader Model 7 is released, DirectX 12 will accept shaders compiled to SPIR-V” ([DirectX Developer Blog](https://devblogs.microsoft.com/directx/directx-adopting-spir-v/)), describing it as a multi-year transition.

## Compile HLSL to SPIR-V with DXC

Here is a pixel shader written the way many Direct3D shaders are, with registers and no Vulkan annotations:

*Lit.hlsl · HLSL*

```
cbuffer Material : register(b0) { float4 Tint; float Roughness; };
Texture2D<float4> Albedo : register(t0);
SamplerState LinearWrap : register(s0);
 
float4 main(float2 uv : TEXCOORD0) : SV_Target
{
    return Albedo.Sample(LinearWrap, uv) * Tint * (1 - Roughness);
}
```

DXC compiles it, and `spirv-val` accepts the module:

*Terminal · DXC 1.9.0.5512*

```
dxc -spirv -T ps_6_0 -E main Lit.hlsl -Fo lit.spv
spirv-val --target-env vulkan1.2 lit.spv
spirv-dis lit.spv | grep -E "OpDecorate .* Binding"
```

*Output · spirv-val prints nothing on success*

```
               OpDecorate %Material Binding 0
               OpDecorate %Albedo Binding 0
               OpDecorate %LinearWrap Binding 0
```

All three resources landed on binding 0. DXC’s [HLSL to SPIR-V feature mapping manual](https://github.com/microsoft/DirectXShaderCompiler/blob/main/docs/SPIR-V.rst) explains why: with `:register(xX, spaceY)`, the resource goes to descriptor set `Y` and binding `X` “regardless of the register type”, and “this will cause binding number collision”. Direct3D has separate `b`, `t`, `s` and `u` register spaces, while Vulkan has one binding space per set. The module is still valid SPIR-V, because `spirv-val` checks the module, not your descriptor set layout. The collision shows up later, when you build the pipeline.

DXC’s fix is a shift per register type and space, `-fvk-t-shift N M` and friends:

*Terminal · DXC 1.9.0.5512*

```
dxc -spirv -T ps_6_0 -E main -fvk-t-shift 1 0 -fvk-s-shift 2 0 Lit.hlsl -Fo lit.spv
spirv-dis lit.spv | grep -E "OpDecorate .* Binding"
```

*Output*

```
               OpDecorate %Material Binding 0
               OpDecorate %Albedo Binding 1
               OpDecorate %LinearWrap Binding 2
```

Shifts keep the shader source unchanged, at the price of a convention every build script has to repeat.

## The target environment flag

The second thing that catches people is DXC’s default target environment. It assumes Vulkan 1.0, so anything newer, such as [wave intrinsics](https://recursiveai.org/oneshader/articles/hlsl-wave-intrinsics-spirv-metal-wgsl/), fails until you name the environment. This compute shader uses a wave intrinsic and an HLSL 2021 template:

*Wave.hlsl · HLSL 2021*

```
RWStructuredBuffer<uint> Counts : register(u0);
 
template<typename T> T Square(T v) { return v * v; }
 
[numthreads(64, 1, 1)]
void main(uint3 id : SV_DispatchThreadID)
{
    uint n = WaveActiveCountBits(id.x % 3 == 0);
    if (WaveIsFirstLane())
        InterlockedAdd(Counts[0], Square(n));
}
```

*Terminal · DXC 1.9.0.5512*

```
dxc -spirv -T cs_6_0 -E main -HV 2021 Wave.hlsl -Fo wave.spv
```

*Output*

```
Wave.hlsl:8:14: error: Vulkan 1.1 is required for Wave Operation but not permitted to use
    uint n = WaveActiveCountBits(id.x % 3 == 0);
             ^
note: please specify your target environment via command line option -fspv-target-env=
Wave.hlsl:9:9: error: Vulkan 1.1 is required for Wave Operation but not permitted to use
    if (WaveIsFirstLane())
        ^
note: please specify your target environment via command line option -fspv-target-env=
```

Add `-fspv-target-env=vulkan1.2` (or `vulkan1.1`, `vulkan1.3`) and it compiles. OneShader always writes SPIR-V 1.5 for the Vulkan 1.2 environment, so it needs no flag, and it accepts `-fspv-target-env=vulkan1.2` when a build script passes it:

*Terminal · DXC 1.9.0.5512 · OneShader 1.2.0*

```
dxc -spirv -fspv-target-env=vulkan1.2 -T cs_6_0 -E main -HV 2021 Wave.hlsl -Fo wave.dxc.spv
oneshader -spirv -T cs_6_0 -E main -HV 2021 Wave.hlsl -Fo wave.spv
spirv-val --target-env vulkan1.2 wave.dxc.spv
spirv-val --target-env vulkan1.2 wave.spv
```

Both modules pass `spirv-val`, which prints nothing on success.

## Compile HLSL with glslang

glslang compiles HLSL with `-D`, but it parses an older dialect. The same compute shader stops at the template:

*Terminal · glslang 16.6.0*

```
glslangValidator -D -V --target-env vulkan1.2 -S comp -e main Wave.hlsl -o wave.glslang.spv
```

*Output*

```
Wave.hlsl
ERROR: Wave.hlsl:3: 'template' : Reserved word. 
ERROR: 1 compilation errors.  No code generated.
 
 
WARNING: Linking compute stage: Entry point not found
 
SPIR-V is not generated for failed compile or link
```

Without the template, glslang compiles the wave intrinsics too. Given the deprecation notice, it’s a fine choice for an existing GLSL pipeline that also has a few HLSL files, and a poor one to start new HLSL work on.

## Compile HLSL to SPIR-V with OneShader

The `oneshader` command takes DXC’s core flags (`-T`, `-E`, `-D`, `-I`, `-Fo`, `-spirv`, `-O0` to `-O3`, `-Zpr`/`-Zpc`, `-HV 2021`, `-WX`, `-enable-16bit-types`), so most DXC command lines run unchanged. Install it as a .NET tool:

*Terminal · .NET 10 SDK*

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

Give it the first shader, the one DXC compiled with three resources on binding 0:

*Terminal · OneShader 1.2.0*

```
oneshader -spirv -T ps_6_0 -E main Lit.hlsl -Fo lit.spv
```

*Output · exit code 1*

```
Lit.hlsl(2,19): error OSH0330: The resources 'Material' and 'Albedo' both use set 0, binding 0. Add [[vk::binding(binding, set)]] to one of them, or give them different register numbers (t, s, b and u registers share binding numbers on Vulkan).
Lit.hlsl(3,14): error OSH0330: The resources 'Material' and 'LinearWrap' both use set 0, binding 0. Add [[vk::binding(binding, set)]] to one of them, or give them different register numbers (t, s, b and u registers share binding numbers on Vulkan).
```

OneShader maps registers to bindings exactly as DXC does by default. The difference is that it reports a collision at the HLSL line, where DXC writes a module that fails later, at pipeline creation. It doesn’t take the `-fvk-*-shift` options (they stop with a message naming the option), so the numbers go in the shader with `[[vk::binding(binding, set)]]`. That is also what the [Vulkan Guide](https://docs.vulkan.org/guide/latest/hlsl.html) recommends: “Always specify explicit descriptor set and binding indices”.

*LitBound.hlsl · HLSL*

```
cbuffer Material : register(b0) { float4 Tint; float Roughness; };
[[vk::binding(1)]] Texture2D<float4> Albedo : register(t0);
[[vk::binding(2)]] SamplerState LinearWrap : register(s0);
 
float4 main(float2 uv : TEXCOORD0) : SV_Target
{
    return Albedo.Sample(LinearWrap, uv) * Tint * (1 - Roughness);
}
```

*Terminal · OneShader 1.2.0*

```
oneshader -spirv -T ps_6_0 -E main LitBound.hlsl -Fo lit.spv
spirv-val --target-env vulkan1.2 lit.spv
spirv-dis lit.spv | grep -E "OpDecorate .* Binding"
```

*Output*

```
               OpDecorate %Material Binding 0
               OpDecorate %Albedo Binding 1
               OpDecorate %LinearWrap Binding 2
```

DXC gives the same bindings for this file. Keeping `register(t0)` next to `[[vk::binding]]` keeps the source valid for a Direct3D build with DXC.

### Metal and WGSL from the same command

The same front end writes Metal Shading Language and WGSL with the same bindings and buffer byte layout. Add `--target` once per output, and `--reflect` for a JSON description of every resource, binding and buffer offset:

*Terminal · OneShader 1.2.0*

```
oneshader -T ps_6_0 -E main --target spirv --target msl --target wgsl LitBound.hlsl -Fo out/lit --reflect out/lit.json
ls out
```

*Output*

```
lit.json
lit.metal
lit.spv
lit.wgsl
```

`lit.spv` passes `spirv-val` and `lit.wgsl` passes naga. The details are in [HLSL to WGSL](https://recursiveai.org/oneshader/articles/hlsl-to-wgsl-converter/) and [HLSL to Metal](https://recursiveai.org/oneshader/articles/hlsl-to-metal-converter/).

### From C# or dotnet build

In a .NET application the compiler is a library call, `ShaderCompiler.CompileFile`, with no native DLL to ship, as shown in the [compile HLSL to SPIR-V recipe](https://recursiveai.org/oneshader/cookbook/hlsl-to-spirv/). With the `RecursiveAI.OneShader.Build` package, every `*.hlsl` file compiles during `dotnet build`, and the SPIR-V is embedded next to generated C# structs. [Compiling HLSL in a C# project](https://recursiveai.org/oneshader/articles/csharp-shader-compiler/) compares that with calling DXC.

## Validate the SPIR-V

Run `spirv-val` from the Vulkan SDK on every module, with the environment you target: `spirv-val --target-env vulkan1.2 shader.spv`. It prints nothing and exits 0 when the module is valid. Validation catches malformed SPIR-V. It doesn’t catch resources that collide in a descriptor set, a constant buffer layout your C# struct disagrees with, or a feature the device lacks. For those, enable the Vulkan validation layers when you create the pipeline, and read the bindings back with `spirv-dis shader.spv | grep Binding`, or from [reflection](https://recursiveai.org/oneshader/cookbook/spirv-reflection-csharp/).

The OneShader repository validates every corpus shader with `spirv-val` and runs each one on GPUs next to the same shader compiled by DXC, with the validation layers on ([overview](https://recursiveai.org/oneshader/docs/overview/)).

## Which HLSL to SPIR-V compiler to use

- **DXC** when you need geometry, tessellation, mesh or ray-tracing shaders, DXIL for Direct3D 12 from the same source, the SPIR-V extensions it exposes through `-fspv-extension`, or binding remapping on the command line.
- **OneShader** when your shaders are vertex, pixel and compute, and you want Vulkan, Metal and WebGPU from one HLSL source, a compiler that runs inside a .NET build or app without native binaries, or errors such as binding collisions at the HLSL line. Its [DXC flag table](https://recursiveai.org/oneshader/cookbook/migrate-from-dxc/) lists what carries over from a DXC command line.
- **glslang** for GLSL. For HLSL, only to keep an existing pipeline running while you move off its deprecated front end.

## Common questions

### How do I convert HLSL to SPIR-V?

Compile it with a SPIR-V back end: `dxc -spirv -fspv-target-env=vulkan1.2 -T ps_6_0 -E main shader.hlsl -Fo shader.spv`, or `oneshader -T ps_6_0 -E main shader.hlsl -Fo shader.spv`. Pick the profile for the stage (`vs_6_0`, `ps_6_0`, `cs_6_0`) and name the entry point with `-E`.

### Why do all my HLSL resources get binding 0 on Vulkan?

Because `register(b0)`, `register(t0)` and `register(s0)` all map to binding 0 in set 0. Give each resource its own number with `[[vk::binding(n, set)]]`, use different register numbers, or shift each register type with DXC’s `-fvk-b-shift`, `-fvk-t-shift`, `-fvk-s-shift` and `-fvk-u-shift`.

### Can SPIR-V be converted back to HLSL?

Yes, with SPIRV-Cross, which converts SPIR-V to GLSL, HLSL and Metal ([README](https://github.com/KhronosGroup/SPIRV-Cross)). OneShader goes one way only, from HLSL.

To try OneShader, install the tool with `dotnet tool install -g RecursiveAI.OneShader.Tool` and follow [getting started](https://recursiveai.org/oneshader/docs/getting-started/). The [command line reference](https://recursiveai.org/oneshader/docs/command-line/) lists every flag, and the [SPIR-V backend](https://recursiveai.org/oneshader/docs/spirv-backend/) page shows how HLSL maps to SPIR-V.
