Unrealty logo

Out of the blue, Vito put this idea in my head:

@icculus @EpicRayD @EpicCog Um, you folks heard of anyone getting their UnrealEngine 1 Linux ports cross-compiling under Emscripten?

— Vitorio (@vitor_io) February 17, 2015

…and made sure it stayed there…

@icculus whyyyyyy are you recompiling UE1 code? ARE YOU PORTING UE1 TO EMSCRIPTEN?????

— Vitorio (@vitor_io) December 9, 2015

is there a patreon for UE1 emscripten support anywhere

— Vitorio (@vitor_io) September 12, 2016

And then one late night, I found myself pushing Unreal Tournament '99 through Emscripten, you know, just to see how far I could get before giving up.

Turns out, I got pretty far.

YASSSSSSSSS 😻

— Vitorio (@vitor_io) April 18, 2017

Naturally, I wanted to see more Unreal Engine 1 titles on the web, and Vito wanted to see his Unreal Engine 1 title, so this was a match made in heaven.

Unrealty isn't wildly different from UT99; it was based on a slightly older version of the Engine, but most of the details we cared about still lined up, and Unreal Engine 1 takes some effort to remain backwards compatible with data files from older revisions in any case.

Unrealty added a few native classes (C++ code in .DLL files that the engine would load at runtime and glue together with UnrealScript, the engine's custom scripting language), but otherwise, it's just the same engine as Unreal Tournament.

But here, with those handful of native classes, our troubles began.

We spent a good amount of time digging through email archives, old spindles of CD-ROMs, and even boxes full of 15-year-old shipping envelopes trying to find the source code to those pieces! The UnrealScript, compiled down to a .u file, would Just Work on any platform, but we needed the source to the C++ pieces or we were out of luck. We briefly joked about writing a win32 DLL loader for Emscripten before Vito finally hit the jackpot and dug out an email with an old version of the missing pieces. I spent time disassembling the existing binaries to see what it wanted versus what we had, and with a little digital spackle, we were back in business.

Our next big obstacle was the renderer. Unrealty is a little heavier than Unreal Tournament, and the OpenGL renderer stumbles in the same way that every other game does—Direct3D is more forgiving of inefficient rendering patterns—so no one notices until the Linux/Mac/whatever port arrives.

For UT99/emscripten, I had taken the original UT99 fixed-function pipeline OpenGL code and coerced it into running in a web browser, where there is neither OpenGL nor a fixed-function pipeline at all. This was non-trivial. Using libRegal, Unreal's GL 1.1 renderer would make OpenGL calls, converted to OpenGL ES 2.0 calls, which Emscripten converts to WebGL calls (which, perhaps, then the browser pushes through libANGLE to convert to Direct3D calls). It's not efficient! Totally reasonable GPUs would get single-digit framerates on some scenes. We needed a better solution.

That better solution came in the form of XOpenGLDrv, written by the talented Smirftsch at OldUnreal. XOpenGL is a custom OpenGL 4 renderer for Unreal, with all the legacy fixed-function pipeline support replaced by sleek shaders and vertex buffers and such. Added benefit: it also supports OpenGL ES 3.0! Since Emscripten will turn GLES3 into WebGL2 directly, we spent the time to port XOpenGLDrv to Emscripten. Now we were rocking complex scenes at 60fps in a browser window.

We took a strange hybrid approach to browser compatibility: almost anything will handle asm.js (that is, everything that doesn't handle asm.js will just run it like any other JavaScript; good enough), but WebAssembly is the new hotness and we wanted to support that, since Unreal is a non-trivial piece of code and we'd like the browser to spend less time parsing JavaScript if possible. In addition: the linker magic we needed to do to support OpenGLDrv, with various OpenGL symbols overridden by both libRegal and Emscripten, can not live in the same build as XOpenGLDrv…but not everything we consider reasonable can use XOpenGLDrv, which requires WebGL2 support from both the browser and GPU.

We ended up having the main loader page decide what your browser can do, and toss you to one of four different builds of the game: asm.js+WebGL1, wasm+WebGL1, asm.js+WebGL2, or wasm+WebGL2. A decent GPU on Firefox or Chrome will get wasm+WebGL2, an older GPU will get wasm+WebGL1, Safari will get asm.js+WebGL1, etc.

The main loader page is also responsible for downloading assets before launching the engine; The theory is that index.html will run some Javascript to load the correct wasm/asm.js+opengl/xopengl combo, and then will load the game's base data (which is mostly a default Unrealty install) into an IndexedDB. Then it will load the requested locale the same way. Once things are in the IndexedDB, they don't need to be redownloaded unless the manifest.json file on the web server says something is new/deleted/different. This allows us to make sure you only have to download Unrealty once, even if you come back to visit later or mash the reload button. If something gets fixed on our end later, your browser only needs to download what's changed to get started again. If you want to explore a different locale, you already have all the base data and just have to grab a little bit more on your first visit to the new locale.

Once everything is downloaded, we copy it from the IndexedDB to an Emscripten MEMFS and start the Unreal engine running; this takes more memory than we'd like, but in modern times one can keep the entire project in RAM on low-end hardware without noticing, in most cases. Unreal would need serious redesigns to make all file i/o asynchronous, but if that ever happens, we could work with the data files right out of the IndexedDB.

Now the engine, actually running, sees the Default.ini from the locale, reads in the appropriate files from the locale and base install, and everything (hopefully) does what you'd expect Unreal to do and cool pictures show up on your screen.

More or less, that was the hard work. After that, it was a matter of seeing how this performed in various browsers and wrestling the original unrealty.net domain name away from a domain squatter. Thanks to the effort, Unrealty can now continue from here as a living artifact of Unreal Engine history.

Thanks for reading! My work on this project was funded by smart and pretty people on my Patreon. I can tell from all the way over here that you are also smart and pretty, so you should go hang out with all the others by contributing. Your support makes fun projects like this possible!

Oh yes, if you also have an Unreal Engine 1 game that you might like to be able to play in a web browser, please get in touch; we might be able to get you up and running pretty quickly!

—ryan.