I started a project intending to answer a few questions:

  • How feasible is it to run c++ and opengl in the browser at reasonable frame rates?
  • How much performance will need to be sacrificed?
  • What features, compared to running native code on the desktop, won’t work?
  • What missing functionality can be worked around or at least partially restored?

Live Demo

Native Code

There have been ImGui demos compiled into javascript or asm.js before. I wanted to experiment in a even more binary, and hopefully more performant format, called wasm, or web assembly. Generally speaking, turning c++ into asmjs requires the help of the emscripten toolchain. Current versions of unmodified LLVM, the backend to Clang, are capable of producing wasm output, and that output can be loaded into web pages without the help of emscripten. However, emscripten still provides a lot of interop libraries for going between web assembly and javascript, and implements a lot of standard headers and libraries that you don’t get when using LLVM alone.

While some distros do provide an emscripten package, I still recommend installing the Emscripten SDK and configuring your build environment to use it. This will have several benefits: your build errors won’t have platform specific modifications, as debian often does; it will be easier to distinguish between system headers and emsdk headers, as you will have to be fully explicit about your sdk path in your build toolchain; and you will be able to use the latest supported version of emsdk, as opposed to the often out of date one provided by distros. You will still be able to keep the benefits of having a fixed sdk version, as each sdk component in emsdk installs to a version-specific directory.

In my testing, Firefox and Chrome both ran the webasm and WebGL demo, including the WebGL extensions I enabled. Other browsers do not support webasm at all: Safari, IE, Edge, and Opera and therefore will not be able to load the demo. There are strategies to fallback to either asm.js or to a binary webasm interpreter in javascript. I opted to not implement any fallback solution, as I want to both understand and demonstrate the most performant solutions available for webasm/WebGL based projects. Cross browser compatibility is often the bane of any web developer, and I want to explore how much of using c++ with webasm can alleviate that pain.

Main Loop

Like most 3d renderers, and any event driven system, there is a main loop. The pseudo code looks like this:

int main() {
    init();
    while(true) {
        loop();
    }
}

On the emscripten platform, you need to let the browser control the loop rate and run browser logic. To accomplish this, you register your main loop as a animation callback.

#include <emscripten.h>
int main() {
    init();
    emscripten_set_main_loop(&loop, 0, 1);
}

The emsripteen_set_main_loop call takes a function pointer for the browser to call into at at regular intervals as though it were updating an animation. For me I was able to maintain about 60fps during testing, which is the refresh rate of my display hardware.

The html5 Canvas Element

As far as html, javascript, and css you actually have to write as a c++ developer, there isn’t much.

<canvas id="canvas"></canvas>
<script>
    Module = {
        canvas: document.getElementById('canvas')
    }
</script>
<script async="index.js"</script>

There are a lot of convenience functions you can fill out, but very few of them are really necessary. I added a couple to this demo to center and improve the look and feel of an embedded canvas element. For most of my testing and development, I didn’t need any of decorative changes because I would call emscripten_enter_soft_fullscreen at the start of the demo. This takes the canvas element and essentially makes it take the full width and height of the browser window. I found I preferred this to true full screen because it didn’t prompt the user every time they wanted to click back into the demo. Soft full screen lets the user move the browser window around, and maximizing the browser window has a good feel for being fulls creen, even though the browser’s user interface is still showing.

The canvas element added in the above example serves as the OpenGL window, and the WebGL graphics context is created from that canvas element.

emscripten.h and html5.h

I approached writing c++ code for emscripten as though I were porting a project to another platform with it’s own set of quirks, headers, and compilation rules. The major difference between doing a real port and this project is there wasn’t actually an existing codebase, so supporting multiple platforms is not a requirement.

Most of the platform specific api calls are in emscripten.h. The initialization code, some platform specific macros, etc are all here. The documentation provided is pretty good, and provides a fairly complete set of examples for every call. Under the hood, most of the implementations for the emscripten.h header are implemented in library_browser.js. In fact, a lot of standard system and library headers were implemented in javascript files. This does mean that a lot of the interop code is not running as web assembly, but still running as a minified javascript.

A useful macro provided by emscripten.h is EM_ASM() This macro allows you to call out to javascript from within a cpp file. This does mean that you are granted all of the power of javascript from within your project code, and you can organize the implementation details any way that makes sense for the project.

The other heavily used header file in this project is html5.h Effectively all of the user interface, input and output, was handled by this file, Mouse, keyboard, and other events are implemented asynchronously via callbacks. The callback syntax was pretty standard function pointer based callback, so it looked like pretty much every other callback system in c/c++ that I have come across: you register a callback function that implements some function signature, and the system calls into you when it has updates. The callback registration functions always took a void* userdata that was then passed into the callback. There are no threading concerns, as there is effectively no threading, and your callback functions only get called when your main loop is not running.

As a fairly strange “quirck” of the platform, certain functions can only be executed from within a «event handler for a user-generated event.» e.g. pointer capture and fullscreen mode. I opted to not use either of these functions for the demo, because they interrupt the flow of the app. There are situations where pointer capture would have been a nice feature if I could have made it seamless, but it would always require the user to press a key or click the mouse or perform some similar action to satisfy the browser’s security concerns.

Clipboard support also has some restrictions, as browsers want to protect the clipboard buffers of their users. Within the imgui demo, copy and paste does work as expected, but trying to copy and paste from inside the demo to outside, or vice-versa, does not. Some webpages have some complicated clipboard workarounds, but adapting them to work for wasm/webgl applications is going to take some more research.

OpenGl

In emscripten land, for webgl support you pull in the GLES2.h header, and kind of pretend you are working in OpenGL ES2 instead of WebGL. I did have to explicitly enable OES_vertex_array_object support and there were some differences in the shader language betwen gles2 and webgl, but nearly everything else in the api is identical. There is some preliminary support for webgl2/GLES3.h in emscripten, but since support for webgl is disabled by default in both firefox and chrome, I opted to use the older version. The GLES2 version of a ImGui backend is very similar to either of the OpenGL3 examples provided by imgui.

ImGui

The acronym behind the name ImGui stands for «Immediate Mode Graphical User Interface». The immediate mode refers to the fact that ImGui is stateless between render frames. You have to rebuild the entire UI for every loop. This is by design, and not maintaining state between data you are trying to visualize and the visualization layer ends up working pretty well.

ImGui doesn’t come explicitly with a rendering backend you generally either have to write your own, or rip one from one the provided examples. ImGui makes no assumptions about the platform that it is going to be running on, and the implementer is expected to wire up all the platform specific stuff to a few interface functions during your event loop.

The render phase of ImGui calls a user supplied callback, and is handed a ImGui draw list object. The implementer is then responsible for taking the elements imgui wants to draw and actually rendering them out to screen.

I chose imgui because I want to continue to test out native interfaces to browser specific features, but I don’t want to spend a lot of time coming up with test UI for them. ImGui is a fairly complete debug UI system designed for very similar use cases to what I am doing. Once ImGui is wired up, attempting to iterate on new interfaces and test functionality should get a lot easier.

3rd Party Libraries

  • emscripten - a c++ to wasm compiler
  • imgui - immediate mode graphical user interface