Vulkan does not directly consume shaders in a human-readable text format, but instead uses SPIR-V as an intermediate representation. This opens the option to use shader languages other than e.g. GLSL, as long as they can target the Vulkan SPIR-V environment.
One such language is the High Level Shading Language (HLSL) by Microsoft, used by DirectX. Thanks to recent additions to Vulkan 1.2 it is now considered a first class shading language for Vulkan that can be used just as easily as GLSL.
With a few exceptions, all Vulkan features and shader stages available with GLSL can be used with HLSL too, including recent Vulkan additions like hardware accelerated ray tracing. On the other hand, HLSL to SPIR-V supports Vulkan exclusive features that are not (yet) available in DirectX.
If you are new to HLSL, a good starting point are the HLSL resources over at Microsoft Learn. Another great source is the DirectX-Specs document. It contains valuable information on recent shader features and HLSL’s shader models.
From the application’s point-of-view, using HLSL is exactly the same as using GLSL. As the application always consumes shaders in the SPIR-V format, the only difference is in the tooling to generate the SPIR-V shaders from the desired shading language.
A great starting point on using HLSL in Vulkan via SPIR-V is the HLSL to SPIR-V feature mapping manual. It contains detailed information on semantics, syntax, supported features and extensions and much more and is a must-read. The decoder ring also has a translation table for concepts and terms used in Vulkan and DirectX.
To make HLSL compatible with Vulkan, an implicit namespace has been introduced that provides an interface for for Vulkan-specific features.
Similar to regular programming languages, HLSL and GLSL differ in their syntax. While GLSL is more procedural (like C), HLSL is more object-oriented (like C++).
Here is the same shader written in both languages to give quick comparison on how they basically differ, including the aforementioned namespace that e.g. adds explicit locations:
#version 450
layout (location = 0) in vec3 inPosition;
layout (location = 1) in vec3 inColor;
layout (binding = 0) uniform UBO
{
mat4 projectionMatrix;
mat4 modelMatrix;
mat4 viewMatrix;
} ubo;
layout (location = 0) out vec3 outColor;
void main()
{
outColor = inColor * float(gl_VertexIndex);
gl_Position = ubo.projectionMatrix * ubo.viewMatrix * ubo.modelMatrix * vec4(inPosition.xyz, 1.0);
}
struct VSInput
{
[[vk::location(0)]] float3 Position : POSITION0;
[[vk::location(1)]] float3 Color : COLOR0;
};
struct UBO
{
float4x4 projectionMatrix;
float4x4 modelMatrix;
float4x4 viewMatrix;
};
cbuffer ubo : register(b0, space0) { UBO ubo; }
struct VSOutput
{
float4 Pos : SV_POSITION;
[[vk::location(0)]] float3 Color : COLOR0;
};
VSOutput main(VSInput input, uint VertexIndex : SV_VertexID)
{
VSOutput output = (VSOutput)0;
output.Color = input.Color * float(VertexIndex);
output.Pos = mul(ubo.projectionMatrix, mul(ubo.viewMatrix, mul(ubo.modelMatrix, float4(input.Position.xyz, 1.0))));
return output;
}
Aside from the syntax differences, built-ins use HLSL names. E.g. gl_vertex
becomes VertexIndex
in HLSL. A list of GLSL to HLSL built-in mappings can be found here.
As is the case with GLSL to SPIR-V, to use HLSL with Vulkan, a shader compiler is required. Whereas glslang is the reference GLSL to SPIR-V compiler, the DirectXShaderCompiler (DXC) is the reference HLSL to SPIR-V compiler. Thanks to open source contributions, the SPIR-V backend of DXC is now supported and enabled in official release builds and can be used out-of-the box. While other shader compiling tools like glslang also offer HLSL support, DXC has the most complete and up-to-date support and is the recommended way of generating SPIR-V from HLSL.
The LunarG Vulkan SDK includes pre-compiled DXC binaries, libraries and headers to get you started. If you’re looking for the latest releases, check the official DXC repository.
Compiling a shader offline via the pre-compiled dxc binary is similar to compiling with glslang:
dxc.exe -spirv -T vs_6_0 -E main .\triangle.vert -Fo .\triangle.vert.spv
-T
selects the profile to compile the shader against (vs_6_0
= Vertex shader model 6, ps_6_0
= Pixel/fragment shader model 6, etc.).
-E
selects the main entry point for the shader.
Extensions are implicitly enabled based on feature usage, but can also be explicitly specified:
dxc.exe -spirv -T vs_6_1 -E main .\input.vert -Fo .\output.vert.spv -fspv-extension=SPV_EXT_descriptor_indexing
The resulting SPIR-V can then be directly loaded, same as SPIR-V generated from GLSL.
DXC can also be integrated into a Vulkan application using the DirectX Compiler API. This allows for runtime compilation of shaders. Doing so requires you to include the dxcapi.h
header and link against the dxcompiler
library. The easiest way is using the dynamic library and distributing it with your application (e.g. dxcompiler.dll
on Windows).
Compiling HLSL to SPIR-V at runtime then is pretty straight-forward:
#include "include/dxc/dxcapi.h"
...
HRESULT hres;
// Initialize DXC library
CComPtr<IDxcLibrary> library;
hres = DxcCreateInstance(CLSID_DxcLibrary, IID_PPV_ARGS(&library));
if (FAILED(hres)) {
throw std::runtime_error("Could not init DXC Library");
}
// Initialize DXC compiler
CComPtr<IDxcCompiler3> compiler;
hres = DxcCreateInstance(CLSID_DxcCompiler, IID_PPV_ARGS(&compiler));
if (FAILED(hres)) {
throw std::runtime_error("Could not init DXC Compiler");
}
// Initialize DXC utility
CComPtr<IDxcUtils> utils;
hres = DxcCreateInstance(CLSID_DxcUtils, IID_PPV_ARGS(&utils));
if (FAILED(hres)) {
throw std::runtime_error("Could not init DXC Utiliy");
}
// Load the HLSL text shader from disk
uint32_t codePage = DXC_CP_ACP;
CComPtr<IDxcBlobEncoding> sourceBlob;
hres = utils->LoadFile(filename.c_str(), &codePage, &sourceBlob);
if (FAILED(hres)) {
throw std::runtime_error("Could not load shader file");
}
// Select target profile based on shader file extension
LPCWSTR targetProfile{};
size_t idx = filename.rfind('.');
if (idx != std::string::npos) {
std::wstring extension = filename.substr(idx + 1);
if (extension == L"vert") {
targetProfile = L"vs_6_1";
}
if (extension == L"frag") {
targetProfile = L"ps_6_1";
}
// Mapping for other file types go here (cs_x_y, lib_x_y, etc.)
}
// Configure the compiler arguments for compiling the HLSL shader to SPIR-V
std::vector<LPCWSTR> arguments = {
// (Optional) name of the shader file to be displayed e.g. in an error message
filename.c_str(),
// Shader main entry point
L"-E", L"main",
// Shader target profile
L"-T", targetProfile,
// Compile to SPIRV
L"-spirv"
};
// Compile shader
DxcBuffer buffer{};
buffer.Encoding = DXC_CP_ACP;
buffer.Ptr = sourceBlob->GetBufferPointer();
buffer.Size = sourceBlob->GetBufferSize();
CComPtr<IDxcResult> result{ nullptr };
hres = compiler->Compile(
&buffer,
arguments.data(),
(uint32_t)arguments.size(),
nullptr,
IID_PPV_ARGS(&result));
if (SUCCEEDED(hres)) {
result->GetStatus(&hres);
}
// Output error if compilation failed
if (FAILED(hres) && (result)) {
CComPtr<IDxcBlobEncoding> errorBlob;
hres = result->GetErrorBuffer(&errorBlob);
if (SUCCEEDED(hres) && errorBlob) {
std::cerr << "Shader compilation failed :\n\n" << (const char*)errorBlob->GetBufferPointer();
throw std::runtime_error("Compilation failed");
}
}
// Get compilation result
CComPtr<IDxcBlob> code;
result->GetResult(&code);
// Create a Vulkan shader module from the compilation result
VkShaderModuleCreateInfo shaderModuleCI{};
shaderModuleCI.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
shaderModuleCI.codeSize = code->GetBufferSize();
shaderModuleCI.pCode = (uint32_t*)code->GetBufferPointer();
VkShaderModule shaderModule;
vkCreateShaderModule(device, &shaderModuleCI, nullptr, &shaderModule);
When compiling HLSL with DXC you need to select a target shader profile. The name for a profile consists of the shader type and the desired shader model.
Vulkan shader stage | HLSL target shader profile | Remarks |
---|---|---|
|
|
|
|
|
Hull shader in HLSL terminology |
|
|
Domain shader in HLSL terminology |
|
|
|
|
|
Pixel shader in HLSL terminology |
|
|
|
|
|
All raytracing related shaders are built using the |
|
|
Amplification shader in HLSL terminology. Must use at least shader model 6.5 (e.g. |
|
|
Must use at least shader model 6.5 (e.g. |
So if you for example you want to compile a compute shader targeting shader model 6.6 features, the target shader profile would be cs_6_6
. For a ray tracing any hit shader it would be lib_6_3
.
DirectX and HLSL use a fixed shader model notion to describe the supported feature set. This is different from Vulkan and SPIR-V’s flexible extension based way of adding features to shaders. The following table tries to list Vulkan’s coverage for the HLSL shader models without guarantee of completeness:
Shader Model | Supported | Remarks |
---|---|---|
Shader Model 5.1 and below |
✔ |
Excluding features without Vulkan equivalent |
✔ |
Wave intrinsics, 64-bit integers |
|
✔ |
SV_ViewID, SV_Barycentrics |
|
✔ |
16-bit types, Denorm mode |
|
✔ |
Hardware accelerated ray tracing |
|
✔ |
Shader integer dot product, SV_ShadingRate |
|
DXR1.1 (KHR ray tracing), Mesh and Amplification shaders, additional Wave intrinsics |
||
VK_NV_compute_shader_derivatives, VK_KHR_shader_atomic_int64, VK_EXT_descriptor_buffer, VK_EXT_mutable_descriptor_type |
||
VK_KHR_shader_quad_control, VkPhysicalDeviceFeatures::shaderStorageImageMultisample |