Constants
How const declarations work in BWSL, where they can appear, and why they disappear from compiled output.
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 EditA const declaration names a value that never changes. In BWSL a constant is a compile-time name, not a variable that ends up in the shader: the compiler substitutes the value wherever the name is used, so the name is gone from the generated code.
const float PI = 3.14159265359;
const float3 UP = float3(0.0, 1.0, 0.0);
const int TAP_COUNT = 4;
Where Constants Can Appear
Constants can be declared at four scopes:
module Tonemap {
const float WHITE_POINT = 11.2; // Module: Tonemap::WHITE_POINT
}
pipeline Lit {
import Tonemap
const float GAIN = 2.0; // Pipeline: visible in every pass
pass "Main" {
const float BIAS = 0.5; // Pass: visible in this pass's stages
fragment {
const float exposure = GAIN * 0.5; // Local
// ...
}
}
}
Module constants are read with Module::NAME, or by bare name after using Module. See Modules.
Rules
-
An initializer is required.
const float k;is an error. -
Constants are read-only. Assigning to a constant is an error, and so is writing through a member, swizzle or element (
k.x = 1.0,table[0] = 1.0). Taking a constant's address with^is also an error. -
Module constants must be compile-time values. Their initializers can use literals, other constants, scalar arithmetic and scalar or vector constructors:
bwslmodule Directions {const float2 NORTH = float2(0.0, -1.0);const float HALF_PI = 3.14159265359 * 0.5;}Vector arithmetic (
float2(1.0) * 2.0), swizzles (NORTH.y) and matrices are not supported in module constant initializers yet. -
Local constants can be initialized from runtime values, e.g.
const float u = input.uv.x * 2.0;. These act as read-only locals. -
Pointer constants: a
constpointer cannot be pointed somewhere else, but the value it points to can still be written through it.
Constants Vanish from the Output
The compiler folds constants into the expressions that use them, then keeps folding: arithmetic, comparisons, conversions, vector construction, swizzles, dot, cross, transpose and matrix products on constant inputs all become constants themselves. The constant's name never reaches the generated code. This holds for every target, GLSL included, even though GLSL has its own const and keeping the declaration would compile to the same machine code in the end.
pipeline Folding {
const float GAIN = 2.0;
pass "Main" {
const float BIAS = 0.5;
// ...
fragment {
output.color = float4(GAIN * input.uv.x + BIAS);
}
}
}
The fragment shader in GLSL output:
void main()
{
_9 = vec4((2.0 * v_uv.x) + 0.5);
}
Folding also removes work that depends only on constants. An if whose condition folds to a constant keeps just the branch it takes, and a switch on a constant keeps just the matching case. That makes constants a cheap way to configure shader code without runtime cost.
Folding is conservative
The compiler does not fold results that could differ between GPUs: NaN, infinities, denormals, -0.0, integer division of negative values, and conversions that are out of range or inexact. Those stay as runtime operations.
Because the name is gone, you can't look a constant up from the host. If the host needs to set or read a value, declare it as a resource instead.
Constant Arrays
Fixed-size arrays can be const at any of the four scopes:
const float[4] WEIGHTS = {0.125, 0.25, 0.375, 0.25};
float blurred = 0.0;
for (i in 0..4) {
blurred += sampleTap(i) * WEIGHTS[i];
}
- The brace initializer must provide exactly one value per element.
- Elements cannot be assigned.
- Module constant arrays must use compile-time element values.
Reads with a constant index fold to the element value, just like scalar constants. An array read with a runtime index needs the data at runtime, so the array is emitted as a single local array initializer (in GLSL, float _19[4] = float[](0.125, 0.25, 0.375, 0.25);). That is the one case where a constant leaves a trace in the output.
Constants and Eval
const values can drive eval blocks and shader variant logic. A pipeline-scope eval function can compute a local constant:
pipeline Shapes {
const float PI = 3.14159265359;
eval circleArea :: (float radius) -> float {
return PI * radius * radius;
}
pass "Main" {
// ...
fragment {
const float area = circleArea(2.0); // Folds to 12.566...
// ...
}
}
}
Pipeline- and module-scope constant initializers cannot call functions, including eval functions.