Luma is now gpu's built-in shader language/compiler. Most gpu code should use the easy shaderkit facade; compiler tools can import gpu/shader directly.
import shaderkit "path/to/gpu/shaderkit"
handle, ok := shaderkit.create_shader_from_source(
"material.frag",
.Fragment,
source,
{opt_level = .Basic},
)
The facade chooses the backend target from gpu.REQUIRED_SHADER_FORMAT, applies the D3D/HLSL defaults, logs diagnostics, creates the backend shader module, and destroys compiler output.
For compiler tools that need bytecode and reflection:
import shader "path/to/gpu/shader"
result := shader.compile(source, {target = .SPIR_V, stage = .Vertex}, "shader.luma")
defer shader.destroy_compile_result(&result)
bytecode := result.output
reflection := result.reflection
Compile_Result owns output, diagnostics, and reflection. They remain valid across later compiles and must be released with destroy_compile_result.
Each stage is exposed independently for custom tooling:
// Parse only (AST inspection, syntax highlighting)
ast, diags := shader.compile_parse(source, "shader.luma")
// Parse + desugar + typecheck (IDE diagnostics)
ast, sema, diags := shader.compile_check(source, "shader.luma")
// Full lowering to IR (custom optimization passes)
ir_module, diags := shader.compile_lower(source, "shader.luma")
// Dump AST (parse + desugar only)
ast, diags := shader.compile_dump_ast(source, "shader.luma")
Extract binding layouts, I/O variables, and struct metadata:
result := shader.compile(source, {target = .SPIR_V, stage = .Fragment}, "shader.luma")
defer shader.destroy_compile_result(&result)
info := result.reflection
json := shader.reflect_to_json(info)
// Or access structured data directly:
for entry in info.entry_points {
log.infof("Entry: %s (%v)", entry.name, entry.stage)
for io in entry.inputs {
log.infof(" in: %s @ location %d", io.name, io.location)
}
}
for b in info.bindings {
log.infof("Binding: %s group=%d binding=%d", b.name, b.group, b.binding)
}
For Vulkan pipeline creation with descriptor set layouts:
info, diags := shader.compile_reflect_spirv(source, "shader.luma")
json := shader.reflect_spirv_to_json(info)
// Descriptor set layout creation
for ds in info.descriptor_sets {
// ds.set is the descriptor set number
for binding in ds.bindings {
// binding.descriptor_type: "UNIFORM_BUFFER", "STORAGE_BUFFER",
// "SAMPLED_IMAGE", "SAMPLER"
// binding.binding: binding number
// binding.stage_flags: ["VERTEX", "FRAGMENT", etc.]
// binding.block_size: buffer size in bytes
// binding.members: field-level layout with offsets
}
}
shader.Compile_Options :: struct {
target: shader.Target, // GLSL_450, GLSL_450_OPENGL, HLSL_SM6, MSL_2_4, WGSL, SPIR_V
opt_level: shader.Opt_Level, // None, Basic, Aggressive
stage: shader.Shader_Stage,
entry: string,
debug: bool, // Emit debug info (SPIR-V OpLine/OpSource)
validate: bool,
include_dirs: []string, // Include search paths
file_reader: shader.File_Reader, // Custom file reader for virtual filesystem
}
For virtual filesystems or asset pipelines, provide a custom file reader:
my_reader :: proc(path: string) -> (content: string, ok: bool) {
// Read from your asset system
data, read_ok := asset_system.read(path)
return data, read_ok
}
result := shader.compile(source, shader.Compile_Options{
target = .SPIR_V,
file_reader = my_reader,
include_dirs = []string{"shaders/include"},
})
defer shader.destroy_compile_result(&result)
Diagnostics include source location, severity, and message:
for d in result.diagnostics {
switch d.level {
case .Error:
msg := shader.format_diagnostic(d)
log.errorf("%s", msg)
delete(msg)
case .Warning:
msg := shader.format_diagnostic(d)
log.warnf("%s", msg)
delete(msg)
case .Note:
msg := shader.format_diagnostic(d)
log.infof("%s", msg)
delete(msg)
}
}
// Rich formatting with source context (for CLI tools)
for d in result.diagnostics {
msg := shader.format_diagnostic_with_source(d, source)
fmt.eprintln(msg)
delete(msg)
}