WebGL / Emscripten Build¶
Orca can be compiled to WebAssembly and run in a browser using the
Emscripten toolchain. The libs/platform
submodule already contains a complete WebGL backend (libs/platform/webgl/)
so only the engine's Makefile and a few small source-level guards needed to be
added.
Quick Start¶
# 1. Install the Emscripten SDK (one-time setup)
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk && ./emsdk install latest && ./emsdk activate latest
source ./emsdk_env.sh # adds emcc/emmake to PATH
# 2. Build the platform library for WebGL
emmake make -C libs/platform OUTDIR=../../build/lib
# 3. Build Orca as orca.html + orca.js + orca.wasm
make webgl
# 4. Serve the output directory from a local HTTP server
# (browsers block direct file:// access to WebAssembly)
python3 -m http.server --directory build/webgl
# then open http://localhost:8000/orca.html
The share/ assets are always packed into the WASM virtual file system (they
contain fonts and Lua plugins that the engine requires at runtime).
Editor-only source files (.xcf GIMP files) are automatically excluded from
the bundle via --exclude-file *.xcf.
To bundle a project's data directory into the build as well:
This additionally packs the project directory and compiles PROJECTDIR into
the binary so the engine opens the bundled project automatically without
needing a command-line argument.
The build uses -Oz --closure 1 together with several additional flags for
minimum output size (see Size optimisation flags
below).
Debug Build¶
When a production build produces an unhelpful error like:
the underlying C source location is hidden by JS minification (--closure 1)
and disabled runtime assertions (-sASSERTIONS=0). Use the debug target to
get C file names and line numbers instead:
emmake make webgl-debug # share/ assets only
emmake make webgl-debug WEBGL_DATA=samples/Example # bundle a project
# or use the convenience alias:
emmake make webgl-debug-demo
Serve the output directory (Chrome/Firefox block file:// WebAssembly access):
The debug target differs from the production build in the following ways:
| Flag | Effect |
|---|---|
-g |
Embed DWARF debug information in orca.wasm |
-gsource-map --source-map-base ./ |
Emit orca.wasm.map — maps every WASM byte back to the original C file and line number. --source-map-base ./ ensures the browser looks for the map file in the same directory as orca.wasm rather than embedding it inline in orca.js. Chrome DevTools shows the C source location automatically when the map file is served alongside orca.wasm. |
-sASSERTIONS=2 |
Enable strict runtime checks: out-of-bounds memory accesses, null-pointer dereferences, type mismatches, and stack overflows all produce descriptive error messages that name the offending C symbol. |
-sSAFE_HEAP=1 |
Validate every heap read and write for alignment and bounds. This catches the class of errors that appear as "Out of bounds memory access" in production builds. |
-sSTACK_SIZE=1048576 |
Set the C stack size to 1 MB (up from the 256 KB production default). The WASM stack grows downward: SP starts at STACK_MAX (high address) and decreases with each function call; STACK_BASE = STACK_MAX − STACK_SIZE is the lower boundary. The larger stack gives more headroom when analysing deep call chains. |
-sSTACK_OVERFLOW_CHECK=2 |
Detect stack overflows with precise per-call checking. Reports the offending function when the C stack is exhausted. |
-O1 (not -Oz) |
Minimal optimisation — code structure is preserved and stack traces are readable. |
(no --closure 1) |
JavaScript output is not minified, so DevTools stack traces show real function names instead of single-letter identifiers like a, b, c. |
(no -flto) |
Link-time optimisation is disabled for faster iteration. |
Tip: Open DevTools → Sources tab after loading the debug build. If
orca.wasm.mapis served correctly, Emscripten errors will include a clickable link to the exact C file and line that triggered the fault.
Prerequisites¶
Emscripten ports (automatic)¶
These libraries are fetched and compiled automatically by Emscripten via
-sUSE_* linker flags — no manual work required:
| Library | Flag |
|---|---|
| zlib | -sUSE_ZLIB=1 |
| libpng | -sUSE_LIBPNG=1 |
| libjpeg | -sUSE_LIBJPEG=1 |
| freetype | -sUSE_FREETYPE=1 |
WASM libraries (built from source with emmake)¶
The following libraries must be compiled for WASM before running make webgl.
Run emmake make wasm-deps once (with emcc in PATH) to build and install
them into build/wasm-deps/:
| Library | Version | Source |
|---|---|---|
| lua 5.4 | 5.4.7 | https://www.lua.org/download.html |
| libxml2 | 2.9.14 | https://gitlab.gnome.org/GNOME/libxml2 |
| liblz4 | 1.10.0 | https://github.com/lz4/lz4 |
All three are compiled with CFLAGS="-O2 -DNDEBUG" (no -g) so that no
DWARF debug sections are embedded in the resulting .a archives. Without
this, libxml2's autoconf default CFLAGS would include -g, causing DWARF
entries that reference every transitively-included header (including emsdk's
internal OpenSSL headers) to propagate into the final orca.js / orca.wasm
output via Emscripten's DWARF processing pass.
How the WebGL Backend Works¶
The libs/platform/webgl/ directory contains three source files:
| File | Purpose |
|---|---|
webgl_system.c |
axInit / axShutdown, timing, platform strings |
webgl_window.c |
Canvas sizing, WebGL context, axBeginPaint/axEndPaint |
webgl_event.c |
Emscripten input callbacks → internal event queue; axPollEvent |
The platform Makefile detects EMSCRIPTEN automatically when invoked with
emmake and selects only the webgl/ sources.
Main Loop¶
The engine's main loop is a Lua while true do … end that polls for events
via axPollEvent. Browsers cannot run a blocking loop on the main thread;
the JavaScript event loop must be allowed to yield periodically.
Solution: The WebGL platform backend registers the main loop iteration
function with emscripten_set_main_loop so the browser schedules each frame
via requestAnimationFrame. When the event queue is empty, axPollEvent
returns immediately, letting the browser handle input callbacks and repaints
before the next iteration.
Size Optimisation Flags¶
The production webgl target uses several linker flags beyond -Oz --closure 1
to reduce the size of orca.js and orca.wasm and to speed up startup,
especially on Android Chrome (V8):
| Flag | Effect |
|---|---|
-sENVIRONMENT=web |
Strips Node.js / worker compatibility code from the JS glue — the app only needs to run in a web browser. |
-sELIMINATE_DUPLICATE_FUNCTIONS=1 |
Finds identical function bodies in the WASM output and merges them into a single copy. Typically saves 5–10% of .wasm size. |
-sSTREAMING_WASM_COMPILATION=1 |
Allows V8/SpiderMonkey to start compiling the WASM module while it is still downloading. Very effective for large modules (8 MB+): startup on Android Chrome can drop from 10+ seconds to under 5 seconds on mid-tier devices. |
-sMALLOC=emmalloc |
Uses Emscripten's lightweight allocator instead of dlmalloc. emmalloc is smaller and faster for single-threaded builds (no pthreads are used) and saves ~20 KB of .wasm code. |
-sTEXTDECODER=2 |
Delegates UTF-8 → JS string decoding to the browser's native TextDecoder API instead of emitting Emscripten's own decoder. Reduces JS glue size and is faster at runtime. |
-sINITIAL_MEMORY=64MB |
Sets the WASM heap to 64 MB (down from a higher default). Smaller heap means less memory zeroing at startup; raise this if the engine reports out-of-memory at runtime. |
GLSL Shader Version¶
The renderer dynamically prepends a version preamble to every shader. On
desktop it uses #version 330 core; on WebGL 2 it must use
#version 300 es. The guard in source/renderer/r_shader.c was extended to
cover __EMSCRIPTEN__ in addition to the existing __QNX__ case.
Modules Excluded from the WebGL Build¶
Some engine modules cannot work inside a browser and are excluded from the
WEBGL_MODULES list in the Makefile:
| Module | Reason |
|---|---|
network |
Uses BSD sockets / libcurl; use fetch() or WebSockets instead |
vsomeip |
C++ SOME/IP service-discovery library, host-only |
server |
Server-mode listener; host-only |
editor |
Native desktop file-dialog and font-rendering features |
Findings: What Should Move to libs/platform¶
While wiring up the WebGL build the following platform-specific concerns in
the main engine were identified. They are candidates to be abstracted into
libs/platform so that the engine core stays portable:
1. Executable / Resource Path Discovery¶
source/orca.c::get_exe_filename() uses readlink("/proc/<pid>/exe"),
_NSGetExecutablePath, or GetModuleFileName to locate the binary, then
derives LIBDIR, SHAREDIR, and PLUGDIR from it.
In WebGL there is no executable on a filesystem. The engine currently falls
back to the axLibDirectory() / axShareDirectory() helpers already
provided by platform.h, but the path-stripping logic (strrchr(exename, '/')
strip two segments) still runs.
Recommendation: Add a axGetExecutablePath(char *buf, size_t sz) to
platform.h and implement it per-platform (including a no-op for WebGL that
fills buf with ".") so orca.c has a single call for all platforms.
2. Lua C-Module Loading (.so Path)¶
orca.c appends LIBDIR/lib?.so to package.cpath so Lua's require can
find shared libraries. This works on Linux/macOS but is a no-op in WebGL
because all C modules are statically linked into the binary.
For WebGL the .so path line is skipped (#ifndef __EMSCRIPTEN__), but the
engine still calls require 'orca', require 'orca.core', etc. which will
fail unless the C functions are pre-registered before the Lua state starts.
Status: resolved. All built-in C modules are pre-registered by
luaopen_orca via luaL_preload before Lua code runs; no .so discovery is
needed for WebGL.
3. Dynamic Plugin Loading¶
Plugins (build/lib/liborca/*.so) are discovered and loaded at runtime via
dlopen. Emscripten supports SIDE_MODULE / MAIN_MODULE dynamic linking
but it requires all side modules to be listed at link time; fully runtime
dlopen is not supported.
Status: resolved. The Makefile webgl target now:
- Lists the plugins to bundle in
WEBGL_PLUGINS(UIKit, SceneKit, SpriteKit, DarkReign — vsomeip is excluded because it is C++ / SOME/IP). - Compiles all plugin
.csources directly into the single WASM binary alongside the engine modules. - Auto-generates
build/webgl/plugins_luaopen.hat build time by scanning the plugin sources forORCA_API int luaopen_orca_*symbols and building aplugin_modules[]registration table. The Lua module name is derived from the C symbol name by stripping theluaopen_prefix and replacing_with.(e.g.luaopen_orca_UIKit→"orca.UIKit"). - Passes
-DPLUGINS_LUAOPENwhen compilingorcalib.c. Under that guard the generated header is included and each plugin module is registered vialuaL_preloadinluaopen_orca, exactly like the built-in modules.
4. Thread / Blocking I/O¶
source/network/*.c uses blocking POSIX sockets and source/sysutil/w_system.c
uses opendir/readdir for directory listing. Directory listing works via
Emscripten's VFS; network operations require replacing curl/sockets with
Emscripten's emscripten_fetch API.
Recommendation: Move the network abstraction into libs/platform
(axFetchURL, axOpenSocket) so the engine calls a single API and each
platform provides the appropriate implementation (POSIX vs. Fetch API).
5. axSleep / Frame Pacing¶
axSleep(ms) is currently a nanosleep on POSIX and a no-op on WebGL.
The comment in webgl_system.c suggests using emscripten_sleep(msec) (with
ASYNCIFY) for a proper sleep. This is already partially handled by the
emscripten_sleep(0) yield in queue.c, but a non-zero duration sleep may
be useful for CPU throttling.
Recommendation: Implement axSleep in webgl_system.c as
emscripten_sleep(msec) once ASYNCIFY is confirmed stable. The ASYNCIFY
build flag is already set in the webgl Makefile target.