Slang Shading Language in Vulkan
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 Slang Shading Language developed by NVIDIA. It was designed to address the evolving needs of real-time graphics development, especially with regard to shader code bases getting larger and more complex. It supports multiple APIs, among them is first-class support for Vulkan’s SPIR-V.
Slang is developed in the Open Source and is under the governance of Khronos, meaning it has broad industry support and is actively maintained.
Its syntax is similar to HLSL with additions like modules to make the language easier to use and to better handle complex code bases.
It has great tooling support with debuggers like RenderDoc and Nsight. Syntax highlighting is available for most popular IDEs like Visual Studio and Visual Studio Code.
Key differences between Slang and GLSL
This section summarizes the most important differences a developer will encounter when moving from GLSL to Slang.
| Feature | Slang | GLSL |
|---|---|---|
Programming style |
Object-oriented (C++-like) |
Procedural (C-like) |
Stages per source file |
Multiple |
One |
Entry point name |
User-defined |
Always |
Stage declaration |
|
Compiler flag or file extension |
Vector types |
|
|
Matrix types |
|
|
Default matrix layout |
Column-major (Command-line), row-major (Library) |
Column-major |
Resource binding |
Automatic or explicit |
|
File reuse |
|
|
Namespaces |
Yes |
No |
Interfaces and generics |
Yes |
No |
Pointers |
Yes (for GPU-side use) |
No |
Syntax comparison
Slang and GLSL differ heavily in their syntax. While GLSL is more procedural (like C), Slang is more object-oriented (like C++).
Here is the same shader written in both languages to give a quick comparison on how they basically differ, including the aforementioned namespace that e.g. adds explicit locations:
GLSL
In GLSL, you need one shader per stage.
Vertex shader
#version 450
layout (location = 0) in vec3 inPos;
layout (location = 1) in vec2 inUV;
layout (binding = 0) uniform UBO
{
mat4 projection;
mat4 model;
} ubo;
layout (location = 0) out vec2 outUV;
void main()
{
outUV = inUV;
gl_Position = ubo.projection * ubo.model * vec4(inPos.xyz, 1.0);
}
Slang
In Slang, a single file can contain multiple shader stages. This helps reduce duplication. Also note how we don’t have to specify explicit bindings, as they’re implicitly deduced for ubo and samplerColor from their ordering.
struct VSInput
{
float3 Pos;
float2 UV;
float3 Normal;
};
struct VSOutput
{
float4 Pos : SV_POSITION;
float2 UV;
};
struct UBO
{
float4x4 projection;
float4x4 model;
};
ConstantBuffer<UBO> ubo;
Sampler2D samplerColor;
[shader("vertex")]
VSOutput vertexMain(VSInput input)
{
VSOutput output;
output.UV = input.UV;
output.Pos = mul(ubo.projection, mul(ubo.model, float4(input.Pos.xyz, 1.0)));
return output;
}
[shader("fragment")]
float4 fragmentMain(VSOutput input)
{
return samplerColor.Sample(input.UV);
}
Additional Language Features
Beyond the syntax differences shown above, Slang adds several language features not available in GLSL or standard HLSL.
Generics
Slang adds full support for generics, similar to C# or Java, allowing for type-safe parameterized code:
T min<T>(T a, T b)
{
return a < b ? a : b;
}
// Usage
float result = min<float>(1.0, 2.0);
int intResult = min<int>(5, 3);
Interfaces and Polymorphism
Slang introduces interfaces, enabling polymorphic behavior in shaders:
interface IMaterial
{
float3 evaluateBRDF(float3 viewDir, float3 lightDir, float3 normal);
}
struct LambertianMaterial : IMaterial
{
float3 albedo;
float3 evaluateBRDF(float3 viewDir, float3 lightDir, float3 normal)
{
return albedo / 3.14159;
}
}
// Use polymorphically, without knowing the concrete type
void shadeSurface(IMaterial material, float3 viewDir, float3 lightDir, float3 normal)
{
float3 result = material.evaluateBRDF(viewDir, lightDir, normal);
}
Modules and Namespaces
Slang provides a module system for better code organization. Symbols must be marked public to be visible outside their module, and modules do not introduce namespaces - imported symbols are used directly:
// In file: lighting.slang
module Lighting;
public float3 calculateDirectLighting(float3 normal, float3 lightDir, float3 color)
{
float NdotL = max(0, dot(normal, lightDir));
return color * NdotL;
}
// In file: main.slang
import Lighting;
float3 directLight = calculateDirectLighting(normal, lightDir, color);
Resource Binding Model
In addition to implicit, order-based binding, Slang supports grouping related resources into parameter blocks:
struct MaterialResources
{
Texture2D albedoMap;
SamplerState samplerState;
};
ParameterBlock<MaterialResources> material;
// Usage
float4 albedo = material.albedoMap.Sample(material.samplerState, uv);
Parameter blocks are assigned a descriptor set and binding automatically. An explicit binding can still be set with vk::binding when a stable layout across shader edits is required
[[vk::binding(0, 1)]]
ParameterBlock<MaterialResources> material;
Using Slang in your application
From the application’s point-of-view, using Slang 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.
Compiling shaders
To get SPIR-V from Slang requires a compiler. Just like GLSL and HLSL, Slang comes with both an offline compiler (a binary for multiple operating systems) and a library for runtime compilation. Both can be downloaded via github and are also part of the Vulkan SDK.
Offline compilation using the stand-alone compiler
Compiling a shader offline via the pre-compiled slangc binary is similar to compiling with glslang:
slangc texture.slang -target spirv -o texture.vert.spv
This will generate a single SPIR-V file with all shader stages provided in the Slang source file.
Specific shader stages can be compiled like this:
slangc texture.slang -target spirv -entry vertexMain -stage vertex -o texture.vert.spv
Capabilities are implicitly enabled based on feature usage, but can also be explicitly specified:
slangc heap.slang -target spirv -entry vertexMain -stage vertex -o heap.vert.spv -capability spvDescriptorHeapEXT
The resulting SPIR-V can then be directly loaded by the app, same as SPIR-V generated from GLSL.
slangc can also compile HLSL or GLSL source directly to SPIR-V, using the -lang option to select the input language:
# HLSL compatibility mode
slangc -target spirv -lang hlsl -entry main -o shader.spv shader.hlsl
# GLSL compatibility mode
slangc -target spirv -allow-glsl -entry main -o shader.spv shader.glsl
Runtime compilation using the library
Slang can also be integrated into a Vulkan application using the Slang Compiler API. This allows for runtime compilation of shaders. Doing so requires you to include the slang compiler headers and link against the slang (or slang-compiler) library.
Compiling Slang to SPIR-V at runtime then is pretty straight-forward:
#include "slang/slang.h"
#include "slang/slang-com-ptr.h"
...
// Initialize the Slang shader compiler
slang::createGlobalSession(slangGlobalSession.writeRef());
auto slangTargets{ std::to_array<slang::TargetDesc>({ {.format{SLANG_SPIRV}, .profile{slangGlobalSession->findProfile("spirv")} } }) };
auto slangOptions{ std::to_array<slang::CompilerOptionEntry>({ { slang::CompilerOptionName::EmitSpirvDirectly, {slang::CompilerOptionValueKind::Int, 1} } }) };
slang::SessionDesc slangSessionDesc{
.targets{slangTargets.data()},
.targetCount{SlangInt(slangTargets.size())},
// Match GLSL's matrix layout
.defaultMatrixLayoutMode = SLANG_MATRIX_LAYOUT_COLUMN_MAJOR,
.compilerOptionEntries{slangOptions.data()},
.compilerOptionEntryCount{uint32_t(slangOptions.size())}
};
// Load and compile the shader
Slang::ComPtr<slang::ISession> slangSession;
slangGlobalSession->createSession(slangSessionDesc, slangSession.writeRef());
Slang::ComPtr<slang::IModule> slangModule{ slangSession->loadModuleFromSource("shader", "shader.slang", nullptr, nullptr) };
Slang::ComPtr<ISlangBlob> spirv;
slangModule->getTargetCode(0, spirv.writeRef());
VkShaderModuleCreateInfo shaderModuleCI{
.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO,
.codeSize = spirv->getBufferSize(),
.pCode = (uint32_t*)spirv->getBufferPointer()
};
// Create the shader module to be used by the application
VkShaderModule shaderModule{};
chk(vkCreateShaderModule(device, &shaderModuleCI, nullptr, &shaderModule));
// Take shader stages from the single module we just compiled
VkPipelineShaderStageCreateInfo vertexShader{
.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
.stage = VK_SHADER_STAGE_VERTEX_BIT,
.module = shaderModule, .pName = "main"
};
VkPipelineShaderStageCreateInfo fragmentShader{
.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
.stage = VK_SHADER_STAGE_FRAGMENT_BIT,
.module = shaderModule, .pName = "main"
};
VkGraphicsPipelineCreateInfo pipelineCI{
...
.stageCount = 2,
.pStages = shaderStages.data(),
};
chk(vkCreateGraphicsPipelines(device, VK_NULL_HANDLE, 1, &pipelineCI, nullptr, &pipeline));
Best Practices for Writing Shaders in Slang for Vulkan
Code Organization
-
Use modules: Organize code into logical modules with clear responsibilities
-
Leverage imports: Use imports instead of includes for better dependency management
-
Use interfaces: Define interfaces for common functionality to enable polymorphism
Performance Considerations
-
Profile interface dispatch: Test the performance impact of interface-based polymorphism; interfaces are often resolved at compile time through specialization, but this isn’t guaranteed for every use
-
Leverage compile-time evaluation: Use generics and specialization to move work to compile time
-
Consider specialization: Generate specialized shader variants for performance-critical paths
Vulkan-Specific Best Practices
-
Use parameter blocks: Organize resources into parameter blocks for better organization
-
Let Slang assign bindings: Slang automatically assigns descriptor set and binding indices, so explicit
vk::bindingattributes are usually unnecessary; add them only when you need a stable binding layout across shader edits or need to match bindings set up elsewhere in your application -
Cross-API compatibility: Use conditional compilation for Vulkan-specific code where parameter blocks aren’t a good fit (see below)
Advanced Topics
Cross-Compilation and Portability
Slang does not automatically define target-specific preprocessor macros, since preprocessing runs once regardless of how many targets you compile for. Define your own macro on the command line (e.g. -DTARGET_VULKAN=1) for each target build:
// TARGET_VULKAN is defined on the command line,
// e.g. `slangc -target spirv -DTARGET_VULKAN=1 ...`
#ifdef TARGET_VULKAN
[[vk::binding(0, 0)]]
#endif
ParameterBlock<GlobalResources> globals;
Shader Composition and Reuse
Interfaces, generics, and modules are independent Slang features that combine well for building reusable shader code:
// In lighting.slang
module Lighting;
public interface ILight
{
float3 calculateLighting(float3 position, float3 normal, float3 viewDir);
}
// In directional_light.slang
module Lights.Directional;
import Lighting;
public struct DirectionalLight : ILight
{
float3 direction;
float3 color;
float3 calculateLighting(float3 position, float3 normal, float3 viewDir)
{
// Implementation
}
}
// In main shader
module Main;
import Lighting;
import Lights.Directional;
float3 shadeSurface(ILight light, float3 position, float3 normal, float3 viewDir)
{
return light.calculateLighting(position, normal, viewDir);
}
Reflection and Shader Introspection
Slang provides a reflection API for inspecting compiled shader code from host application code:
-
Type and field layout: Query the fields, types, and memory layout of a program
-
Resource binding layout: Query the descriptor set and binding assigned to each resource, which can be used to build descriptor set layouts without hardcoding bindings
-
Entry point parameters: Query the parameters of a given entry point
Reflection data is retrieved after compiling or linking a program, through slang::ProgramLayout and related interfaces such as TypeLayoutReflection and VariableLayoutReflection. See the Slang reflection API documentation for details and examples.