Types2 min read

Structs

User-defined struct types, member access, methods, const receivers, and resource payloads in BWSL.

Reading Time
2 min
Word Count
324
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

Structs are user-defined nominal types for grouping shader data. They can be declared at pipeline scope or inside modules, used as local values, passed to functions, returned from functions, nested inside other structs, and used as resource payload shapes.

Declaring Structs

Define fields inside a struct body. Fields use the same type names as ordinary declarations, and fixed-size array fields use the canonical Type[N] name form:

bwsl
struct Light {
float3 position;
float intensity;
float4[4] shadowPlanes;
}
struct Material {
float3 albedo;
float roughness;
float metallic;
}

A trailing semicolon after the closing brace is accepted, but not required.

Constructing and Accessing

Create struct values with either field assignments or positional constructors. Constructor arguments follow field declaration order.

bwsl
Material assigned;
assigned.albedo = float3(0.9, 0.4, 0.2);
assigned.roughness = 0.35;
assigned.metallic = 0.0;
Material constructed = Material(float3(0.9, 0.4, 0.2), 0.35, 0.0);
float3 baseColor = constructed.albedo;
float reflectance = constructed.metallic;

Nested structs and struct arrays use normal member and index access:

bwsl
struct Pair {
float a;
float b;
}
struct Surface {
Material material;
Pair[4] samples;
}
Surface surface;
surface.material.albedo = float3(1.0, 0.8, 0.6);
surface.samples[0] = Pair(1.0, 2.0);
float sampleA = surface.samples[0].a;

Struct Methods

Methods are declared inside the struct body with the same name :: (...) -> ReturnType form as functions. Call them through a receiver expression with dot syntax:

bwsl
struct Basis {
float3 tangent;
float3 bitangent;
float3 normal;
to_world :: (float3 v) -> float3 {
return tangent * v.x + bitangent * v.y + normal * v.z;
}
}
Basis basis;
basis.tangent = float3(1.0, 0.0, 0.0);
basis.bitangent = float3(0.0, 1.0, 0.0);
basis.normal = float3(0.0, 0.0, 1.0);
float3 world = basis.to_world(float3(1.0, 2.0, 3.0));

Inside a method, self is available implicitly. Fields can be read by bare name, and self.field is useful when a parameter or local variable shadows a field:

bwsl
struct ShadowBox {
float value;
select :: (float value) const -> float {
float local = value * 2.0;
return local + self.value;
}
}

Non-const methods can mutate receiver fields:

bwsl
struct Accumulator {
float total;
add :: (float v) -> void {
total = total + v;
}
reset :: () -> void {
self.total = 0.0;
}
}

Const Methods

Place const after the parameter list to declare a method that can be called on a const receiver:

bwsl
struct Accumulator {
float total;
add :: (float v) -> void {
total = total + v;
}
read :: () const -> float {
return self.total;
}
}
Accumulator acc;
acc.total = 1.0;
acc.add(2.0);
const Accumulator frozen = acc;
float value = frozen.read();

const methods cannot assign to receiver fields, and a const receiver cannot call a non-const method.

Method Overloads

Struct methods can be overloaded by parameter types. const receiver eligibility is also part of method selection: mutable receivers prefer a matching non-const method, while const receivers can only call matching const methods.

bwsl
struct Preference {
float v;
pick :: () -> float3 {
return float3(v + 2.0, 0.0, 0.0);
}
pick :: () const -> float {
return v + 1.0;
}
}
Preference pref;
pref.v = 10.0;
float3 mutablePick = pref.pick();
const Preference frozen = pref;
float constPick = frozen.pick();

A struct cannot declare the same method name, parameter signature, and constness more than once.

Module Structs

Structs declared in modules can have methods too. Use Module::Type in ordinary type positions and call methods on values of that type:

bwsl
module ToneLib {
struct Tone {
float3 color;
float gain;
tint :: (float scale) const -> float3 {
return color * gain * scale;
}
boost :: (float value) -> void {
gain = gain + value;
}
}
}
pipeline UsesTone {
import ToneLib
pass "Compute" {
compute "Main" [1, 1, 1] {
ToneLib::Tone tone;
tone.color = float3(0.25, 0.5, 1.0);
tone.gain = 2.0;
tone.boost(1.0);
float3 result = tone.tint(0.5);
}
}
}

Resource Payloads

Structs can describe host-facing resource payloads:

bwsl
struct CameraData {
mat4 viewProj;
float3 cameraPosition;
}
resources {
camera: CameraData
}

Imported module structs can also be resource payloads. In a resources {} block, module-defined payload types use Module.Type spelling:

bwsl
module RenderTypes {
struct FrameData {
float4 scaleOffset;
}
}
pipeline UsesFrameData {
import RenderTypes
resources {
frame: RenderTypes.FrameData
}
}

After declaration, access fields through resources.<name> like any other resource:

bwsl
float2 p = attributes.position.xy * resources.frame.scaleOffset.zw
+ resources.frame.scaleOffset.xy;

See Also