Tools4 min read

Bindings and Reflection

The bindings.json format, the names BWSL gives GL/Metal/HLSL interface objects, and how a host should bind resources.

Reading Time
4 min
Word Count
670
Sections
8
Try It Live

Turn the guide into code

Take the key idea from this page into the playground and validate it in a real shader instead of leaving it as theory.

Open Playground

The compiler assigns every binding itself. Hosts should read the result from the reflection output rather than infer bindings from declaration order or hard-code generated names. Pass -bindings (implied by -all) to write <stem>.bindings.json next to the shaders.

bash
bwslc lit.bwsl -o out/ -gles -bindings

bindings.json

For this pipeline:

bwsl
pipeline Lit {
attributes { position: float3 uv: float2 }
struct Camera { mat4 viewProj; float3 eye; }
resources {
camera: Camera
tint: float4
albedo: texture2D
units: texture2D[2]
}
pass "Main" {
use attributes { position, uv }
use resources { camera, tint, albedo, units }
// ...
}
}

the compiler writes:

json
{
  "pass": "Main",
  "resources": [
    { "name": "camera", "type": "uniform", "set": 0, "binding": 0,
      "stages": ["vertex"], "access": "readonly", "glName": "ub_camera" },
    { "name": "tint", "type": "uniform", "set": 0, "binding": 1,
      "stages": ["fragment"], "access": "readonly", "glName": "ub_tint" },
    { "name": "albedo", "type": "texture", "set": 0, "binding": 2,
      "stages": ["fragment"], "access": "readonly", "glName": "t_albedo" },
    { "name": "units", "type": "texture", "set": 0, "binding": 3, "count": 2,
      "stages": ["fragment"], "access": "readonly", "glName": "t_units" }
  ]
}

Resource Fields

FieldMeaning
nameThe resource's name in the resources {} block
typeuniform, storage_buffer, texture, sampler, …
set, bindingDescriptor set and binding (Vulkan/SPIR-V). Samplers that are separate from their textures use set 2.
countElement count for texture arrays such as texture2D[4]. Absent for single resources.
stagesStages that actually reference the resource
accessreadonly, writeonly or readwrite, from how the shader uses it
glNameThe GL name to look up: pass it to glGetUniformBlockIndex for uniform blocks or glGetUniformLocation for combined textures
abiHow a texture or sampler is bound when it isn't a plain combined pair (see below)
defaultSamplerForOn a compiler-generated sampler: the texture it serves when that texture is sampled without an explicit sampler
combinedSamplerUniformsOn a separate texture, for GLSL/GLES: one entry per texture/sampler pair, giving the combined uniform name, the sampler, its samplerSet and samplerBinding, and stages

Only referenced resources are listed. Unused declarations are dropped, and the remaining resources are packed into the available slots.

Combined and Separate Samplers

A texture that is only ever used with one sampler is bound as a combined sampled image. When a texture is used with several samplers, or one sampler is shared between textures, the compiler keeps them separate:

  • the texture gets "abi": "separate_image";
  • each sampler gets "abi": "sampler" and is placed in set 2;
  • a sampler the compiler generates for implicit sample(tex, uv) calls is named <texture>__default_sampler and carries defaultSamplerFor.

GL has no separate samplers, so for GLSL and GLES each pair becomes its own combined sampler uniform. Look those up through combinedSamplerUniforms:

json
{ "name": "a", "type": "texture", "set": 0, "binding": 0, "abi": "separate_image",
  "combinedSamplerUniforms": [
    { "name": "bwsl_tex_0_0_sampler_2_0", "sampler": "linearSamp", "samplerSet": 2, "samplerBinding": 0, "stages": ["fragment"] },
    { "name": "bwsl_tex_0_0_sampler_2_1", "sampler": "pointSamp",  "samplerSet": 2, "samplerBinding": 1, "stages": ["fragment"] }
  ] }

HLSL Extra Resources

HLSL has no built-in for the compute dispatch size. A compute stage that reads input.num_workgroups needs a constant buffer the host fills, listed under hlslResources:

json
"hlslResources": [
  { "name": "bwsl_num_workgroups", "type": "uniform", "set": 3, "binding": 0,
    "stages": ["compute"], "builtin": "num_workgroups", "valueType": "uint3", "byteSize": 16 }
]

Bind 16 bytes at register b0, space3: the x, y and z group counts plus one padding word.

Interface Names

GL and GLES output uses predictable names derived from your source. Metal and HLSL output use the same names.

SourceGenerated nameExample
Uniform resource rblock ub_r, instance bwsl_ub_r, member bwsl_u_runiform ub_camera { Camera bwsl_u_camera; } bwsl_ub_camera;
Texture texcombined sampler t_texuniform highp sampler2D t_albedo;
Texture array text_tex[N]uniform highp sampler2D t_units[2];
Default sampler for texbwsl_s_tex
Attribute aa_ain vec3 a_position;
Varying vv_vout vec2 v_uv;

Struct type and member names are kept as written, so nested uniform structs link across GL stages.

Because these names are emitted as written, some source names would collide with them and are rejected:

  • a struct named like an interface name (for example a struct a_position when there is an attribute position);
  • a sampler named like a texture's combined name (a sampler t_atlas next to a texture atlas);
  • two varyings whose names are the same in their first 31 characters.

Changed in v0.11.0

Older compilers emitted generated names such as _12_14, UB_<resource>, u_<texture>, attr0 and varying0. Hosts that looked those up must switch to the names above, ideally by reading glName from bindings.json.

WebGL Stage Sidecars

With -gles, each stage also gets a small JSON sidecar (<stem>.vert.json, <stem>.frag.json) mapping attribute names to locations and resources to the uniform blocks and samplers that stage uses:

json
{
  "attributes": { "position": 0, "uv": 1 },
  "uniforms": { "camera": 0 },
  "samplers": {}
}

WGSL Bindings

The experimental WGSL backend (-wgsl) writes its own <stem>.<stage>.wgsl.bindings.json per stage, with WebGPU group/binding pairs instead of set/binding:

json
{
  "version": 3,
  "target": "wgsl",
  "stage": "compute",
  "entryPoint": "main",
  "workgroupSize": [64, 1, 1],
  "resources": [
    { "name": "data", "type": "storage_buffer", "group": 0, "binding": 0,
      "access": "read_write", "elementType": "UINT", "stride": 4 }
  ]
}

Storage buffers go in group 0, uniforms in group 1, sampled textures in group 2 and samplers in group 3. The implicit sampler for texture slot N is at binding 32 + N. This layout differs from the SPIR-V one, so WebGPU hosts must read the WGSL file. See CLI Reference.

See Also