#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:
|
|
Cool! index.js
is just gonna be the WASM loader. Here’s the needed code:
|
|
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
|
|
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:
|
|
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.
|
|
I’m gonna need some basic functions to retrieve the information about the buffer
|
|
Additionally, I’ll need a way to set pixels within this buffer.
|
|
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:
|
|
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
andtv
- Make that point the new
rp
- Repeat these steps n-times per frame (more steps = better looking fractal)
- From
Let’s start with some necessary declarations.
|
|
A function to select a random point is needed as well:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
This is called from Javascript the following way:
|
|
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:
|
|
I’m gonna add this code to the rendering function so it is gonna be called on every frame.
|
|
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.