Harbor

branch main
showing the latest snapshot on main
README.md 7.3 KB · Markdown
gpu/shader/README.md 0644 Raw

Luma

A Lua-inspired shader language that cross-compiles to GLSL, HLSL, MSL, WGSL, and SPIR-V. Written in pure Odin. Ships as both a standalone CLI and an embeddable library.

Quick Start

# Build
odin build . -out:luma

# Compile a shader
./luma compile shader.luma --target=glsl
./luma compile shader.luma --target=spirv -o shader.spv
./luma compile shader.luma --target=spirv --debug -o shader.spv  # with debug info

# Type-check only
./luma check shader.luma

# Inspect
./luma dump-ast shader.luma
./luma dump-ir shader.luma
./luma reflect shader.luma
./luma reflect --spirv shader.luma  # Vulkan descriptor set layout

Example

Preferred workflow syntax lowers to the existing AST/IR model. The old attribute syntax remains valid during migration.

struct Uniforms
    model: mat4
    view_proj: mat4
end

group frame = 0
    uniform uniforms: Uniforms
end

vertex main
    in position: vec3 @location(0)
    in uv: vec2 @location(1)
    out clip: vec4 @builtin(position)
    out uv: vec2
do
    clip = uniforms.view_proj * uniforms.model * vec4(position, 1.0)
    out.uv = uv
end

Compatibility syntax:

struct Uniforms
    model: mat4
    view_proj: mat4
end

@group(0) @binding(0)
uniform uniforms: Uniforms

struct VertexInput
    @location(0) position: vec3
    @location(1) uv: vec2
end

struct VertexOutput
    @builtin(position) position: vec4
    @location(0) uv: vec2
end

@entry(vertex)
function main(input: VertexInput) -> VertexOutput
    let clip_pos = uniforms.view_proj * uniforms.model * vec4(input.position, 1.0)
    return VertexOutput {
        position = clip_pos,
        uv = input.uv,
    }
end

Language Overview

Types

CategoryTypes
Scalarbool, int, uint, float, half
Vectorvec2-vec4, ivec2-ivec4, uvec2-uvec4, bvec2-bvec4
Matrixmat2-mat4, mat2x3, mat3x4, etc.
Samplersampler2D
Array[N]T (e.g. [16]vec4)
StructUser-defined with struct ... end

Entry Points

@entry(vertex)    -- vertex shader
@entry(fragment)  -- fragment shader
@entry(compute) @workgroup_size(8, 8, 1)  -- compute shader

Workflow stage blocks are preferred for new code:

vertex main
    in position: vec3 @location(0)
    out clip: vec4 @builtin(position)
do
    clip = vec4(position, 1.0)
end

fragment shade
    in uv: vec2
    out color: vec4
do
    return vec4(uv, 0.0, 1.0) -- fragment-only single-output sugar
end

compute simulate @workgroup_size(64, 1, 1)
do
    let i = work.global_id.x
    -- write buffers/images; compute has no out slots
end

Workflow output writes are assignments (color = expr or out.color = expr). They may appear in if/elseif/else when every non-discard path definitely assigns each output before stage exit. Overwrites are allowed. Assignments in loops are allowed as overwrites but do not prove final assignment after the loop. return expr is only fragment single-output top-level sugar and cannot be mixed with explicit out writes.

Bindings

@group(0) @binding(0) uniform uniforms: MyStruct   -- uniform buffer
@group(0) @binding(1) buffer data: MyBuffer         -- storage buffer
@group(1) @binding(0) uniform tex: sampler2D        -- auto-splits to texture + sampler

group material = 1
    uniform albedo: sampler2D                       -- group block, auto binding
    @binding(4) buffer storage particles: ParticleBuffer
end

Groups default to 0. Bindings auto-increment within each group. Group blocks are source-level workflow buckets; they lower to the same @group(N) binding attributes and keep explicit @binding(N) pins.

Control Flow

if condition then
    -- ...
elseif other then
    -- ...
else
    -- ...
end

for i = 0, 10 do ... end
for i = 0, 100, 2 do ... end   -- with step
while condition do ... end

Builtins

Math: abs, sign, floor, ceil, round, fract, sqrt, sin, cos, tan, pow, exp, log, min, max, clamp, mix, smoothstep, step

Vector: normalize, length, dot, cross, distance, reflect, refract

Matrix: transpose, inverse, determinant

Texture: sample(tex, uv), sample_level(tex, uv, lod)

Compute: barrier()

Workflow builtin namespaces:

NamespaceMembers
vertexindex, instance
fragcoord, front_facing
workglobal_id, local_id, local_index, group_id

Attributes

AttributeUsage
@entry(stage)Mark function as shader entry point
@location(N)I/O location (auto-assigned if omitted)
@builtin(name)Built-in variable (position, vertex_index, etc.)
@group(N)Descriptor set / bind group
@binding(N)Binding number within group
@workgroup_size(X,Y,Z)Compute workgroup dimensions
@varyingStruct shared between vertex output and fragment input
@spec(id)Specialization constant

Inline Entry Points

-- Tuple return syntax
@entry(fragment)
function main(@location(0) uv: vec2) -> (color: vec4)
    return vec4(uv, 0.0, 1.0)
end

-- Single output shorthand
@entry(fragment)
function main(@location(0) uv: vec2) -> vec4
    return vec4(uv, 0.0, 1.0)
end

Backend Targets

TargetFlagVersion
GLSL (Vulkan)--target=glsl4.50
GLSL (OpenGL)--target=glsl-opengl4.50 core
HLSL--target=hlslShader Model 6.0
MSL--target=mslMetal 2.4
WGSL--target=wgslWebGPU
SPIR-V--target=spirv1.5

CLI Reference

luma compile [options] <file>    Compile shader to target
  --target=<target>              glsl, glsl-opengl, hlsl, msl, wgsl, spirv (default: glsl)
  --include-dir=<path>           Add include search path
  --debug                        Emit debug info (SPIR-V: OpLine/OpSource)
  -o <file>                      Output file (default: stdout)

luma check <file>                Parse and typecheck only
luma dump-ast <file>             Print abstract syntax tree
luma dump-ir <file>              Print intermediate representation
luma reflect [options] <file>    Emit binding reflection as JSON
  --spirv                        SPIR-V-specific reflection (descriptor sets, layout)

Library Usage

import shader "path/to/gpu/shader"

// Full compilation
result := shader.compile(source, shader.Compile_Options{
    target = .GLSL_450,
})
defer shader.destroy_compile_result(&result)

// result.output, result.diagnostics, and result.reflection are owned by result
// and remain valid until destroy_compile_result.
for binding in result.reflection.bindings {
    // group/binding/layout metadata for pipeline/material setup
}

// Individual pipeline stages
ast, diags := shader.compile_parse(source, "shader.luma")
ast, sema, diags := shader.compile_check(source, "shader.luma")
ir_module, diags := shader.compile_lower(source, "shader.luma")
info, diags := shader.compile_reflect(source, "shader.luma")
spirv_info, diags := shader.compile_reflect_spirv(source, "shader.luma")

Building

Requires Odin compiler.

odin build . -out:luma

For SPIR-V validation, install spirv-tools:

spirv-val output.spv

Testing

bash tests/run_golden.sh           # run all 40 golden tests
bash tests/run_golden.sh --update  # regenerate golden files