-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
Run Bevy in web worker context #8278
base: main
Are you sure you want to change the base?
Conversation
…creencanvas elements on the wasm target.
…ent. Doesn't handle OffscreenCanvas properly yet.
…cked up by OffscreenCanvas.
… that somehow got lost in refactors
Welcome, new contributor! Please make sure you've read our contributing guide and we look forward to reviewing your pull request shortly ✨ |
I also put together a simple working example here. |
Hi, this is really good work, thank you @haibane-tenshi, and I hope it gets some more attention. We are using bevy inside a broader HTML app, and need to have the app remain responsive while the game part chugs, which it must on load. Having it off the main thread is pretty crucial. Without a web worker, the alternative is booting up an iframe, serving it from a completely distinct origin, and relying on modern browsers to spin up a separate thread because the origin is different, thus emulating a web worker. This is quite complicated to do, you can't just whack the iframe html+js+wasm on Basically, if you want process isolation today, you need to buy another domain. It would be great if bevy could support passing in an OffscreenCanvas and rendering to it from a web worker. It seems the main caveat is that winit can't subscribe to resize events on its own because it won't have access to the DOM tree -- am I getting that right? Is there anything else keeping this PR a draft? |
The current beta release of Winit has resolved this and other related issues already. See rust-windowing/winit#2778 and rust-windowing/winit#2834. |
I have looked into all this further. Sadly I don't think bevy in a worker context is going to work anytime soon. The winit PR 2788 would suffice for the following scenario, aka #4078 bevy multithreaded on wasm:
This PR is a different approach, aka "bevy completely inside a web worker, including winit". I have realised that it's a bit more difficult than I thought. Winit running on a worker thread won't be able to receive any interaction events, like clicks. I'm not sure how rust-windowing/raw-window-handle#134 really helps. What would winit even do with a Single-threaded bevy in an iframe has none of these issues, as it can access all the DOM it likes within the iframe, with the aforementioned caveats about getting process isolation to work. So I think we'll stick with that for now. Other people are similarly motivated to get at least some of bevy off the main thread and one of these options will end up being the path of least resistance. It will probably be multithreading, IMO. |
FWIW, I've been using multi-threaded Wasm with Winit and rendering in a separate thread with Wgpu for a while now and it works just fine.
rust-windowing/winit#2834 makes |
@daxpedda do you have a repo demonstrating this set up? |
I don't, but I can make one! |
I can't speak too much to the winit side here, but, to take inspiration from pure JS land, in Three.js, the workflow for rendering to an offscreen canvas is described here: https://threejs.org/manual/#en/offscreencanvas All click/touch interactions would need to be received "manually" via messages to the web worker from the main thread. It might be helpful to note that messages can be sent to workers via the worker.postMessage API, but also through the MessageChannel API. Both of these APIs allow sending transferrable objects to workers cheaply. |
@haibane-tenshi any thoughts on using the internal JsValue integer to identify the canvas? E.g., changing wgpu-hal to something like this: unsafe fn create_surface(
&self,
_display_handle: raw_window_handle::RawDisplayHandle,
window_handle: raw_window_handle::RawWindowHandle,
) -> Result<Surface, crate::InstanceError> {
match window_handle {
raw_window_handle::RawWindowHandle::Web(handle) => {
let window = unsafe {
wasm_bindgen::JsValue::from_abi(handle.id)
};
match window.dyn_into::<web_sys::HtmlCanvasElement>() {
Ok(canvas) => {
return self.create_surface_from_canvas(canvas);
}
Err(window) => match window.dyn_into::<web_sys::OffscreenCanvas>() {
Ok(offscreen) => {
return self.create_surface_from_offscreen_canvas(offscreen);
}
Err(_) => {
return Err(crate::InstanceError)
}
}
}
}
_ => Err(crate::InstanceError)
}
} |
FWIW: see gfx-rs/wgpu#4888 and rust-windowing/raw-window-handle#157. |
@daxpedda those changes are now released as wgpu 0.19.1 and raw-window-handle 0.6.0. Would you be interested in co-authoring a PR to bump bevy to these versions so that we have offscreen support in the next release? |
Unfortunately I don't believe I would have the time for that, but I don't think it's necessary as Bevy is already hard at work: #11280. To whom it may concern: I've finally found some time reworking my threading library and closing in on a release, you can follow the progress in https://github.com/daxpedda/wasm-worker. |
Indeed, somehow I missed that.
Interesting stuff! I look forward to seeing some more examples/tests for your messaging API, in particular, I am curious as to how you handle stuff that needs to be posted or transferred. I have been working on a RPC implementation to solve this problem here: https://github.com/rustifybe/worker-rpc/tree/master/worker-rpc/tests |
@daxpedda I am trying out When I construct a
However, when it's retrieved in
This is how I am trying to set things up: struct OffscreenPlugin {
handle: RawHandleWrapper,
}
impl OffscreenPlugin {
fn new(canvas: OffscreenCanvas) -> OffscreenPlugin {
let handle = WebOffscreenCanvasWindowHandle::from_wasm_bindgen_0_2(&canvas);
OffscreenPlugin {
handle: RawHandleWrapper {
window_handle: RawWindowHandle::WebOffscreenCanvas(handle),
display_handle: RawDisplayHandle::Web(WebDisplayHandle::new()),
}
}
}
}
impl Plugin for OffscreenPlugin {
fn build(&self, simulator: &mut App) {
simulator
.add_event::<WindowResized>()
.add_event::<WindowCreated>()
.add_event::<WindowClosed>()
.world
.spawn(bevy::window::Window {
resolution: bevy::window::WindowResolution::new(500.0, 500.0),
..Default::default()
})
.insert(bevy::window::PrimaryWindow)
.insert(self.handle.clone());
}
} have you seen something similar on your end? |
Ok, the - let handle = WebOffscreenCanvasWindowHandle::from_wasm_bindgen_0_2(&canvas);
+ let handle = WebOffscreenCanvasWindowHandle::from_wasm_bindgen_0_2(Box::leak(Box::new(canvas))) Ideally I would just store the |
In Winit we use a See also the |
Objective
This is a tweak to Bevy which (potentially) allows it to run as a web worker.
Related work
This PR is based on those two PRs:
Although at this point it is almost an entire rewrite since there was a lot of changes to related Bevy APIs during 0.9 -> 0.11.
Regarding the state of those two:
The first PR seems to be no longer applicable.
The purpose of that PR was to change
Window.raw_window_handle
to an option to allow to create windows without an associated raw handle. However, the field since was removed. Now,WinitPlugin
is in full control over creation of raw handles. Other changes are mostly various helper methods.I would suggest closing this one, as there is no hope for it to get merged in this state and there is no tangible way forward without a full rewrite.
The second PR in large parts also seem to be no longer applicable due to API flux around
Window
struct. Current PR borrowed ideas from it but adapted them to modern version of Bevy.Regarding the state of this PR: it isn't intended for merge in this form, but rather it is an exploration of how much effort is needed to make Bevy run on web workers. I wanted this for my pet project, but also needed a newer Bevy version compared to the other PR.
Also, relevant issue: rust-windowing/raw-window-handle#102
High-level summary
The core issue we are trying to address is correct creation of
wgpu::Instance
. On the web it requires types fromweb_sys
(HtmlCanvasElement
orOffscreenCanvas
) to properly initialize WebGL context.Instance relies on
RawWindowHandle
to provide rendering context. However,RawWindowHandle::Web
contains a singleu32
. It makes sense in context ofraw-window-handle
library, but causes issues for us.wgpu::Instance
internally uses the integer to look up corresponding canvas element via css selectors - and this is where it goes wrong. There is no DOM access from inside worker thread.We combat this by introducing an
AbstractHandleWrapper
which can hold eitherRawHandleWrapper
or web canvases.Changelog
Introduced
AbstractHandleWrapper
Replace usage of
RawHandleWrapper
withAbstractHandleWrapper
including inside ECS.More specifically it affected implementation of
RenderPlugin::build
,bevy_winit::system::create_window
,bevy_render::view::windows::prepare_windows
and changed type ofExtractedWindow.handle
.Added
bevy_window::Window.web_element: WebElement
which allows to specify how Bevy should discover rendering context on web platform.The field is ignored on non-wasm targets.
Removed
bevy_window::Window.canvas
as its functionality was subsumed byweb_element
.Changes to logic of
WinitPlugin
to properly takeWindow.web_element
into account.Fallout
It is important to keep in mind Bevy can be run in two different modes on web: on main event loop or inside worker.
When running Bevy as main situation remains largely unchanged. Internally, we end up eagerly committing canvas into ECS and using that instead of discovering it every time using selectors. Externally, users get an ability to provide canvas object directly - minor ergonomic improvements, I guess?
However, currently
WinitPlugin
will panic when encountering any window withWebElement::OffscreenCanvas
. The reason for that is simple: we cannot create awinit
window out of offscreen canvas - which causes it to create a newHtmlCanvasElement
.The result is confused state: we render to
OffscreenCanvas
but listen to events of some other unrelated element. To prevent thisWinitPlugin
disallows creation of windows out of offscreen canvases.When running Bevy as worker things are... well, let's call it manageable. There are lots of sharp edges:
Attempt to create window with anything but
WebElement::OffscreenCanvas
is doomed to panic:Generate
andCssSelector
require access to DOM which worker doesn't have.HtmlCanvasElement
, so even if you get one (which I believe you shouldn't be able to because it is not transferrable) it won't work.By extension it implies that including
WinitPlugin
results in guaranteed panic: it is unable to create windows out ofOffscreenCanvas
as already discussed.This implies that we need to set up event loop ourselves since that is job of
WinitPlugin
. Also default event loop panics too, but I didn't look into that yet.This also implies the need to manually pipe events through to Bevy as that is also job of
WinitPlugin
.I tried to teach
WinitPlugin
to work with "virtual" windows (e.g. Bevy windows without a backing winit window),but that was messy. It will require some sweeping refactors to
bevy_winit
crate to clean up the logic.Open questions
I don't mind tinkering with this more as I go, but there are some questions about directions.
AbstractWindowHandle
is a weird beast.First part (which I didn't fix) is that
RawWindowHandle
is completely ignored on wasm. So the type is less of a enum, but more like a struct with platform-dependent fields.Second part, we cannot avoid platform specific code around instantiation of
wgpu::Instance
because ofRawWindowHandle
. Or could we?Integer provided by
RawWindowHandle::Web
is ultimately for us to interpret. Its justInstance
uses it for css selector query. One idea that I had is instead to use the index to look up canvas objects in global JS scope. It can be done either inside Bevy (by interceptingRawWindowHandle
before it is passed toInstance
like it is done here) or via introducing change towgpu
.Downsides of such setup are relatively obvious: this is rather obscure for anyone coming from the side and it is also prone to accidental breakage.
The upside - platform-specific code around rendering is moved inside
wgpu
. Bevy still needs to put canvases where it can be discovered, but it will be mostly transparent to users.I guess this is an addition to discussion in linked issue.
WinitPlugin
andOffscreenCanvas
support.As indicated by one of the linked PRs there is some interest in virtual windows (e.g. Bevy windows without backing winit/OS? window). But as I already mentioned, this will require some large refactors to
bevy_winit
, so it is better suited for a separate PR.After that is sorted out, this PR can use the new machinery to properly support offscreen canvases.
I guess another option is to maybe try to craft self-made frankenstein winit windows, but I didn't look into that either.
General experience for running on web worker.
What do we do about sharp edges? I guess most sensible option is to introduce a feature which disables nonsensical options in
WebElement
andWinitPlugin
, but that leads to more issues.For example,
Window
cannot implementDefault
for web workers (since we cannot provide a good defaultOffscreenCanvas
) which falls out into more complexity for other code (e.g.WindowPlugin
cannot create default primary window).Without
WinitPlugin
there is no one to make window setup for rendering. There is no event loop either. Also, how do we forward input events to Bevy?I think those questions loosely point toward the following:
web-worker
feature is probably a good idea to fence off unusable functionality.We might need a separate set of default plugins for web workers.
WinitPlugin
does a bit too much:It made sense while everything could be covered by winit, but it isn't anymore. It could be good to split the plugin into smaller more modular pieces.
And, obviously, name bikeshedding is always open.
Anyway, there you have it.
Migration guide
TODO