Porting a complete C game engine to html5 through Emscripten

While I used Windows / Visual studio for porting my game engine, pretty much all advices listed here are valid for other platforms.

Tools used

  • Emscripten 1.27.2
  • emsdk for Windows
  • Emscripten Visual studio 2010 plugin
  • Chrome / Firefox for testing

If you never installed Emscripten before, go to the official website and follow the instructions described here:

Emscripten website

Important stuff before attempting a port

The most important thing to do before porting your engine to Emscripten is to make it cross-platform. Emscripten use clang to compile c code, which could be considered as a drop in replacement for GCC. It is also POSIX compliant, so if your base code is Windows only, it’s probably better to make it cross-platform first.

Things that DON’T WORK under Emscripten :

  • Threads, so if you have some background resources loading, you will need to adapt / rework this portion of code, You can use web worker as a work around, but that won’t give you a fully fledged threading support
  • plug-in system through dlopen / dlsym
  • dynamic linking, every library must be compiled as a static LLVM bitcode file (.bc) and linked when you compile you game

That’s the stuff I ran into / noticed when porting my engine, there is more limitation to be aware of, for more details, see the porting guidelines on Emscripten website:

Porting guidelines

Which libraries I used in my C engine

These are the libraries that the desktop / mobile version of my engine use, Almost all these libraries worked through Emscripten with minor changes or were already included in Emscripten. Some still require specific configuration / build flags.

  • glfw 3 (input / window management)
  • chipmunk 6.2.1 (Physics)
  • freetype 2.4.11 (ttf fonts support)
  • lua 5.2.3 (Scripting)
  • libogg 1.3.2 (Ogg decoding)
  • libpng 1.6.2 (png image support)
  • libvorbis 1.3.4 (Vorbis decoding)
  • libsndfile 1.0.25 (Reading sound files)
  • protobuf-c 0.15 (For custom binary file type like level data)
  • sqlite 3 (file based SQL database, used for save files and localization files)
  • libzip 0.11 (Reading zip archives)
  • zlib 1.2.8 (Decoding zip)
  • portaudio v19 (Playing audio content)

Porting libraries

All libraries are compiled as LLVM static bitcode (.bc file) it’s these files you provide to your game as input libraries when you will finally build it.

GLFW
Emscripten include glfw 2 and 3, so you don’t need to port it. Just compile your game with the option “-s USE_GLFW=3” (or 2 if your using GLFW 2)

Chipmunk
Chipmunk “almost” work out of the box, I made a specific patch to fix a bug I got with it.

The main bug with chipmunk / Emscripten is located in cpHashSetEach.c, emscripten doesn’t like the cpHashSetIteratorFunc function pointer and crashed for me right away. I fixed that by creating a specific cpHashSetIteratorFunc, replacing the void* type with the correct struct name. It also required me to create a specific header file with the struct definition so that cpHashSetEach.c and cpBBTree.c can access it.

patch below (winmerge format)

cpBBTree.patch

cpHashSet.patch

emscripten.h

Also, It’s better to use -s FORCE_ALIGNED_MEMORY=1 in your game EMCC linker options, I tend to have random crash without.

Freetype
Work, but some fonts make the library crash (My test with a 20px font made the library crash, while 8px font work, so test it beforehand)

lua
Work only if you use LUA_COMPAT_ALL in preprocessor definitions when compiling lua. Also, you must use -s FORCE_ALIGNED_MEMORY=1 in your game EMCC linker option.

libogg
Work out of the box

libvorbis
Work out of the box

libsndfile
I changed the library code to remove all references to FLAC (I only kept oggvorbis support), just exclude flac.c from your build setup and comment all references to FLAC in libsndfile code.

You also need a specific configuration, some basic type under Emscripten has a different size that what libsndfile expect, especially off_t, here’s the modified files:

config.h

sndfile.h

Finally, open the sndfile.c file, and remove the assert in the sf_open function, (should be around line 312):

assert (sizeof (sf_count_t) == 8) ;

In our case, sf_count_t size is 4, not 8, mainly because the off_t type don’t have the same size under emscripten.

protobuf-c
Work out of the box

sqlite
Work out of the box

libzip
Compile out of the box (untested in my Emscripten setup, I use the virtual file system of Emscripten, thus no need for zip packages)

zlib
Work out of the box

portaudio
The only library I ended up dropping in favour of SDL_audio. SDL_audio is included in Emscripten and use webaudio which is the best way to have sound in your html5 game at the moment. If you already use the callback interface of portaudio the port is pretty straight forward, I ended up reusing my whole mixing callback function I used for portaudio, with some minor adjustement to take into account the difference with the SDL_audio interface.

Callback function in portaudio
static int Callback(const void *input,void *output,unsigned long frameCount,const PaStreamCallbackTimeInfo* paTimeInfo,PaStreamCallbackFlags statusFlags,void *userData)

Callback function in SDL_audio
static void sdl_audio_callback(void* userData,Uint8* _stream,int _length)

The frameCount in SDL_audio can be retrieved this way :

(sf_count_t)((_length / sizeof(Sint16)) / num_channels);

(in this case, I read sound data as short values (SInt16) just use the datatype corresponding to your case)

The output pointer translate to the _stream pointer in SDL_audio.

For the other variables like input / paTimeInfo and statusFlags, you can just use the userData pointer to pass these variables around.

Another thing to consider is the threading handling. In my original code, I used atomic variables to manage multi-threading access to the sound functions. With the Emscripten version, I replace all the atomic code with SDL_LockAudio() / SDL_UnlockAudio() . Also, my existing buffer size for audio didn’t work well, so I settled on a audio buffer of 1024 bytes (so 512 bytes per channel), which is a nice compromise between audio quality / latency. I had small sound glitch under Chrome but nothing problematic.

Porting engine

As long as your engine compile under GCC, and don’t use threading, you should be safe. The biggest part is probably the rendering code if your engine is not already compatible with OpenGL ES 2.0 (WebGL work the same way than OpenGL ES). Basically, you have to remove all Fixed function pipeline code and replace it with OpenGL core 3.0 compatible code to be compatible with OpenGL ES / WebGL, You also can’t render vertices array directly and have to use VBO for everything. If you need more info about how OpenGL 3.0 work, have a look at opengl-tutorial.org

Don’t forget that if you don’t already use shaders, you will need to write some to be compatible with WebGL. About shader port, read the next section.

A note on shaders and webgl

WebGL only support GLSL shader #version 100, so you could have to port that part of your engine.

For example this code

#version 150

in vec2 inPos;
in vec2 inTex;

out vec4 color;
out vec2 texcoord;

in version 150 become this code

#version 100

attribute vec2 inPos;
attribute vec2 inTex;
varying mediump vec4 color;
varying mediump vec2 texcoord;

in version 100.

You could probably use the preprocessor to keep everything in the same shader file (with #ifdef GL_ES for GLES/WebGL specific code)
.In my case, I preferred to have separate shaders files to properly make the difference between a WebGL shader and a Desktop PC shader.

Emscripten specific gameloop

Rather than using a while() loop, with Emscripten, you just have to use this line to setup your game main loop:

emscripten_set_main_loop(renderoneframe,0,1);

where renderoneframe is your function where all your game loop code is. For more details, see the doc on Emscripten website:

emscripten_set_main_loop

Compiling the final game code

Another thing that I had to change is how I load my game DLL. In my engine, I have a module system where through dlopen / dlsym I have access to game specific functions. dlopen / dlsym isn’t supported in Emscripten, so the solution in my case was to build a .bc library file of my game DLL, reference the library header file, link it when compiling the final application, and replace my function locating code:

Desktop version
mngr->game_init_objects = (game_init_objects_t)dlsym(mngr->game_module,"game_init_objects");

Emscripten version.
mngr->game_init_objects = (game_init_objects_t)game_init_objects;

Obviously, it’s not suitable for a more complex plugin code, so you probably need to drop / refactor from the ground up any code that use plugin in native DLL form.

Here the final linker command I used :

-s USE_GLFW=3 -s TOTAL_MEMORY=67108864 -s NO_EXIT_RUNTIME=1 -s FORCE_ALIGNED_MEMORY=1

Packaging resources

Emscripten has a virtual file system available to replace the regular file system used on desktop. It work exactly the same way with fopen / fread call, so no need to change that. You only need to package your files this way:

python \emscripten\1.25.0\tools\file_packager.py emscripten_data.data --preload assets scripts locale.db save.db config.lua > emscripten_data.js

This will generate emscripten_data.data and emscripten_data.js files. The first file contains all your data, and the second file register your game files so you can access them. In this case the directories “assets” and “scripts” as well as the files “locale.db” “save.db” and “config.lua” are packaged in the emscripten_data.data file.

It’s important to run this command directly from your expected working directory. In this case, the file locale.db will be opened this way:

fopen("locale.db","rb").

If you want to open a file in the assets directory, then use:

fopen("assets/file.png","rb").

Finally, don’t forget to add your .js file in the .html file generated by Emscripten like this:

So your game code can find packaged files at runtime.

This entry was posted in Emscripten, HTML5, WebGL. Bookmark the permalink.

Comments are closed.