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.
Layout
Section titled “Layout”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 |
Header
Section titled “Header”| 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 |
Uniform table
Section titled “Uniform table”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.
Instructions
Section titled “Instructions”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:
fbmkeeps its number of octaves in operand C, as a plain number.jmpnonekeeps the index of the body instruction to continue at in operand B.rethas no destination. Its three operands are red, green and blue.
In the prologue, instruction number n writes frame register n.
A program, byte by byte
Section titled “A program, byte by byte”This is the shader from the previous page, 102 bytes:
4C 53 48 31 "LSH1"03 3 tile registers01 1 uniform04 00 4 constants03 00 3 frame registers05 00 5 body instructions01 1 uniform slot00 00 00
01 04 74 69 6D 65 uniform: 1 component, 4 letters, "time"
00 00 00 3F constant 0 = 0.500 00 F8 41 constant 1 = 3100 00 7C 42 constant 2 = 6300 00 00 00 constant 3 = 0
09 00 00 80 00 00 00 00 p0 = sin uniform slot 014 01 00 40 00 C0 00 00 p1 = mul constant 0, frame 012 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 115 01 01 00 02 40 00 00 r1 = div register 1, constant 228 02 00 00 02 C0 03 40 r2 = select register 0, frame 2, constant 328 00 00 00 03 40 02 C0 r0 = select register 0, constant 3, frame 22E 00 02 00 01 00 00 00 ret register 2, register 1, register 0The opcodes
Section titled “The opcodes”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.
Limits
Section titled “Limits”| 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 |
What the panel checks before running it
Section titled “What the panel checks before running it”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
LSH1and 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
jmpnonejumps forward, to a place inside the body. - The body ends with
ret, and there is no otherret. - Every
fbmhas 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.