It’s 2016 and we’re still stuck with various shading languages - the current contenders being HLSL for Direct3D, and GLSL for OpenGL and as the “default” front-end language to generate SPIR-V for Vulkan. SPIR-V may become eventually the IL of choice for everything, but that will take a while, so right now, you need to convert HLSL to GLSL or vice versa if you want to target both APIs.
I won’t dig into the various cross-compilers today - that’s a huge topic - and focus on the language similarities instead. Did you ever ask yourself how your SV_Position input is called in GLSL? Then this post is for you!
This is by no means complete. It’s meant as a starting point when you’re looking to port some shaders between GLSL to HLSL. I’m omitting functions which are the same for instance.
System values & built-in inputs
Direct3D specifies a couple of system values, GLSL has the concept of built-in variables. The mapping is as following:
|SV_CullDistance||gl_CullDistance if ARB_cull_distance is present|
|SV_Coverage||gl_SampleMaskIn & gl_SampleMask|
|SV_DepthGreaterEqual||layout (depth_greater) out float gl_FragDepth;|
|SV_DepthLessEqual||layout (depth_less) out float gl_FragDepth;|
|SV_InstanceID||gl_InstanceID & gl_InstanceIndex (latter in Vulkan with different semantics)|
|SV_Position||gl_Position in a vertex shader, gl_FragCoord in a fragment shader|
|The equivalent functionality is available through EvaluateAttributeAtSample||gl_SamplePosition|
|SV_StencilRef||gl_FragStencilRef if ARB_shader_stencil_export is present|
|SV_Target||layout(location=N) out your_var_name;|
|SV_VertexID||gl_VertexID & gl_VertexIndex (latter Vulkan with different semantics)|
These map fairly easily. Interlocked becomes atomic. So InterlockedAdd becomes atomicAdd, and so on. The only difference is InterlockedCompareExchange which turns into atomicCompSwap.
|GroupMemoryBarrierWithGroupSync||groupMemoryBarrier and barrier|
|DeviceMemoryBarrierWithGroupSync||memoryBarrier, memoryBarrierImage, memoryBarrierImage and barrier|
|DeviceMemoryBarrier||memoryBarrier, memoryBarrierImage, memoryBarrierImage|
|AllMemoryBarrierWithGroupSync||All of the barriers above and barrier|
|AllMemoryBarrier||All of the barriers above|
Before Vulkan, this is bundled and not trivial to emulate. Fortunately, this changes with Vulkan, where the semantics are the same as in HLSL. The main difference is that in HLSL, the access method is part of the “texture object”, while in GLSL, they are free functions. In HLSL, you’ll sample a texture called Texture with a sampler called Sampler like this:
Texture.Sample (Sampler, coordinate)
In GLSL, you need to specify the type of the texture and the sampler, but otherwise, it’s similar:
texture (sampler2D(Texture, Sampler), coordinate)
|CalculateLevelOfDetail & CalculateLevelOfDetailUnclamped||textureQueryLod|
|Load||texelFetch and texelFetchOffset|
|GetDimensions||textureSize, textureQueryLevels and textureSamples|
|Gather||textureGather, textureGatherOffset, textureGatherOffsets|
|Sample, SampleBias||texture, textureOffset|
GLSL and HLSL differ in their default matrix interpretation. GLSL assumes column-major, and multiplication on the right (that is, you apply \(M * v\)) and HLSL assumes multiplication from left (\(v * M\)) While you can usually ignore that - you can override the order, and multiply from whatever side you want in both - it does change the meaning of m with m being a matrix. In HLSL, this will return the first row, in GLSL, the first column. That also extends to the constructors, which initialize the members in the “natural” order.
|atan2(y,x)||atan, with parameters swapped|
|saturate||clamp(x, 0.0, 1.0)|
Anything else I’m missing? Please add a comment and I’ll update this post!