Porting a C++ Rendering Engine to WebAssembly

Benjamin Deleze
Cyberbotics
Published in
10 min readJul 26, 2021
Screenshot of a Webots simulation running with a WebAssembly compiled Rendering Engine.

I recently compiled the whole rendering engine of Webots, more than 15’000 lines of code, from C++ to WebAssembly. In this article, I want to share challenges, solutions to problems I encountered, and the final results. The improvements are amazing!

Let’s start with some context. Webots is an open-source robot simulator. To fit its need, Webots has its own rendering engine: Wren (Webots Rendering Engine). Wren is written in C++ and is relying on OpenGL 3.3. However, the public API of Wren is in C, we will see why later, but it is important.

Additionally, Webots offers the possibility to record animations of simulations (think of a movie but where you are able to navigate the scene during the playback) or to stream simulations. You can then see the resulting animations or streams in a browser. Before, we were using Three.js as a rendering engine to display Webots simulations on the web. Three.js works well but it has some fundamental differences with Wren and so it was really difficult to obtain the same graphical quality both in Webots desktop and on the web.

After a thorough analysis, we decided to invest time and port Wren to WebAssembly.

To compile the C++ code to WebAssembly, I used Emscripten. You will be able to find all the code produced during this project in this pull request from the Webots GitHub repository.

In the pictures below you can see the graphical evolution allowed by Wren compiled in WebAssembly.

Left: Desktop version, middle: web with Three.js, right: web with Wren in WebAssembly

Roadmap

I can easily divide this project into two main parts:

  1. Prepare the code to be able to compile it with Emscripten.
  2. Adapt the code to solve the problems introduced by the compilation to WebAssembly.

I will then write a bit about the main problems I encountered and give you some general advice.

Step one: preparation of the code

To be able to compile the code with WebAssembly, I first needed to prepare it. I had three main tasks to do:

  • Reduce the number of dependencies if possible.
  • First adaptions of the code.
  • Prepare the functions and/or enumerations for exportation.

Dependencies

Dependencies can quickly become a nightmare when you want to export your code with Emscripten. Luckily for me, Wren has only three dependencies: OpenGL, glad, and glm.

Emscripten has built-in support for OpenGL, but, logically, only for the WebGL2 compatible subset of OpenGL.

glad is an OpenGL loading library and is not needed because Emscripten handles this part.

glm is a header-only math library. We could add it to the build with the -I option.

Due to the simplicity, for glm, and built-in support, for OpenGL, there was close to nothing to do with regard to the dependencies. However, I still wanted to mention it, because I think it can become a major challenge if you have big complicated dependencies, like a physics engine for example.

Change headers and exclude problematic functions

I had to update the headers to support Emscripten. Most of the changes I made in this step were to switch from glad headers to pure OpenGL headers

Then, I add to exclude some functions of the rendering engine that would not have been compatible with the web. I did not know in advance exactly what would not be compilable with Emscripten so I proceeded by trial and error.

But in the end, they were mainly two types of functions that I had to exclude:

  • Some OpenGL functions that cannot be compiled with Emscripten towards WebGL2. Concerning, this category, I just put them between `#ifdef` and postpone the problem to step two.
  • Some functions that needed to read or write to the disk, to load fonts for example. As the code is compiled to be executed on the web, Emscripten forbids access to the disk. If you really want to read a file from the disk, you can preload it at linking time in a virtual file system provided by Emscripten. I used this virtual file system to preload all the shaders.

Prepare the functions/enumerations for exportation

At this point, I had to set some correct flags (mainly concerning OpenGL) in the Makefile and I could already compile the rendering engine. Amazing no? yes but no.

Problem was, I could not access any of the functions of Wren from JavaScript when it was precisely what I wanted to do. The reason for this setback is that Emscripten compiles the files you give it as if it was an executable: once it is compiled, you launch it and it is supposed to do something. However, this is not the way I was planning to use it, I wanted to access individual functions as if it was a library.

Luckily, solutions exist for that. In fact, there is one solution if you are using C and two if your code is in C++.

The C solution is much simpler and faster to implement than the C++ ways. For C, you only need to specify during linking the name of all functions you want to be able to use in JavaScript. The only thing to not forget is that you must add an underscore at the beginning of the name of your function.

For C++, it is more complex, you have to write some header-like structures for each function/class. I will not discuss more about it here as I didn’t use it, but you can find more information here.

Remember when I told you that it was important that the public API of Wren was in C even so the code is in C++? This is where it saved me a lot of time by allowing me to use the C method of exporting functions with Emscripten.

As I had a lot of functions to export (around 330) I made a small python script using pyclibrary to parse all headers and find the function names. You can find this script here. Note that Emscripten does not offer a way to export C enumerations. That is why my script also parses all enumerations and writes them directly to a .js file.

After that, I only had to add this option to the linking:

-s EXPORTED_FUNCTIONS=’[$(shell cat functions_to_export.txt)]’

and I could access my functions, individually, from JavaScript.

At this point, I successfully compiled the whole rendering engine. The result is three files:

  • A .wasm file that contains the WebAssembly code.
  • A .js file that is a “glue” file that makes the link between the WebAssembly and the other JavaScript or HTML files you want to interface with it.
  • A .data file that contains the files that I preloaded in the virtual file system of Emscripten.

Please note that this is not because I managed to compile the rendering engine without errors that it was ready to work on the web. I still had a long way to go…

Step two: adaptation of the code

Now the tedious work is beginning.

Webots was using Three.js on the web and the goal was to replace it with Wren compiled in WebAssembly. Problem: there is no one-to-one matching between Three.js and Wren. So I had to begin again nearly from scratch. I started with the simplest proof of concept: a white cube. Then I built on top of it: other geometries, appearances, lightning, shadows,…

But what interested us in this article is the WebAssembly part.

My approach was the following:

  1. Choose a class, let’s say the sphere class.
  2. Adapt it to use the newly compiled rendering engine.
  3. Bump into some bugs due to the conversion from C++ to WebAssembly.
  4. Fix them.
  5. Repeat from step 1.

You must ask yourself, what were those problems? Luckily for you, this is the subject of my next section!

Problems

The vast majority of difficulties I encountered can be classified in two categories:

  • Problems with pointers.
  • Problems with OpenGL.

Pointers

The C interface of Wren is using pointers all the time: functions take pointers and return pointers.

On the other hand, there is no pointer in JavaScript. And to use Wren on the web I had to interface it with JavaScript.

So what happens when you assign the return value of a C function, a pointer, to a JavaScript variable? It is considered by JavaScript as a simple integer. But the things are well done because if you give this integer back to another C function that is waiting for a pointer, it will work out perfectly well.

The difficulties arise when you try to give a JavaScript array or object as a parameter to a C function that expects a pointer. The C function will do its best to understand what it received but will most likely fail miserably.

Example

A concrete case I encountered at the beginning of my project was the following:

  • I had a C function, compiled with WebAssembly, to change the background color. It was awaiting a const float* color.
  • In JavaScript, the color was stored in a simple JavaScript array.
  • I tried, naively, to pass this array to the C function.
  • I could load my web page with no error and no warning.

I could load my web page with no error and no warning.

The background color was set, but it was always red, regardless of the value in the JavaScript array.

What happened, in this case, is that the C function tried to translate what I gave it and it eventually interpreted the whole JavaScript structure as the red value.

There is one important lesson to learn from this case if you are planning to use Emscripten compiled functions in JavaScript: watch out for silent failures! In this case, it was obvious that something was wrong, but it is not always the case and it can drive you crazy!

Luckily, there are solutions to pass JavaScript objects to Emscripten. I used a combination of three of them in this project:

  1. Use the Emscripten methods ccall and cwrap. To use them, you have to add the following compilation option:
-s ‘EXPORTED_RUNTIME_METHODS=[“ccall”, “cwrap”]’

As you can see below, ccall and cwrap can be used to call some C functions and you need to specify the type of argument. It is very useful for strings but those functions are very limited when you need to pass arrays.

Note that this.previousInverseViewMatrixPointer, a pointer that we got from another C function, is defined as simply being of type number.

2. For arrays, you may want to use the Emscripten built-in implementation of malloc and free. It is a bit more cumbersome as you can see on this example from the Emscripten website :

This code allocates a buffer, fills it, passes it to a C function and then free it.

3. Use an intermediary C static function. Let’s take again the example from above where I wanted to pass a vector of color to a C function. As this was something I had to do quite often, I didn’t want to bother allocating and releasing a buffer each time. The alternative was to design an additional C helper function like that:

This function takes three individual elements (colors in this example) and returns a pointer to a static array that contains these elements. However, it also has some drawbacks. For example, it works only for arrays of predefined size and you can have only one (color) array at a time.

OpenGL

The problems come from the fact that Webots uses OpenGL 3.3 and on the web, we are using WebGL2. WebGL2 is the corresponding version of OpenGL 3.3 but is not equal to it. Moreover, Emscripten compiled functions with OpenGL ES 3, which is also slightly different from OpenGL 3.3.

What it means is that sometimes you have a function that is available only in OpenGL 3.3 and not in the other, or that exists in both OpenGL3.3 and WebGL2 but not in OpenGL ES 3. Or a function exists in the three versions but does not accept the same parameters.

How to solve these issues?

There is no solution that fits them all, but I can give you some tricks I used:

  • Modify the C++ code to replace it with a different way to do the same thing but that is compatible both with gcc (the desktop version of the rendering engine must still work) and WebAssembly
  • use EM_ASM, a handy method that allows us to write WebGL code directly in C. Very useful if a function is not available in OpenGL ES 3 but is in WebGL 2.
  • Be creative. I reimplemented some non-available functions directly in the shaders or in the code and used some ifdef to define different ways of doing a task if the compiler was Emscripten or gcc.

Advice

• Prefer a pure C API, it is much more straightforward to export than C++.

• Be flexible, there is no magical solution to fix all problems introduced by the compilation to WebAssemby.

• Be attentive to silence failure, it will happen a lot with the exported functions.

Manage your pointer carefully and double-check that what you give to WebAssembly is well interpreted.

• Reduce the number of dependencies of your code.

Result

The transition from Three.js to our WebAssembly compiled rendering engine allowed us to greatly improve the graphical quality of our web simulations.

The new WebAssembly rendering engine was used to display the matches of the RoboCup Humanoid League that you can watch here.

You can also see other applications of this rendering engine in the documentation of Webots, or in this example of animations

You will find below some comparisons between the desktop version of Webots, the web viewer with Three.js, and finally, the web viewer with the rendering engine compiled with Emscripten.

Left: Desktop version, middle: web with Three.js, right: web with Wren in WebAssembly

--

--