Pass Blocks
Reusable pass-returning functions, interface mappings, and variant remapping in BWSL.
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 EditPass blocks let a function return a reusable pass body. They are useful when a module owns a complete graphics or compute pass shape, but the caller pipeline owns the concrete attributes, resources, and variants.
Declaring Pass Blocks
A pass-block helper returns pass_block and its body must contain a pass { ... } block:
module SpritePasses {
attributes {
position: float3
normal: float3
texCoord: float2
}
resources {
viewProj: mat4
atlas: texture2D
atlasSampler: sampler
}
variants {
localTint: bool = false
}
sprite :: () -> pass_block {
pass {
use attributes { position, normal?, texCoord }
use resources { viewProj, atlas, atlasSampler }
vertex {
output.position = resources.viewProj * float4(attributes.position, 1.0);
output.uv = attributes.texCoord;
}
fragment {
float4 texel = sample(resources.atlas, resources.atlasSampler, input.uv);
if (variants.localTint) {
output.color = texel;
} else {
output.color = float4(input.uv, 0.0, 1.0);
}
}
}
}
}
The pass { ... } wrapper is required. A bare body that starts with use, vertex, fragment, or compute is rejected.
Instantiating Pass Blocks
Instantiate a pass block with pass "Name" = helperCall(). A pipeline-local helper can be used directly:
pipeline LocalPipeline {
attributes {
position: float3
uv: float2
}
localPass :: () -> pass_block {
pass {
use attributes { position, uv }
vertex {
output.position = float4(attributes.position, 1.0);
output.uv = attributes.uv;
}
fragment {
output.color = float4(input.uv, 0.0, 1.0);
}
}
}
pass "Local" = localPass();
}
Pass-block instantiation must be a direct function call. Ternaries or arbitrary expressions on the right-hand side are rejected.
Mapping Interfaces
When a pass block comes from a module and uses attributes or resources, the caller must map the module-local interface names to names declared in the caller pipeline:
pipeline GameUI {
import SpritePasses
attributes {
meshPosition: float3
meshNormal: float3
uv: float2
}
resources {
cameraVP: mat4
uiAtlas: texture2D
uiSampler: sampler
}
variants {
tintEnabled: bool = true
}
pass "Sprites" = SpritePasses::sprite() {
use attributes { position = meshPosition, normal = meshNormal, texCoord = uv }
use resources { viewProj = cameraVP, atlas = uiAtlas, atlasSampler = uiSampler }
variants { localTint = tintEnabled }
}
}
Inside the mapping body, only these forms are accepted:
use attributes { localName = pipelineName, sameName }
use resources { localName = pipelineName, sameName }
variants { localVariant = pipelineVariant }
The left side is the name used by the pass-block helper. The right side is the caller pipeline name. A bare entry maps a name to itself.
Mapping Rules
Attribute mappings require matching attribute type and decorators. Resource mappings require matching resource type. Variant mappings require both sides to be declared variants with matching types.
Variant mapping targets must be caller pipeline variants, not arbitrary expressions:
variants {
tintEnabled: bool = true
}
pass "Sprites" = SpritePasses::sprite() {
variants { localTint = tintEnabled }
}
Optional attribute and resource facts are remapped with their interface names. For example, a module pass using normal? can refer to variants.has_normal; when the caller maps normal = meshNormal, that optional fact follows the caller's mapped attribute.
The instantiation body is for mapping only. You cannot add or override vertex, fragment, compute, outputs, or pass-local helper functions from the caller.
Parameters
Pass-block helpers may take parameters, but instantiation arguments must be compile-time constants:
makeShadow :: (float bias) -> pass_block {
pass {
vertex {
output.position = float4(0.0, 0.0, bias, 1.0);
}
fragment = null
}
}
pass "Shadow" = makeShadow(0.001);
Runtime values such as attributes.position.x cannot be passed into a pass_block helper.
Compute Pass Blocks
Pass blocks can return compute passes too:
module ComputePasses {
resources {
values: buffer<float>
result: buffer<float>
}
copyValues :: () -> pass_block {
pass {
use resources { values, result }
compute "Main" [8, 1, 1] {
uint idx = input.global_id.x;
resources.result[idx] = resources.values[idx];
}
}
}
}
pipeline CopyPipeline {
import ComputePasses
resources {
sourceValues: buffer<float>
outputValues: buffer<float>
}
pass "Copy" = ComputePasses::copyValues() {
use resources { values = sourceValues, result = outputValues }
}
}
See Also
- The Pass - Inline passes and stage assignment
- Functions - Function declarations and return types
- Modules - Sharing reusable shader code
- Shader Variants - Variant declarations and specialization