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.
# 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
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
| Category | Types |
|---|---|
| Scalar | bool, int, uint, float, half |
| Vector | vec2-vec4, ivec2-ivec4, uvec2-uvec4, bvec2-bvec4 |
| Matrix | mat2-mat4, mat2x3, mat3x4, etc. |
| Sampler | sampler2D |
| Array | [N]T (e.g. [16]vec4) |
| Struct | User-defined with struct ... end |
@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.
@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.
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
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:
| Namespace | Members |
|---|---|
vertex | index, instance |
frag | coord, front_facing |
work | global_id, local_id, local_index, group_id |
| Attribute | Usage |
|---|---|
@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 |
@varying | Struct shared between vertex output and fragment input |
@spec(id) | Specialization constant |
-- 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
| Target | Flag | Version |
|---|---|---|
| GLSL (Vulkan) | --target=glsl | 4.50 |
| GLSL (OpenGL) | --target=glsl-opengl | 4.50 core |
| HLSL | --target=hlsl | Shader Model 6.0 |
| MSL | --target=msl | Metal 2.4 |
| WGSL | --target=wgsl | WebGPU |
| SPIR-V | --target=spirv | 1.5 |
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)
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")
Requires Odin compiler.
odin build . -out:luma
For SPIR-V validation, install spirv-tools:
spirv-val output.spv
bash tests/run_golden.sh # run all 40 golden tests
bash tests/run_golden.sh --update # regenerate golden files