---
title: "Specialization constants on Vulkan, Metal and WebGPU"
url: https://recursiveai.org/oneshader/cookbook/specialization-constants/
description: "Declare HLSL specialization constants once with [[vk::constant_id]] and set them when the pipeline is created: VkSpecializationInfo on Vulkan, function…"
version: "1.2.0"
---

# Specialization constants on Vulkan, Metal and WebGPU

Declare HLSL specialization constants once with `[[vk::constant_id]]` and set them when the pipeline is created: `VkSpecializationInfo` on Vulkan, function constants on Metal and pipeline-overridable constants on WebGPU, from one compiled shader.

A specialization constant is a value the shader reads like a constant but the pipeline chooses, so one compiled shader serves many variants without recompiling. OneShader 1.2.0 compiles `[[vk::constant_id(N)]] const T Name = default;` (`bool`, `int`, `uint` or `float`) to each target’s mechanism, and generates the C# to fill them in.

`Shaders/Blur.hlsl`, a separable Gaussian blur whose radius, direction and sigma are pipeline-time constants:

```hlsl
// Pipeline-time constants: one compiled shader, many pipelines.
[[vk::constant_id(0)]] const int Radius = 4;
[[vk::constant_id(1)]] const bool Horizontal = true;
[[vk::constant_id(2)]] const float Sigma = 2.0;

Texture2D<float4> Source : register(t0);
[[vk::image_format("rgba8")]] RWTexture2D<float4> Target : register(u1);

[shader("compute")]
[numthreads(8, 8, 1)]
void CSMain(uint3 id : SV_DispatchThreadID)
{
    int2 step = Horizontal ? int2(1, 0) : int2(0, 1);
    float4 sum = 0;
    float total = 0;
    for (int i = -Radius; i <= Radius; i++)
    {
        float w = exp(-(i * i) / (2 * Sigma * Sigma));
        sum += Source[int2(id.xy) + step * i] * w;
        total += w;
    }
    Target[id.xy] = sum / total;
}
```

The project references `RecursiveAI.OneShader.Build` 1.2.0 with `OneShaderTargets` set to `SpirV;Msl;Wgsl`. `Program.cs`:

```csharp
using OneShader.Layouts;
using OneShader.Layouts.Metal;
using OneShader.Layouts.Vulkan;
using Spec.Shaders;

// The constants, their ids and defaults, generated from the shader.
Console.WriteLine($"Radius: id {Blur.SpecializationConstants.Radius.Id}, default {Blur.SpecializationConstants.Radius.Default}");
Console.WriteLine($"CSMain reads ids [{string.Join(", ", Blur.CSMain.SpecConstantIds.ToArray())}]");

// A vertical blur with radius 8: set fields on the generated struct.
Blur.Specialization s = Blur.Specialization.Defaults;
s.Radius = 8;
s.Horizontal = 0;   // a bool is a 4-byte 0 or 1

// Vulkan: the contents of VkSpecializationInfo.
SpecializationInfo vk = s.ToVulkan(Blur.CSMain.SpecConstantIds);
foreach (SpecializationMapEntry e in vk.MapEntries)
    Console.WriteLine($"Vulkan: constantID {e.ConstantId}, offset {e.Offset}, size {e.Size}");
Console.WriteLine($"Vulkan: data {Convert.ToHexString(vk.Data)}");

// Metal: MTLFunctionConstantValues entries, by index and MTLDataType.
foreach (FunctionConstantValue v in s.ToMetal(Blur.CSMain.SpecConstantIds))
    Console.WriteLine($"Metal: index {v.Index}, MTLDataType {v.Type.DataType}, bits 0x{v.Bits:x8}");

// WebGPU: the compute stage's constants record, keyed by decimal id.
Console.WriteLine($"WebGPU: {Record(s.ToWebGpu(Blur.CSMain.SpecConstantIds))}");

// The same by name, from the entry layout (this also works for shaders compiled at run time).
SpecializationValues byName = SpecializationValues.For(Blur.CSMain.Layout).Set("Radius", 8).Set("Horizontal", false);
Console.WriteLine($"By name: {Record(byName.ToWebGpu())}");

// What each target compiled the constant to.
Console.WriteLine(Line(Blur.CSMain.Wgsl, "override Radius"));
Console.WriteLine(Line(Blur.CSMain.Msl, "function_constant(0)"));

static string Record(IReadOnlyDictionary<string, double> c) =>
    "{ " + string.Join(", ", c.Select(kv => $"\"{kv.Key}\": {kv.Value}")) + " }";

static string Line(ReadOnlySpan<byte> utf8, string text) =>
    System.Text.Encoding.UTF8.GetString(utf8).Split('\n').First(l => l.Contains(text)).Trim();
```

```text
Radius: id 0, default 4
CSMain reads ids [0, 1, 2]
Vulkan: constantID 0, offset 0, size 4
Vulkan: constantID 1, offset 4, size 4
Vulkan: constantID 2, offset 8, size 4
Vulkan: data 080000000000000000000040
Metal: index 0, MTLDataType 29, bits 0x00000008
Metal: index 1, MTLDataType 53, bits 0x00000000
Metal: index 2, MTLDataType 3, bits 0x40000000
WebGPU: { "0": 8, "1": 0, "2": 2 }
By name: { "0": 8, "1": 0, "2": 2 }
@id(0) override Radius: i32 = 4i;
constant int Radius [[function_constant(0)]];
```

The SPIR-V passes `spirv-val` and the WGSL passes naga. On SPIR-V the constants are `OpSpecConstant` (and `OpSpecConstantTrue` for the `bool`) with `SpecId` 0, 1 and 2, the ids DXC writes for the same source.

## Passing them to each API

- **Vulkan:** `MapEntries` and `Data` are the contents of `VkSpecializationInfo` (`pMapEntries`, `pData`). Each constant is 4 bytes, in declaration order: `08000000` is the radius 8, `00000000` the `false`, `00000040` the float 2.0.
- **Metal:** set each value on an `MTLFunctionConstantValues` at its index, with the `MTLDataType` number (29 is `int`, 53 `bool`, 3 `float`). A function that declares function constants has to be created with `makeFunction(name:constantValues:)`, even to get the defaults: pass an empty `MTLFunctionConstantValues` for those. The MSL reads each constant through `is_function_constant_defined`, with the default as the fallback.
- **WebGPU:** the record goes in the stage’s `constants` when creating the pipeline. Pass the entry point’s `SpecConstantIds`, as above, because WebGPU rejects constants the entry point doesn’t declare. Vulkan and Metal ignore extra ones.
- **By name:** `SpecializationValues.For(layout)` takes an `EntryLayout` from the generated code or from run-time reflection (`EntryLayout.TryCreate`), with typed `Set` overloads. A value of the wrong type or an unknown name throws.

## Fixing constants at compile time

To bake some values in instead, compile with `CompileOptions.SpecializationOverrides` (`RecursiveAI.OneShader` 1.2.0):

```csharp
using OneShader;

// Fix Radius at compile time: it becomes a plain constant that the optimizer folds.
// The others stay pipeline-time constants.
CompileResult result = ShaderCompiler.CompileFile("Shaders/Blur.hlsl", new CompileOptions
{
    SpecializationOverrides = [new("Radius", 2), new("Blurr", 1)],
});
foreach (var d in result.Diagnostics)
    Console.WriteLine(d);
Console.WriteLine($"left as constants: {string.Join(", ", result.Reflection!.SpecConstants.Select(c => $"{c.Name} (id {c.Id})"))}");

CompileResult bad = ShaderCompiler.CompileFile("Shaders/Blur.hlsl", new CompileOptions
{
    SpecializationOverrides = [new("Radius", 2.5)],
});
foreach (var d in bad.Diagnostics)
    Console.WriteLine(d);
```

```text
Shaders/Blur.hlsl(1,1): warning OSH0335: The shader declares no specialization constant named 'Blurr', so the override is ignored. Check the spelling against the [[vk::constant_id]] declarations.
left as constants: Horizontal (id 1), Sigma (id 2)
Shaders/Blur.hlsl(1,1): error OSH0335: The value 2.5 does not fit the int specialization constant 'Radius': use a whole number in its range.
```

## Limits

- **Types:** `bool`, `int`, `uint` and `float` scalars. A vector or struct, `static`, a missing or non-constant default, a negative id or a duplicate id is error OSH0335.
- **Not where the size must be known at compile time:** an array size, a `[numthreads]` argument or a `case` label that uses one is error OSH0310, as DXC refuses the same uses:

  ```text
  /tmp/nt.hlsl(3,33): error OSH0310: Argument 1 of 'numthreads' must be an integer constant. It uses a specialization constant, whose value is only known when the pipeline is created: use a literal or a 'static const' there, or choose the value with a #define.
  ```

- **Hot reload:** `InterfaceCompatibility` reports a removed constant or a changed id or type. New constants and changed defaults stay compatible.

For variants that change resources or code structure, compile them separately with [Axes](https://recursiveai.org/oneshader/cookbook/shader-variants-axes/). The generated members are under Specialization constants in [Generated bindings](https://recursiveai.org/oneshader/docs/generated-bindings/).
