Language4 min read

Shader I/O

How data flows through attributes, input, output, and built-in stage values.

Reading Time
4 min
Word Count
604
Sections
12
Try It Live

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 Edit

BWSL uses four well-known namespaces inside shaders:

  • attributes for declared vertex inputs
  • input for stage inputs and built-ins
  • output for stage outputs
  • resources for shader-visible data declared in the pipeline resources {} 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

position: float3normal: float3texcoord: float2color: float4
attributes.*

Vertex Shader

PROGRAMMABLE

Reads from

attributes.positionattributes.normal
input.vertex_idinput.instance_id

Writes to

output.positionreqoutput.worldPosoutput.normaloutput.texcoord
output.*

Rasterizer

Interpolates vertex outputs across the triangle surface using barycentric coordinates

input.*

Fragment Shader

PROGRAMMABLE

Reads from

input.worldPosinput.normalinput.texcoord
input.position.z

Writes to

output.colorreqoutput.depth
output.color

Render Target

Screen or texture output

Attributes

Vertex inputs are declared at pipeline scope and selected per pass:

bwsl
attributes {
position: float3
normal: float3
texcoord: float2
color: float4
}

Read them from attributes.<name> in the vertex stage:

bwsl
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:

bwsl
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:

bwsl
vertex {
uint vid = input.vertex_id;
uint iid = input.instance_id;
}
FieldTypeDescription
input.vertex_iduintIndex of the current vertex in the draw call
input.instance_iduintIndex 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:

bwsl
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:

bwsl
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:

bwsl
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
  • @noperspective is 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:

bwsl
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):

bwsl
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)]]:

bwsl
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:

bwsl
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:

bwsl
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

bwsl
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

ObjectStagePurpose
attributes.*VertexDeclared vertex input data
input.vertex_idVertexIndex of the current vertex
input.instance_idVertexIndex of the current instance
output.positionVertexRequired clip-space position
output.*VertexAny value to pass to the fragment shader
input.positionFragmentBuilt-in fragment position
input.*FragmentInterpolated values from the vertex shader
output.colorFragmentDefault color output or declared color output
output.*FragmentColor output declared in the pass outputs block
output.depthFragmentOptional built-in depth output
input.global_id etc.ComputeCompute built-ins for dispatch/workgroup access

See Also