Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Overview

Note: We assume some familiarity with GLSL and ML terminology

A GLML program is a single pure function from a vec2 pixel coordinate to a vec4 color:

let main (coord : vec2) = [0.6, 0.2, 0.8, 1.0]

The host pipes in uniforms with #extern, builtins are prefixed with #, and the rest of the language looks like a small ML, with the classic let, let rec, match, fun, records, variants, and Hindley–Milner inference. GLML supports first class functions/closures and recursion. The compiler lowers it to a fragment shader that runs anywhere WebGL2 or GLES 3.0 runs, but is primarily designed for quick Shadertoy-like development.

Example: Mandelbrot Shader

Here is the classic Mandelbrot shader, which can be written as a terse and typesafe recursive function without any annotations:

#extern vec2 u_resolution
#extern float u_time

type option['a] = Some of 'a | None

let mandelbrot c =
  let rec mandel z i =
    if i > 150 then None
    else if #length z > 4 then
      let nu = #log2 (#log2 (#length z)) in
      Some ((i - nu) / 150)
    else
      let zx = z.0 * z.0 - z.1 * z.1 in
      let zy = 2 * z.0 * z.1 in
      mandel ([zx, zy] + c) (i + 1)
  in
  mandel [0, 0] 0

let main (coord : vec2) =
  let uv =
    let top = 2 * coord - u_resolution in
    let bot = #min u_resolution.0 u_resolution.1 in
    top / bot
  in
  let zoom = #exp (#sin (u_time * 0.4) * 4.5 + 3.5) in
  let seahorse_valley = [-0.7453, 0.1127] + uv / zoom in
  let col =
    match mandelbrot seahorse_valley with
    | None   -> [0, 0, 0]
    | Some n -> #sin (n * [10, 20, 30] + u_time) * 0.5 + 0.5
  in
  [col.0, col.1, col.2, 1.0]

Compiled GLSL
#version 300 es
precision highp float;
out vec4 fragColor;
struct option {
    int tag;
    float Some_0;
};
option mandel_0(vec2 c, vec2 z, int i) {
    int _iter = 0;
    while (true) {
        bool _lim_cond = (_iter < 1000);
        if (_lim_cond) {
            bool anf = (i > 150);
            if (anf) {
                return option(1, 0.);
            } else {
                float anf_0 = length(z);
                bool anf_1 = (anf_0 > 4.);
                if (anf_1) {
                    float anf_2 = length(z);
                    float anf_3 = log2(anf_2);
                    float nu = log2(anf_3);
                    float anf_4 = float(i);
                    float anf_5 = (anf_4 - nu);
                    float anf_6 = (anf_5 / 150.);
                    return option(0, anf_6);
                } else {
                    float anf_7 = z[0];
                    float anf_8 = z[0];
                    float anf_9 = (anf_7 * anf_8);
                    float anf_10 = z[1];
                    float anf_11 = z[1];
                    float anf_12 = (anf_10 * anf_11);
                    float zx = (anf_9 - anf_12);
                    float anf_13 = z[0];
                    float anf_14 = (2. * anf_13);
                    float anf_15 = z[1];
                    float zy = (anf_14 * anf_15);
                    vec2 anf_16 = vec2(zx, zy);
                    vec2 anf_17 = (anf_16 + c);
                    int anf_18 = (i + 1);
                    int _iter_inc = (_iter + 1);
                    _iter = _iter_inc;
                    z = anf_17;
                    i = anf_18;
                    continue;
                }
            }
        } else {
            option _zero = option(0, 0.);
            return _zero;
        }
    }
}
uniform vec2 u_resolution;
uniform float u_time;
void main() {
    vec2 coord = gl_FragCoord.xy;
    vec2 anf_20 = (2. * coord);
    vec2 top = (anf_20 - u_resolution);
    float anf_21 = u_resolution[0];
    float anf_22 = u_resolution[1];
    float bot = min(anf_21, anf_22);
    vec2 uv = (top / bot);
    float anf_23 = (u_time * 0.4);
    float anf_24 = sin(anf_23);
    float anf_25 = (anf_24 * 4.5);
    float anf_26 = (anf_25 + 3.5);
    float zoom = exp(anf_26);
    vec2 anf_27 = vec2(-0.7453, 0.1127);
    vec2 anf_28 = (uv / zoom);
    vec2 seahorse_valley = (anf_27 + anf_28);
    vec2 anf_19_0 = vec2(0., 0.);
    option _lv_scrut = mandel_0(seahorse_valley, anf_19_0, 0);
    int _lv_tag = _lv_scrut.tag;
    vec3 col;
    switch (_lv_tag) {
        case 1: {
            col = vec3(0., 0., 0.);
            break;
        }
        default: {
            float _lv_Some_0 = _lv_scrut.Some_0;
            vec3 anf_29 = vec3(10., 20., 30.);
            vec3 anf_30 = (_lv_Some_0 * anf_29);
            vec3 anf_31 = (anf_30 + u_time);
            vec3 anf_32 = sin(anf_31);
            vec3 anf_33 = (anf_32 * 0.5);
            col = (anf_33 + 0.5);
            break;
        }
    }
    float anf_34 = col[0];
    float anf_35 = col[1];
    float anf_36 = col[2];
    fragColor = vec4(anf_34, anf_35, anf_36, 1.);
}

Example: GLSL vs GLML

The following is the same scene in both GLSL and GLML, where we define a SDF of a circle and rectangle.

// kind 0 = circle (a = radius), kind 1 = rect (a, b)
struct Shape { int kind; float a; float b; };

float sdShape(Shape s, vec2 p) {
    if (s.kind == 0) return length(p) - s.a;
    if (s.kind == 1) {
        vec2 d = abs(p) - vec2(s.a, s.b);
        return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0);
    }
    return 1.0;
}

float scene(vec2 p) {
    Shape c; c.kind = 0; c.a = 0.3;
    Shape r; r.kind = 1; r.a = 0.7; r.b = 0.1;
    return min(sdShape(c, p), sdShape(r, p));
}
type shape = Circle of float | Rect of float * float

let sdf_shape (s : shape) = fun p ->
  match s with
  | Circle r    -> #length p - r
  | Rect (w, h) ->
    let d = #abs p - [w, h] in
    #length (#max d [0, 0]) + #min (#max d.0 d.1) 0

let union f g = fun p -> #min (f p) (g p)

let scene = union (sdf_shape (Circle 0.3)) (sdf_shape (Rect (0.7, 0.1)))

In the GLSL version, the compiler can’t check you handled every kind of shape. In the GLML version, shape is a typed variant. match is exhaustive, each constructor carries its own typed payload, and union is a function that returns a function.

Unlike most ML languages that don’t provide any form of ad-hoc polymorphism, GLML provides a limited set of constraints (broadcasting over vectors and scalars, promotion of integers to floats) that enables classic HM inference without the burden of having explicit operators over various shapes.

Example: Optimization

Each ML feature has a corresponding lowering pass that erases it before GLSL is emitted

  • Variants become discriminated structs of plain scalars
  • Records become bundles of locals
  • Closures are defunctionalized into tagged dispatch
  • Polymorphic functions are monomorphized to one specialized copy per concrete use
  • Recursion is rewritten to a while loop

By the time the program reaches the GPU, it is straight GLSL with no allocation, no function pointers, and no runtime tag dispatch beyond what the algorithm actually needs.

Below is GLML code rendering a simple union of two SDFs (open the optimized GLSL that is generated, the code before vec3 base; is all that is needed to represent the SDF!)

#extern vec2 u_resolution
#extern vec2 u_mouse

type sdf = vec2 -> float

type shape =
  | Circle of float
  | Rect of float * float

let sdf_shape (s : shape) : sdf =
  fun p ->
    match s with
    | Circle r -> #length p - r
    | Rect (w, h) ->
      let d = #abs p - [w, h] in
      #length (#max d [0, 0]) + #min (#max d.0 d.1) 0

let union (f : sdf) (g : sdf) : sdf = fun p -> #min (f p) (g p)

let scene = union (sdf_shape (Circle 0.3)) (sdf_shape (Rect (0.7, 0.1)))

let uv coord =
  (2 * coord - u_resolution) / #min u_resolution.0 u_resolution.1

let main (coord : vec2) =
  let p = uv coord in
  let m = uv u_mouse in
  let d = scene p in

  let base  = if d > 0 then [0.9, 0.6, 0.3, 1] else [0.65, 0.85, 1.0, 1] in
  let shade = (1 - #exp (-6 * #abs d)) * (0.8 + 0.2 * #cos (150 * d)) in
  let edge  = 1 - #smoothstep 0 0.01 (#abs d) in
  let dm    = #length (p - m) in
  let ds    = #abs (dm - #abs (scene m)) in
  let mouse = 1 - #smoothstep 0 0.005 (#min (ds - 0.0025) (dm - 0.015)) in

  let col = base * shade in
  let col = #mix col [1, 1, 1, 1] edge in
  let col = #mix col [1, 1, 0, 1] mouse in
  [col.0, col.1, col.2, 1.0]

Compiled GLSL
#version 300 es
precision highp float;
out vec4 fragColor;
uniform vec2 u_mouse;
uniform vec2 u_resolution;
vec2 uv_m(vec2 coord) {
    vec2 anf_20 = (2. * coord);
    vec2 anf_21 = (anf_20 - u_resolution);
    float anf_22 = u_resolution[0];
    float anf_23 = u_resolution[1];
    float anf_24 = min(anf_22, anf_23);
    return (anf_21 / anf_24);
}
void main() {
    vec2 coord_0 = gl_FragCoord.xy;
    vec2 p_1 = uv_m(coord_0);
    vec2 m = uv_m(u_mouse);
    float anf_2_6 = length(p_1);
    float anf_18_2 = (anf_2_6 - 0.3);
    vec2 anf_3_5 = abs(p_1);
    vec2 anf_4_5 = vec2(0.7, 0.1);
    vec2 d_6 = (anf_3_5 - anf_4_5);
    vec2 anf_5_5 = vec2(0., 0.);
    vec2 anf_6_5 = max(d_6, anf_5_5);
    float anf_7_5 = length(anf_6_5);
    float anf_8_5 = d_6[0];
    float anf_9_5 = d_6[1];
    float anf_10_5 = max(anf_8_5, anf_9_5);
    float anf_11_5 = min(anf_10_5, 0.);
    float anf_19_2 = (anf_7_5 + anf_11_5);
    float d_0 = min(anf_18_2, anf_19_2);
    bool anf_25 = (d_0 > 0.);
    vec4 base;
    if (anf_25) {
        base = vec4(0.9, 0.6, 0.3, 1.);
    } else {
        base = vec4(0.65, 0.85, 1., 1.);
    }
    float anf_26 = abs(d_0);
    float anf_27 = (-6. * anf_26);
    float anf_28 = exp(anf_27);
    float anf_29 = (1. - anf_28);
    float anf_30 = (150. * d_0);
    float anf_31 = cos(anf_30);
    float anf_32 = (0.2 * anf_31);
    float anf_33 = (0.8 + anf_32);
    float shade = (anf_29 * anf_33);
    float anf_34 = abs(d_0);
    float anf_35 = smoothstep(0., 0.01, anf_34);
    float edge = (1. - anf_35);
    vec2 anf_36 = (p_1 - m);
    float dm = length(anf_36);
    float anf_2_2 = length(m);
    float anf_18_1 = (anf_2_2 - 0.3);
    vec2 anf_3_1 = abs(m);
    vec2 anf_4_1 = vec2(0.7, 0.1);
    vec2 d_2 = (anf_3_1 - anf_4_1);
    vec2 anf_5_1 = vec2(0., 0.);
    vec2 anf_6_1 = max(d_2, anf_5_1);
    float anf_7_1 = length(anf_6_1);
    float anf_8_1 = d_2[0];
    float anf_9_1 = d_2[1];
    float anf_10_1 = max(anf_8_1, anf_9_1);
    float anf_11_1 = min(anf_10_1, 0.);
    float anf_19_1 = (anf_7_1 + anf_11_1);
    float anf_37 = min(anf_18_1, anf_19_1);
    float anf_38 = abs(anf_37);
    float anf_39 = (dm - anf_38);
    float ds = abs(anf_39);
    float anf_40 = (ds - 0.0025);
    float anf_41 = (dm - 0.015);
    float anf_42 = min(anf_40, anf_41);
    float anf_43 = smoothstep(0., 0.005, anf_42);
    float mouse = (1. - anf_43);
    vec4 col = (base * shade);
    vec4 anf_44 = vec4(1., 1., 1., 1.);
    vec4 col_0 = mix(col, anf_44, edge);
    vec4 anf_45 = vec4(1., 1., 0., 1.);
    vec4 col_1 = mix(col_0, anf_45, mouse);
    float anf_46 = col_1[0];
    float anf_47 = col_1[1];
    float anf_48 = col_1[2];
    fragColor = vec4(anf_46, anf_47, anf_48, 1.);
}

Overall, we get the elegance of a functional language that is performant enough for shaders!