Language5 min read

Modules

Reusable code organization with modules for sharing functions, structs, and constants across shaders.

Reading Time
5 min
Word Count
755
Sections
19
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

Modules in BWSL provide a way to organize and reuse code across multiple shaders. They allow you to define functions, structs, constants, and enums that can be imported into pipelines or other modules.

Modules are file-scope declarations. A source file can contain only modules, only pipelines, or a mix of file-scope modules and pipelines. A module declaration is not valid inside a pipeline block.

Defining a Module

A module is declared with the module keyword followed by the module name:

bwsl
module Math {
const float PI = 3.14159265358979323846;
const float TAU = 6.28318530717958647692;
square :: (float x) -> float {
return x * x;
}
}

File-Scope Modules

Modules can live in their own .bwsl files or next to pipelines in the same source file:

bwsl
module LocalMath {
scale :: (float x) -> float {
return x * 2.0;
}
}
pipeline UsesLocalModule {
import LocalMath
pass "Main" {
compute "Main" [1, 1, 1] {
float value = LocalMath::scale(4.0);
uint idx = input.global_id.x;
}
}
}

File-scope modules are registered before pipelines are parsed, so a pipeline can import a module declared later in the same file:

bwsl
pipeline FileScopeModuleAfterPipeline {
import LateHelpers
pass "Main" {
compute "Main" [1, 1, 1] {
float value = LateHelpers::scale(2.0);
uint idx = input.global_id.x;
}
}
}
module LateHelpers {
scale :: (float x) -> float {
return x * 2.0;
}
}

Submodules

Submodules let a separate declaration extend an existing module namespace:

bwsl
module Lighting {
const float PI = 3.14159265359;
lambert :: (float3 n, float3 l) -> float {
return saturate(dot(n, l));
}
}
submodule LightingBRDF extends Lighting {
wrappedLambert :: (float3 n, float3 l, float wrap) -> float {
return saturate((dot(n, l) + wrap) / (1.0 + wrap));
}
}

Import the parent module, then access both parent and submodule declarations through the parent name:

bwsl
pipeline UsesLighting {
import Lighting
pass "Main" {
compute "Main" [1, 1, 1] {
float ndotl = Lighting::wrappedLambert(float3(0.0, 1.0, 0.0), float3(0.0, 1.0, 0.0), 0.35);
uint idx = input.global_id.x;
}
}
}

Inside a submodule body, declarations already in the parent module are available unqualified:

bwsl
submodule LightingDebug extends Lighting {
debugDiffuse :: (float3 n, float3 l) -> float {
return lambert(n, l);
}
}

Submodules can live in the same file as the parent module, or in separate .bwsl files found through the same module search roots as normal modules. When the parent module is loaded, the compiler scans those roots for submodule ... extends ParentName declarations and folds them into the parent.

Rules:

  • submodule declarations are file-scope only
  • the parent module must exist or be resolvable
  • import the parent module, not the submodule name
  • a submodule name cannot conflict with an existing module name
  • duplicate submodule declarations are rejected

Submodule Names Are Not Imports

A declaration like submodule LightingBRDF extends Lighting contributes members to Lighting. Code should import Lighting; import LightingBRDF is rejected because LightingBRDF is not a standalone module namespace.

Importing Modules

To use a module in a pipeline or another module, use the import statement:

bwsl
pipeline MyPipeline {
import Math
pass "MainPass" {
vertex {
output.position = float4(0.0, 0.0, 0.0, 1.0);
}
fragment {
float x = 0.5;
float result = Math::square(x);
output.color = float4(result, result, result, 1.0);
}
}
}

Multiple imports can be comma-separated:

bwsl
import Math, Noise

Embedded Standard Modules

The compiler embeds the standard library modules, so imports such as Math, Random, Noise, Color, Compression, Debug, Packing, PBR, Globals, PostFX, Sampling, SDF, and Spaces work without adding a -modules search path:

bwsl
pipeline UsesStdlib {
import Math, Debug
pass "Main" {
compute "Main" [1, 1, 1] {
float value = Math::safe_rcp(2.0);
float3 color = Debug::heatmap(value);
}
}
}

Project modules still come from the input file's directory or paths passed with -modules. Standard-library module names are reserved by the embedded library; a disk file named Math.bwsl, for example, cannot override import Math.

Project module names must be unique within a compilation unit. Declaring the same module name twice is a diagnostic instead of a last-one-wins override.

Import Aliases

Use as when a module name should be shorter locally or when two modules would otherwise be awkward to distinguish:

bwsl
pipeline MaterialPreview {
import PBR as BRDF
shade :: (BRDF::PBRMaterial mat, float3 normal) -> float3 {
return mat.albedo * saturate(normal.y);
}
}

After an alias is declared, module-qualified access can use the alias:

bwsl
float3 lit = BRDF::calculateDirectLighting(mat, N, V, L, radiance);

Aliases are scoped to the pipeline or module that declares them. An alias declared inside one module does not leak into an importing pipeline.

Accessing Module Members

Module members are accessed using the :: (scope resolution) operator:

bwsl
float angle = Math::PI * 0.5;
float doubled = Math::square(value);

Using Declarations

using ModuleName makes an already imported module available for unqualified function and constant lookup:

bwsl
pipeline UtilityPass {
import Math as M
using M
pass "Main" {
compute "Main" [1, 1, 1] {
float wave = triangle_wave(0.25);
float angle = PI * wave;
}
}
}

using does not import a module by itself. The module must already be imported in the same scope, either by its real name or by an alias.

Use unqualified lookup sparingly for modules whose names are part of the surrounding convention. Module-qualified calls remain clearer when several libraries export similar helper names.

Type Aliases

using Alias = Type creates a scoped type alias:

bwsl
pipeline MaterialDemo {
import PBR as BRDF
using Material = BRDF::PBRMaterial
shade :: (Material mat, float3 normal) -> float3 {
return mat.albedo * saturate(normal.y);
}
}

Type aliases can target built-in types, local custom types, or module-qualified custom types. They do not create a new nominal type; they only give an existing type another name in the current scope.

Module Contents

Modules can contain:

  • Imports: Dependencies on other modules
  • Using declarations: Module unqualified lookup or scoped type aliases
  • Constants: Compile-time constant values
  • Functions: Reusable shader functions
  • Structs: Custom data types
  • Enums: Named enum and payload enum types

Constants

bwsl
module Math {
const float PI = 3.14159265358979323846;
const float TAU = 6.28318530717958647692;
const float E = 2.71828182845904523536;
const float PHI = 1.61803398874989484820;
const float EPSILON = 1e-6;
const float SQRT_2 = 1.41421356237309504880;
}

Functions

Functions use the :: syntax for declaration:

bwsl
module Math {
square :: (float x) -> float {
return x * x;
}
cube :: (float x) -> float {
return x * x * x;
}
// Function overloading is supported
square :: (float3 x) -> float3 {
return x * x;
}
}

Structs

bwsl
module PBR {
struct PBRMaterial {
float3 albedo;
float roughness;
float metallic;
float ao;
};
}

Module-defined structs can be used as ordinary shader types from imported modules, including as resource payload types. In a resources {} block, spell module-defined resource types with member access syntax:

bwsl
module RenderTypes {
struct FrameData {
float4 scaleOffset;
};
}
pipeline UsesModuleResource {
import RenderTypes
attributes {
position: float3
}
resources {
frame: RenderTypes.FrameData
}
pass "Main" {
use attributes { position }
use resources { frame }
vertex {
float2 p = attributes.position.xy * resources.frame.scaleOffset.xy;
p += resources.frame.scaleOffset.zw;
output.position = float4(p, attributes.position.z, 1.0);
}
fragment {
output.color = float4(1.0);
}
}
}

Use Module::Type or an alias-derived type alias in expression and function type positions, and Module.Type when declaring a module-defined resource payload.

Module Dependencies

Modules can import other modules:

bwsl
module PBR {
import Math
// Fresnel-Schlick Approximation
fresnelSchlick :: (float cosTheta, float3 F0) -> float3 {
return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
}
// Lambertian Diffuse uses Math::PI
lambertianDiffuse :: (float3 albedo) -> float3 {
return albedo / Math::PI;
}
}

Imports, aliases, and using declarations inside a module stay local to that module.

Example: Math Module

Here's a comprehensive Math module with common utilities:

bwsl
module Math {
// Constants
const float PI = 3.14159265358979323846;
const float TAU = 6.28318530717958647692;
const float E = 2.71828182845904523536;
const float EPSILON = 1e-6;
const float SQRT_2 = 1.41421356237309504880;
const float INV_PI = 0.31830988618379067154;
// Range Mapping
inverse_lerp :: (float a, float b, float value) -> float {
return (value - a) / (b - a);
}
remap :: (float value, float in_min, float in_max, float out_min, float out_max) -> float {
float t = (value - in_min) / (in_max - in_min);
return out_min + t * (out_max - out_min);
}
// Safe Operations
safe_divide :: (float a, float b) -> float {
if (abs(b) > EPSILON) {
return a / b;
}
return 0.0;
}
safe_normalize :: (float3 v) -> float3 {
float len_sq = dot(v, v);
if (len_sq > EPSILON * EPSILON) {
return v * rsqrt(len_sq);
}
return float3(0.0, 1.0, 0.0);
}
// Power Shortcuts
square :: (float x) -> float {
return x * x;
}
cube :: (float x) -> float {
return x * x * x;
}
// 2D Transformations
rotate_2d :: (float2 p, float angle) -> float2 {
float c = cos(angle);
float s = sin(angle);
return float2(p.x * c - p.y * s, p.x * s + p.y * c);
}
// Step Functions
linear_step :: (float edge0, float edge1, float x) -> float {
return saturate((x - edge0) / (edge1 - edge0));
}
// Perlin's improved smoothstep (quintic)
smootherstep :: (float edge0, float edge1, float x) -> float {
float t = saturate((x - edge0) / (edge1 - edge0));
return t * t * t * (t * (t * 6.0 - 15.0) + 10.0);
}
// Easing Functions
ease_in_quad :: (float t) -> float {
return t * t;
}
ease_out_quad :: (float t) -> float {
return t * (2.0 - t);
}
ease_in_out_quad :: (float t) -> float {
if (t < 0.5) {
return 2.0 * t * t;
}
return 1.0 - 2.0 * (1.0 - t) * (1.0 - t);
}
// Wave Functions
triangle_wave :: (float x) -> float {
return abs(fract(x) * 2.0 - 1.0);
}
}

Example: PBR Module

A module for physically-based rendering calculations:

bwsl
module PBR {
import Math
struct PBRMaterial {
float3 albedo;
float roughness;
float metallic;
float ao;
};
// Fresnel-Schlick Approximation
fresnelSchlick :: (float cosTheta, float3 F0) -> float3 {
return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
}
// GGX/Trowbridge-Reitz Normal Distribution Function
distributionGGX :: (float NdotH, float roughness) -> float {
float alpha = roughness * roughness;
float alphaSqr = alpha * alpha;
float denom = NdotH * NdotH * (alphaSqr - 1.0) + 1.0;
return alphaSqr / (Math::PI * denom * denom);
}
// Smith's Geometry Function (Schlick-GGX)
geometrySchlickGGX :: (float NdotV, float roughness) -> float {
float k = (roughness + 1.0) * (roughness + 1.0) / 8.0;
return NdotV / (NdotV * (1.0 - k) + k);
}
geometrySmith :: (float NdotV, float NdotL, float roughness) -> float {
float ggx1 = geometrySchlickGGX(NdotV, roughness);
float ggx2 = geometrySchlickGGX(NdotL, roughness);
return ggx1 * ggx2;
}
// Lambertian Diffuse
lambertianDiffuse :: (float3 albedo) -> float3 {
return albedo / Math::PI;
}
// Complete Cook-Torrance Specular BRDF
cookTorranceSpecular :: (float3 N, float3 V, float3 L, float3 F, float roughness) -> float3 {
float3 H = normalize(V + L);
float NdotH = max(dot(N, H), 0.0);
float NdotV = max(dot(N, V), 0.0);
float NdotL = max(dot(N, L), 0.0);
float D = distributionGGX(NdotH, roughness);
float G = geometrySmith(NdotV, NdotL, roughness);
float3 numerator = D * F * G;
float denominator = 4.0 * NdotV * NdotL + 0.001;
return numerator / denominator;
}
// Complete Direct Lighting Calculation
calculateDirectLighting :: (PBRMaterial mat, float3 N, float3 V, float3 L, float3 radiance) -> float3 {
float NdotL = max(dot(N, L), 0.0);
float3 H = normalize(V + L);
float VdotH = max(dot(V, H), 0.0);
float3 F0 = lerp(float3(0.04), mat.albedo, mat.metallic);
float3 F = fresnelSchlick(VdotH, F0);
float3 kD = (1.0 - F) * (1.0 - mat.metallic);
float3 diffuse = kD * lambertianDiffuse(mat.albedo);
float3 specular = cookTorranceSpecular(N, V, L, F, mat.roughness);
return (diffuse + specular) * radiance * NdotL;
}
}

Using PBR in a Pipeline

bwsl
pipeline PBRShader {
import PBR as BRDF
import Math
using Material = BRDF::PBRMaterial
resources {
mvp: mat4
model: mat4
normalMatrix: mat3
cameraPos: float3
lightDir: float3
lightColor: float3
albedoMap: texture2D
roughnessMap: texture2D
metallicMap: texture2D
aoMap: texture2D
materialSampler: sampler
}
pass "Lighting" {
use resources {
mvp, model, normalMatrix, cameraPos, lightDir, lightColor,
albedoMap, roughnessMap, metallicMap, aoMap, materialSampler
}
vertex {
output.position = resources.mvp * float4(attributes.position, 1.0);
output.worldPos = (resources.model * float4(attributes.position, 1.0)).xyz;
output.normal = normalize(resources.normalMatrix * attributes.normal);
}
fragment {
Material mat;
mat.albedo = sample(resources.albedoMap, resources.materialSampler, input.uv).rgb;
mat.roughness = sample(resources.roughnessMap, resources.materialSampler, input.uv).r;
mat.metallic = sample(resources.metallicMap, resources.materialSampler, input.uv).r;
mat.ao = sample(resources.aoMap, resources.materialSampler, input.uv).r;
float3 N = normalize(input.normal);
float3 V = normalize(resources.cameraPos - input.worldPos);
float3 L = normalize(resources.lightDir);
float3 color = BRDF::calculateDirectLighting(mat, N, V, L, resources.lightColor);
color *= mat.ao;
output.color = float4(color, 1.0);
}
}
}

Performance

Modules are parsed once and cached to avoid redundant work. When a module is imported, only the functions and types that are actually used are folded into the final shader. This keeps compiled shaders lean and avoids code bloat.

See Also