Introduction

This is the report of the initial effort to add Web Assembly (WASM) support to the Amethyst game engine.

Feel free to refer to this to learn from our experience.

Report accurate as of 2020-05-06.

Initial State

Before beginning any implementation work, some scout work was done to collate existing information from pre-existing attempts. The summary from each attempt is listed below.

Jojolepro – amethyst:wasm

Attempt at getting `amethyst` to compile with `wasm-bindgen`.
  • Top down approach – try to get amethyst to compile for wasm with minimal features, then incrementally enable crates.
  • Partial update of winit to 0.21.
  • Places crates / code behind feature toggles.

Relevant commits:

  • f6dd55e4

    • removes backtrace from amethyst_error

    • places the following crates under a feature flag in Cargo.toml:

      • amethyst_controls
      • amethyst_input
      • amethyst_ui
      • amethyst_utils
      • amethyst_window
      • winit
      • failure
  • 2c9a78f2: adds #[cfg(feature = "renderer")] in main amethyst crate.

  • ebeec5fb

    • Bumps winit to 0.21 and updates features.

    • Puts the following behind feature flags:

      • amethyst_audio
      • amethyst_ui
    • Removes #[cfg(feature = "renderer")] from src/app.rs:30

Semtexv – amethyst:rendy-all (based on #2040)

Updates `amethyst` to use `winit 0.21` and `rendy 0.5`.

Summary:

  • Updates winit to 0.21, including Event and screen logical / physical size changes.
  • Updates rendy to 0.5.1.
  • Updates most (all?) examples to properly run with winit's new event loop mechanism.

Interesting diff:

/// # Examples
///
/// ~~~no_run
/// let event_loop = EventLoop::new();
/// let mut game = Application::new(assets_dir, Example::new(), game_data)?;
/// game.initialize();
/// event_loop.run(move |event, _, control_flow| {
///     #[cfg(feature = "profiler")]
///     profile_scope!("run_event_loop");
///     log::trace!("main loop run");
///     game.run_winit_loop(event, control_flow)
/// })
/// ~~~

Omni-viral – amethyst:gl (PR #1877)

The first attempt that compiled and ran.
  • Updates winit to 0.20.0-alpha2.

  • Disables the following crates:

    • amethyst_audio
    • amethyst_network

    Also for amethyst_gltf:

    • mikktspace
  • Removes usage of rayon::ThreadPool.

  • Changes shader compilation script, and recompiles all shaders.

Jojolepro – web_worker

Allows rayon::ThreadPool to be used by using a custom constructor.

Jaynus – rendy:jaynus-fixes

  • Bumps gfx-hal to latest git master as of March 11, 2020
  • Compiles rendy/rendy examples to WASM using wasm-bindgen

Plan

Because a game engine covers many complex domains -- event loop, audio, graphics, etcetera -- we want to make sure there is a "good" starting state, and be able to develop and test each domain independently. In addition, we want to encourage contribution from the community where possible.

Jump Start

The plan to create a stable equilibrium used is:

  1. Get amethyst to compile for the wasm32-unknown-unknown target.
  2. Set up an end-to-end project to test usage of amethyst.
  3. Set up automated build to ensure all contributions maintain that base quality check.
  4. Make it easier for potential contributors to get into the "build, test, contribute" loop.

Equilibrium

There are two workflows for ongoing development:

  • Discovery

    1. Run end-to-end project to discover issues.
    2. Open a GitHub issue for each issue, providing any stack traces or screenshots, and the expected behaviour for the resolution of the issue.
    3. Provide some initial investigation to where the root cause of the problem lies, to make it easier to begin working on a fix.
  • Implementation

    1. Contributor finds an issue they wish to do.
    2. Locally, the contributor implements a fix, and tests it against end-to-end project.
    3. The change is submitted for review, and amended if necessary.
    4. After passing review and the automated build, the contribution is merged.

Implementation

This section covers the WASM support implementation in chronological (time) order.

ℹ️ Note: There is no design process as we are simply adding WASM support to existing functionality, or gating features as unsupported by WASM.

  • Week 1: Contribution Baseline – Preparation of repositories and guidelines.

  • Week 2: End-to-End Discovery – Event loop execution and first successful run of Pong.

  • Week 3: Stability and UI – WASM memory fix and UI rendering correction.

  • Week 4: Audio and Rendering – Audio playback and rendering fixes on Windows.

  • Week 5: Tidying – Input loading and automated testing with GL.

  • Week 6: Fin Ack – Web sockets, logging, and UI rendering correction.

Week 1: Contribution Baseline

Summary

Date: 2020-03-16 to 2020-03-21

  • Amethyst compiles to a WASM library.
  • Repository forks and branches are created.
  • Contribution guide is written.
  • Basic CI check is set up.
RepositoryCommit Range
pong_wasm9b403f69^..9f6240fe
amethyst8044b2a5^..0af12f84
rendy27e5cdc1^..757c4aa9
glutin (fork)f29d87a3
gfx-rs (fork)3e6db5f0
winit (fork)26e4374a^..04225a39
builder (CI agent)13730efd^..2bb67aae

End Result

$ wasm-pack build -- --features "wasm gl"
# ..
[INFO]: :-) Done in 37.87s
[INFO]: :-) Your wasm pkg is ready to publish at ./pkg.

Implementation

Based off the #2040 branch, which has the changes to use the new winit event loop introduced in winit 0.20.0.

  1. Get amethyst to compile with winit 0.22.0 for better WASM support. (winit#1478)

  2. Attempt to compile amethyst with wasm-pack.

    Instructions from https://rustwasm.github.io/docs/book/ were followed to package the library.

    When building with:

    wasm-pack build --target no-modules -- --features "wasm gl"
    
    • If it fails due to usage of a -sys library, feature gate the dependency and the code that uses it.

    • If it fails and requires a code change:

      1. Fork the repository.
      2. Create a wasm branch.
      3. Amend the code.
      4. Point amethyst at the forked repository.
      5. Make a pull request back to the original repository.

    Repeat until wasm-pack succeeds.

  3. Create end-to-end application, and make sure it compiles: pong_wasm.

  4. Write contribution guidelines. (amethyst#2171)

    Make it easy:

    • Links to all forks and branches.
    • Cut and paste commands.
    • Setup and development instructions.
  5. Create CI job to build amethyst as a WASM library. (amethyst#2175)

    Note: CI agent needs wasm32-unknown-unknown target, and rust-src component.

  6. Publicise the WASM effort on the community forum and chat server.

Week 2: End-to-End Discovery

Summary

Date: 2020-03-22 to 2020-03-28

  • Limitations discovered around:

    • winit event loop and WASM event loop requirements.
    • Web worker threading requirements.
    • Audio loading requirements.
    • Texture loading requirements.
  • Assets load from HTTP source using XHRs.

RepositoryCommit Range
pong_wasm3bcf94de^..de355df6
amethyst65a1e27a^..1d491d18
rendye1e03fee^..4de9ca2a
winit (fork)8595aec7^..9827b34a
gfx-rs (fork)a9a4419d^..672f551a
web_worker892abf29^..2b78b6ca

End Result

Implementation

  1. Clean up web_worker repository.

    • Move jojolepro/web_worker to amethyst/web_worker
    • Allow constructing thread pool without JavaScript (workers still need worker.js to run).
  2. Get dispatcher to execute serially – "no-parallel". (amethyst#2177, amethyst#2189, amethyst#2191)

  3. Assets load from server via XmlHttpRequests. (amethyst#2180)

  4. Get GL to render correctly. (amethyst#2198)

  5. Update pong_wasm to run. (pong_wasm#3bcf94d)

Week 3: Stability and UI

Summary

Date: 2020-03-29 to 2020-04-04

  • UI pass works
  • WASM app doesn't crash 90% of the time from double mutable borrow in winit.

End Result

Implementation

  1. Winit stability fixes.

  2. Take in HTML <canvas> element from user. (amethyst#2202, rendy#48915cb, pong_wasm#1)

    • Update to gfx-hal 0.5.
    • Don't invert coordinate system. (rendy#5d10084)
  3. Fixed UI pass by using consistent shader variable names. (amethyst#2205, amethyst#2207)

Week 4: Audio and Rendering

Summary

Date: 2020-04-05 to 2020-04-11

  • GL depth buffer fix -- rendering is corrected on Windows.
  • Audio plays in WASM, albeit delayed.

End Result

Implementation

  1. Split audio logic from other systems. (amethyst#2215, amethyst#2216, pong_wasm#2)

    This allows the rest of Pong to run even if audio is disabled.

  2. Initialization stability on Windows:

    Make drag and drop winit feature optional. (amethyst/winit#a2eea3e, winit#1524)

  3. Fixed gfx issues.

  4. Get audio to play on WASM.

Week 5: Tidying

Summary

Date: 2020-04-12 to 2020-04-18

  • Input configuration loads from server.

  • amethyst_test updated to work with winit 0.22.0 event loop.

    Tests that require a graphical backend can run on CI without dedicated graphics cards by using the software GL rendering backend -- tests can be run through XVFB.

End Result

Implementation

  1. Load input configuration from server using JavaScript. (amethyst#2214, pong_wasm#4)

    This is simply a fetch invocation. For better UX, we should:

    1. Initialize the fetch from within the application
    2. Return control to the browser so that it is responsive, and indicate to the user that a resource is being fetched, such as by rendering a spinner.
    3. Resume application initialization after the fetch has returned.
  2. Remove requirement to specify shred in crate [patch] section. (amethyst#2238)

  3. Update amethyst_test framework to work with new winit.

Week 6: Fin Ack

Summary

Date: 2020-04-19 to 2020-04-25

  • UI Coordinates / Screen Dimensions correction
  • Configurable Web Logger
  • Net Server and Client

End Result

Implementation

  1. Allow web logger to be configured. (amethyst#2249, amethyst#2250, console_log#6)

    This was necessary as the rate of log messages was about 1000 messages per second. This meant:

    • Filtering for relevant log messages was extremely difficult.
    • The browser would lag and be unusable to work with.
  2. Set canvas width/height only if unset. (amethyst#2247, amethyst/gfx#8537dfb, gfx#3224, gfx#3225)

    Quirk in this bug fix is, when setting the "width" attribute on the <canvas> element, the next read of width() still returns the old value. However, the updated value is returned after interacting with the canvas' WebGL2 graphics context.

  3. Provide WebSocket transport layer implementation for amethyst_network. (amethyst#2251, amethyst#2253, autexousious#209, autexousious#221)

    • Use tungstenite for native targets.
    • Use web-sys::WebSocket for wasm32-unknown-unknown target.

Issue Summary

ItemCrate(s)WASM Rush linksUpstream Status
Events must be Clonewinit, amethyst#2211, #2040#1478
rendy to use gfx-hal 0.5rendy, amethyst_rendyrendy:wasm, #2198#275, #277
WASM: parallel dispatchamethyst, web_worker#2177, #2189#2191
Load assets asynchronouslyamethyst_assets#2180, #2182#2228
Load textures as thread localamethyst_rendy#2174✔️ #2198
winit double borrow mutwinit4fbf95b✔️ #1512
winit::requestAnimationFramewinit3d5274b✔️ #1493, #1519
Use user provided <canvas>rendy, amethyst_rendy#2202, 48915cb, #1
Use same shader variable namesamethyst_ui#2205, #2207✔️
winit drag-n-drop optionalwinita2eea3e#1524
Clear GL depth buffergfx-hal35c45ac✔️ #3202, #3205
Load Rgb8Srgb texture formatgfx-halc4b75d3✔️ #3222, #3223
Proper AudioSocket supportcpalee1ee1a, #2195, #2219, #3#372, #2222
Load configuration from serveramethyst#2214, #4
Integration test supportamethyst_test#2241, #222, #223✔️ #2240, #2245
Configurable web loggerconsole_log, amethyst#2249, #2250✔️ #6
Overwritten canvas dimensionsgfx-hal#2247, 8537dfb✔️ #3224, #3225
WebSocket transport layeramethyst_network#2251, #209, #221✔️ #2253

Future Work

Future work is largely tracked by the issues and PRs marked ❌ on the issue summary page, and can also be seen in the amethyst repository's WASM project and on the issue tracker under the "WASM Support" tag.

Notable items are:

These are necessary for applications to "just work" across major browsers and OSes, and work performantly.