Home       About Me       Contact       Downloads       Part 79    Part 81   

Part 80: Emscripten

April 27, 2013

I'm interested in WebGL for two reasons: it lets users try the demos with a single click, and the WebGL implementation is more uniform across platforms than OpenGL. The big problem for me is that I don't like Javascript. Aside from being an annoying language, I don't really want to ditch the C++ versions. On the desktop, OpenGL (or DirectX) is more capable than WebGL, and C++ performance is much better than Javascript.

The ideal solution would be to continue to program in C++ and have that code run both in the browser (for casual users) and as a desktop app (for people who want the extra performance/graphics). Emscripten promises to allow exactly that. So I gave it a try.

Although Emscripten supposedly runs under Windows (it's written in Javascript), I set this up under Linux. Most open source projects install easier there and run with fewer bugs. Unfortunately, my installation was a bit of a mess. Somewhere along the line, I had installed older versions of the Clang compiler and LLVM. Emscripten complained and I had to remove the existing installations and download the latest versions. Once that was done, the tutorial ran just fine.

The tutorial takes you through a "Hello, world" example and pretty much stops there. I have all this OpenGL code that runs in a window. I also use my own libraries for everything from strings to graphics. I needed a simpler test case.

Fortunately, when I was writing my GUI code, I put together an OpenGL-only test case without using any of my libraries. That was GuiTestGL, and it just displays the rotating planet from the SeaOfMemes demo. With all the GUI code taken out, this was my test.

First OpenGL Test

The first significant issue I ran into with Emscripten is linking. The tool compiles all of your code to LLVM intermediate code, then turns that into Javascript. It does a link step and will eliminate code that isn't called. However, it won't tell you when there are unresolved references. Emscripten basically assumes you have a working piece of C++ with a good makefile, and just want to cross compile to Javascript. It's not big on error messages.

My second problem was with OpenGL. Emscripten recognizes the OpenGL calls and tries to convert them into WebGL. According to the documentation, it will do this in one of three ways: convert the subset that WebGL supports, convert OpenGL ES, or try to emulate desktop OpenGL under WebGL.

I was including my own version of the "glew.h" library, renamed "linux_glew.h" in my builds. Emscripten does not recognize this include and compiled all the glew macros, causing crashes whenever it tried to make a real OpenGL call. I later switched to including "SDL/SDL_opengl.h" and Emscripten recognized that. However, it still didn't work. The specific problem I ran into was mip-mapping. There are two ways to do this:

glTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, GL_TRUE);
or
glGenerateMipmap(GL_TEXTURE_2D);
On some platforms, the first is required, and on others, the second. WebGL only supports the second form, but when you compile with the "SDL_opengl.h" headers, Emscripten tries to use the first form (since it's declared) and fails at runtime.

Looking through the examples, I found they included the OpenGL ES headers. I added those lines instead of the SDL version:

#include <GLES2/gl2.h>
#include <EGL/egl.h>
I don't have either of these headers on my machine, but Emscripten has its own versions and can then issue errors against the parts of OpenGL it can't compile. That let me get the OpenGL code correct for Emscripten.

Texture Images

Next, I had to load my texture images. Under C++, I've been doing most images as JPG files, using the open source JPG library to decode them to memory, and then create an OpenGL texture. Some of my textures have an alpha plane though. For those, I've been encoding the alpha as a separate image file (usually grey-scale GIF files) and then combining them in memory.

Under Javascript/WebGL, as in Part 78, I just used the Javascript Image object to request the images as separate URLs. To handle the alpha plane, I converted the JPG+GIF pair of files into a single PNG file. That worked just fine, as you can see in the demos.

Under Emscripten, things are more complicated. Using the "preload-file" option, I can create a virtual local directory with all my files in it -- shaders, cursor patterns, options and texture images. The question then is, how am I going to decode these into memory for use as OpenGL textures?

I don't really want to compile all of the JPG and GIF libraries into Javascript. This is particularly absurd when you realize all of this code is already built into the browser for loading images. I figured that Emscripten must have something that it recognizes in C++ and converts into a Javascript image load.

That call appears to be the SDL image load function. The code looks like this:

#include <SDL/SDL_image.h>

glPixelStorei(GL_UNPACK_ALIGNMENT,4);

// load XMIN image
SDL_Surface* surface = IMG_Load(xminFile);

glTexImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_X, 0, GL_RGBA, 
    surface->w, surface->h, 0, GL_RGBA, GL_UNSIGNED_BYTE, surface->pixels);

SDL_FreeSurface(surface);

This definitely loads the image into memory, but it didn't work quite right for the alpha plane data. In my planet demo, I'm using the alpha plane to indicate reflections, so it's 1.0 over water, and 0.0 over land. This is independent of the land and water colors (which are in the RGB planes). So I need to load true 32-bit color data.

Looking through the Emscripten generated Javascript, it looks like they create an image from the PNG data in memory. Then they render it to a 2d canvas and extract the bits from that to make the SDL Surface.

During the rendering of the RGB+alpha, something odd is happening. There's still an alpha component on the surface, but where the source data is something like (0.5, 0.5, 0.5, 0.0), the result is all zeros. I don't see a general premultiplication by alpha, so it's something else. I even tried using a minimum alpha value of 0.1, to just avoid some alpha=0 special case, but it didn't work.

I tried switching to 32-bit BMP files, but got a different problem. Under Chrome, the image is rendered the same as a PNG -- RGB values disappear when alpha equals zero. Under Firefox, the alpha plane is ignored and always equal to one.

At this point, I don't have a solution for loading RGBA data via Emscripten.

The Event Loop

Since the SDL image load worked (sort of), I decided to implement the main loop with SDL calls. The code I have, and the other examples I've seen on the net, all have the form:

SDL_Event ev;
while (running)
{
  while (SDL_PollEvent(&ev)) 
  { 
    // ...handle event
  }
  // ...draw the view
  SDL_GL_SwapBuffers();

  // ...sleep until time for another refresh cycle
}
If I use this code, Emscripten takes it, but the resulting Javascript is treated as an infinite loop by the browser. Nothing shows, and eventually you get a popup telling you the script is taking too long to respond.

This issue is actually addressed in the FAQ, but it's near the bottom and I missed it. The page you want is here. You rewrite the main loop as

void mainLoop()
{
  while (SDL_PollEvent(&ev)) 
  { 
    // ...handle event
  }
  // ...draw the view
  SDL_GL_SwapBuffers();
}

int main()
{  
  // ... use SDL initialization routines

  // give the main loop function, fps, and "simulate infinite loop"
  emscripten_set_main_loop(mainLoop, 0, true);
}

Before I found this solution, I had given up on SDL calls and switched to GLUT. A GLUT main loop also works, without changes.

The Page Framework

If you look at one of the standard demos, such as the glxGears demo, you'll see that the default output of Emscripten builds a page with the OpenGL canvas at top, and a console text area below. I want a full-page canvas without any other controls.

I don't see anything in the FAQ or their "settings.js" file which changes the way the page is built. However, there are two forms of output: as a complete HTML file, or as a Javascript file. The HTML version has the page items, some setup, and then all the same code as the Javascript file.

I was able to create a simplified HTML page that just has one big canvas, and changed the handling of error messages. Then I included the generated Javascript file and sort of got what I wanted. But there's one big drawback: resizing the page does not create an SDL resize event in the code. The canvas is resized by the browser, but this just stretches the OpenGL view.

Googling, I can't find a resize event on the canvas object. The code I based the other demos on doesn't have a resize event either. Instead, it just checks the canvas size before each update, and resizes the viewport to match the current canvas size.

I could assume the canvas size is the window size, and in a window resize event handler, try to get their SDL code to send a resize event. Or I could change their SDL library to check canvas size and generate resize events. I'd have to do something similar to the GLUT library to be really consistent.

What I'd really like to do is have my own page framework with my own event handlers, all in native Javascript, and call into my application functions to perform redraws, resizes, handle keystrokes, etc. In fact, if I want to handle tablet touch events, I need to do something like this. Their framework doesn't really lend itself to that however.

I wish they had broken the output up into pieces: one piece which was the Javascript generated from your code; another piece which was the system infrastructure needed to run converted code; and a final piece which supported their little demo page with the canvas and log.

As it is, there's a single big file that does everything. It starts with HTML page elements, does some setup, defines many, many utility functions, then your functions, handles progress bars while the page loads, and finally runs your main routine. None of this is broken into pieces so you can modify it.

I'm sure I could restructure this code to do what I want. It's all in Javascript and the source code is there. Right now, it's more than I want to mess with.

Converting a Complete Demo

I was tempted to stop at this point, but decided to go ahead and try to get the Trees demo from last part running with no changes to the C++ source. To do that, I converted the OpenGL 2.1 version of my display support to WebGL, and added that into the framework. The various Emscripten/SDL calls are implemented as a new "Script" platform.

I did finally get this to mostly work. I'm still having problems with my texture atlas functions, and general input issues. Compiling the code with optimization level 2 causes an infinite loop in the browser. Compiling at level 0 or 1 works, but produces a large file with pretty slow execution.

On my Windows machine, the native C++ code runs at 400 fps. That includes the time spent calculating the tree branch segments, not just display. Using Chrome on the same machine, the Javascript version runs at 48 fps. This is better than the demo last week (which ran at 20 fps), but not great.

The C++ native code is 857K. The converted HTML code is 1350K. That might get better with a higher optimization level on the compiler.

Here are my notes from doing the conversion:

Compiler Issues

  • As mentioned above, I want a full-page app, and I want to write my own Javascript framework that calls into my compiled code. The current Emscripten compiler doesn't seem to have any options that allow this.

  • The generated Javascript is unreadable and undebuggable. It's generated from the intermediate code, so it's very low level. Here's a sample:
    var $34 = HEAP32[5255412 >> 2];
    var $35 = $34 + $27 | 0;
    HEAP32[5255412 >> 2] = $35;
    HEAP32[5255424 >> 2] = $26;
    var $36 = $35 | 1;
    var $_sum42 = $_sum + 4 | 0;
    var $37 = $newbase + $_sum42 | 0;
    var $38 = $37;
    HEAP32[$38 >> 2] = $36;
    
    Even if these variables had sensible names, the pointers would be indexes into their heap object, and so you wouldn't be able to look at values with the debugger.

    The run-time error messages are semi-useless. A typical example is "FUNCTION_TABLE[$8] is not a function."

    The bottom line is that if anything goes wrong with the generated code, you'll be debugging with print statements and hoping it's not a compiler error.

  • I used wide character strings in my string object. Some of the wide character classification routines are not implemented. So "iswalnum" is missing.

  • As mentioned, compiling at -O2 produced unusable output. I have no idea why, and don't look forward to stepping through generated code in the debugger trying to figure out why.

  • There are no error messages when functions are missing. I had a nasty bug where I left the "class::" prefix off a function. That created a plain C function that was hidden by the declaration of the method. With no warnings from the linker about a missing function, it was a pain to find!

Basically, you need to have completely working C++ code, and then just use Emscripten to produce an HTML version, and hope it works. Using this as a development environment and trying to debug is a chore.

WebGL Issues

  • You need to convert all the shaders to WebGL. There's a slightly different syntax, and a much stricter parser (no automatic conversion of integers to floats, for example.)

  • Index buffer entries are limited to 16 bits, so no more than 64K vertexes in a mesh, if you are using an index buffer to cut the size.

  • As mentioned above, 32-bit images (RGBA) are not handled very well by the image load function.

  • You are basically limited to the functions of OpenGL ES. I think this constrains shaders quite a bit, but I don't have a list of differences. No texture arrays!

Platform Issues

  • No C++ threads are supported, so I can't write completely platform independent code that uses multiple processors. This is a huge performance hit. I could try to restructure the app somehow to let me use some C++ cover over Web Workers, but I'm not sure what that would look like.

  • I can't use C++ socket routines. Instead, I would have a cover over WebSockets. The only real problem there is that the protocol to the server is different than straight sockets would be. A server will have to handle both native C++ clients and HTML clients.

  • I need a version of my audio library that talks to Javascript audio functions. I can't use the C++ version implemented on OpenAL.

  • My 2D text, used for help and menus, etc., needs to be reimplemented in Javascript using 2D canvas operations. To do it entirely in C++ means compiling the FreeType library and letting it do lots of low-level bit operations in memory. Performance would probably not be good.

The Bottom Line

Emscripten does mostly work, and it would be nice to be able to just recompile my demos for the web, rather than having a completely different version. It would be nice to have WebGL demos that people could click on and run, without installing a demo zip file.

There's still a lot to do to make this completely work though, and I expect performance problems and restrictions on OpenGL functions to continue to get in the way. I haven't made up my mind as to whether this is worth pursuing.

Home       About Me       Contact       Downloads       Part 79    Part 81   

blog comments powered by Disqus