Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Run Bevy in web worker context #8278

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
28 changes: 22 additions & 6 deletions crates/bevy_render/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ pub mod prelude {
};
}

use bevy_window::{PrimaryWindow, RawHandleWrapper};
use bevy_window::PrimaryWindow;
use globals::GlobalsPlugin;
pub use once_cell;

Expand Down Expand Up @@ -189,12 +189,14 @@ pub struct RenderApp;
impl Plugin for RenderPlugin {
/// Initializes the renderer, sets up the [`RenderSet`](RenderSet) and creates the rendering sub-app.
fn build(&self, app: &mut App) {
use bevy_window::AbstractHandleWrapper;

app.add_asset::<Shader>()
.add_debug_asset::<Shader>()
.init_asset_loader::<ShaderLoader>()
.init_debug_asset_loader::<ShaderLoader>();

let mut system_state: SystemState<Query<&RawHandleWrapper, With<PrimaryWindow>>> =
let mut system_state: SystemState<Query<&AbstractHandleWrapper, With<PrimaryWindow>>> =
SystemState::new(&mut app.world);
let primary_window = system_state.get(&app.world);

Expand All @@ -205,10 +207,24 @@ impl Plugin for RenderPlugin {
});
let surface = primary_window.get_single().ok().map(|wrapper| unsafe {
// SAFETY: Plugins should be set up on the main thread.
let handle = wrapper.get_handle();
instance
.create_surface(&handle)
.expect("Failed to create wgpu surface")
match wrapper {
AbstractHandleWrapper::RawHandle(handle) => instance
.create_surface(&handle.get_handle())
.expect("Failed to create wgpu surface"),
#[cfg(target_arch = "wasm32")]
AbstractHandleWrapper::WebHandle(web_handle) => {
use bevy_window::WebHandle;

match web_handle {
WebHandle::HtmlCanvas(canvas) => {
instance.create_surface_from_canvas(canvas).unwrap()
}
WebHandle::OffscreenCanvas(offscreen_canvas) => instance
.create_surface_from_offscreen_canvas(offscreen_canvas)
.unwrap(),
}
}
}
});

let request_adapter_options = wgpu::RequestAdapterOptions {
Expand Down
34 changes: 28 additions & 6 deletions crates/bevy_render/src/view/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use bevy_app::{App, Plugin};
use bevy_ecs::prelude::*;
use bevy_utils::{tracing::debug, HashMap, HashSet};
use bevy_window::{
CompositeAlphaMode, PresentMode, PrimaryWindow, RawHandleWrapper, Window, WindowClosed,
AbstractHandleWrapper, CompositeAlphaMode, PresentMode, PrimaryWindow, Window, WindowClosed,
};
use std::ops::{Deref, DerefMut};
use wgpu::TextureFormat;
Expand Down Expand Up @@ -42,7 +42,7 @@ impl Plugin for WindowRenderPlugin {
pub struct ExtractedWindow {
/// An entity that contains the components in [`Window`].
pub entity: Entity,
pub handle: RawHandleWrapper,
pub handle: AbstractHandleWrapper,
pub physical_width: u32,
pub physical_height: u32,
pub present_mode: PresentMode,
Expand Down Expand Up @@ -76,7 +76,14 @@ impl DerefMut for ExtractedWindows {
fn extract_windows(
mut extracted_windows: ResMut<ExtractedWindows>,
mut closed: Extract<EventReader<WindowClosed>>,
windows: Extract<Query<(Entity, &Window, &RawHandleWrapper, Option<&PrimaryWindow>)>>,
windows: Extract<
Query<(
Entity,
&Window,
&AbstractHandleWrapper,
Option<&PrimaryWindow>,
)>,
>,
) {
for (entity, window, handle, primary) in windows.iter() {
if primary.is_some() {
Expand Down Expand Up @@ -186,9 +193,24 @@ pub fn prepare_windows(
.or_insert_with(|| unsafe {
// NOTE: On some OSes this MUST be called from the main thread.
// As of wgpu 0.15, only failable if the given window is a HTML canvas and obtaining a WebGPU or WebGL2 context fails.
let surface = render_instance
.create_surface(&window.handle.get_handle())
.expect("Failed to create wgpu surface");
let surface = match &window.handle {
AbstractHandleWrapper::RawHandle(handle) => render_instance
.create_surface(&handle.get_handle())
.expect("Failed to create wgpu surface"),
#[cfg(target_arch = "wasm32")]
AbstractHandleWrapper::WebHandle(web_handle) => {
use bevy_window::WebHandle;

match web_handle {
WebHandle::HtmlCanvas(canvas) => render_instance
.create_surface_from_canvas(canvas)
.expect("Failed to create wgpu surface"),
WebHandle::OffscreenCanvas(canvas) => render_instance
.create_surface_from_offscreen_canvas(canvas)
.expect("Failed to create wgpu surface"),
}
}
};
let caps = surface.get_capabilities(&render_adapter);
let formats = caps.formats;
// For future HDR output support, we'll need to request a format that supports HDR,
Expand Down
3 changes: 3 additions & 0 deletions crates/bevy_window/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,6 @@ raw-window-handle = "0.5"

# other
serde = { version = "1.0", features = ["derive"], optional = true }

[target.'cfg(target_arch = "wasm32")'.dependencies]
web-sys = "0.3"
63 changes: 63 additions & 0 deletions crates/bevy_window/src/raw_handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ use bevy_ecs::prelude::Component;
use raw_window_handle::{
HasRawDisplayHandle, HasRawWindowHandle, RawDisplayHandle, RawWindowHandle,
};
#[cfg(target_arch = "wasm32")]
use web_sys::{HtmlCanvasElement, OffscreenCanvas};

/// A wrapper over [`RawWindowHandle`] and [`RawDisplayHandle`] that allows us to safely pass it across threads.
///
Expand Down Expand Up @@ -74,3 +76,64 @@ unsafe impl HasRawDisplayHandle for ThreadLockedRawWindowHandleWrapper {
self.0.get_display_handle()
}
}

/// Handle used for creating surfaces in the render plugin
///
/// Either a raw handle to an OS window or some canvas flavor on wasm.
/// For non-web platforms it essentially compiles down to newtype wrapper around `RawHandleWrapper`.
///
/// # Details
///
/// `RawHandleWrapper` is not particularly useful on wasm.
///
/// * `RawDisplayHandle` is entirely ignored as Bevy has no control over
/// where the element is going to be displayed.
/// * `RawWindowHandle::Web` contains a single `u32` as payload.
/// `wgpu` uses that in a css selector to discover canvas element.
///
/// This system is overly rigid and fragile.
/// Regardless of how we specify the target element `wgpu` have to land on `WebGl2RenderingContext`
/// in order to render anything.
/// However that prevents us from directly specifying which element it should use.
/// This is especially bad when Bevy is run from web-worker context:
/// workers don't have access to DOM, so it inevitably leads to panic!
///
/// It is understandable why `RawWindowHandle` doesn't include JS objects,
/// so instead we use `AbstractHandleWrapper` to provide a workaround.
///
/// # Note
///
/// While workable it might be possible to remove this abstraction.
/// At the end of the day interpretation of `RawWindowHandle::Web` payload is up to us.
/// We can intercept it before it makes to `wgpu::Instance` and use it to look up
/// `HtmlCanvasElement` or `OffscreenCanvas` from global memory
/// (which will be different on whether Bevy runs as main or worker)
/// and pass that to `wgpu`.
/// This will require a bunch of extra machinery and will be confusing to users
/// which don't rely on `bevy_winit` but can be an option in case this abstraction is undesirable.
#[derive(Debug, Clone, Component)]
pub enum AbstractHandleWrapper {
/// The window corresponds to an operator system window.
RawHandle(RawHandleWrapper),

/// A handle to JS object containing rendering surface.
#[cfg(target_arch = "wasm32")]
WebHandle(WebHandle),
}

/// A `Send + Sync` wrapper around `HtmlCanvasElement` or `OffscreenCanvas`.
///
/// # Safety
///
/// Only safe to use from the main thread.
#[cfg(target_arch = "wasm32")]
#[derive(Debug, Clone, Component)]
pub enum WebHandle {
HtmlCanvas(HtmlCanvasElement),
OffscreenCanvas(OffscreenCanvas),
}

#[cfg(target_arch = "wasm32")]
unsafe impl Send for WebHandle {}
#[cfg(target_arch = "wasm32")]
unsafe impl Sync for WebHandle {}
69 changes: 62 additions & 7 deletions crates/bevy_window/src/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,14 +142,37 @@ pub struct Window {
///
/// - iOS / Android / Web / Wayland: Unsupported.
pub window_level: WindowLevel,
/// The "html canvas" element selector.
/// Instructs which web element window should be associated with.
///
/// If set, this selector will be used to find a matching html canvas element,
/// rather than creating a new one.
/// Uses the [CSS selector format](https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelector).
/// ## Platform-specific
///
/// This value has no effect on non-web platforms.
pub canvas: Option<String>,
/// This field is ignored for non-web platforms.
/// You can safely initialize it to `Default::default()`.
///
/// For web platform the enum determines how `WinitPlugin` is going to discover
/// which web element the window should be associated with.
///
/// ## Panic safety
///
/// On `wasm32` it is important to know *how* Bevy is going to be run.
/// Wasm can be run either as **main** (e.g. on main JS event loop) or as web **worker**.
///
/// * When run as **main**, all web APIs are available so all variants for `WebElement` will work.
/// * When run as **worker** only `WebElement::OffscreenCanvas` is safe, other variants will panic.
///
/// This happens because:
/// * `WebElement::Generate` and `WebElement::CssSelector` require access to DOM which worker doesn't have.
/// * Worker cannot directly interact with WebGL context of `HtmlCanvasElement`.
///
/// For more details on web-worker APIs see [MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API).
///
/// Note that by default the field is initialized to `Generate` and it will panic for web workers!
///
/// ## Reflection
///
/// On `wasm32` this field contains `js-sys` objects which don't implement `Reflect`.
#[reflect(ignore)]
pub web_element: WebElement,
/// Whether or not to fit the canvas element's size to its parent element's size.
///
/// **Warning**: this will not behave as expected for parents that set their size according to the size of their
Expand Down Expand Up @@ -206,9 +229,9 @@ impl Default for Window {
transparent: false,
focused: true,
window_level: Default::default(),
web_element: Default::default(),
fit_canvas_to_parent: false,
prevent_default_event_handling: true,
canvas: None,
}
}
}
Expand Down Expand Up @@ -826,3 +849,35 @@ pub enum WindowLevel {
/// The window will always be on top of normal windows.
AlwaysOnTop,
}

/// Instructs which web element window should be associated with.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub enum WebElement {
/// Generate a new `HtmlCanvasElement` and attach it to body.
///
/// This option is good for quick testing/setup,
/// but consider choosing more controllable behavior.
#[default]
Generate,

/// Discover `HtmlCanvasElement` via a css selector.
///
/// # Panic
///
/// This option will panic if the discovered element is not a canvas.
#[cfg(target_arch = "wasm32")]
CssSelector(String),

/// Use specified `HtmlCanvasElement`.
#[cfg(target_arch = "wasm32")]
HtmlCanvas(web_sys::HtmlCanvasElement),

/// Use specified `OffscreenCanvas`.
#[cfg(target_arch = "wasm32")]
OffscreenCanvas(web_sys::OffscreenCanvas),
}

#[cfg(target_arch = "wasm32")]
unsafe impl Send for WebElement {}
#[cfg(target_arch = "wasm32")]
unsafe impl Sync for WebElement {}
Loading