Contents

#2 WebAssembly and C++: Standalone programming - fun with graphics

This post is part of a WebAssembly series focused on WASM and C++. The goal is to gain a thorough understanding of how WebAssembly works, how to use it as a compilation target for C++ code and hopefully have fun along the way. So, stick with me for this exciting journey.

Wherever mentioned, working WASM examples will be embedded directly on the page. If your browser supports it, you should be able to see them running.

Recap

In part 1, I’ve described absolute basics of WASM, runtimes and how to compile your code to WASM bytecode using clang. So far, these basic concepts don’t really have much practical use but in this instalment of the series, I want to have some fun before I go any further.

Retro graphics - Web 2.0

In part 1, the discussed example code was compiled in standalone mode, meaning without standard libraries. This is of course, limiting and, most likely, insufficient for any complex solution, still though, you’d be surprised how much stuff can be done without any dependencies at all.

Turns out, it’s enough to literally go back to 1990s and have some fun with retro graphics. Modern web APIs provide the canvas - which is perfect for software rendering. C++ compiled to WASM is perfect for rendering to such canvas.

In a sense, this brings me back to the 90s when I was having fun with graphics programming in DOS and my all time favourite interrupt 0x10. Mode 0x13 was changing the video mode to 320x200 256 colours and then, with direct access to video memory, you could render whatever you wanted.

Project layout

Let’s get started! First, I need some canvas. Let’s create index.html with the following content:

1
2
3
4
5
6
7
8
<html>
    <head>
    </head>
    <body>
        <canvas id="myCanvas" width="640" height="480"></canvas>
        <script type="text/javascript" src="index.js"></script>
    </body>
</html>

Cool! index.js is just gonna be the WASM loader. Here’s the needed code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const importObject = {
  env : {
  }
};

var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');

WebAssembly.instantiateStreaming(fetch("sierp.wasm"), importObject)
    .then((obj) => {
      let wasm_buff = obj.instance.exports.memory.buffer;
      let wasm_exp = obj.instance.exports;

      let started_at = Date.now();

      let render_frame = () => {
        let prior = started_at;
        started_at = Date.now();
        let delta_ms = started_at - prior;

        // render contents
        // ...

        // request another frame
        window.requestAnimationFrame(render_frame);
      };

      requestAnimationFrame(render_frame);
    });

All right! I think the code is self-explanatory so, I’m not gonna focus too much on it. Once the WASM module is loaded, I’m defining a rendering function render_frame. This is initially called using requestAnimationFrame and after that, further invocations are requested from within render_frame itself using the same API. Additionally, the delay between current and previous frame is calculated so, it’s possible to determine how much time has elapsed in between the animation frames.

Let’s focus on C++ code now. Let’s create two empty cpp files and some simple script to actually build them:

touch sierp.cpp wasm.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#!/bin/bash

clang++ \
    -Os \
    -target wasm32 \
    --no-standard-libraries \
    -Wl,--export-all \
    -Wl,--no-entry \
    -Wl,--allow-undefined \
    -I. \
    canvas.cpp \
    sierp.cpp \
    -o sierp.wasm

This script reveals something interesting. Specifically, -Wl,--allow-undefined. This silences the linker in case there are undefined symbols in the resulting binary. Why? I’m gonna discuss that later on.

The project layout should look like so:

1
2
3
4
5
6
7
$ tree
.
├── build.sh
├── canvas.cpp
├── index.html
├── index.js
└── sierp.cpp

After invoking build.sh, we should have sierp.wasm generated. It’s possible now to check if all that “works” - just start Python’s SimpleHttpServer:

python -m http.server

and open the page in the browser. So far, it should result with a blank page with no errors or warnings in the JavaScript console.

Canvas “framework”

The plan

The plan is to declare a memory buffer in C++ which will act as a canvas. The C++ application will render to that buffer and with every frame this buffer will be copied to HTML canvas to present it to the screen. In a sense, it’s similar to double buffering rendering. The back buffer is in C++ and the screen is the HTML canvas. The rendering loop is driven by JavaScript but the actual rendering happens in C++/WASM.

I’m gonna start with the back buffer declaration. The dimensions have been chosen to match the HTML canvas. Ideally, JavaScript should get the dimensions from WASM and adjust the HTML canvas size accordingly but I decided to omit that detail and focus on the actual meat and potatoes here.

1
2
3
4
5
namespace canvas {
constexpr unsigned width = 640;
constexpr unsigned height = 480;
unsigned buffer[width * height] = {0x00};
} // namespace

I’m gonna need some basic functions to retrieve the information about the buffer

1
2
3
unsigned canvas_get_width() { return canvas::width; }
unsigned canvas_get_height() { return canvas::height; }
unsigned canvas_get_size() { return sizeof(canvas::buffer); }

Additionally, I’ll need a way to set pixels within this buffer.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
void canvas_set_pixel(unsigned x, unsigned y, unsigned color) {
  if (x < canvas::width && y < canvas::height) {
    canvas::buffer[x + y * canvas::width] = color;
  }
}

void canvas_fill(unsigned color) {
  for (unsigned i = 0; i < sizeof(canvas::buffer) / sizeof(canvas::buffer[0]);
       ++i) {
    canvas::buffer[i] = color;
  }
}

unsigned canvas_make_color(unsigned r, unsigned g, unsigned b) {
  return 0xff000000 | ((b & 0xff) << 16) | ((g & 0xff) << 8) |
         ((r & 0xff) << 0);
}

void canvas_clear() { canvas_fill(canvas_make_color(0, 0, 0)); }

The pixel format, in this case, is hard-coded to match what HTML canvas expects, which is ABGR.

This set of functions defines the “drawing SDK” that will make coding graphics a bit easier.

Sierpinski triangle

Sierpinski triangle is a very simple fractal to generate. In most basic form, it can even be generated using the logical AND operation. Believe it or not but it’s as simple as:

1
2
3
4
5
6
7
for (auto x = 0; x < screen_width; x++) {
    for (auto y = 0; y < screen_height; y++) {
        if (!(x & y)) {
            put_pixel(x, y);
        }
    }
}

This approach works but I like the “chaos game” algorithm better. This algorithm is described well on Wikipedia. The main advantage is that you can move the vertices of the triangle so, it’s possible to animate it in an arbitrary fashion.

Just to recap, generation of Sierpinski’s triangle using chaos game happens in the following steps:

  • Pick 3 points in 2D space defining the vertices of the triangle: vertices
  • Pick a random point in 2D space (any point): rp
  • In every frame
    • From vertices pick a random vertex: tv
    • Put pixel in coordinates between rp and tv
    • Make that point the new rp
    • Repeat these steps n-times per frame (more steps = better looking fractal)

Let’s start with some necessary declarations.

1
2
3
4
5
6
7
typedef struct Vertex {
  unsigned x;
  unsigned y;
} Vertex;

Vertex vertices[3];
Vertex rp;

A function to select a random point is needed as well:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
extern "C" {
extern float js_random();
}

Vertex get_random_point() {
  Vertex v{};
  v.x = js_random() * canvas_get_width();
  v.y = js_random() * canvas_get_height();
  return v;
}

This is the tricky part. As you remember, I’m working in standalone mode so, I can’t use random number generators from <random> nor <stdlib>. The beauty of WASM is that it’s possible to inject these functions from Javascript!

In the code above, I declared a function js_random but never defined it. This function can be injected into the WASM module using the import object, when instantiating the module:

1
2
3
4
5
6
7
8
9
<script type="text/javascript">
const importObject = {
  env : {
    js_random : Math.random,
  }
};

WebAssembly.instantiateStreaming(fetch("sierp.wasm"), importObject);
</script>

This is why we need -Wl,--allow-undefined. When building the module, this function is never defined - but will be patched in runtime. In this case, js_random is implemented using Math.random. I’m gonna focus on how that works in details in future posts, where we’ll eventually land up learning what WASI is and why it is needed! For now, all that we need to know is that missing functions can be declared extern in C++ and their implementation can be provided in Javascript.

Okay, I’ve got a random point. I need a function that’ll return a point that lies in between two points as well. Here it is:

1
2
3
4
5
6
Vertex lerp2d(const Vertex *p1, const Vertex *p2, float pos) {
  Vertex interpolated{};
  interpolated.x = (p2->x + p1->x) * pos;
  interpolated.y = (p2->y + p1->y) * pos;
  return interpolated;
}

This is pretty simple. Just a 2D interpolation. pos can have values in range: (0, 1>. The returned interpolated point in case of pos = 0 will effectively be equal to p1, pos = 1 will be equivalent to p2 and anything else will return a point lying in between the two, in respective proportion of the distance.

With these functions at hand, the algorithm generating the triangle, looks like so:

1
2
3
4
5
6
7
8
void sierpinski_generate(int n, float pos) {
  while (n--) {
    const int vert_idx = js_floor(js_random() * 3);
    const auto *v = &vertices[vert_idx % 3];
    rp = lerp2d(v, &rp, pos);
    canvas_set_pixel(rp.x, rp.y, canvas_make_color(0x11, 0xff, 0x11));
  }
}

You probably noticed that I’m using js_floor here as well - again something that I’m gonna patch in runtime.

The rendering loop in C++ will look like so:

1
2
3
4
5
6
7
8
9
unsigned *canvas_draw_frame(unsigned delta_ms) {
  canvas_clear();
  animation_draw_frame(delta_ms);
  return canvas::buffer;
}

void animation_draw_frame(unsigned delta_ms) {
  sierpinski_generate(10000, 0.5f);
}

This is called from Javascript the following way:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
const importObject = {
  env : {
    js_floor : Math.floor,
    js_random : Math.random,
    js_sinf : Math.sin,
    js_cosf : Math.cos,
  }
};

var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');

WebAssembly.instantiateStreaming(fetch("sierp.wasm"), importObject)
    .then((obj) => {
      let wasm_buff = obj.instance.exports.memory.buffer;
      let wasm_exp = obj.instance.exports;

      let canvas_width = wasm_exp.canvas_get_width();
      let canvas_size = wasm_exp.canvas_get_size();

      wasm_exp.canvas_initialise();

      let started_at = Date.now();

      let render_frame = () => {
        let prior = started_at;
        started_at = Date.now();
        let delta_ms = started_at - prior;

        // render loop
        let canvas_ptr = wasm_exp.canvas_draw_frame(delta_ms);
        let buf = new Uint8ClampedArray(wasm_buff, canvas_ptr, canvas_size);
        let img_data = new ImageData(buf, canvas_width);
        ctx.putImageData(img_data, 0, 0);

        // request another frame
        window.requestAnimationFrame(render_frame);
      };

      requestAnimationFrame(render_frame);
    });

To clarify, I’m creating buf which is an instance of Uint8ClampedArray. The underlying memory belongs to the WASM module (wasm_buff). canvas_ptr is basically an offset in wasm_buff. Having the base pointer (wasm_buff) and the offset (canvas_ptr), the instance of Uint8ClampedArray represents the WASM pixel buffer (buffer in C++).

Having the buffer, I’m declaring an instance of ImageData. ImageData can be directly put to the canvas using the 2D context. This approach follows the guidelines as described in the Web API documentation.

Lissajous curves

As I already mentioned, the advantage of the “chaos game” algorithm is that the vertices of the triangle can be moved freely in the screen space. It’s possible to animate the fractal. Smooth movements can be implemented using the Lissajous curves.

To make that happen I’m gonna implement one more function:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
float time = 0;

// Update triangle vertices
void update_vertices_position(float t) {
  const float horiz_ampl = 300;
  const float vert_ampl = 180;
  const float horiz_freq = 3;
  const float vert_freq = 2;
  const float vert_delta = 3.14f / 5;

  for (auto &v : vertices) {
    float x = horiz_ampl * js_sinf(horiz_freq * t) + canvas_get_width() / 2;
    float y = vert_ampl * js_cosf(vert_freq * t) + canvas_get_height() / 2;
    v.x = static_cast<unsigned>(x);
    v.y = static_cast<unsigned>(y);
    t += vert_delta;
  }
}

I’m gonna add this code to the rendering function so it is gonna be called on every frame.

1
2
3
4
5
6
void a
nimation_draw_frame(unsigned delta_ms) {
  time += (float)delta_ms / 2000;
  update_vertices_position(time);
  sierpinski_generate(10000, 0.5f);
}

This uses sin and cos functions which normally would be available through <cmath> but I’m gonna patch them in via the import object similarly as with js_random.

Having all of that in place, it’s possible to run the resulting WASM code in the browser.

Compiled module is only slightly over 1kB which is quite impressive and probably smaller than the native code.

Code

Complete code can be found on gitlab.

Demo

The discussed WASM module is included below. If your browser supports WASM, you should see it running.