# 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 ```sh # 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. ```lua 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: ```lua 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 | 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 Points ```lua @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: ```lua 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 ```lua @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 ```lua 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: | Namespace | Members | |-----------|---------| | `vertex` | `index`, `instance` | | `frag` | `coord`, `front_facing` | | `work` | `global_id`, `local_id`, `local_index`, `group_id` | ### Attributes | 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 | ### Inline Entry Points ```lua -- 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 | 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 | ## CLI Reference ``` luma compile [options] Compile shader to target --target= glsl, glsl-opengl, hlsl, msl, wgsl, spirv (default: glsl) --include-dir= Add include search path --debug Emit debug info (SPIR-V: OpLine/OpSource) -o Output file (default: stdout) luma check Parse and typecheck only luma dump-ast Print abstract syntax tree luma dump-ir Print intermediate representation luma reflect [options] Emit binding reflection as JSON --spirv SPIR-V-specific reflection (descriptor sets, layout) ``` ## Library Usage ```odin 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](https://odin-lang.org) compiler. ```sh odin build . -out:luma ``` For SPIR-V validation, install [spirv-tools](https://github.com/KhronosGroup/SPIRV-Tools): ```sh spirv-val output.spv ``` ## Testing ```sh bash tests/run_golden.sh # run all 40 golden tests bash tests/run_golden.sh --update # regenerate golden files ```