Shader I/O
How data flows through attributes, input, output, and built-in stage values.
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 uses four well-known namespaces inside shaders:
attributesfor declared vertex inputsinputfor stage inputs and built-insoutputfor stage outputsresourcesfor shader-visible data declared in the pipelineresources {}block
The most common flow is vertex output.* becoming fragment input.* with matching field names.
Shader I/O Data Flow
How data moves between shader stages
Attribute Declarations
attributes { }Vertex Shader
PROGRAMMABLEvertex { }Reads from
Writes to
Rasterizer
Interpolates vertex outputs across the triangle surface using barycentric coordinates
Fragment Shader
PROGRAMMABLEfragment { }Reads from
Writes to
Render Target
Screen or texture output
Attributes
Vertex inputs are declared at pipeline scope and selected per pass:
attributes {
position: float3
normal: float3
texcoord: float2
color: float4
}
Read them from attributes.<name> in the vertex stage:
vertex {
float3 pos = attributes.position;
float2 uv = attributes.texcoord;
output.position = float4(pos, 1.0);
output.uv = uv;
}
The Output Object
In a vertex block, use output to declare values that should be passed to the fragment stage:
vertex {
output.position = float4(attributes.position, 1.0);
output.worldPos = attributes.position;
output.viewDir = normalize(float3(0.0, 0.0, 5.0) - attributes.position);
output.texcoord = attributes.texcoord;
}
output.position is special: it defines clip-space position and is required in every vertex stage.
The Input Object
The input object behaves differently depending on the shader stage.
Vertex Stage Input
In a vertex block, input provides access to built-in vertex identifiers:
vertex {
uint vid = input.vertex_id;
uint iid = input.instance_id;
}
| Field | Type | Description |
|---|---|---|
input.vertex_id | uint | Index of the current vertex in the draw call |
input.instance_id | uint | Index of the current instance |
These map to gl_VertexID and gl_InstanceID in GLSL-family back ends.
Named vertex attributes are not read through input. Use attributes.<name> for declared vertex inputs; non-built-in input.<name> reads in a vertex stage are diagnosed, with a suggestion to use attributes.<name> when the name matches a declared attribute.
Fragment Stage Input
In a fragment block, named outputs from the vertex stage appear automatically on input, and input.position exposes the fragment position:
fragment {
float3 worldPos = input.worldPos;
float3 viewDir = normalize(input.viewDir);
float2 uv = input.texcoord;
float depth = input.position.z;
float3 color = normalize(worldPos) * 0.5 + 0.5;
color *= 0.5 + 0.5 * abs(viewDir.z);
color *= 1.0 - depth * 0.25;
output.color = float4(color, 1.0);
}
Values in input are automatically interpolated across the triangle during rasterization.
Interpolation Qualifiers
User varyings use the backend's default perspective-correct interpolation unless the vertex output write is decorated.
Use @flat for values that should not interpolate, such as material IDs or per-instance indices:
vertex {
output.position = float4(attributes.position, 1.0);
@flat output.materialIndex = float(input.instance_id);
}
fragment {
float materialIndex = input.materialIndex;
}
Use @noperspective for floating-point scalar or vector varyings that should interpolate in screen space without perspective correction:
vertex {
output.position = float4(attributes.position, 1.0);
@noperspective output.screenUV = attributes.texcoord;
}
fragment {
float2 screenUV = input.screenUV;
}
Rules:
- interpolation decorators apply to vertex-stage
output.*assignments only - the target must be a user varying, not a built-in such as
output.position - later writes to the same varying may omit the qualifier, but cannot specify a conflicting one
@noperspectiveis only valid for floating-point scalar and vector varyings
Fragment Outputs
Without an explicit pass outputs block, fragment shaders can write output.color at color attachment location 0:
fragment {
output.color = float4(1.0, 0.0, 0.0, 1.0);
}
Graphics passes can declare multiple color outputs with an outputs block. Declaration order assigns locations unless an output uses @location(n):
pass "GBuffer" {
outputs {
color: float4 // location 0
bloom: float4 // location 1
material: float4 @location(3)
}
fragment {
output.color = float4(1.0);
output.bloom = float4(0.0);
output.material = float4(0.0, 0.5, 1.0, 1.0);
}
}
Extra fragment color outputs require an outputs declaration. If a pass omits outputs, only output.color and the built-in depth output are valid fragment outputs.
output.depth is a built-in depth output and is not declared in outputs. When it is written, the compiler emits the target backend's depth output, such as SPIR-V FragDepth, GLSL/GLES gl_FragDepth, HLSL SV_Depth, or Metal [[depth(any)]]:
fragment {
float depth = saturate(input.position.z);
output.depth = depth;
output.color = float4(depth, depth, depth, 1.0);
}
Automatic Type Inference
BWSL infers types for interpolated values. Assign to output.yourName in the vertex stage and read input.yourName in the fragment stage:
vertex {
output.position = float4(attributes.position, 1.0);
output.uv = attributes.texcoord;
output.worldNormal = attributes.normal;
output.tint = float4(1.0, 0.8, 0.6, 1.0);
}
fragment {
float2 uv = input.uv;
float3 normal = input.worldNormal;
float4 tint = input.tint;
}
Compute Input
Compute stages use the same input namespace for workgroup built-ins:
compute "Main" [64, 1, 1] {
uint3 gid = input.global_id;
uint3 lid = input.local_id;
uint3 group = input.workgroup_id;
uint3 groups = input.num_workgroups;
uint flat = input.local_index;
}
Complete Example
pipeline ShaderIO {
attributes {
position: float3
normal: float3
texcoord: float2
color: float4
}
pass "Main" {
use attributes { position, normal, texcoord, color }
vertex {
uint vid = input.vertex_id;
output.position = float4(attributes.position, 1.0);
output.worldPos = attributes.position;
output.normal = attributes.normal;
output.texcoord = attributes.texcoord;
output.vertexColor = attributes.color;
@flat output.vertexIndex = float(vid);
}
fragment {
float3 normal = normalize(input.normal);
float4 vertexColor = input.vertexColor;
float vertexFade = 0.8 + 0.2 * fract(input.vertexIndex * 0.125);
float lit = max(dot(normal, normalize(float3(1.0, 1.0, 1.0))), 0.0);
output.color = float4(vertexColor.rgb * lit * vertexFade, vertexColor.a);
}
}
}
Summary
| Object | Stage | Purpose |
|---|---|---|
attributes.* | Vertex | Declared vertex input data |
input.vertex_id | Vertex | Index of the current vertex |
input.instance_id | Vertex | Index of the current instance |
output.position | Vertex | Required clip-space position |
output.* | Vertex | Any value to pass to the fragment shader |
input.position | Fragment | Built-in fragment position |
input.* | Fragment | Interpolated values from the vertex shader |
output.color | Fragment | Default color output or declared color output |
output.* | Fragment | Color output declared in the pass outputs block |
output.depth | Fragment | Optional built-in depth output |
input.global_id etc. | Compute | Compute built-ins for dispatch/workgroup access |
See Also
- Language Overview - BWSL syntax and concepts
- Compute Shaders - Compute built-ins and workgroup behavior
- Operators - Operator reference for shader math