Web Target

1. Introduction

Warning

This feature is in development now. We can already build and run a CGE application in the browser but it’s not yet our real rendering — WebAssembly code only renders an unimpressive yellow rectangle. Stay tuned!

The goal of the web target in Castle Game Engine is to allow you to recompile more-or-less any application you wrote using Castle Game Engine (using TCastleWindow) to the web. So you can put your application on a website, as part of a regular HTML webpage, and users can play it instantly, without the need to download / install any native application or browser plugin.

Castle Game Engine - Pas2js initialized WebGL context Castle Game Engine in Firefox Castle Game Engine in Vivaldi

2. How it works (or will work)

2.1. Already working

  • We’re using FPC WebAssembly target.

    Huge thank you go to the whole FPC team for making it possible! For general documentation (unrelated to CGE), see this article by Michael Van Canneyt.

  • To run the WebAssembly in a web browser, there is additional "glue" code, which we also write in Pascal and compile using Pas2js.

    The integration between Pas2js and FPC + WebAssembly is extremely easy, again thanks to the work of everyone involved!

  • For development purposes, we can run a simple webserver on the localhost. Pas2js features a ready compileserver, we just use it.

    Note
    FpWeb also can instantiate a standalone HTTP server in just a few lines of code. But it seems we don’t need even this, compileserver is all that we need.
  • Our build tool takes care of everything. You usually want to execute 2 commands:

    1. castle-engine compile --target=web --mode=debug

      This command:

      • generates web-specific files (in castle-engine-output/web/),

      • compiles the glue code using Pas2js,

      • compiles the application code using FPC for WebAssembly.

    2. castle-engine run --target=web

      This command:

    Note

    It is also possible to just compile using FPC + WebAssembly to a .wasm file. This does a subset of work performed by castle-engine compile --target=web. To do this, just execute:

    castle-engine compile --os=wasi --cpu=wasm32

    You likely don’t need to use above options, unless you have your own idea how to deploy the .wasm file. The --target=web does everything for you.

  • We create a HTML file with the <canvas id="castle-canvas"> element (see canvas docs at MDN). This HTML runs the JS code (compiled by Pas2js) that will in turn run the WASM code of your application, which will initialize and use WebGL context on this canvas.

    This HTML file can be customized / replaced / rejected as you wish. It only needs to contain a reference to the generated JavaScript file using <script> and a <canvas> element with ID castle-canvas. You can surround it with any content and style.

    For nice look, our generated HTML also uses Bootstrap, but again: this can be customized / removed. We don’t depend on any special CSS or JS libraries.

    Some alternative HTML templates may be available in the future, as well as the ability to provide your own template. For now, just customize the engine’s tools/build-tool/data/web/dist/index.html file.

  • We use WebGL API from WebAssembly through the JOB units implemented in Pas2js + FPC. This is a collection of units (on both Pas2js and FPC sides) that cooperate with each other. The goal is to allow our WASM applications to access WebGL API defined in this WEBIDL file.

    We create our unit CastleInternalJobWeb using this script and WEBIDLs: https://github.com/castle-engine/castle-engine/tree/webassm_platformer_test/src/base_rendering/web/webidl .

    Note
    The WebGL API is available in the browser to Pas2js (that is, any JavaScript code in the browser). If not for JOB, we would expose this API for the WebAssembly, following the canvas example described in the 2nd part of this article. But, well, using JOB makes things even easier.

2.2. Plans (TODOs)

  • TODO, IN PROGRESS NOW: We render using WebGL 1.0, with optional WebGL 2.0 features. This is very similar to current rendering on mobile, where we use OpenGL ES 2.0, with optional OpenGL ES 3.0 features.

  • TODO: Our editor also does the above steps, just change the platform using "Run → Platform → Web" menu item and hit "Build and Run" (F9).

  • TODO: For audio, we will add a new sound backend using WebAudio. This will be the default sound backend on the web (like the OpenAL is now the default backend on non-web platforms).

    It is also possible our FMOD sound backend will also be ported to web, as FMOD supports HTML5. This would make FMOD a truly cross-platform sound backend, working on every platform we support in CGE.

    Note
    About WebAudio in the context of X3D 4.0. Supporting advanced X3D 4.0 audio nodes is not our priority, but since WebAudio will happen anyway, on the web…​ they will open this possibility. There is also LabSound that provides WebAudio-like API on non-web platforms, and in principle it could one day replace OpenAL, making web and non-web audio handling closer.
  • TODO: For data delivery, we know we’ll have to invent a simple format to carry our game data as one big binary blob. The simple plan is to just pack game data into zip.

    Both FPC and Delphi have built-in support for ZIP handling, we even use it in one example, we’ll likely create a more full-featured wrapper that works on both FPC and Delphi.

    Packing resources into ZIP is beneficial over just placing all data files separately in the webserver directory. It allows to download the ZIP once (at application load) and then our WebAssembly code can load data from it synchronously, thus it can work similarly to how a non-web game data is loaded. Naturally we also want to have TCastleDownload working and being able to load things asynchronously (see our network docs).

    TODO: FPC provides some ready file implementation in WASI RTL, it seems. How it works? In browser, in wasmtime?

  • Possibly throw in additional compression of data and/or wasm executable?

    So far, file-size tests are promising. Compilation of examples/platformer, which practically uses 100% of CGE units, yields a binary platformer.wasm that has 16 MB. Gzipped it has 3.4 MB.

    The gzipped size is really what matters — both web browsers and servers support gzip-(de)compression on the fly, you can also just put ready-gzipped version on the server and tell the browser to just decompress. So in all practical cases, users will download 3.4 MB, not 16 MB.

    Note that above is for a release build. The debug build weights 51 MB, and gzipped 12 MB. (Debug build contains debugging symbols.)

    We could also use Brotli, a newer compression method also commonly supported by web browsers and servers.

3. Prerequisites

  • Pas2js.

    You can get it along with FPC or install it separately from here.

    Warning
    We test with the latest Pas2js from GIT right now, so this is also what we advise to use. The stable release 3.0.1 from https://getpas2js.freepascal.org/ is not supported right now (due to incompatibilities around Job.Js and friends usage, which are all solved in latest Pas2js + FPC). With time we may change this (adjust to older Pas2js) and/or a new release of Pas2js may happen which will make things easier :)

    Test that it works. Create the following simple program, save it as hello_pas2js.lpr:

    begin
      Writeln('Hello from pas2js!');
    end.

    If everything is installed and configured OK, then you should be able to compile it using this from the command-line:

    pas2js -Jc -Jirtl.js -Tbrowser hello_pas2js.lpr

    If everything went well, you should get a file hello_pas2js.js in the same directory.

    Note

    If pas2js reports an error like Error: can’t find unit "System", it most likely means that the pas2js.cfg configuration file is missing or wrong. You can either generate it using createconfig tool or take and adjust the sample from pas2js.cfg wiki page, then place the resulting file in a system-specific location indicated on that wiki page.

    It does seem that fpcupdeluxe, although it provides pas2js and compileserver binaries (good!), but it doesn’t provide the createconfig tool nor does it contain the relevant pas2js package structure (you need files like packages/rtl/system.js). So instead get pas2js from https://getpas2js.freepascal.org/ and make sure it’s the first on $PATH. TODO: Submit issue to fpcupdeluxe about it.

  • compileserver from Pas2js.

    This is a very useful tool distributed along with Pas2js. Make sure you got it, test it by executing on the command-line this:

    compileserver

    Leave it running and access http://localhost:3000/ in a web browser.

    Kill it by Ctrl+C in the terminal.

    Why is this useful? Most dynamic webpages should be tested through a webserver running on your system (like compileserver, though you can also spin a full Apache or Nginx) that serves the HTML, JS, WASM and other files. You view them using a http://localhost:…​/ URL, which means you access the webserver on your own system. In contrast, just opening the HTML files in a web browser (which results in URLs like file:///…​/index.html) will usually not work, because of security: JavaScript code on such pages will not be able to load extra resources (like WASM files). It’s possible to configure your browser to circumvent this (disable CORS checking) but it’s a hassle. Using a "temporary webserver" like compileserver is the easiest way to test your applications.

  • FPC with WebAssembly support.

    In our opinion, it’s easiest to get this using fpcupdeluxe: install FPC "trunk" version, then install FPC cross-compiler for OS=Wasi and CPU=Wasm32. You can also just get it from GIT and compile manually, following FPC wiki about WebAssembly.

    Test that it works. Create the following simple program, saved as hello_wasm.lpr:

    begin
      Writeln('Hello from WebAssembly!');
    end.

    If everything is installed and configured OK, then you should be able to compile it using this command:

    fpc -Twasi -Pwasm32 hello_wasm.lpr

    If everything went well, you should get hello_wasm.wasm in the same directory.

4. How to test what we have now

Branch webassm_platformer_test in CGE contains the work on WebAssembly. To test it:

  1. Get the webassm_platformer_test branch of CGE.

  2. Follow compiling from source docs to compile at least new CGE build tool from this CGE branch.

  3. Enter any CGE project and compile and run it for WebAssembly. For example, to compile the examples/platformer project:

    castle-engine compile --target=web --mode=debug
    castle-engine run --target=web

Currently, the project executes, printing logs (using our CastleLog) to the browser console. The rendering is not yet working.

5. TODO: How to continue

  • The current code fails (both in wasmtime and browsers) at glGetString(GL_VERSION) call, because OpenGLES routines are nil. So the situation is clear, making the rendering work using WebGL will eliminate this. (In particular glGetString(GL_VERSION) → should be routed to gl.getParameter(gl.VERSION) in WebGL.)

    We added an exception Exception: Failed to load OpenGL(ES) library when not Assigned(glGetString) to make it clear.

  • Make the rendering work, by routing calls to WebGL on the web.

    • We already call WebGL from WebAssembly using JOB integration in CastleInternalWebJob.

    • TODO: Next steps: integrate WebGL usage throughout CGE code, utilizing the fact that it’s similar (though not identical) to OpenGL ES 2.0.

    • Maybe: Define CastleInternalWebGL unit, exposing these calls in a flat way, to be similar to OpenGLES, and add macro CastleGLES:=CastleInternalWebGL in castleconf.conf. This is to be decided, how much we want to "pretend that WebGL is OpenGLES".

    • Test rendering first on some simple application without any data, just do some DrawRectangle from CGE in TCastleUserInterface.Draw. Hack any CGE example for this.

    • Once we got this rendering, we need to pass resources, from our data, to web application — preferably as one big zip, to allow synchronous reading of it in WebAssembly. See "How" plans.

  • Then finish the plans in "How" section above: data from zip, sound, automatic webserver, etc.

  • Trung Le (Kagamma) has been doing lots of work with CGE + FPC WebAssembly. His fork contains a branch wasm32-wasi-port. Merge and/or cherry-pick this work to CGE as needed. Thank you!

6. Running WASM code outside of the browser

WebAssembly is not limited to running in a web browser. We support running the compiled WASM binaries also using Wasmtime, though without rendering.

Warning

Trying to render anything in Wasmtime will fail, as we don’t have access to WebGL API and context. So you cannot even create TCastleWindow, i.e. it will fail inside Wasmtime with

Exception: Failed to load OpenGL(ES) library

and we don’t have immediate plans to make it work. But, at least everything else (all the code up to the rendering) will work on Wasmtime.

If you know of an easy way to pass WebGL API and context to application run in Wasmtime (e.g. implementing WebGL by OpenGL(ES) on the desktop), let us know. And note that we prefer to pass WebGL API, not OpenGL(ES) API, to our application run in Wasmtime. We want the wasmtime to work as close to web browser as possible, 1. because it avoids wasmtime-specific maintenance, 2. because it makes wasmtime more suited to debugging what happens on the web, which is its main use-case for us.

Just run then like this on the command-line:

export WASMTIME_BACKTRACE_DETAILS=1 # get a more detailed backtrace
wasmtime <project-name>.wasm

The WASM file is in the project top-level directory if you compiled using

castle-engine compile --os=wasi --cpu=wasm32 --mode=debug

or inside castle-engine-output/web/dist/ if you compiled using

castle-engine compile -web=target --mode=debug

The main use-case of this is additional debugging approach. In case of a crash, the wasmtime will give you a useful stack trace with line numbers (if only you compiled with --mode=debug, as shown above).

6.1. Running engine test-suite on WASM

As a particular application of the above ("you can run engine code using wasmtime, except rendering"), you can run our test suite on WASM.

Warning
Making castle-tester.wasm work this way is a work-in-progress. The time_measurements_tests.wasm mentioned below works already.
cd tests/
castle-engine compile --os=wasi --cpu=wasm32 --mode=debug
wasmtime castle-tester.wasm --console --no-window-create

cd time_measurements_tests/ # enter tests/time_measurements_tests/ in CGE
castle-engine compile --os=wasi --cpu=wasm32 --mode=debug
wasmtime time_measurements_tests.wasm

7. Thank you

I want to thank everyone involved in this and let’s push forward! Web target is a really cool feature, from my talks I know it’s an important feature for many CGE users, and I feel we have it in our reach. Let’s keep coding and enjoy making games :)


To improve this documentation just edit this page and create a pull request to cge-www repository.