building.md - Virtastic Notepad

OpenMW manual: Building the engine from source

--- title: "Building the engine from source" description: "Build the openmw-web WebAssembly engine yourself: the repository layout, the Emscripten 6.0.1 toolchain, the wasm64 target, the dependency stack and running what you built." book: "Developers" order: 60 --- You do not need any of this to play or to run a server - the [releases](https://github.com/Virtastic/openmw-web/releases) ship a prebuilt engine. This is for people changing the engine itself. Stuck? Ask in [Discord](https://discord.gg/PzFfDkbSue).

## What's in the repository

A code-only repo - game assets, dependency caches and build outputs are gitignored. | Path | Purpose | |------|---------| | `openmw/` | OpenMW engine source (upstream plus local WASM changes) | | `play/` | Browser front-end: launcher, game page, dev server | | `server/` | The server (TypeScript/Node): admin dashboard, worlds, SSO, storage, savegames | | `wasm-build/` | Build scripts, patches and tooling for the WASM engine | | `deploy/` | Container and reverse-proxy config | The `openmw/` tree tracks upstream at the commit recorded in [`.openmw-base-commit.txt`](https://github.com/Virtastic/openmw-web/blob/main/.openmw-base-commit.txt); how the desktop engine was made to run in a browser tab is written up in [`WASM_ADAPTATIONS.md`](https://github.com/Virtastic/openmw-web/blob/main/WASM_ADAPTATIONS.md).

## Building

You need **Emscripten 6.0.1**, CMake, Ninja, and the cross-compiled dependency stack under `deps/wasm64` (see [Dependency stack](#dependency-stack)). ```bash export ROOT=$PWD ./wasm-build/build-deps.sh # -> deps/wasm64 (once; see below) ./configure-openmw.sh ninja -C build-wasm64 components openmw-lib ./wasm-build/link-openmw.sh cp build-wasm64/openmw.{js,wasm,data} play/ ```

## wasm64 (MEMORY64) is the only target

The engine is built for wasm64 and nothing else: it lifts the heap above the 4 GiB a 32-bit module can address, which a Tamriel Rebuilt load order needs, and the client gates on MEMORY64 to match. `OMW_WASM64=0` refuses with an error. Browsers need memory64: Chrome/Edge 133+ or Firefox 134+ (the pages feature-detect and say so). Notes, all learned the hard way: - **`-m64`, not `-sMEMORY64=1`** - the `-s` spelling is deprecated, and `-m64` is a compiler flag so it reaches CMake's `try_compile` probes. - Pointer models **cannot share a build tree or dep prefix**. A mixed archive fails only at the final link (`wasm32 object file can't be linked in wasm64 mode`); `build-deps.sh` stamps each source tree with its model and cleans on change. - [`wasm-build/memory64-gate.sh`](https://github.com/Virtastic/openmw-web/blob/main/wasm-build/memory64-gate.sh) checks the toolchain, threads, >4 GiB allocation and the JS pointer ABI without building the engine - run it first when something looks wrong.

## The link step is scripted for reasons

- `main.cpp.o` is passed directly on the link line; `ninja components openmw-lib` does not rebuild it - `link-openmw.sh` does. - Everything uses `-fwasm-exceptions`. Do not add `-flto` (wasm-ld crashes or miscompiles boot) or `-sWASM_LEGACY_EXCEPTIONS=0`. - Hand-built deps must be `-pthread`; ICU uses the sysroot `-mt` variants. - A killed link leaves a mismatched `openmw.js`/`openmw.wasm` pair - check both mtimes match before deploying.

## Dependency stack

Static libs in `deps/wasm64/lib`, headers in `deps/wasm64/include`: OSG 3.6.5, Bullet (double-precision), MyGUI, FFmpeg 6, Boost (program_options + iostreams), Lua 5.4, LZ4, RecastNavigation. SDL2, FreeType, HarfBuzz, png, jpeg, zlib, ogg and vorbis come from emscripten ports at link time; OpenAL is emscripten's built-in. `./wasm-build/build-deps.sh` builds the whole stack (or named targets: `build-deps.sh bullet lua`). Sources are expected under `deps/src/` and are **not** fetched: `osg` (OpenSceneGraph-3.6.5, patched), `bullet3` (3.25), `recast` (v1.6.0), `mygui` (MyGUI3.4.3), `ffmpeg-6.1.2`, `boost_1_85_0`, `lua-5.4.7`, `lz4-1.10.0`. **In Docker** (the image has cmake but not ninja, and unbounded jobs exhaust RAM): ```bash docker run --rm -i -v "$PWD:/repo" -w /repo -m 12g emscripten/emsdk:6.0.1 bash -s <<'SH' apt-get update -qq && apt-get install -y -qq ninja-build export EM_LIBEXEC=/emsdk/upstream/emscripten ROOT=/repo CMAKE_BUILD_PARALLEL_LEVEL=6 bash wasm-build/build-deps.sh SH ``` **OSG** is the involved one: apply `wasm-build/patches/osg-emscripten.patch` to an `OpenSceneGraph-3.6.5` checkout at `deps/src/osg`, then `./wasm-build/build-osg.sh`. The patch's most important fix is the RTT `drawBuffers` one - without it every render-to-texture camera silently discards its color output. **Two headers are force-included into every translation unit** (`wasm-build/include/`): `gl_compat.h` (desktop-GL vocabulary the GLES headers omit but OSG/OpenMW still name - all spec-fixed values feeding runtime no-ops) and `mygui_char_traits_fix.h` (`std::char_traits` for the wide types modern libc++ dropped and MyGUI still needs). They are build inputs and live in the repo; add to `gl_compat.h` only when a real compile error demands it. **On Windows, check line endings first.** A `.patch` or `.sh` checked out with CRLF is unusable. `.gitattributes` pins `eol=lf`; if you cloned before it existed, run `git add --renormalize .`.

## Running what you built

The runtime needs cross-origin isolation (SharedArrayBuffer). The dev server sets the headers, serves the `.br` siblings, and handles range requests: ```bash cd play python3 server.py # http://localhost:8910 (PORT=... to override) ```

### Multiplayer against a local server

The game page and the server must share one origin - the page will not hand its session ticket to a different hostname. `server.py` proxies the server paths for that: ```bash cd play OPENMW_MP_UPSTREAM=127.0.0.1:8080 OPENMW_LAUNCHER=1 python3 server.py ``` Something must be listening on the upstream, or those paths 502 - which is the correct signal, not a bug. Single player needs no server at all. If you touch the launcher or the proxy, test the WebSocket explicitly (HTTP/2 cannot carry an upgrade): ```bash curl -i -N --http1.1 -H 'Connection: Upgrade' -H 'Upgrade: websocket' \ -H 'Sec-WebSocket-Version: 13' -H 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==' \ http://127.0.0.1:8910/w/<worldId> # expect 101 Switching Protocols ``` Pull requests are welcome; see [CONTRIBUTING.md](https://github.com/Virtastic/openmw-web/blob/main/CONTRIBUTING.md).