In this post I will describe how I ported the Surreal Engine game engine to WebAssembly.
https://demensdeum.com/demos/SurrealEngine/”
Surreal Engine – a game engine that implements most of the functionality of the Unreal Engine 1, famous games on this engine – Unreal Tournament 99, Unreal, Deus Ex, Undying. It refers to classic engines that worked primarily in a single-threaded execution environment.
I originally had the idea of taking on a project that I couldn’t complete in any reasonable time frame, thus showing my Twitch followers that there are projects that even I can’t do. During my first stream, I suddenly realized that the task of porting Surreal Engine C++ to WebAssembly using Emscripten was feasible.
A month later, I can demonstrate my fork and engine assembly on WebAssembly:
https://demensdeum.com/demos/SurrealEngine/
Control, as in the original, is carried out using the keyboard arrows. Next, I plan to adapt it for mobile control (tachi), adding correct lighting and other graphic features of the Unreal Tournament 99 render.
Where to start?
The first thing I want to say is that any project can be ported from C++ to WebAssembly using Emscripten, the only question is how complete the functionality will be. Choose a project whose library ports are already available for Emscripten; in the case of Surreal Engine, you are very lucky, because the engine uses the SDL 2, OpenAL – libraries. they are both ported to Emscripten. However, Vulkan is used as a graphics API, which is currently not available for HTML5, work is underway to implement WebGPU, but it is also in the draft stage, and it is also unknown how simple the further port from Vulkan to WebGPU will be, after it is fully standardized. Therefore, I had to write my own basic OpenGL-ES / WebGL renderer for Surreal Engine.
Building the project
Build system in Surreal Engine – CMake, which also simplifies porting, because Emscripten provides its native builders – emcmake, emmake.
The Surreal Engine port was based on the code of my latest game in WebGL/OpenGL ES and C++ called Death-Mask, because of this the development was much simpler, I had all the necessary build flags with me and code examples.
One of the most important points in CMakeLists.txt is the build flags for Emscripten, below is an example from the project file:
set(CMAKE_CXX_FLAGS "-s MIN_WEBGL_VERSION=2
-s MAX_WEBGL_VERSION=2
-s EXCEPTION_DEBUG
-fexceptions
--preload-file UnrealTournament/
--preload-file SurrealEngine.pk3
--bind
--use-preload-plugins
-Wall
-Wextra
-Werror=return-type
-s USE_SDL=2
-s ASSERTIONS=1
-w
-g4
-s DISABLE_EXCEPTION_CATCHING=0
-O3
--no-heap-copy
-s ALLOW_MEMORY_GROWTH=1
-s EXIT_RUNTIME=1")
The build script itself:
clear
emmake make -j 16
cp SurrealEngine.data /srv/http/SurrealEngine/SurrealEngine.data
cp SurrealEngine.js /srv/http/SurrealEngine/SurrealEngine.js
cp SurrealEngine.wasm /srv/http/SurrealEngine/SurrealEngine.wasm
cp ../buildScripts/Emscripten/index.html /srv/http/SurrealEngine/index.html

Next, let’s prepare index.html, which includes the project file system preloader. To upload to the web, I used Unreal Tournament Demo version 338. As you can see from the CMake file, the unpacked game folder was added to the build directory and linked as a preload-file for Emscripten.
Main code changes
Then we had to change the game loop of the game, you can’t run an endless loop, this leads to the browser freezing, instead you need to use emscripten_set_main_loop, I wrote about this feature in my 2017 note “Porting SDL C++ games to HTML5 (Emscripten)”
We change the code for exiting the while loop to if, then we display the main class of the game engine, which contains the game loop, in the global scope, and write a global function that will call the game loop step from the global object:
#if __EMSCRIPTEN__
#include <emscripten.h>
Engine *EMSCRIPTEN_GLOBAL_GAME_ENGINE = nullptr;
void emscripten_game_loop_step() {
EMSCRIPTEN_GLOBAL_GAME_ENGINE->Run();
}
#endif
After this, you need to make sure that there are no background threads in the application; if there are, then get ready to rewrite them for single-threaded execution, or use the phtread library in Emscripten.
The background thread in Surreal Engine is used to play music, data comes from the main engine thread about the current track, the need to play music, or its absence, then the background thread receives a new state via a mutex and starts playing new music, or pauses. The background thread is also used to buffer music during playback.
My attempts to build Surreal Engine for Emscripten with pthread were unsuccessful, because the SDL2 and OpenAL ports were built without pthread support, and I didn’t want to rebuild them for the sake of music. Therefore, I transferred the functionality of the background music stream to single-threaded execution using a loop. By removing pthread calls from the C++ code, I moved the buffering and music playback to the main thread, so that there would be no delays, I increased the buffer by a few seconds.
Next, I will describe specific implementations of graphics and sound.
Vulkan is not supported!
Yes, Vulkan is not supported in HTML5, although all the marketing brochures present cross-platform and broad platform support as the main advantage of Vulkan. For this reason, I had to write my own basic graphics renderer for a simplified OpenGL type – ES, it is used on mobile devices, sometimes it does not contain the fashionable features of modern OpenGL, but it ports very well to WebGL, which is exactly what Emscripten implements. Writing basic tile rendering, bsp rendering, for the simplest GUI display, and rendering models + maps was completed in two weeks. This was perhaps the most difficult part of the project. There is still a lot of work ahead to implement the full functionality of Surreal Engine rendering, so any help from readers is welcome in the form of code and pull requests.
OpenAL supported!
The big luck is that Surreal Engine uses OpenAL for audio output. Having written a simple hello world in OpenAL and assembled it in WebAssembly using Emscripten, it became clear to me how simple everything was, and I set off to port the sound.
After several hours of debugging, it became obvious that the OpenAL implementation of Emscripten has several bugs, for example, when initializing reading the number of mono channels, the method returned an infinite number, and after trying to initialize a vector of infinite size, C++ crashes with the exception vector::length_error.
We managed to get around this by hardcoding the number of mono channels to 2048:
alcGetIntegerv(alDevice, ALC_MONO_SOURCES, 1, &monoSources);
alcGetIntegerv(alDevice, ALC_STEREO_SOURCES, 1, &stereoSources);
#if __EMSCRIPTEN__
monoSources = 2048; // for some reason Emscripten's OpenAL gives infinite monoSources count, bug?
#endif
Is there a network?
Surreal Engine does not currently support online play, play with bots is supported, but we need someone to write AI for these bots. Theoretically, you can implement a network game on WebAssembly/Emscripten using Websockets.
Conclusion
In conclusion, I would like to say that the porting of Surreal Engine turned out to be quite smooth due to the use of libraries for which there are Emscripten ports, as well as my past experience in implementing a game in C++ for WebAssembly on Emscripten. Below are links to sources of knowledge and repositories on the topic.
M-M-M-MONSTER KILL!
Also, if you want to help the project, preferably with WebGL/OpenGL ES rendering code, then write to me in Telegram:
https://t.me/demenscave
Links
https://demensdeum.com/demos/SurrealEngine/
Leave a Reply