Byte Engine Docs

BESL - Byte Engine Shader Language

An introduction to BESL, the Byte Engine Shader Language, designed for high-performance shader development.

Byte Engine Shader Language

BESL is Byte Engine's shader language. It exists so shader code can participate in the engine material pipeline, resource reflection, render-model code generation, and platform shader generation.

The syntax is Rust-inspired, but BESL is not Rust. It is a compact shader description language that is parsed, lexed into a semantic graph, transformed by render-model code, and then emitted for the active graphics backend.

Backend coverage is still evolving. The engine has GLSL, MSL, HLSL, and platform-generator paths, but individual features are implemented unevenly across stages and backends.

File shape

A BESL source file is a list of declarations. Common declarations include structs, constants, resource bindings, interface inputs and outputs, helper functions, and a main function.

VertexInput: struct {
	position: vec3f,
	normal: vec3f,
	uv: vec2f,
}

albedo: binding CombinedImageSampler set=0 binding=0 read

uv: input vec2f location=0
color: output vec4f location=0

main: fn () -> void {
	let texel: vec4f = sample(albedo, uv);
	color = texel;
}

The parser keeps the source structure first. The lexer then resolves names, types, members, intrinsics, and references between declarations.

Declarations

Structs

Struct declarations use Name: struct.

Light: struct {
	position: vec3f,
	color: vec3f,
	intensity: f32,
}

Members use name: type. Array members use the compact type form, such as u32[4].

Cluster: struct {
	light_indices: u32[64],
}

Constants

Module-level constants are known at compile time.

MAX_LIGHTS: const u32 = 64
OFFSETS: const u32[4] = u32[4](0, 1, 2, 3)

Functions

Functions use name: fn.

mod_by_16: fn (x: i32) -> i32 {
	return x % 16;
}

The main function is the stage entry point after render-model transformation.

main: fn () -> void {
	return;
}

Variables

Local variables use let.

let color: vec4f = vec4f(1.0, 0.0, 1.0, 1.0);

Assignments, member access, and array indexing are expression forms:

output.color = material.color;
let first: u32 = indices[0];

Control flow and operators

BESL supports if, for, return, and continue.

main: fn () -> void {
	for (let i: u32 = 0; i < 4; i = i + 1) {
		if (i >= 2) {
			continue;
		}
	}
}

Supported operators include:

  • arithmetic: +, -, *, /, %
  • shifts: <<, >>
  • bitwise: &, |
  • assignment: =
  • comparison: ==, !=, <, >, <=, >=
  • logical: &&, ||

Shader interface declarations

Render-model generators can add interface declarations programmatically, and shader authors may also encounter them in generated BESL.

Inputs and outputs carry a location:

uv: input vec2f location=0
color: output vec4f location=0

Outputs can be arrays when a stage needs multiple values of the same interface shape.

Push constants are small uniform values provided outside normal descriptor bindings:

push_constant: push_constant {
	material_index: u32,
}

Bindings describe resources visible to the shader. The semantic graph records set, binding, read/write mode, type, and optional array count.

Conceptually:

materials: binding Buffer<MaterialData> set=0 binding=0 read
output_image: binding Image<rgba8> set=0 binding=1 write
texture_sampler: binding CombinedImageSampler set=0 binding=2 read

The exact binding node shape is often generated by engine code rather than written by hand. Users will still see the concepts in API docs and reflection metadata.

Raw code escape hatch

BESL supports raw backend code nodes. They can carry GLSL, HLSL, or MSL snippets plus declared input and output dependencies.

This is mainly a bridge while higher-level generation catches up. Prefer normal BESL when possible so the graph, binding analysis, and backend emitters can understand the program.

Intrinsics

Intrinsics are built-in calls that the lexer resolves specially and backend generators lower to platform code. Some intrinsics are ordinary math functions. Others represent texture, image, compute, atomic, or mesh-stage operations.

Math

Scalar and vector math intrinsics include:

  • min
  • max
  • clamp
  • log2
  • pow
  • abs
  • sqrt
  • exp
  • sin
  • cos
  • tan
  • round
  • fract
  • fwidth
  • step
  • smoothstep
  • mix
  • radians
  • inversesqrt
  • dot
  • cross
  • normalize
  • reflect
  • length
let n: vec3f = normalize(normal);
let light: f32 = max(dot(n, direction), 0.0);
let rough: f32 = clamp(roughness, 0.04, 1.0);

Constructors and casts

Vector, matrix, scalar, and array constructors are parsed as calls. The backend generators also special-case casts such as f32(...) and u32(...).

let v: vec3f = vec3f(1.0, 0.0, 0.0);
let i: u32 = u32(4);

Texture and image access

Texture intrinsics include:

  • sample(texture, uv)
  • sample_normal(texture, uv)
  • texture_lod(texture, uv)
  • fetch(texture, coord)
  • fetch_u32(texture, coord)
  • texture_size(texture)

Image intrinsics include:

  • image_load(image, coord)
  • image_load_u32(image, coord)
  • image_size(image)
  • write(image, coord, value)
  • guard_image_bounds(image, coord)
let color: vec4f = sample(texture_sampler, uv);
let texel: vec4f = fetch(texture, coord);

guard_image_bounds(output_image, coord);
write(output_image, coord, color);

guard_image_bounds emits an early return when the coordinate is outside the image. It is useful in compute-style shaders that write to storage images.

Atomics

Atomic intrinsics include:

  • atomic_add(pointer_or_location, value)
  • atomic_load(pointer_or_location)
  • atomic_store(pointer_or_location, value)
  • image_atomic_or(image, coord, value)

Backend support differs. For example, image atomics have explicit lowering in GLSL and HLSL paths, while other atomic forms depend on the target resource representation.

Compute and mesh stage helpers

Compute helpers expose invocation identity:

  • thread_id()
  • thread_idx()
  • threadgroup_position()

Mesh-stage helpers include:

  • set_mesh_output_counts(vertex_count, primitive_count)
  • set_mesh_vertex_position(vertex_index, position)
  • set_mesh_triangle(primitive_index, indices)

These are stage-specific. They only make sense when the shader generator is producing the matching stage and backend support exists.

Program evaluation

The resource-management shader layer can evaluate a BESL program to extract useful metadata.

Current evaluation includes:

  • used bindings, with set, binding, read, and write flags
  • rough opacity classification for material decisions

This is why it is better to express resources and outputs in BESL instead of hiding them in raw backend code. The more the graph can see, the more the engine can reflect, validate, and optimize.

Practical guidance

Keep handwritten BESL small and domain-focused. Let render-model generators add repetitive bindings, push constants, and platform details.

Prefer explicit types. The parser and lexer are still young, so clear type declarations are easier to diagnose than clever inference-like patterns.

Use raw backend code only when the normal BESL path cannot express what you need yet. Raw snippets are useful, but they reduce what the engine can understand about the shader.

On this page