diff --git a/crates/bevy_render/src/lib.rs b/crates/bevy_render/src/lib.rs index dd47d53212b6b..e6e56f9b89e95 100644 --- a/crates/bevy_render/src/lib.rs +++ b/crates/bevy_render/src/lib.rs @@ -39,7 +39,7 @@ pub mod prelude { }; } -use bevy_window::{PrimaryWindow, RawHandleWrapper}; +use bevy_window::PrimaryWindow; use globals::GlobalsPlugin; pub use once_cell; @@ -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::() .add_debug_asset::() .init_asset_loader::() .init_debug_asset_loader::(); - let mut system_state: SystemState>> = + let mut system_state: SystemState>> = SystemState::new(&mut app.world); let primary_window = system_state.get(&app.world); @@ -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 { diff --git a/crates/bevy_render/src/view/window.rs b/crates/bevy_render/src/view/window.rs index 8118e4fd06d8f..a029290221765 100644 --- a/crates/bevy_render/src/view/window.rs +++ b/crates/bevy_render/src/view/window.rs @@ -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; @@ -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, @@ -76,7 +76,14 @@ impl DerefMut for ExtractedWindows { fn extract_windows( mut extracted_windows: ResMut, mut closed: Extract>, - windows: Extract)>>, + windows: Extract< + Query<( + Entity, + &Window, + &AbstractHandleWrapper, + Option<&PrimaryWindow>, + )>, + >, ) { for (entity, window, handle, primary) in windows.iter() { if primary.is_some() { @@ -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, diff --git a/crates/bevy_window/Cargo.toml b/crates/bevy_window/Cargo.toml index c65bcc6e3b97d..3b0780e19fd7e 100644 --- a/crates/bevy_window/Cargo.toml +++ b/crates/bevy_window/Cargo.toml @@ -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" diff --git a/crates/bevy_window/src/raw_handle.rs b/crates/bevy_window/src/raw_handle.rs index 6c535605991f0..dfaf31bf1d798 100644 --- a/crates/bevy_window/src/raw_handle.rs +++ b/crates/bevy_window/src/raw_handle.rs @@ -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. /// @@ -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 {} diff --git a/crates/bevy_window/src/window.rs b/crates/bevy_window/src/window.rs index 5b079d5137f71..5337afc6e42c3 100644 --- a/crates/bevy_window/src/window.rs +++ b/crates/bevy_window/src/window.rs @@ -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, + /// 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 @@ -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, } } } @@ -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 {} diff --git a/crates/bevy_winit/src/system.rs b/crates/bevy_winit/src/system.rs index 592251cbcf1e4..6abd3ad3c14bb 100644 --- a/crates/bevy_winit/src/system.rs +++ b/crates/bevy_winit/src/system.rs @@ -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}, @@ -20,7 +19,7 @@ use winit::{ }; #[cfg(target_arch = "wasm32")] -use crate::web_resize::{CanvasParentResizeEventChannel, WINIT_CANVAS_SELECTOR}; +use crate::web_resize::CanvasParentResizeEventChannel; use crate::{ accessibility::{AccessKitAdapters, WinitActionHandlers}, converters::{self, convert_window_level}, @@ -44,6 +43,8 @@ pub(crate) fn create_window<'a>( #[cfg(target_arch = "wasm32")] event_channel: ResMut, ) { for (entity, mut window) in created_windows { + use bevy_window::AbstractHandleWrapper; + if winit_windows.get_window(entity).is_some() { continue; } @@ -65,27 +66,57 @@ pub(crate) fn create_window<'a>( window .resolution .set_scale_factor(winit_window.scale_factor()); - commands - .entity(entity) - .insert(RawHandleWrapper { - window_handle: winit_window.raw_window_handle(), - display_handle: winit_window.raw_display_handle(), - }) - .insert(CachedWindow { - window: window.clone(), - }); - - #[cfg(target_arch = "wasm32")] - { - if window.fit_canvas_to_parent { - let selector = if let Some(selector) = &window.canvas { - selector - } else { - WINIT_CANVAS_SELECTOR + + let handle: AbstractHandleWrapper = { + let handle; + + #[cfg(not(target_arch = "wasm32"))] + { + use bevy_window::RawHandleWrapper; + use raw_window_handle::{HasRawDisplayHandle, HasRawWindowHandle}; + + 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 { + // Canvas is already created/discovered by winit. + WebElement::Generate | WebElement::CssSelector(_) => { + use winit::platform::web::WindowExtWebSys; + + WebHandle::HtmlCanvas(winit_window.canvas()) + } + WebElement::HtmlCanvas(canvas) => WebHandle::HtmlCanvas(canvas.clone()), + WebElement::OffscreenCanvas(canvas) => { + WebHandle::OffscreenCanvas(canvas.clone()) + } }; - event_channel.listen_to_selector(entity, selector); + + if window.fit_canvas_to_parent { + match &web_handle { + WebHandle::HtmlCanvas(canvas) => { + event_channel.listen_to_element(entity, canvas.clone()); + } + // OffscreenCanvas exists outside DOM tree. + WebHandle::OffscreenCanvas(_) => (), + } + } + + handle = AbstractHandleWrapper::WebHandle(web_handle); } - } + + handle + }; + + commands.entity(entity).insert(handle).insert(CachedWindow { + window: window.clone(), + }); event_writer.send(WindowCreated { window: entity }); } @@ -278,8 +309,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." ); diff --git a/crates/bevy_winit/src/web_resize.rs b/crates/bevy_winit/src/web_resize.rs index a53075dea3883..6b08beffff8ca 100644 --- a/crates/bevy_winit/src/web_resize.rs +++ b/crates/bevy_winit/src/web_resize.rs @@ -3,6 +3,7 @@ use bevy_app::{App, Plugin, Update}; use bevy_ecs::prelude::*; use crossbeam_channel::{Receiver, Sender}; use wasm_bindgen::JsCast; +use web_sys::HtmlCanvasElement; use winit::dpi::LogicalSize; pub(crate) struct CanvasParentResizePlugin; @@ -36,33 +37,34 @@ fn canvas_parent_resize_event_handler( } } -fn get_size(selector: &str) -> Option> { - let win = web_sys::window().unwrap(); - let doc = win.document().unwrap(); - let element = doc.query_selector(selector).ok()??; - let parent_element = element.parent_element()?; - let rect = parent_element.get_bounding_client_rect(); - return Some(winit::dpi::LogicalSize::new( - rect.width() as f32, - rect.height() as f32, - )); -} - -pub(crate) const WINIT_CANVAS_SELECTOR: &str = "canvas[data-raw-handle]"; - impl Default for CanvasParentResizeEventChannel { fn default() -> Self { let (sender, receiver) = crossbeam_channel::unbounded(); - return Self { sender, receiver }; + Self { sender, receiver } } } +fn get_size_element(element: &HtmlCanvasElement) -> Option> { + let parent_element = element.parent_element()?; + let rect = parent_element.get_bounding_client_rect(); + Some(winit::dpi::LogicalSize::new( + rect.width() as f32, + rect.height() as f32, + )) +} + impl CanvasParentResizeEventChannel { - pub(crate) fn listen_to_selector(&self, window: Entity, selector: &str) { + /// Listen to resize events on the element + /// + /// ## Panic + /// + /// Do not call from a web-worker! + /// This method uses global `window` object to attach events to, + /// which doesn't exist inside worker context. + pub(crate) fn listen_to_element(&self, window: Entity, element: HtmlCanvasElement) { let sender = self.sender.clone(); - let owned_selector = selector.to_string(); let resize = move || { - if let Some(size) = get_size(&owned_selector) { + if let Some(size) = get_size_element(&element) { sender.send(ResizeEvent { size, window }).unwrap(); } }; diff --git a/crates/bevy_winit/src/winit_windows.rs b/crates/bevy_winit/src/winit_windows.rs index 6bc5995490714..b1bb3d18fc455 100644 --- a/crates/bevy_winit/src/winit_windows.rs +++ b/crates/bevy_winit/src/winit_windows.rs @@ -120,22 +120,36 @@ impl WinitWindows { #[cfg(target_arch = "wasm32")] { - use wasm_bindgen::JsCast; + use bevy_window::WebElement; use winit::platform::web::WindowBuilderExtWebSys; - if let Some(selector) = &window.canvas { - let window = web_sys::window().unwrap(); - let document = window.document().unwrap(); - let canvas = document - .query_selector(&selector) - .expect("Cannot query for canvas element."); - if let Some(canvas) = canvas { - let canvas = canvas.dyn_into::().ok(); - winit_window_builder = winit_window_builder.with_canvas(canvas); - } else { - panic!("Cannot find element: {}.", selector); + let canvas = match &window.web_element { + // We let winit create canvas element for us. + // We will attach it to DOM later. + WebElement::Generate => None, + WebElement::CssSelector(selector) => { + use wasm_bindgen::JsCast; + + let win = web_sys::window() + .expect("bevy must run from main loop to use css selector"); + let doc = win.document().unwrap(); + let element = doc + .query_selector(selector) + .unwrap() + .expect("no element is fitting the selector query"); + let canvas = element + .dyn_into::() + .expect("selector must point to a canvas element"); + + Some(canvas) } - } + WebElement::HtmlCanvas(canvas) => Some(canvas.clone()), + WebElement::OffscreenCanvas(_) => { + panic!("winit cannot manage windows backed by OffscreenCanvas") + } + }; + + winit_window_builder = winit_window_builder.with_canvas(canvas); winit_window_builder = winit_window_builder.with_prevent_default(window.prevent_default_event_handling) @@ -190,9 +204,10 @@ impl WinitWindows { #[cfg(target_arch = "wasm32")] { + use bevy_window::WebElement; use winit::platform::web::WindowExtWebSys; - if window.canvas.is_none() { + if let &WebElement::Generate = &window.web_element { let canvas = winit_window.canvas(); let window = web_sys::window().unwrap();