Language2 min read

Shader Variants

Declare pipeline-level variant inputs, constrain legal combinations, and specialize passes at compile time.

Reading Time
2 min
Word Count
333
Sections
7
Try It Live

Pressure-test the syntax

Take the concept from this page into the playground and deliberately break a pass, binding, or type signature to see how the compiler responds.

Try a Live Edit

Shader variants let one pipeline describe a family of related compiled shaders. You declare the variant space once at pipeline scope, use variants.<name> inside pass logic, and specialize the pipeline for a concrete selection when compiling or integrating it in an engine.

Declaring Variants

Variants live in a variants block inside a pipeline.

bwsl
pipeline LitMesh {
enum LightingMode {
Unlit
Forward
Clustered
}
attributes {
position: float3
normal: float3
}
variants {
skinning: bool = false;
lighting: LightingMode = LightingMode::Forward;
}
}

Rules:

  • variant declarations are pipeline-scoped
  • supported variant types are bool and plain enum types
  • the default value must be a compile-time constant
  • variant names must be unique within the pipeline

Using Variants in Passes

Access variants through the variants namespace.

bwsl
skinnedVertex :: () -> vertex_function {
vertex {
output.position = float4(0.25, 0.0, 0.0, 1.0);
}
}
staticVertex :: () -> vertex_function {
vertex {
output.position = float4(0.5, 0.0, 0.0, 1.0);
}
}
pass "Main" {
use attributes { position, normal? }
vertex = variants.skinning ? skinnedVertex() : staticVertex()
fragment {
float brightness = variants.skinning ? 0.75 : 0.5;
if (variants.has_normal) {
brightness = brightness + 0.25;
}
if (variants.lighting == LightingMode::Unlit) {
output.color = float4(1.0, 0.0, 0.0, 1.0);
} else {
float blue = variants.lighting == LightingMode::Clustered ? 1.0 : 0.5;
output.color = float4(brightness, 0.0, blue, 1.0);
}
}
}

Variants can drive:

  • stage assignment expressions such as vertex = ...
  • regular expressions inside vertex, fragment, or compute
  • compile-time branching and constant folding

When a variant selection is chosen, BWSL specializes the pipeline before lowering. Inactive branches and unused stage paths are removed from the generated shader output.

Variant Visualizer
12 concrete shaders from one pipeline
4 / 12
skinning
lighting
attribute mask: normalOptional `normal?` maps to the implicit `variants.has_normal` fact from bit 1.
Branch Pruning
if (variants.skinning)emit vertex wave deformation
elsekeep static vertex path
if (variants.has_normal)read the optional normal stream
else / fallback pathuse the generated fallback normal path
lighting == Unlitemit warm unlit color
lighting == Forwardemit forward-lit blue channel
lighting == Clusteredemit clustered blue channel
N
skinning=falselighting=Forwardhas_normal=trueattributeMask=0b10
Input
One BWSL pipeline with declared variants and an optional attribute.
Selection
Declared variants plus the attribute mask fold the branches.
Output
Dead paths disappear from the specialized shader.
active variant conditioninactive pruned codenormal? -> variants.has_normal from attributeMask bit 1
1pipeline VariantDemo {
2 enum LightingMode {
3 Unlit
4 Forward
5 Clustered
6 }
7
8 variants {
9 skinning: bool = false;
10 lighting: LightingMode = LightingMode::Forward;
11 }
12
13 attributes {
14 position: float3
15 normal: float3
16 }
17
18 pass "Main" {
19 use attributes { position, normal? }
20
21 vertex {
22 float3 p = attributes.position;
23 float3 n = float3(0.0, 0.0, 1.0);
24 if (variants.skinning) {
25 p.y = p.y + sin(p.x * 4.0) * 0.08;
26 }
27 if (variants.has_normal) {
28 n = attributes.normal;
29 }
30 output.position = float4(p, 1.0);
31 output.normal = n;
32 }
33
34 fragment {
35 float brightness = variants.skinning ? 0.85 : 0.55;
36 float normalBoost = -0.05;
37 if (variants.has_normal) {
38 normalBoost = 0.18;
39 }
40 if (variants.lighting == LightingMode::Unlit) {
41 output.color = float4(brightness + normalBoost, 0.25, 0.15, 1.0);
42 } else {
43 float blue = variants.lighting == LightingMode::Clustered ? 1.0 : 0.55;
44 output.color = float4(brightness + normalBoost, 0.35, blue, 1.0);
45 }
46 }
47 }
48}

Rules

The optional rules block constrains which selections are legal.

bwsl
variants {
skinning: bool = false;
lighting: LightingMode = LightingMode::Forward;
rules {
require skinning -> has_normal;
conflict lighting == LightingMode::Unlit, skinning;
}
}

Supported rule forms:

  • require <expr> -> <expr>;
  • conflict <expr>, <expr>;

Notes:

  • rule expressions must evaluate to compile-time booleans
  • inside rules, you reference variant names directly, not variants.<name>
  • invalid selections are rejected during specialization

For example, the combination skinning=true and lighting=Unlit is illegal in the rules above and fails compilation.

Implicit Attribute Variants

Optional attributes create implicit boolean variant facts.

bwsl
pass "Main" {
use attributes { position, normal? }
}

The normal? marker introduces variants.has_normal.

Use this when the same pipeline should specialize for different available vertex streams:

bwsl
fragment {
if (variants.has_normal) {
output.color = float4(normalize(input.normal) * 0.5 + 0.5, 1.0);
} else {
output.color = float4(1.0, 1.0, 1.0, 1.0);
}
}

In plain bwslc CLI usage, implicit attribute facts default to true. Engine-side compiler-service integration can provide an attribute mask so those has_<attribute> values reflect the actual enabled vertex streams for a compiled variant.

CLI Specialization

Use -variant to select a concrete variant value at compile time.

bash
bwslc shader.bwsl \
  -variant skinning=true \
  -variant lighting=Clustered \
  -metal -hlsl

Details:

  • -variant is repeatable
  • boolean variants accept true, false, 1, or 0
  • enum variants accept either a bare member such as Clustered or a qualified name such as LightingMode::Clustered
  • implicit has_<attribute> variants cannot be overridden directly

Inspecting Variant Space

Use -dump-variant-space to inspect the declared variants, implicit variants, rules, and currently selected values without emitting shader binaries.

bash
bwslc shader.bwsl -dump-variant-space

Example output shape:

json
{
  "declared": [
    { "name": "skinning", "type": "bool", "default": "false" },
    { "name": "lighting", "type": "LightingMode", "default": "Forward" }
  ],
  "implicit": [
    { "name": "has_normal", "type": "bool", "attributeIndex": 1 }
  ],
  "selected": [
    { "name": "skinning", "type": "bool", "value": "false", "implicit": false },
    { "name": "lighting", "type": "LightingMode", "value": "Forward", "implicit": false },
    { "name": "has_normal", "type": "bool", "value": "true", "implicit": true, "attributeIndex": 1 }
  ]
}

This is useful for tooling, build validation, and engine-side variant caching.