Skip to content

The bytecode

A compiled shader travels to the panel as a block of bytes in a format called LSH1. It is a custom format: it exists only to feed the panel’s shader VM, and it is not Lua bytecode or machine code. In a script it appears as hex digits:

shader = gfx.shader("4C534831030104000300050001000000010474696D65...")

gfx.shader also accepts the raw bytes.

All numbers are little-endian.

Part Size Contents
Header 16 bytes Magic and counts
Uniform table Variable One entry per declared uniform
Constants 4 bytes each 32-bit floats, each different value once
Prologue 8 bytes per instruction Runs once per frame
Body 8 bytes per instruction Runs for the pixels; ends with the only ret
Offset Size Field
0 4 The letters LSH1
4 1 Number of tile registers
5 1 Number of uniforms
6 2 Number of constants
8 2 Number of frame registers, which is also the number of prologue instructions
10 2 Number of body instructions
12 1 Number of uniform slots
13 3 Zero

For each uniform, in alphabetical order: one byte for its number of components (1 to 4), one byte for the length of its name, then the name. Each component gets a slot, numbered in table order. A float followed by a vec3 uses slots 0, then 1 to 3.

Every instruction is 8 bytes:

Offset Size Field
0 1 Opcode
1 1 Destination register
2 2 Operand A
4 2 Operand B
6 2 Operand C

The top two bits of an operand say what kind of value it is, and the lower 14 bits which one:

Top bits Kind Means
00 Tile register A value that differs per pixel
01 Constant An entry in the constant table
10 Uniform slot One component of a uniform
11 Frame register A result of the prologue

So 0x0001 is register 1, 0x4002 is constant 2, 0x8000 is uniform slot 0 and 0xC002 is frame register 2.

Three opcodes use a field differently:

  • fbm keeps its number of octaves in operand C, as a plain number.
  • jmpnone keeps the index of the body instruction to continue at in operand B.
  • ret has no destination. Its three operands are red, green and blue.

In the prologue, instruction number n writes frame register n.

This is the shader from the previous page, 102 bytes:

4C 53 48 31 "LSH1"
03 3 tile registers
01 1 uniform
04 00 4 constants
03 00 3 frame registers
05 00 5 body instructions
01 1 uniform slot
00 00 00
01 04 74 69 6D 65 uniform: 1 component, 4 letters, "time"
00 00 00 3F constant 0 = 0.5
00 00 F8 41 constant 1 = 31
00 00 7C 42 constant 2 = 63
00 00 00 00 constant 3 = 0
09 00 00 80 00 00 00 00 p0 = sin uniform slot 0
14 01 00 40 00 C0 00 00 p1 = mul constant 0, frame 0
12 02 00 40 01 C0 00 00 p2 = add constant 0, frame 1
1A 00 00 00 01 40 00 00 r0 = gt register 0, constant 1
15 01 01 00 02 40 00 00 r1 = div register 1, constant 2
28 02 00 00 02 C0 03 40 r2 = select register 0, frame 2, constant 3
28 00 00 00 03 40 02 C0 r0 = select register 0, constant 3, frame 2
2E 00 02 00 01 00 00 00 ret register 2, register 1, register 0

There are 47. “Operands” is how many of the three operand fields the instruction reads. “Cost” is the price used for the frame budget, in units of about 0.11 microseconds per pixel.

Code Name Operands Cost Result
0 mov 1 1 a
1 neg 1 1 -a
2 not 1 1 1 if a is 0, else 0
3 abs 1 1
4 floor 1 2
5 ceil 1 2
6 fract 1 2 a - floor(a)
7 sign 1 1 -1, 0 or 1
8 sqrt 1 8
9 sin 1 6
10 cos 1 6
11 tan 1 15
12 asin 1 40
13 acos 1 40
14 atan 1 40
15 exp 1 40
16 log 1 40
17 hash1 1 8 hash of one coordinate
18 add 2 1 a + b
19 sub 2 1 a - b
20 mul 2 1 a × b
21 div 2 3 a / b
22 floordiv 2 5 floor(a / b)
23 mod 2 5 a - floor(a / b) × b
24 pow 2 80 a to the power b
25 lt 2 1 1 if a < b, else 0
26 gt 2 1 1 if a > b, else 0
27 le 2 1 1 if a ≤ b, else 0
28 ge 2 1 1 if a ≥ b, else 0
29 eq 2 1 1 if a = b, else 0
30 ne 2 1 1 if a ≠ b, else 0
31 and 2 1 1 if both are non-zero
32 or 2 1 1 if either is non-zero
33 andnot 2 1 1 if a is non-zero and b is 0
34 min 2 1
35 max 2 1
36 step 2 1 0 if b < a, else 1
37 atan2 2 40 angle of the point (b, a)
38 hash2 2 8 hash of two coordinates
39 noise 2 37 smooth noise at (a, b)
40 select 3 1 b where a is non-zero, c elsewhere
41 clamp 3 1 a kept between b and c
42 mix 3 2 a + (b - a) × c
43 smoothstep 3 6 smooth ramp of c between a and b
44 fbm 2 37 layered noise at (a, b); cost is per octave
45 jmpnone 1 1 skip ahead if a is 0 for every pixel in use
46 ret 3 3 the pixel’s red, green, blue

True and false are stored as the floats 1 and 0, so a comparison’s result can be used as a mask by select and combined with and, or, andnot and not.

The costs of arithmetic, sin, cos and fbm were measured on the panel. The others are estimates, set on the high side.

Maximum
Tile registers 32
Uniforms 16
Uniform slots 64
Uniform name 15 characters
Constants 256
Frame registers 256
Body instructions 1,024
fbm octaves 8
Cost per pixel, per frame 1,000

The panel does not trust the bytes. gfx.shader reads the whole program once and refuses it unless all of this holds:

  • The magic is LSH1 and every count is within the limits above.
  • The size of the data matches the counts exactly: no missing bytes and none left over.
  • Every opcode is one of the 47.
  • Every destination is a register that exists.
  • Every operand points at a register, constant, uniform slot or frame register that exists.
  • Prologue instructions read only constants, uniforms and frame registers written before them.
  • Every jmpnone jumps forward, to a place inside the body.
  • The body ends with ret, and there is no other ret.
  • Every fbm has 1 to 8 octaves.

After that the VM runs the program without checking anything. A program that passes cannot read or write outside its own memory, and because jumps only go forward it cannot loop: it always finishes, in a time that is known from its length. A script that hands gfx.shader damaged or made-up bytes gets an error, nothing worse.

Next: The shader VM.