Control Flow
if/else, switch, early returns, discard, and how break and skip behave inside a switch.
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 EditBWSL has C-like control flow with a few differences worth learning up front. The biggest one: switch cases never fall through.
Loops (for, while, loop, range loops, break, skip) have their own page: Loops.
If and Else
if takes a parenthesized bool condition. Bodies can be blocks or single statements, and else if chains as usual:
float3 color;
if (ndotl > 0.5) {
color = litColor;
} else if (ndotl > 0.0) {
color = lerp(shadowColor, litColor, ndotl * 2.0);
} else {
color = shadowColor;
}
if (alpha < 0.01) discard;
The condition is a scalar. A numeric condition counts as true when it is non-zero, but an explicit comparison such as if (count != 0) reads better.
Ternary
cond ? a : b picks one of two values. Only the selected arm is evaluated, and both arms must have the same type:
float weight = isBackFace ? 0.5 : 1.0;
float3 tint = isSelected ? float3(1.0, 0.8, 0.2) : float3(1.0);
// Error: arms have different types (float3 and float2)
// float3 bad = isSelected ? float3(1.0) : float2(1.0);
The arms can be scalars, vectors, matrices, structs or arrays. When both arms are short and have no side effects, the compiler emits a branch-free select instead of an if.
Switch
switch selects one case by an integer value:
float3 debugColor;
switch (resources.debugMode) {
case 0:
debugColor = albedo;
case 1:
debugColor = normal * 0.5 + 0.5;
case 2, 3:
debugColor = float3(roughness, metallic, 0.0);
default:
debugColor = float3(1.0, 0.0, 1.0);
}
No Fallthrough
Each case runs only its own statements and then leaves the switch. There is no fallthrough into the next case and no break is needed to stop it.
To share one body between several values, list them in a single case, separated by commas:
switch (materialId) {
case 1, 2: // Both 1 and 2 run this body
roughness = 0.2;
default:
roughness = 0.8;
}
Coming from C, HLSL or GLSL
Stacked labels do not share a body. In case 1: case 2: roughness = 0.2;, value 1 runs the empty first case and does nothing. Write case 1, 2: instead.
Selectors and Labels
- The selector must be an integer:
int,uintor an enum value. Afloatselector is rejected. - Case labels must be compile-time values: integer literals (including negative, hex and binary literals), named constants, or enum members such as
case Side::Left:. A runtime variable as a label is an error. - Each value can appear in only one case, and a switch can have at most one
default. Duplicates are compile errors. defaultis optional. Without it, a value that matches no case skips the switch.- A case body can be a list of statements or a
{ ... }block. Each case is its own scope, so two cases can declare locals with the same name.
Break and Skip Inside a Switch
A case never needs break. When one appears inside a switch, it applies to the enclosing loop:
- Inside a loop,
breakin a case exits the loop, not just the switch.skipin a case moves on to the loop's next iteration. - Outside any loop,
breakin a case does nothing, because the case already ends there.
float total = 0.0;
for (int i = 0; i < 8; i++) {
switch (i) {
case 1:
skip; // Next iteration of the for loop
case 5:
break; // Leaves the for loop
default:
total += float(i);
}
total += 10.0;
}
// i = 0, 2, 3, 4 each add i + 10, so total is 49.0
Switching on Variants
A switch on a shader variant with a compile-time value is resolved during compilation: only the matching case is kept in each specialized shader. When the selector itself is a constant, the switch also compiles to just the matching case.
Returning Early
return leaves the current function or stage. BWSL also has conditional forms that keep guard clauses on one line:
shade :: (float3 n, float3 l) -> float {
float ndotl = dot(n, l);
return 0.0 if (ndotl <= 0.0); // Return a value only when the condition holds
return ndotl * ndotl;
}
compute "Main" [64, 1, 1] {
uint id = input.global_id.x;
return if (id >= resources.count); // Bounds guard for the extra invocations
resources.values[id] *= 2.0;
}
| Form | Meaning |
|---|---|
return; | Leave a void function or a stage |
return expr; | Return a value |
return if (cond); | Leave only when cond is true (void functions and stages) |
return expr if (cond); | Return expr only when cond is true |
A function with a return type must return a value on every path that can reach the end of the function. Otherwise it is a compile error. A switch without a default counts as a path that can fall out of the switch.
Discard
In a fragment stage, discard; drops the current fragment: nothing is written to any color target or to depth.
float4 texel = sample(resources.albedo, input.uv);
if (texel.a < 0.5) {
discard;
}
discard is a statement, so it takes no parentheses.
Summary
| Statement | Notes |
|---|---|
if (c) { } else if (c) { } else { } | c is a scalar condition |
c ? a : b | Only the chosen arm runs; both arms have the same type |
switch (x) { case 1, 2: ... default: ... } | Integer or enum selector; no fallthrough |
break / skip in a case | Apply to the enclosing loop |
return expr if (c); | Conditional return |
discard; | Fragment stages only |
See Also
- Loops -
for,while,loop, range loops,break ifandskip if - Shader Variants - Compile-time
switchon variants - Eval -
eval ifand compile-time loops