Projects / 03
oksl
SkSL in plain Odin. A compiler for Skia runtime-effect shaders that emits SPIR-V, MSL and a CPU program, so nobody needs a shader compiler installed.
- Odin
- MIT
- core: only
- SPIR-V · MSL · CPU
- Shadertoy in
By the numbers
- test shaders match, GPU vs CPU
- 17 / 17
- 15 within 1/255; the other two differ only at threshold edges
- targets from one source
- 3
- SPIR-V for Vulkan, MSL for Metal, a CPU program
- shader compilers to install
- 0
- the compiler depends on Odin's core: only
- SkSL programs a frame
- 18
- onimite's shader_mode example, one of them Shadertoy
Parity from tools/gpu_parity (2026-10-10, SDL_GPU on Vulkan, RX 9070 XT, against the CPU interpreter); program count from onimite's BENCHMARK.md.
What's in it
01SkSL as Skia takes it
half4 main(float2 coord), uniforms, uniform shader children sampled with .eval, structs, arrays, matrices and the GLSL ES 3 built-ins.
02Shadertoy in
GLSL from Shadertoy is rewritten to SkSL as fframes does it, a text rewrite rather than a second parser, with errors reported on the original lines.
03SPIR-V and MSL
SPIR-V 1.0 for Vulkan and MSL for Metal, laid out for SDL_GPU's bindings, plus a committed layer vertex shader.
04A CPU interpreter that keeps up
Every function inlines into one flat program over 16 SoA lanes (four 2x2 quads). One switch per instruction, never per lane, and arithmetic runs as loops the compiler vectorizes.
05GPU semantics on the CPU
Divergent control flow runs under execution masks, helper lanes keep dFdx/dFdy correct, and integer division, mod and shifts match the GPU.
06Reflection
A std140 uniform block with the Shadertoy built-ins at fixed offsets; Module.uniforms reports every user uniform's offset, size and stride.
07Useful errors
line:column messages, several per compile, and clear diagnostics for what SkSL leaves out: no preprocessor, no discard, no sampler2D.
08Data-oriented
Tokens, syntax, symbols and types are flat arrays of plain structs referenced by integer ids. A compile allocates from one arena, released by destroy.
Not yet
- Fragment shaders only, and no Shadertoy multipass buffers.
- No struct uniforms, arrays of arrays,
switch, or colorFilter / blender children. - MSL is compiled and tested on macOS only; Windows runs SDL_GPU's Vulkan driver (no DXIL).
Use it
uniform float3 iResolution;
uniform float iTime;
uniform float uKick;
half4 main(float2 coord) {
float2 uv = (coord - 0.5 * iResolution.xy) / iResolution.y;
float r = length(uv);
float a = atan(uv.y, uv.x);
float w = sin(18.0 * r - 4.0 * iTime + 3.0 * sin(3.0 * a));
float ring = 0.012 / abs(r - 0.3 - 0.04 * w - 0.06 * uKick);
half3 col = 0.5 + 0.5 * cos(iTime + 6.0 * r + half3(0, 2, 4));
col *= smoothstep(-0.3, 1.0, w) * (1.1 - r);
col += ring * half3(0.7, 1.0, 0.25);
return half4(col, 1.0);
}m, errors, ok := oksl.compile(source) // SkSL
// m, errors, ok := oksl.compile_shadertoy(glsl) // Shadertoy, errors in the original lines
defer oksl.destroy(m)
if !ok {
fmt.eprint(oksl.format_errors(errors)) // "12:7: undeclared identifier \"foo\""
return
}
// GPU: SDL_CreateGPUShader with m.spirv (entry oksl.SPIRV_ENTRY) or m.msl
// (entry oksl.MSL_ENTRY), num_samplers = m.num_samplers, num_uniform_buffers = 1;
// vertex shader oksl.LAYER_VERTEX_SPIRV / LAYER_VERTEX_MSL.
// CPU:
block := make([]u8, m.block_size)
oksl.write_builtins(block, {resolution = {f32(w), f32(h), 1}, time = t, channel_resolution = {{tw, th, 1}, {}, {}, {}}})
oksl.set_uniform(m, block, "uSpeed", {0.5})
oksl.run(&m.program, block, w, h, pixels, w * 4, {sampler = my_sampler})odin run tools/okslc -- a.sksl b.glsl ... # compile (.glsl: Shadertoy); -o prefix writes .spv / .metal
odin run tools/cpu_render -- shader.sksl out.png # CPU render
odin run tools/gpu_parity -- out_dir # SDL_GPU vs CPU interpreter on testdata/
odin test . -vet -strict-style # the test suite (+ spirv-val when installed)