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.
Allowed
Section titled “Allowed”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, vendArithmetic. + - * /, // (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, 1endfor 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.4endHelper 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.2endConstants 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.
Not allowed
Section titled “Not allowed”| 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.2endThe rule about the rest of the script
Section titled “The rule about the rest of the script”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.
local title = "plasma"
function fragment(x, y, u) return 0, 0, 0.5endMove such lines into _init and make the variable a global there.
Errors you may see
Section titled “Errors you may see”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 |
Size and cost
Section titled “Size and cost”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.