Skip to content

Commit

Permalink
Undeprecate WebAssembly support (#47)
Browse files Browse the repository at this point in the history
* WASM test

(cherry picked from commit 11e2cde)

* Cleanup the changes from the previous commit

(cherry picked from commit 52016dd)

* Set default build target to `wasm32-unknown-unknown`

This is so that rust-analyzer will use this build target when analyzing
code.

(cherry picked from commit ddfa884)

* Enable WASM threading and disable WASM audio

I turned on some compiler flags to enable experimental support for
atomics and shared memory in WebAssembly, because this will probably be
needed if we end up using web workers to handle the filesystem
operations in WebAssembly builds.

Unfortunately, rodio doesn't support web workers, and in fact rodio
crashes Luminol in WebAssembly when I simply turn on those compiler
flags without making any changes to the code!

The audio in WebAssembly builds will have to be reimplemented without
rodio. I don't think this will be too hard, given that all we really use
rodio for is straightforward audio playback...

* Disable WASM threading flags

Apparently you can't use shared memory from inside of WebAssembly
without passing extra arguments to wasm-bindgen that Trunk doesn't
support. That's okay, we'll make do without these compiler flags.

We still need coi-serviceworker.js because `Atomics.wait()` can't be
used without it in most browsers.

rodio also still won't work because we're going to run in a web worker,
where the audio APIs that rodio needs aren't available.

* Revert previous commit

Nevermind, there's no flag incompatibility issue with Trunk.
I'm probably just sleep-deprived...

* Refactor to not use `eframe::Frame`

I'm making a completely custom egui app runner for WebAssembly builds,
since eframe's web runner makes such widespread assumptions about the
availability of certain JavaScript APIs that are only present outside of
web workers that it's unsalvageable when it comes to getting it to run
in a web worker. (Both egui and wgpu can run in a web worker, though, so
a custom runner should work.)

But I ran into problems because implementing a custom runner requires
creating `eframe::Frame`s which can't be done outside of the eframe
crate.

In the future it would be best to stop using eframe entirely, but I
don't want to also have to rewrite the native runner right now so this
is the best I can do.

* Rudimentary web worker rendering

I added a new app runner that can run in a web worker.

The entire program now runs in a web worker, which means we can now use
things like mutexes, channels and other synchronization constructs
without making the browser angry when they block.

If you try it right now, the canvas will be very small and will not
react to user input. These things will be fixed later.

* Rewrite web worker launcher code in Rust

The WebAssembly code now runs in both the main thread and the worker
thread with the same code and the same memory (i.e. multithreading),
but with different entry points.

The code on the main thread gets some information from the environment
and sends it to the code on the worker thread. The worker thread runs
the actual program.

The purpose of doing this is so that we can write event handlers in
Rust, for things like detecting user input on the canvas from the main
thread and sending it to the worker thread, or sending a request for a
filesystem operation from the worker thread to the main thread.

* Add a hook to the main thread for detecting screen size changes

The main thread now listens for changes to the screen size and then
sends them to the worker thread so that it can resize the canvas to fill
the screen.

We use a `std::sync::mpsc::Sender` on the main thread to send the new
screen sizes to a `std::sync::mpsc::Receiver` on the worker thread. This
has the benefit of being more convenient than `Worker.postMessage()` and
also is probably faster because we don't have to deal with the
serialization overhead associated with `Worker.postMessage()`.

The Rust senders and receivers internally use WebAssembly's experimental
atomics and shared memory features since I enabled these things in the
compiler flags. They need Cross-Origin Isolation to be enabled to work,
so don't remove the coi-serviceworker.js shim.

* Web worker runner now responds to mouse events

* Move event handler code into web_worker_runner.rs

* Remove duplicate `window` from event handler code

* Use Tokio channels instead of std channels

We need a channel with an asynchronous `recv()` method in order to send
messages from the worker thread to the main thread, so we might as well
use Tokio channels everywhere.

* Fix memory leak in `WebWorkerRunner::setup_render_hooks()`

At least I *think* this was a memory leak.

Anyways, the use of `CustomFrame` was somehow causing the rendering to
slow down slightly every frame. So that makes me think there was a
memory leak somewhere.

I couldn't figure out how to fix it properly so I just removed
`CustomFrame` since we don't need it anyways. But the most important
thing is that it runs at full speed now!

* Add a channel for filesystem operations

This commit adds a channel from the worker thread to the main thread so
that the worker thread can send the main thread requests for filesystem
operations. The results are sent back using a Tokio oneshot channel.

* Channel senders are now stored as clones instead of by reference

This frees us from having to deal with manually specifying lifetimes
while still otherwise behaving exactly the same.

* Set `max_texture_side` properly in `egui::RawInput`

* Implement `FileSystem` for WebAssembly builds

The `File` implementation is not done yet, but it will be soon!

* `FileSystemCommand` variants are now private

* Implement `File` for WebAssembly builds

We now have a complete filesystem implementation for web browsers.
Finally.

* Implement project loading for WebAssembly builds

Project loading is quite slow!

I will be experimenting with a path cache for the filesystem later to
see if that helps with speed.

* Use egui context's time instead of system time

`std::time::Instant` can't be used in WASM.

* Also stop using system time in tilepicker.rs

* Split sprite.wgsl and tilemap.wgsl into two parts

* Enable wasm-bindgen support for reference types and weakrefs

* Use uniforms instead of push constants in web builds

* Remove `USING_PUSH_CONSTANTS` from shader headers

* Tilemap now changes the projection matrix instead of the viewport rect

Before, the tilemap implemented panning and scaling by simply panning
and scaling the viewport rectangle that it rendered onto. This meant
if any part of the tilemap were offscreen, that part of the viewport
would be offscreen, too.

The WebGPU standard actually specifies that you're not allowed to have
any part of the viewport outside of the screen! (i.e. the intersection
of the viewport rectangle and the screen rectangle must be equal to the
viewport rectangle)

Consequently, most web browsers will not allow you to do this.

The version of wgpu that we currently use, version 0.16.3, allows you to
do this on native builds, but only because the developers forgot to
implement a check for this. It's actually undefined behaviour - some
drivers will allow this and some won't. It will be explicitly disallowed
in a future release of wgpu and already is in their trunk branch.

So, I made a minor change to the tilemap so that its viewport rectangle
always stays on screen. Now, the viewport rectangle is always the same
as the UI rectangle that egui allocates for the tilemap, and the
viewport projection matrix changes instead to handle panning and
scaling.

This should be the last change needed for the map editor to work in
WebAssembly, although it also needs to be applied to the event graphics
and the tilepicker, which is not done yet.

* Tilemap event graphics now use projection matrix

The event graphics on the tilemap now render using the area allocated by
egui for the map editor as the viewport and handle scaling and panning
by modifying the projection matrix.

The event graphics in the hover tooltips do not do this, they just
render using the tooltip as the viewport because the tooltip doesn't
normally go offscreen. For some reason, egui sometimes draws the tooltip
offscreen for one frame before snapping it back onscreen, so a check is
added to disable rendering the tooltip graphic unless the graphic is
fully onscreen.

* Tilepicker now uses projection matrix

The map editor should now be working in web builds!
Except you won't be able to use the keyboard because I haven't put the
keyboard handling code into the app runner yet.

* Move JavaScript bindings into bindings.js and bindings.rs

* Upload bindings.js and bindings.rs

* Fix native builds

Native builds can now be built with `cargo build` in addition to web
builds being able to be built with `trunk build`.

I really don't like these hooks, but we need to enable build-std only in
web builds, and Trunk provides us no other way to do this.

* Apply clippy recommendation

* Add web event handler for keyboard presses/releases

* Add web event handler for modifier keys

* Remove unnecessary field from tile shader.rs

* Cleanup web keyboard handler code

* Add web event handler for text input

This is used by egui when typing in text boxes.

* Add web event handler for scrolling

* Fix log messages not appearing in web builds

* Fix handling of `pixels_per_point` in web builds

* Add web event handler for egui's cursor change requests

* Refactor web initialization code

This refactors the code for initializing Luminol in web builds to not
use global state that is shared between different parts of the program,
and also moves the `USE_PUSH_CONSTANTS` constant into src/components,
the only place it's used from.

* Add web event handlers for clipboard and opening URLs

* Add `Clipboard` feature to web-sys

* Remove unnecessary `mut` in src/filesystem/web.rs

* Add handler for RTP loading in web builds

Since we have no way to load from an arbitrary directory without the
user's permission, they're currently loaded from the "RTP" subdirectory
in the project directory. This can be changed in the future if required.

* Remove another unnecessary `mut` in src/filesystem/web.rs

* Refactor archiver to work in web builds

I don't have any archives to test with, but I can't think of any reason
why these simple changes wouldn't work. Feel free to prove me wrong!

* Remove unnecessary `.clone()` from src/main.rs

* Implement eframe storage and egui memory in web builds

This allows most state to be persisted. Unfortunately, we'll need a
different kind of storage to persist the project directory handles.

* Implement project directory persistence for web builds

Project directory handles are now stored in IndexedDB so that they can
be recovered after the user closes or refreshes the page.

* Unregister render loop and event listeners on panic

This prevents the console from being spammed with WebGPU errors in the
event of a panic.

* Re-add rodio as a dependency in web builds

* Save on mouseleave in web builds

* Restore the fullscreen toggle in native builds

* Restore playtest and console in native builds

* Restore the quit button in native builds

* Implement project creation for web builds

I had to replace surf with reqwest because surf won't compile with
wasm-bindgen > 0.2.84 and eframe won't compile with
wasm-bindgen < 0.2.86.

* Drop directory handles from IndexedDB on error

* Implement audio in web builds

I replaced the Symphonia decoders with rodio's default ones because
rodio can't decode OGG files properly when using Symphonia's decoder.

* Don't include `AudioWrapper` in native builds

* Use `.removeEntry` to remove files/dirs instead of `.remove`

`FileSystemDirectoryHandle.removeEntry` is the standard way to remove
files and directories. `FileSystemHandle.remove` is nonstandard.

* Fix `Drop` implementation in `AudioWrapper`

It's possible for the `AudioWrapper` to be dropped on a different thread
than it was created on. This commit handles that case.

* Show an alert box in web builds on panic

* Fix command names in hooks on Windows

* Use `wgpu::Adapter::features` to detect push constants support

* Don't show panic message if COI isn't enabled

* Archiver filesystem now wraps a file instead of a filesystem

* Apply clippy recommendations

* Re-enable sound effect picker in the items window

* Make some minor stylistic changes to names in src/filesystem/web.rs

* Fix RTPs not being detected properly in web builds

That's seriously embarassing, haha.

* Auto-enable `Write` when `Truncate` or `Create` are set in web builds

This probably shouldn't be necessary given that Rust's documentation
states that you need to manually enable `Write` to use `Truncate` or
`Create`, but I guess if it worked before in native then it should be
supported in web builds, too.

* Web filesystem `remove_dir` is now recursive

This is for consistency with the host filesystem's implementation.

* Make `project_path` in web builds consistent with native

* Optimize path cache to avoid double-loading the web RTP folder

* Move `skip` variable from previous commit outside of the loop

* Fix crash when loading from nonexistent folder in web builds
  • Loading branch information
white-axe authored Oct 12, 2023
1 parent 1bbbcc2 commit 8fdb26a
Show file tree
Hide file tree
Showing 60 changed files with 4,792 additions and 934 deletions.
3 changes: 3 additions & 0 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@ linker = "rust-lld"

[target.x86_64-unknown-linux-gnu]
rustflags = ["-C", "linker=clang", "-C", "link-arg=-fuse-ld=mold"]

[target.'cfg(target_arch = "wasm32")']
rustflags = ["--cfg=web_sys_unstable_apis", "-C", "target-feature=+atomics,+bulk-memory,+mutable-globals"]
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
**/coi-serviceworker.js linguist-vendored
Loading

0 comments on commit 8fdb26a

Please sign in to comment.