Shader Variants
Declare pipeline-level variant inputs, constrain legal combinations, and specialize passes at compile time.
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 EditShader 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.
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
booland 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.
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, orcompute - 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.
1pipeline VariantDemo {2 enum LightingMode {3 Unlit4 Forward5 Clustered6 }7 8 variants {9 skinning: bool = false;10 lighting: LightingMode = LightingMode::Forward;11 }12 13 attributes {14 position: float315 normal: float316 }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.
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, notvariants.<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.
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:
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.
bwslc shader.bwsl \
-variant skinning=true \
-variant lighting=Clustered \
-metal -hlsl
Details:
-variantis repeatable- boolean variants accept
true,false,1, or0 - enum variants accept either a bare member such as
Clusteredor a qualified name such asLightingMode::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.
bwslc shader.bwsl -dump-variant-space
Example output shape:
{
"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.