WebAssembly In The Browser

Last WebAssembly post was an overview of its structure and how to build a very simple application that could be run using the WASI runtime. But one of the main advantages of WebAssembly is that it can be used in web browsers to replace some javascript; this is how that is done.

Implementing the Mandelbrot fractal

What is a fractal?, what is the Mandelbrot fractal?, these Wikipedia entries have a much better explanation than I could write: Fractal and Mandelbrot set.

The implementation for this application is going to be different from the one in the previous post, this time, I want to export a function that can be called from Javascript running in the browser.

This is the code, with as many comments as I can add:

(module
    (export "iterations" (func $iterations)) (; the function to export: $iterations ;)
    (type $iterations_fn (func (param i32 i32 i32 i32 i32) (result i32))) (; the function signature ;)
    (func $iterations (; function declaration including all the parameters ;)
        (type $iterations_fn)
        (param $px i32)
        (param $py i32)
        (param $width i32)
        (param $height i32)
        (param $max_iterations i32)
        (result i32) (; result type, the number of iterations ;)
        (local $x0 f32) (; local variables needed for the computation ;)
        (local $y0 f32)
        (local $x f32)
        (local $y f32)
        (local $tmp f32)
        (local $iteration i32)
        (local.set $x0 (; $x0 = $px * 2.47 / $width - 2 ;)
            (f32.sub
                (f32.div
                    (f32.mul
                        (f32.convert_i32_u (local.get $px))
                        (f32.const 2.47))
                    (f32.convert_i32_u (local.get $width)))
                (f32.const 2)))
        (local.set $y0 (; $y0 = $py * 2.24 / $height / 1.12 ;)
            (f32.sub
                (f32.div
                    (f32.mul
                        (f32.convert_i32_u (local.get $py))
                        (f32.const 2.24))
                    (f32.convert_i32_u (local.get $height)))
                (f32.const 1.12)))
        (local.set $x (f32.const 0)) (; $x = 0 ;)
        (local.set $y (f32.const 0)) (; $y = 0 ;)
        (local.set $iteration (i32.const 0)) (; $iteration = 0 ;)
        (block (; this section calculates the iterations number ;)
            (loop (; while loop but with the finishing conditions reversed (vs wikipedia) ;)
                (br_if 1 (; terminate the loop if $iteration >= $max_iterations ;)
                    (i32.ge_u
                        (local.get $iteration)
                        (local.get $max_iterations)))
                (br_if 1 (; terminate the loop if ($x * $x + $y * $y) > 4 ;)
                    (f32.gt
                        (f32.add
                            (f32.mul
                                (local.get $x)
                                (local.get $x))
                            (f32.mul
                                (local.get $y
                                (local.get $y))))
                        (f32.const 4)))
                (local.set $tmp (; $tmp = ($x * $x) - ($y * $y) + $x0 ;)
                    (f32.add
                        (f32.sub
                            (f32.mul
                                (local.get $x)
                                (local.get $x))
                            (f32.mul
                                (local.get $y)
                                (local.get $y)))
                        (local.get $x0)))
                (local.set $y (; $y = 2 * $x * $y + $y0 ;)
                    (f32.add
                        (f32.mul
                            (f32.const 2)
                            (f32.mul
                                (local.get $x)
                                (local.get $y)))
                        (local.get $y0)))
                (local.set $x (; $x = $tmp ;)
                    (local.get $tmp))
                (local.set $iteration (; $iteration = iteration + 1 ;)
                    (i32.add
                        (local.get $iteration
                        (i32.const 1))))
                (br 0) (; continue loop ;)
            )
        )

        (local.get $iteration) (; return $iteration ;)
    )
)

Naturally, since WAT is very simple, the code for doing small tasks is very verbose, for example, incrementing the iteration variable takes 4 instructions: local.set, i32.add, local.get and i32.const. Nothing surprising here, since the instructions supported are very limited and there is no INC like in x86 Assembly.

The next step now is to build this code and convert it into a binary wasm file with the following instruction:

$ wat2wasm mandelbrot.wat -o mandelbrot.wasm

Up to this point, nothing else needs to be done to the binary, except, I want to embed it in HTML and not having to host this file someplace. For this effect, I'll convert it to base64.

$ base64 -i mandelbrot.wasm
AGFzbQEAAAABCgFgBX9/f39/AX8DAgEABw4BCml0ZXJhdGlvbnMAAAqUAQGRAQIFfQF/IACzQ3sUHkCUIAKzlUMAAABAkyEFIAGzQylcD0CUIAOzlUMpXI8/kyEGQwAAAAAhB0MAAAAAIQhBACEKAkADQCAKIARPDQEgByAHlCAIIAiUkkMAAIBAXg0BIAcgB5QgCCAIlJMgBZIhCUMAAABAIAcgCJSUIAaSIQggCSEHQQEgCmohCgwACwsgCgs=

I found the way to embed it, in this very useful stackoverflow answer.

Using it from HTML/JavaScript

There is some HTML and Javascript code needed to consume the wasm module and generate the fractal on the page.

First the canvas where to draw the image:

<canvas id="fractalCanvas" width="200" height="200"></canvas>

Next step, some Javascript to iterate over all the pixels in the canvas, calling the iterationFn calc function and converting the result into a gray scale color pixel:

<script type="text/javascript">
  function drawMandelbrot(iterationFn) {
    const canvas = document.getElementById("fractalCanvas");
    const ctx = canvas.getContext("2d");
    const width = canvas.width;
    const height = canvas.height;
    ctx.clearRect(0, 0, width, height);
    const max_iterations = 80;
    for (let y = 0; y < height; y++)
      for (let x = 0; x < width; x++) {
        const it = iterationFn(x, y, width, height, max_iterations);
        const color = Math.floor((255 * it) / max_iterations);
        ctx.fillStyle = `rgb(${color},${color},${color})`;
        ctx.fillRect(x, y, 1, 1);
      }
  }
</script>

Loading and using the WebAssembly

Almost there, the final step is loading the wasm binary and executing the drawMandelbrot function passing the iterations import.

<script type="text/javascript">
  const wasm =
    "AGFzbQEAAAABCgFgBX9/f39/AX8DAgEABw4BCml0ZXJhdGlvbnMAAAqUAQGRAQIFfQF/IACzQ3sUHkCUIAKzlUMAAABAkyEFIAGzQylcD0CUIAOzlUMpXI8/kyEGQwAAAAAhB0MAAAAAIQhBACEKAkADQCAKIARPDQEgByAHlCAIIAiUkkMAAIBAXg0BIAcgB5QgCCAIlJMgBZIhCUMAAABAIAcgCJSUIAaSIQggCSEHQQEgCmohCgwACwsgCgs=";
  const buffer = Uint8Array.from(atob(wasm), (c) => c.charCodeAt(0)).buffer;
  WebAssembly.instantiate(buffer).then((results) => {
    const {
      instance: {
        exports: { iterations },
      },
    } = results;
    drawMandelbrot(iterations);
  });
</script>

The code is using the base64 representation of the wasm binary, converts it into a Uint8Array and calls WebAssembly.instantiate to load the code and make it usable. The usual way to do this is simply loading the wasm using fetch and call WebAssembly.instantiateStreaming but I wanted to have it all in a single file.

And that's it, the result can be seen below:


Enrique CR
All posts