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: a constant buffer at b0, a texture at [[vk::binding(1)]] and a sampler at [[vk::binding(2)]].
From the command line
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:
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):
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;
PSMain: 856 bytes, magic 0x072302030x07230203 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 (
%Atlasabove).--no-names(orKeepNames = false) leaves them out: here the pixel shader goes from 856 to 692 bytes. - Optimization is on by default (
-O3and-O0work as in DXC;Optimize = falsein C#). - Validation. The repository validates every corpus shader with
spirv-valand 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_0is a command-line error naming the supported profiles.
The full SPIR-V mapping is in SPIR-V (Vulkan). To compile the same shader during dotnet build, see Compile HLSL in a C# project.
Choosing a compiler: HLSL to SPIR-V compiler: DXC, glslang and OneShader compared.