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
Prev Previous commit
Next Next commit
Groom canvas-related APIs, OffscreenCanvas still not working.
  • Loading branch information
haibane-tenshi committed Mar 30, 2023
commit 7a7de7c365667e1ca3a5792858a1845d68b09d97
17 changes: 11 additions & 6 deletions crates/bevy_render/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -212,13 +212,18 @@ impl Plugin for RenderPlugin {
.create_surface(&handle.get_handle())
.expect("Failed to create wgpu surface"),
#[cfg(target_arch = "wasm32")]
AbstractHandleWrapper::HtmlCanvas(canvas) => {
instance.create_surface_from_canvas(canvas).unwrap()
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(),
}
}
#[cfg(target_arch = "wasm32")]
AbstractHandleWrapper::OffscreenCanvas(offscreen_canvas) => instance
.create_surface_from_offscreen_canvas(offscreen_canvas)
.unwrap(),
}
});

Expand Down
19 changes: 12 additions & 7 deletions crates/bevy_render/src/view/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -200,13 +200,18 @@ pub fn prepare_windows(
.create_surface(&handle.get_handle())
.expect("Failed to create wgpu surface"),
#[cfg(target_arch = "wasm32")]
AbstractHandleWrapper::HtmlCanvas(canvas) => render_instance
.create_surface_from_canvas(canvas)
.expect("Failed to create wgpu surface"),
#[cfg(target_arch = "wasm32")]
AbstractHandleWrapper::OffscreenCanvas(canvas) => render_instance
.create_surface_from_offscreen_canvas(canvas)
.expect("Failed to create wgpu surface"),
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;
Expand Down
74 changes: 50 additions & 24 deletions crates/bevy_window/src/raw_handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,35 +79,61 @@ unsafe impl HasRawDisplayHandle for ThreadLockedRawWindowHandleWrapper {

/// Handle used for creating surfaces in the render plugin
///
/// Either a raw handle to an OS window or some canvas flavor when compiling on wasm.
#[derive(Clone, Debug, Component)]
pub enum AbstractHandle<T> {
/// 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(T),
#[cfg(target_arch = "wasm32")]
HtmlCanvas(HtmlCanvasElement),
#[cfg(target_arch = "wasm32")]
OffscreenCanvas(OffscreenCanvas),
}
RawHandle(RawHandleWrapper),

impl<T> Default for AbstractHandle<T>
where
T: Default,
{
fn default() -> Self {
Self::RawHandle(Default::default())
}
/// A handle to JS object containing rendering surface.
#[cfg(target_arch = "wasm32")]
WebHandle(WebHandle),
}

pub type AbstractHandlePlaceholder = AbstractHandle<()>;
pub type AbstractHandleWrapper = AbstractHandle<RawHandleWrapper>;

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

#[cfg(target_arch = "wasm32")]
unsafe impl Send for AbstractHandlePlaceholder {}
unsafe impl Send for WebHandle {}
#[cfg(target_arch = "wasm32")]
unsafe impl Sync for AbstractHandlePlaceholder {}
unsafe impl Sync for WebHandle {}
91 changes: 62 additions & 29 deletions crates/bevy_window/src/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ use bevy_reflect::{ReflectDeserialize, ReflectSerialize};

use bevy_utils::tracing::warn;

use crate::raw_handle::AbstractHandlePlaceholder;
use crate::CursorIcon;

/// Marker component for the window considered the primary window.
Expand Down Expand Up @@ -116,26 +115,6 @@ pub struct Window {
/// Note: This does not stop the program from fullscreening/setting
/// the size programmatically.
pub resizable: bool,
/// Determines with which rendering context window should be associated.
///
/// ## Platform-specific
///
/// For non-web platforms there exists only one enum variant that indicates
/// that `winit` should use `RawWindowHandle`.
/// Note that this field doesn't hold handle value itself,
/// i.e. you don't need to create the window yourself.
/// Instead it is generated when winit creates a new window for you.
/// You can safely pass `Default::default()`.
///
/// For web the enum offers two more enum variants: one for html canvas and offscreen canvas.
/// Those are later piped through to `wgpu`.
///
/// ## Reflection
///
/// On `wasm32` this field contains `js-sys` objects which are neither `Send` nor `Sync`
/// nor implement `Reflect`.
#[reflect(ignore)]
pub handle: AbstractHandlePlaceholder,
/// Should the window have decorations enabled?
///
/// (Decorations are the minimize, maximize, and close buttons on desktop apps)
Expand Down Expand Up @@ -163,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 @@ -220,17 +222,16 @@ impl Default for Window {
internal: Default::default(),
composite_alpha_mode: Default::default(),
resize_constraints: Default::default(),
handle: Default::default(),
ime_enabled: Default::default(),
ime_position: Default::default(),
resizable: true,
decorations: true,
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 @@ -852,3 +853,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 {}
81 changes: 43 additions & 38 deletions crates/bevy_winit/src/system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ use bevy_utils::{
tracing::{error, info, warn},
HashMap,
};
use bevy_window::{RawHandleWrapper, Window, WindowClosed, WindowCreated};
use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle};
use bevy_window::{Window, WindowClosed, WindowCreated};

use winit::{
dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize},
Expand Down Expand Up @@ -44,6 +43,8 @@ pub(crate) fn create_window<'a>(
#[cfg(target_arch = "wasm32")] event_channel: ResMut<CanvasParentResizeEventChannel>,
) {
for (entity, mut window) in created_windows {
use bevy_window::AbstractHandleWrapper;

if winit_windows.get_window(entity).is_some() {
continue;
}
Expand All @@ -66,49 +67,53 @@ pub(crate) fn create_window<'a>(
.resolution
.set_scale_factor(winit_window.scale_factor());

let handle = {
use bevy_window::{AbstractHandlePlaceholder, AbstractHandleWrapper};
let handle: AbstractHandleWrapper = {
// Cannot apply attributes to expressions yet :(
#[allow(unused_mut)]
let mut handle;

match &window.handle {
AbstractHandlePlaceholder::RawHandle(()) => {
AbstractHandleWrapper::RawHandle(RawHandleWrapper {
window_handle: winit_window.raw_window_handle(),
display_handle: winit_window.raw_display_handle(),
})
}
#[cfg(target_arch = "wasm32")]
AbstractHandlePlaceholder::HtmlCanvas(canvas) => {
AbstractHandleWrapper::HtmlCanvas(canvas.clone())
}
#[cfg(target_arch = "wasm32")]
AbstractHandlePlaceholder::OffscreenCanvas(canvas) => {
AbstractHandleWrapper::OffscreenCanvas(canvas.clone())
#[cfg(not(target_arch = "wasm32"))]
{
handle = AbstractHandleWrapper::RawHandle(RawHandleWrapper {
window_handle: winit_window.raw_window_handle(),
display_handle: winit_window.raw_display_handle(),
});
}

#[cfg(target_arch = "wasm32")]
{
use bevy_window::{WebElement, WebHandle};

let web_handle = match &window.web_element {
WebElement::Generate => todo!(),
WebElement::CssSelector(_) => todo!(),
WebElement::HtmlCanvas(canvas) => WebHandle::HtmlCanvas(canvas.clone()),
WebElement::OffscreenCanvas(canvas) => {
WebHandle::OffscreenCanvas(canvas.clone())
}
};

if window.fit_canvas_to_parent {
match &web_handle {
WebHandle::HtmlCanvas(canvas) => {
event_channel.listen_to_element(entity, canvas.clone());
}
WebHandle::OffscreenCanvas(_) => {
todo!()
}
}
}

handle = AbstractHandleWrapper::WebHandle(web_handle);
}

handle
};

commands.entity(entity).insert(handle).insert(CachedWindow {
window: window.clone(),
});

#[cfg(target_arch = "wasm32")]
{
use bevy_window::AbstractHandlePlaceholder;

if window.fit_canvas_to_parent {
// We ignore other two variants.
match &window.handle {
AbstractHandlePlaceholder::RawHandle(_) => (),
AbstractHandlePlaceholder::HtmlCanvas(canvas) => {
event_channel.listen_to_element(entity, canvas.clone());
}
AbstractHandlePlaceholder::OffscreenCanvas(_) => {
todo!()
}
}
}
}

event_writer.send(WindowCreated { window: entity });
}
}
Expand Down Expand Up @@ -300,8 +305,8 @@ pub(crate) fn changed_window(
}

#[cfg(target_arch = "wasm32")]
if window.canvas != cache.window.canvas {
window.canvas = cache.window.canvas.clone();
if window.web_element != cache.window.web_element {
window.web_element = cache.window.web_element.clone();
warn!(
"Bevy currently doesn't support modifying the window canvas after initialization."
);
Expand Down
Loading