rpu 0.2.1

RPU is a GLSL like programming language which compiles to WebAssembly (WAT). Fast and embeddable.
Documentation

RPU is a GLSL / C like programming language which compiles to WebAssembly (WAT) and is currently under development.

You can use it as a general purpose programming language, as a shader language for 2D and 3D renderering and as a (very) fast embedded scripting language for Rust based applications.

RPU compiles to WAT code and uses wasmer as a runtime. The GLSL features like vecs, swizzles and math functions get compiled on-demand. They do not introduce any overhead or speed / size penalties if not used.

You can choose between 32 and 64 bit precision during script compile time.

All vector based operations (length, dot, cross etc) are implemented in pure WebAssembly. Trigonometric functions are implemented in Rust and are called via the wasmer runtime.

When working on shaders, RPU uses multiple threads to render the image. This is done by splitting the image into tiles and rendering each tile in parallel.

Currently implemented

  • Basic types: int, ivec2, ivec3, ivec4, float, vec2, vec3, vec4
  • Math operators: +, -, *, /
  • Math functions: dot, cross, mix, smoothstep, length, normalize, sin, cos, sqrt, ceil, floor, fract, abs, tan, degrees, radians, min, max, pow.
  • Control structures: if, else, while, break, return, export
  • Swizzles: vec2.xy, vec3.xyz, vec4.xyzw etc

Why RPU?

Sometimes you want to use high precision offline rendering without all the hazzles of the GPU and getting your shaders to compile.

For example I prefer to render SDFs on the CPU, especially when I want to use a lot of them or if the amount of SDFs to render are not known at compile time (user based input).

Using RPU you can use the same code for both CPU and GPU rendering.

Usage

To execute a function:

let fib = r#"
int fib(int n) {
  if (n <= 1) return n;
  return fib(n - 2) + fib(n - 1);
}

export int main(int x) {
    return fib(x);
}"#;

let rpu = RPU::new();
let use_64_bit = true;
if let Ok(rc) = rpu.compile_and_run(source, "main", [WasmValue::I64(10)], use_64_bit) {
    assert_eq!(
        rc, Ok(vec![WasmValue::I64(55)])
    );
}

If you only want to compile to WAT you can call:

let rc = compile_to_wat(source);

It returns a String containing the WAT source code.

To run the WAT source as a shader use

let mut buffer = ColorBuffer::new(800, 600);
let rc = rpu.compile_wat_and_run_as_shader(&wat, "shader", &mut buffer, use_64_bit);

The color buffer will contain the shader output. This runs the shader in a single thread. To run the shader in parallel use:

let mut buffer = Arc::new(Mutex::new(ColorBuffer::new(800, 600)));
let rc = rpu.compile_wat_and_run_as_tiled_shader(&wat, "shader", &mut buffer, (80, 80), use_64_bit);

Where (80, 80) is the tile size. The buffer is wrapped in an Arc<Mutex<>> to allow multiple threads to write to it.

Examples

Fibonacci example like in a general purpose language:

int fib(int n) {
  if (n <= 1) return n;
  return fib(n - 2) + fib(n - 1);
}

export int main(int x) {
  return fib(x);
}

You can see the generated WAT file here.

A sequence of 42 executes in about a second on my M3.


A simple shader example using raymarching:

// Based on https://www.shadertoy.com/view/WtGXDD

float sdBox(vec3 p, vec3 s) {
    p = abs(p)-s;
	return length(max(p, 0.))+min(max(p.x, max(p.y, p.z)), 0.);
}

float GetDist(vec3 p) {
    float d = sdBox(p, vec3(.5));
    return d;
}

vec3 GetRayDir(vec2 uv, vec3 p, vec3 l, float z) {
    vec3 f = normalize(l-p);
    vec3 r = normalize(cross(vec3(0,1,0), f));
    vec3 u = cross(f,r);
    vec3 c = f*z;
    vec3 i = c + uv.x*r + uv.y*u;
    return normalize(i);
}

vec3 GetNormal(vec3 p) {
    vec2 e = vec2(0.001, 0.);
    vec3 n = GetDist(p) - vec3(GetDist(p-e.xyy), GetDist(p-e.yxy), GetDist(p-e.yyx));
    return normalize(n);
}

export vec4 shader(vec2 coord, vec2 resolution) {
    vec2 uv = (2.0 * coord - resolution.xy) / resolution.y;

    vec3 ro = vec3(.7, .8, -1.);
    vec3 rd = GetRayDir(uv, ro, vec3(0), 1.);

    float t = 0.;
    float max_t = 10.;

    vec4 col = vec4(uv.x, uv.y, 0., 1.);

    while (t < max_t) {
        vec3 p = ro + rd * t;
        float d = GetDist(p);
        if (abs(d) < 0.001) {
            vec3 n = GetNormal(p);
            float dif = dot(n, normalize(vec3(1, 2, 3))) * 0.5 + 0.5;
            col.xyz = pow(vec3(dif), .4545);

            break;
        }
        t = t + d;
    }

    return col;
}

By executing the shader it generates the following image: Raymarch

This runs in about 150ms in 800x600 in 64-bit on my machine.