Skip to content

What compiles

A compiled shader is still Lua, so it also runs interpreted. But the compiler accepts only a part of the language: the part that can be worked out for 64 pixels at once without a Lua interpreter. This page is the list.

Values. Numbers, true and false, and the vectors vec2, vec3, vec4. Every number is a 32-bit float.

Variables. local variables, and assigning to them.

function fragment(x, y, u)
local v = x * 0.1
v = v + y * 0.05
return v, v, v
end

Arithmetic. + - * /, // (divide and round down), %, ^, and unary minus, on numbers and vectors. Comparisons < > <= >= == ~= between numbers. and, or and not between comparisons.

if. With elseif and else, and return inside.

function fragment(x, y, u)
if x < 32 and y < 32 then
return 1, 0, 0
elseif x < 32 then
return 0, 1, 0
end
return 0, 0, 1
end

for with fixed bounds. The start, end and step must be written as numbers, and the loop may run at most 64 times. The compiler writes the body out once per pass.

function fragment(x, y, u)
local sum = 0
for i = 1, 4 do
sum = sum + sin(x * 0.05 * i) / i
end
return 0.5 + 0.25 * sum, 0.2, 0.4
end

Helper functions. Defined at the top level with function or local function. A helper returns exactly one value, with a single return as its last statement. The same helper can be called with numbers in one place and vectors in another.

local function stripes(v, width)
return step(0.5, fract(v / width))
end
function fragment(x, y, u)
return stripes(x, 8), stripes(y, 16), 0.2
end

Constants at the top level. local PI = 3.14159265, local HALF = 0.5, and short names for built-ins such as local sin = math.sin.

The built-ins. Everything on the Shader built-ins page.

Uniforms. Through the third parameter, each declared with -- @uniform name type.

print. Allowed, and removed by the compiler, so you can leave debugging lines in.

Construct Why What to do instead
Tables, strings A shader’s values are numbers and vectors Keep them in _init, _update, _draw
while, repeat, break A loop must have a fixed length A for with fixed bounds
for with computed bounds, or over 64 passes It is unrolled at compile time Use literal bounds; fold the rest into an if
Assigning to part of a vector (p.x = 1) Not supported yet Build a new one: p = vec2(1, p.y)
Comparing vectors Comparisons take numbers Compare components: p.x > 0.5
Functions inside functions, recursion Helpers are copied into the caller Top-level helpers
A helper with two returns A helper returns once, at the end Assign to a local in the if, return it last
Changing a parameter or loop variable They are fixed inside the body Copy it into a local first
Calling gfx, math.random, your own Lua functions Only built-ins and helpers Compute it in _update and pass it as a uniform
Reading a global A shader sees its parameters, locals and constants Pass it as a uniform

Loops that stop early are the common one. A shader that marches a ray until it hits something cannot stop per pixel, because 64 pixels run together. Write a fixed number of steps and guard the work with an if:

function fragment(x, y, u)
local z, hit = 0, 0
for i = 1, 16 do
if hit < 0.5 and sin(z + x * 0.1) * cos(z + y * 0.1) > 0.9 then
hit = 1
end
if hit < 0.5 then
z = z + 0.25
end
end
return z / 4, hit * 0.5, 0.2
end

The compiler reads the whole file. It skips every function whose name starts with an underscore, so _init, _update and _draw can contain anything. Everything else at the top level has to be in the list above, whether or not the shader uses it.

Won't compile: a string at the top level
local title = "plasma"
function fragment(x, y, u)
return 0, 0, 0.5
end

Move such lines into _init and make the variable a global there.

The message names the problem and the line. A few, with what they mean:

Message Meaning
tables are not allowed in shaders A { } outside the _ functions
cannot combine vec2 and vec3 Arithmetic between vectors of different sizes
unknown uniform 'speed'; declare it with '-- @uniform speed float' u.speed is read but not declared
fragment must return a vec3 or three floats, got vec2 The return value is the wrong shape
fragment must end with a return on every path Some route through the function returns nothing
for loop bounds must be number literals A bound is a variable or an expression
'not' needs a bool, got float A number where a comparison was expected
math.sin works on floats only, got vec2; call sin() for vectors math.sin takes numbers; the plain name sin takes vectors
shader keeps more than 32 values alive at once Too many values in use at the same moment; see below

A compiled shader is small by design. The compiler reports these with a line number where it can.

Limit Value
Instructions 1,024
Instructions before the optimizer shrinks them 8,192
Values alive at the same moment 32
Different constants 256
Uniforms 16, with names up to 15 characters
Octaves in one fbm call 8
Cost per pixel 1,000

Loops are written out once per pass and helpers once per call, so nested loops multiply: three loops of 64 passes are 262,144 copies of the body. The compiler stops as soon as a shader passes 8,192 instructions, before the optimizer runs, and says so.

Cost is the compiler’s estimate of the work per pixel. Each operation has a price: 1 for arithmetic, 6 for sin or cos, 37 for one octave of noise, more for the slow functions (pow, exp, log, atan). A unit is about a tenth of a microsecond. The editor prints the total after each compile.

A frame’s fills may add up to 1,000 cost units per pixel. Past that, gfx.fill stops the script with shader work exceeds this frame's budget. This is what keeps a heavy shader from freezing the panel. For a sense of scale: the plasma costs 57, four-octave clouds cost about 160, and a frame that runs at 30 frames per second on a 64 × 64 panel costs about 55.

Next: Shader built-ins.