Tetra is a custom bytecode language designed for numerical and graphical computation. Itโs versatile enough for general purpose logic, but also for expressing GLSL-style shader behavior.
Tetra code is executed by a lightweight, cross-platform virtual machine runtime written in C#. Programs are made up of simple, low-level instructions using named variables, vector math, and clean control flow.
TetraShade is a cross-platform UI for visualizing Tetra bytecode that simulates a fragment shader pipeline. Each pixel is computed by running the loaded Tetra code with a fragCoord
input, allowing real-time visual output from shader-style programs.
Credit: "Shield" visual effect by Xor.
- Scoped Variable Stack: Each block or function has its own variable frame. Stack frames are explicitly
pushed/popped using
push_frame
/pop_frame
. - Named Variables: No general-purpose registers. All values are stored and looked up by name.
- Conditional and Unconditional Jumps: Support for jump labels like
mylabel:
and instructions likejmp
,jmp_ne
,jmp_ge
, etc. - Multi-element Float Vector Support: Support for
vec4
operations alongside scalar floats. - Basic Arithmetic: Instructions like
add
,sub
,inc
,dec
. - Debugging Aids: A
print
instruction that outputs variable values along with the line number of the source instruction. - Globals Support: A global frame is automatically pushed on startup. Any code variables defined at the root level are treated as global initialization.
Demonstrates use of arithmetic, loops, and alternating signs to compute an approximation of ฯ using the Leibniz formula:
ld $sum, 0
ld $sign, 1
ld $i, 0
ld $limit, 800
loop:
jmp_ge $i, $limit, done
ld $denominator, $i
mul $denominator, 2
add $denominator, 1
ld $term, 1.0
div $term, $denominator
mul $term, $sign
add $sum, $term
neg $sign
inc $i
jmp loop
done:
mul $sum, 4
print $sum
halt
sum = 3.1403f
Recursively computes and prints the first 10 Fibonacci numbers using function calls and control flow:
ld $i, 0
ld $count, 10
loop:
jmp_ge $i, $count, done
ld $arg0, $i
call fib
print $retval
inc $i
jmp loop
done:
halt
fib:
ld $n, $arg0
jmp_le $n, 1, base_case
ld $arg0, $n
dec $arg0
call fib
ld $a, $retval
ld $arg0, $n
dec $arg0
dec $arg0
call fib
ld $b, $retval
add $a, $b
ret $a
base_case:
ret $n
retval = 0
retval = 1
retval = 1
retval = 2
retval = 3
retval = 5
retval = 8
retval = 13
retval = 21
retval = 34
Tetra supports first-class vector values. You can declare vector literals using ld
with multiple float constants or float variables:
ld $v, 1.1, -2.0, 3.0, 4.2
ld $v, $a, $b
The result is a float vector. Arithmetic operations like add
, mul
, and sub
apply per element when two vectors are used:
ld $a, 1.0, 2.0, 3.0
ld $b, 0.5, 0.5, 0.5
mul $a, $b # $a becomes [0.5, 1.0, 1.5]
Unary operations like neg
apply to each element:
neg $a # $a becomes [-0.5, -1.0, -1.5]
When combining a scalar with a vector, the scalar is broadcast across the vectorโs components:
ld $a, 8.0
ld $b, 6.0, 5.0
min $a, $b # $a becomes [min(8.0, 6.0), min(8.0, 5.0)]
Individual vector components can be accessed using index notation:
ld $v, 1.1, 2.2, 3.3
ld $a, $v[1] # $a = 2.2
You can also construct new vectors from specific elements of an existing vector (swizzling):
ld $v, 1.1, 2.2, 3.3
ld $a, $v[1], $v[0] # $a = [2.2, 1.1]
Vector elements can be assigned to directly:
ld $v, 1.1, 2.2, 3.3
ld $v[1], 3.141 # $v = [1.1, 3.141, 3.3]
Any instruction that operates on scalars (like inc
, mul
, jmp_eq
, etc.) can also target a specific vector component:
inc $v[2]
mul $a, $v[1]
jmp_eq $a, $v[2], label
Accessing vector elements from non-vector variables will throw a runtime error.
Instruction | Description |
---|---|
add $a, $b |
$a = $a + $b |
sub $a, $b |
$a = $a - $b |
mul $a, $b |
$a = $a * $b |
div $a, $b |
$a = $a / $b |
inc $a |
$a = $a + 1 |
dec $a |
$a = $a - 1 |
neg $a |
$a = -$a |
mod $a, $b |
$a = $a % $b (modulo operation) |
Instruction | Description |
---|---|
exp $a, $b |
$a = exp($b) (e to the power of $b ) |
log $a, $b |
$a = log($b) (natural logarithm of $b ) |
pow $a, $b |
$a = pow($a, $b) |
Instruction | Description |
---|---|
abs $a, $b |
$a = abs($b) |
sign $a, $b |
$a = sign($b) (-1, 0, or 1 depending on sign of $b ) |
min $a, $b |
$a = min($a, $b) |
max $a, $b |
$a = max($a, $b) |
Instruction | Description |
---|---|
clamp $a, $min, $max |
$a = clamp($a, $min, $max) |
smoothstep $a, $edge0, $edge1 |
$a = smoothstep($edge0, $edge1, $a) |
Instruction | Description |
---|---|
sin $a, $b |
$a = sin($b) |
cos $a, $b |
$a = cos($b) |
tan $a, $b |
$a = tan($b) |
asin $a, $b |
$a = asin($b) |
acos $a, $b |
$a = acos($b) |
atan $a, $b |
$a = atan($b) |
sinh $a, $b |
$a = sinh($b) |
cosh $a, $b |
$a = cosh($b) |
tanh $a, $b |
$a = tanh($b) |
Instruction | Description |
---|---|
length $a, $b |
$a = length($b) (magnitude of vector $b ) |
normalize $a, $b |
$a = normalize($b) (unit vector in the direction of $b ) |
dot $a, $b, $c |
$a = dot($b, $c) (dot product of $b and $c ) |
cross $a, $b, $c |
$a = cross($b, $c) (cross product of $b and $c ) |
reflect $a, $I, $N |
$a = reflect($I, $N) (reflection of vector $I off surface normal $N ) |
refract $a, $I, $N, $eta |
$a = refract($I, $N, $eta) (refraction vector) |
Instruction | Description |
---|---|
jmp label |
Unconditional jump |
jmp_eq $a, $b, label |
Jump if $a == $b |
jmp_ne $a, $b, label |
Jump if $a != $b |
jmp_lt $a, $b, label |
Jump if $a < $b |
jmp_le $a, $b, label |
Jump if $a <= $b |
jmp_gt $a, $b, label |
Jump if $a > $b |
jmp_ge $a, $b, label |
Jump if $a >= $b |
Instruction | Description |
---|---|
ld $a, 1.0 |
Load constant into $a |
ld $b, $a |
Copy variable $a into $b |
push_frame |
Push a new scope frame manually (used for block scoping) |
pop_frame |
Pop the current scope frame |
Instruction | Description |
---|---|
call label |
Call function at label (creates a new scope frame and pushes return address) |
ret |
Return from function (restores return address and previous scope) |
ret $a |
Return a value; sets $retval in the caller's scope |
Instruction | Description |
---|---|
print $a |
Print the value of $a with line number |
nop |
๐ค Does nothing. Useful for padding or jump targets. |
halt |
Stop execution |
Tetra supports calling functions using the call
instruction, with optional return values via ret $value
.
To pass arguments to a function, use ld
to define named variables like $arg0
, $arg1
, etc., before calling the function:
ld $arg0, 10
ld $arg1, 20
call my_function
When call
is executed:
- A new scope frame is pushed onto the call stack.
- The function code begins execution at the specified label.
- All variables, including
$arg0
,$arg1
, etc., remain shared with the caller because they were defined outside the frame. As such, these arguments behave likeout
parameters.
If the function modifies $arg0
, the change is visible to the caller:
add $arg0, 5 # caller sees modified value
If you want function parameters to behave like 'pass by value' (copied and isolated), you must explicitly copy them into local variables at the start of the function:
ld $a, $arg0
ld $b, $arg1
This way, modifications to $a
and $b
do not affect the caller's $arg0
/$arg1
.
To return a value, use ret $value
. This sets $retval
in the callerโs frame:
ret $result
In the caller:
call my_function
ld $x, $retval
If no value is returned, simply use ret
.
ld $arg0, 5
call double
ld $x, $retval
print $x
halt
double:
ld $a, $arg0
add $a, $a
ret $a
Output: x = 10
- GLSL Frontend: Load and run
.tetra
shader files that implement amain(vec2 fragCoord) -> vec4
function. This will allow real-time rendering using Tetra bytecode in a canvas (e.g., 320x240). - Uniforms Support: Provide built-in global vector (
uniforms
) containing runtime values liketime
,resolution
, etc. - Shadertoy Compatibility: Goal is to support a rewritten Tetra version of common Shadertoy examples, including
iTime
,fragCoord
, and trigonometric color effects. - GLSL-to-Tetra Compiler: Load real GLSL Shadertoy-style code and compile it into Tetra bytecode.
- Optimization Pass: Implement a basic optimizer to remove redundant instructions and streamline generated bytecode.
- Per-Pixel Optimization: Analyze Tetra bytecode at runtime to determine the first instruction that depends on per-pixel inputs (like
fragCoord
). Run the code before this point once per frame, and parallelize only the remaining per-pixel portion. This should significantly reduce runtime overhead. - Extended GLSL Compatibility: Continue expanding built-in instructions to support more GLSL-style functions.