HLSL and GLSL are both C-like shading languages, and on Vulkan the GPU can’t tell them apart: both compile to SPIR-V. GLSL is the Khronos language of OpenGL and Vulkan. HLSL is Microsoft’s language for Direct3D, and since Vulkan 1.2 the Vulkan Guide calls it “a first class shading language for Vulkan that can be used just as easily as GLSL” (HLSL in Vulkan). Neither runs as-is on Metal or WebGPU, which take MSL and WGSL. So the choice comes down to which APIs you ship on, which compiler you want to depend on, and which language features you’ll use.
HLSL vs GLSL at a glance
| GLSL | HLSL | |
|---|---|---|
| Owner | Khronos | Microsoft |
| Native APIs | OpenGL, Vulkan | Direct3D 11 and 12; Vulkan through SPIR-V |
| Reference compiler to SPIR-V | glslang | DXC (“the reference HLSL to SPIR-V compiler”, Vulkan Guide) |
| Direct3D 12 | No direct path | DXIL today; SPIR-V once Shader Model 7 ships (DirectX blog, September 2024) |
| Generic code | No templates | Templates and operator overloading in HLSL 2021 |
| New GPU features | Extensions (#extension GL_KHR_shader_subgroup_arithmetic) | Shader models (-T cs_6_0), plus vk:: attributes for Vulkan-only features |
Syntax differences
Microsoft’s GLSL-to-HLSL reference (updated 20 October 2022) and the Vulkan Guide’s shader language comparison list the mappings in full. The ones you meet in every shader:
| Concept | GLSL | HLSL |
|---|---|---|
| Vector and matrix types | vec4, mat4 | float4, float4x4 |
| Matrix times vector | m * v | mul(m, v) |
| Textures and samplers | sampler2D, or texture2D plus sampler | Texture2D plus SamplerState, tex.Sample(s, uv) |
| Resource binding on Vulkan | layout(set = 0, binding = 1) | [[vk::binding(1, 0)]] or register(t1, space0) |
| Stage inputs and outputs | in/out with layout(location = n), gl_Position | Parameters and return values with semantics, SV_Position, SV_Target |
| Entry point | Always main | Any name, chosen with -E |
| Interpolation and fractions | mix, fract | lerp, frac |
| Screen-space derivatives | dFdx, dFdy | ddx, ddy |
The one that causes real bugs is matrix order. The Vulkan Guide puts it this way: “matrices in GLSL are column-major, while matrices in HLSL and Slang are row-major by default”, which “affects things like matrix construction”. In HLSL, m[i] is row i. In GLSL it is column i. A port that keeps the same data and the same multiplication order without thinking about this transposes every transform.
The same shader in both languages
A fragment shader that samples a texture, blends toward a tint and applies a stripe pattern, written once in each language with the same bindings:
#version 450 layout(set = 0, binding = 0) uniform Params { vec4 Tint; float Fade; }; layout(set = 0, binding = 1) uniform texture2D Albedo; layout(set = 0, binding = 2) uniform sampler LinearWrap; layout(location = 0) in vec2 uv; layout(location = 0) out vec4 color; void main() { vec4 texel = texture(sampler2D(Albedo, LinearWrap), uv); color = mix(texel, Tint, Fade) * vec4(vec3(fract(uv.x * 4.0)), 1.0); }
cbuffer Params : register(b0) { float4 Tint; float Fade; }; [[vk::binding(1)]] Texture2D<float4> Albedo; [[vk::binding(2)]] SamplerState LinearWrap; float4 main(float2 uv : TEXCOORD0) : SV_Target { float4 texel = Albedo.Sample(LinearWrap, uv); return lerp(texel, Tint, Fade) * float4((float3)frac(uv.x * 4.0), 1.0); }
Both compile to valid SPIR-V for Vulkan 1.2, with Params at binding 0, the texture at 1 and the sampler at 2:
glslangValidator -V --target-env vulkan1.2 fade.frag -o fade.glsl.spv spirv-val --target-env vulkan1.2 fade.glsl.spv
dxc -spirv -T ps_6_0 -E main Fade.hlsl -Fo fade.dxc.spv oneshader -T ps_6_0 -E main --target spirv --target msl --target wgsl Fade.hlsl -Fo fade spirv-val --target-env vulkan1.2 fade.dxc.spv spirv-val --target-env vulkan1.2 fade.spv naga fade.wgsl
Validation successfulThe HLSL version has to say a little more about bindings, since register numbers don’t become Vulkan bindings the way you might expect (why). In return, the HLSL file also compiles for Direct3D with DXC, unchanged.
Getting to Metal and WebGPU
Neither language reaches Metal or WebGPU without a translator, and the translators work from SPIR-V or have their own front end:
- From GLSL: glslang to SPIR-V, then SPIRV-Cross for MSL (“Convert SPIR-V to readable, usable and efficient Metal Shading Language”, README) and naga for WGSL. naga also reads GLSL directly, “GLSL 440+ and Vulkan semantics only” (naga README).
- From HLSL: DXC to SPIR-V, then the same two tools. Or OneShader, which writes SPIR-V, MSL and WGSL itself, from one front end, with the same bindings and buffer byte layout on all three.
For the GLSL file above, the SPIR-V route works on this shader:
spirv-cross fade.glsl.spv --msl --output fade.glsl.metal naga fade.glsl.spv fade.glsl.wgsl naga fade.glsl.wgsl
Validation successfulThe HLSL run above produced fade.spv, fade.metal and fade.wgsl in one command, and the WGSL passes naga. Translating through SPIR-V has costs that show up on less simple shaders. The HLSL to WGSL comparison has a constant buffer where it produces WGSL that naga itself rejects.
OneShader reads HLSL only. It has no GLSL input or output, and it doesn’t compile Slang, a third language the Vulkan Guide also covers (Slang in Vulkan).
Which to choose
- GLSL if you target OpenGL or OpenGL ES, or you have a large GLSL codebase on Vulkan and no Direct3D plans. Its tooling is Khronos’s own.
- HLSL if Direct3D is in your future or your present, if your team or engine already writes it, or if you want templates and the shader-model feature set. On Vulkan it is as supported as GLSL. Since glslang’s HLSL front end was deprecated in April 2026 (issue #4210), compile it with DXC or OneShader.
- Either, plus a translator, if you target Metal or WebGPU too. Write HLSL and OneShader covers vertex, pixel and compute shaders for all three non-Microsoft APIs in one step. For geometry, tessellation, mesh or ray-tracing stages, use DXC and a SPIR-V translator.
Common questions
Is HLSL faster than GLSL?
No language is faster in itself: on Vulkan both become SPIR-V, and the driver compiles that. Differences come from what each compiler emits for the same code, so compare the SPIR-V, not the language.
How do I convert GLSL to HLSL?
By hand, using the mapping tables above, or by compiling the GLSL to SPIR-V with glslang and converting that with SPIRV-Cross, which writes HLSL, or naga, whose HLSL back end targets “Shader Model 5.0+ (DirectX 11+)”. The result is generated code, best as a starting point for a port. OneShader doesn’t convert GLSL.
Can I use HLSL with Vulkan?
Yes. Compile it with dxc -spirv or oneshader and load the SPIR-V as you would from GLSL. From the application’s side, the Vulkan Guide says, “using HLSL is exactly the same as using GLSL”.
To try HLSL on Vulkan, Metal and WebGPU from one source, install dotnet tool install -g RecursiveAI.OneShader.Tool and see getting started, or compile HLSL to SPIR-V in the cookbook.