diff --git a/Cargo.toml b/Cargo.toml index 560ae5807f43c..ed933cf7fd24d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1547,6 +1547,13 @@ path = "tests/window/minimising.rs" [package.metadata.example.minimising] hidden = true +[[example]] +name = "virtual" +path = "examples/window/virtual.rs" + +[package.metadata.example.virtual] +hidden = true + [[example]] name = "window_resizing" path = "examples/window/window_resizing.rs" diff --git a/crates/bevy_render/src/lib.rs b/crates/bevy_render/src/lib.rs index 965a0d3e95b21..c7f1510d9e467 100644 --- a/crates/bevy_render/src/lib.rs +++ b/crates/bevy_render/src/lib.rs @@ -21,6 +21,7 @@ pub mod view; use bevy_core::FrameCount; use bevy_hierarchy::ValidParentCheckPlugin; +use bevy_window::AbstractWindowHandle; pub use extract_param::Extract; pub mod prelude { @@ -144,17 +145,27 @@ impl Plugin for RenderPlugin { .register_type::(); if let Some(backends) = options.backends { - let windows = app.world.resource_mut::(); let instance = wgpu::Instance::new(backends); - - let surface = windows - .get_primary() - .and_then(|window| window.raw_window_handle()) - .map(|wrapper| unsafe { - let handle = wrapper.get_handle(); - instance.create_surface(&handle) + let surface = { + let windows = app.world.resource_mut::(); + let raw_handle = windows.get_primary().and_then(|window| unsafe { + match window.window_handle() { + AbstractWindowHandle::RawWindowHandle(handle) => { + Some(instance.create_surface(&handle.get_handle())) + } + AbstractWindowHandle::Virtual => None, + #[cfg(target_arch = "wasm32")] + AbstractWindowHandle::HtmlCanvas(canvas) => { + Some(instance.create_surface_from_canvas(&canvas)) + } + #[cfg(target_arch = "wasm32")] + AbstractWindowHandle::OffscreenCanvas(canvas) => { + Some(instance.create_surface_from_offscreen_canvas(&canvas)) + } + } }); - + raw_handle + }; let request_adapter_options = wgpu::RequestAdapterOptions { power_preference: options.power_preference, compatible_surface: surface.as_ref(), diff --git a/crates/bevy_render/src/view/window.rs b/crates/bevy_render/src/view/window.rs index a44610d0dc81d..90c50502a5181 100644 --- a/crates/bevy_render/src/view/window.rs +++ b/crates/bevy_render/src/view/window.rs @@ -6,7 +6,7 @@ use crate::{ use bevy_app::{App, Plugin}; use bevy_ecs::prelude::*; use bevy_utils::{tracing::debug, HashMap, HashSet}; -use bevy_window::{PresentMode, RawWindowHandleWrapper, WindowClosed, WindowId, Windows}; +use bevy_window::{AbstractWindowHandle, PresentMode, WindowClosed, WindowId, Windows}; use std::ops::{Deref, DerefMut}; /// Token to ensure a system runs on the main thread. @@ -38,7 +38,7 @@ impl Plugin for WindowRenderPlugin { pub struct ExtractedWindow { pub id: WindowId, - pub raw_window_handle: Option, + pub handle: AbstractWindowHandle, pub physical_width: u32, pub physical_height: u32, pub present_mode: PresentMode, @@ -83,7 +83,7 @@ fn extract_windows( .entry(window.id()) .or_insert(ExtractedWindow { id: window.id(), - raw_window_handle: window.raw_window_handle(), + handle: window.window_handle(), physical_width: new_width, physical_height: new_height, present_mode: window.present_mode(), @@ -132,6 +132,8 @@ pub struct WindowSurfaces { /// Creates and (re)configures window surfaces, and obtains a swapchain texture for rendering. /// +/// This will not handle [virtual windows](bevy_window::AbstractWindowHandle::Virtual). +/// /// NOTE: `get_current_texture` in `prepare_windows` can take a long time if the GPU workload is /// the performance bottleneck. This can be seen in profiles as multiple prepare-stage systems all /// taking an unusually long time to complete, and all finishing at about the same time as the @@ -161,21 +163,28 @@ pub fn prepare_windows( render_instance: Res, render_adapter: Res, ) { - for window in windows - .windows - .values_mut() - // value of raw_winndow_handle only None if synthetic test - .filter(|x| x.raw_window_handle.is_some()) - { - let window_surfaces = window_surfaces.deref_mut(); - let surface = window_surfaces - .surfaces - .entry(window.id) - .or_insert_with(|| unsafe { - // NOTE: On some OSes this MUST be called from the main thread. - render_instance - .create_surface(&window.raw_window_handle.as_ref().unwrap().get_handle()) - }); + let window_surfaces = window_surfaces.deref_mut(); + for window in windows.windows.values_mut() { + let surface = match &window.handle { + AbstractWindowHandle::RawWindowHandle(handle) => window_surfaces + .surfaces + .entry(window.id) + .or_insert_with(|| unsafe { + // NOTE: On some OSes this MUST be called from the main thread. + render_instance.create_surface(&handle.get_handle()) + }), + #[cfg(target_arch = "wasm32")] + AbstractWindowHandle::HtmlCanvas(canvas) => window_surfaces + .surfaces + .entry(window.id) + .or_insert_with(|| render_instance.create_surface_from_canvas(canvas)), + #[cfg(target_arch = "wasm32")] + AbstractWindowHandle::OffscreenCanvas(canvas) => window_surfaces + .surfaces + .entry(window.id) + .or_insert_with(|| render_instance.create_surface_from_offscreen_canvas(canvas)), + AbstractWindowHandle::Virtual => continue, + }; let swap_chain_descriptor = wgpu::SurfaceConfiguration { format: *surface diff --git a/crates/bevy_window/Cargo.toml b/crates/bevy_window/Cargo.toml index f1402447072de..f90b282d66bfc 100644 --- a/crates/bevy_window/Cargo.toml +++ b/crates/bevy_window/Cargo.toml @@ -28,3 +28,4 @@ serde = { version = "1.0", features = ["derive"], optional = true } [target.'cfg(target_arch = "wasm32")'.dependencies] web-sys = "0.3" +wasm-bindgen = "0.2" \ No newline at end of file diff --git a/crates/bevy_window/src/window.rs b/crates/bevy_window/src/window.rs index c0eae67f02973..8c3c33b001224 100644 --- a/crates/bevy_window/src/window.rs +++ b/crates/bevy_window/src/window.rs @@ -3,6 +3,10 @@ use bevy_math::{DVec2, IVec2, UVec2, Vec2}; use bevy_reflect::{FromReflect, Reflect}; use bevy_utils::{tracing::warn, Uuid}; use raw_window_handle::RawWindowHandle; +#[cfg(target_arch = "wasm32")] +use wasm_bindgen::JsCast; +#[cfg(target_arch = "wasm32")] +use web_sys::{HtmlCanvasElement, OffscreenCanvas}; #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Reflect, FromReflect)] #[reflect_value(PartialEq, Hash)] @@ -151,7 +155,31 @@ impl WindowResizeConstraints { } } -/// An operating system window that can present content and receive user input. +/// Handle used for creating surfaces in the render plugin +/// +/// Either a raw handle to an OS window or `Virtual` to signify that there is no corresponding OS window. +#[derive(Clone, Debug)] +pub enum AbstractWindowHandle { + /// The window corresponds to an operator system window. + RawWindowHandle(RawWindowHandleWrapper), + /// The window does not to correspond to an operator system window. + /// + /// It differs from a non-virtual window, in that the caller is responsible + /// for creating and presenting surface textures and inserting them into + /// [`ExtractedWindow`](https://docs.rs/bevy/*/bevy/render/view/struct.ExtractedWindow.html). + Virtual, + #[cfg(target_arch = "wasm32")] + HtmlCanvas(HtmlCanvasElement), + #[cfg(target_arch = "wasm32")] + OffscreenCanvas(web_sys::OffscreenCanvas), +} + +#[cfg(target_arch = "wasm32")] +unsafe impl Send for AbstractWindowHandle {} +#[cfg(target_arch = "wasm32")] +unsafe impl Sync for AbstractWindowHandle {} + +/// An operating system or virtual window that can present content and receive user input. /// /// To create a window, use a [`EventWriter`](`crate::CreateWindow`). /// @@ -259,10 +287,10 @@ pub struct Window { cursor_visible: bool, cursor_locked: bool, physical_cursor_position: Option, - raw_window_handle: Option, + window_handle: AbstractWindowHandle, focused: bool, mode: WindowMode, - canvas: Option, + #[cfg(target_arch = "wasm32")] fit_canvas_to_parent: bool, command_queue: Vec, } @@ -368,7 +396,47 @@ impl Window { physical_height: u32, scale_factor: f64, position: Option, - raw_window_handle: Option, + raw_window_handle: RawWindowHandle, + ) -> Self { + Window { + id, + requested_width: window_descriptor.width, + requested_height: window_descriptor.height, + position, + physical_width, + physical_height, + resize_constraints: window_descriptor.resize_constraints, + scale_factor_override: window_descriptor.scale_factor_override, + backend_scale_factor: scale_factor, + title: window_descriptor.title.clone(), + present_mode: window_descriptor.present_mode, + resizable: window_descriptor.resizable, + decorations: window_descriptor.decorations, + cursor_visible: window_descriptor.cursor_visible, + cursor_locked: window_descriptor.cursor_locked, + cursor_icon: CursorIcon::Default, + physical_cursor_position: None, + window_handle: AbstractWindowHandle::RawWindowHandle(RawWindowHandleWrapper::new( + raw_window_handle, + )), + focused: true, + mode: window_descriptor.mode, + #[cfg(target_arch = "wasm32")] + fit_canvas_to_parent: window_descriptor.fit_canvas_to_parent, + command_queue: Vec::new(), + } + } + + /// Creates a new virtual [`Window`]. + /// + /// See [`AbstractWindowHandle::Virtual`]. + pub fn new_virtual( + id: WindowId, + window_descriptor: &WindowDescriptor, + physical_width: u32, + physical_height: u32, + scale_factor: f64, + position: Option, ) -> Self { Window { id, @@ -388,14 +456,112 @@ impl Window { cursor_locked: window_descriptor.cursor_locked, cursor_icon: CursorIcon::Default, physical_cursor_position: None, - raw_window_handle: raw_window_handle.map(RawWindowHandleWrapper::new), + window_handle: AbstractWindowHandle::Virtual, focused: true, mode: window_descriptor.mode, - canvas: window_descriptor.canvas.clone(), + #[cfg(target_arch = "wasm32")] fit_canvas_to_parent: window_descriptor.fit_canvas_to_parent, command_queue: Vec::new(), } } + + /// Creates a new [`Window`] from a canvas. + /// + /// See [`AbstractWindowHandle::HtmlCanvas`]. + #[cfg(target_arch = "wasm32")] + pub fn new_canvas( + id: WindowId, + window_descriptor: &WindowDescriptor, + canvas: HtmlCanvasElement, + ) -> Self { + let size = canvas.get_bounding_client_rect(); + Window { + id, + requested_width: window_descriptor.width, + requested_height: window_descriptor.height, + position: None, + physical_width: size.width() as _, + physical_height: size.height() as _, + resize_constraints: window_descriptor.resize_constraints, + scale_factor_override: window_descriptor.scale_factor_override, + backend_scale_factor: web_sys::window().unwrap().device_pixel_ratio(), + title: window_descriptor.title.clone(), + present_mode: window_descriptor.present_mode, + resizable: window_descriptor.resizable, + decorations: window_descriptor.decorations, + cursor_visible: window_descriptor.cursor_visible, + cursor_locked: window_descriptor.cursor_locked, + cursor_icon: CursorIcon::Default, + physical_cursor_position: None, + window_handle: AbstractWindowHandle::HtmlCanvas(canvas), + focused: true, + mode: window_descriptor.mode, + fit_canvas_to_parent: window_descriptor.fit_canvas_to_parent, + command_queue: Vec::new(), + } + } + + /// Creates a new [`Window`] from a selector to a canvas. + /// + /// The selector format used is a [CSS selector](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors). + /// It uses the first element matching the selector. + /// + /// Returns an `Err` if the selector format is invalid. Panics if it is run from a web worker. + /// + /// Returns Ok(None) when the element could not be found with the selector. + /// + /// See [`AbstractWindowHandle::HtmlCanvas`]. + #[cfg(target_arch = "wasm32")] + pub fn new_canvas_selector( + id: WindowId, + window_descriptor: &WindowDescriptor, + selector: &str, + ) -> Result, wasm_bindgen::JsValue> { + Ok(web_sys::window() + .unwrap() + .document() + .unwrap() + .query_selector(selector)? + .and_then(|element| element.dyn_into().ok()) + .map(|canvas| Self::new_canvas(id, window_descriptor, canvas))) + } + + /// Creates a new [`Window`] from an offscreen canvas. + /// + /// See [`AbstractWindowHandle::OffscreenCanvas`]. + #[cfg(target_arch = "wasm32")] + pub fn new_offscreen_canvas( + id: WindowId, + window_descriptor: &WindowDescriptor, + canvas: OffscreenCanvas, + scale_factor: f64, + ) -> Self { + Window { + id, + requested_width: window_descriptor.width, + requested_height: window_descriptor.height, + position: None, + physical_width: canvas.width() as _, + physical_height: canvas.height() as _, + resize_constraints: window_descriptor.resize_constraints, + scale_factor_override: window_descriptor.scale_factor_override, + backend_scale_factor: scale_factor, + title: window_descriptor.title.clone(), + present_mode: window_descriptor.present_mode, + resizable: window_descriptor.resizable, + decorations: window_descriptor.decorations, + cursor_visible: window_descriptor.cursor_visible, + cursor_locked: window_descriptor.cursor_locked, + cursor_icon: CursorIcon::Default, + physical_cursor_position: None, + window_handle: AbstractWindowHandle::OffscreenCanvas(canvas), + focused: true, + mode: window_descriptor.mode, + fit_canvas_to_parent: window_descriptor.fit_canvas_to_parent, + command_queue: Vec::new(), + } + } + /// Get the window's [`WindowId`]. #[inline] pub fn id(&self) -> WindowId { @@ -747,12 +913,12 @@ impl Window { }); } /// Close the operating system window corresponding to this [`Window`]. - /// + /// /// This will also lead to this [`Window`] being removed from the /// [`Windows`] resource. /// /// If the default [`WindowPlugin`] is used, when no windows are - /// open, the [app will exit](bevy_app::AppExit). + /// open, the [app will exit](bevy_app::AppExit). /// To disable this behaviour, set `exit_on_all_closed` on the [`WindowPlugin`] /// to `false` /// @@ -772,23 +938,10 @@ impl Window { pub fn is_focused(&self) -> bool { self.focused } - /// Get the [`RawWindowHandleWrapper`] corresponding to this window if set. - /// - /// During normal use, this can be safely unwrapped; the value should only be [`None`] when synthetically constructed for tests. - pub fn raw_window_handle(&self) -> Option { - self.raw_window_handle.as_ref().cloned() - } - /// The "html canvas" element selector. - /// - /// 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). - /// - /// This value has no effect on non-web platforms. - #[inline] - pub fn canvas(&self) -> Option<&str> { - self.canvas.as_deref() + /// Get the [`AbstractWindowHandle`] corresponding to this window. + pub fn window_handle(&self) -> AbstractWindowHandle { + self.window_handle.clone() } /// Whether or not to fit the canvas element's size to its parent element's size. @@ -798,6 +951,7 @@ impl Window { /// feature, ensure the parent's size is not affected by its children. /// /// This value has no effect on non-web platforms. + #[cfg(target_arch = "wasm32")] #[inline] pub fn fit_canvas_to_parent(&self) -> bool { self.fit_canvas_to_parent @@ -836,6 +990,15 @@ pub enum MonitorSelection { Index(usize), } +#[cfg(target_arch = "wasm32")] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SendSyncCanvas(pub HtmlCanvasElement); + +#[cfg(target_arch = "wasm32")] +unsafe impl Send for SendSyncCanvas {} +#[cfg(target_arch = "wasm32")] +unsafe impl Sync for SendSyncCanvas {} + /// Describes the information needed for creating a window. /// /// This should be set up before adding the [`WindowPlugin`](crate::WindowPlugin). @@ -906,14 +1069,8 @@ pub struct WindowDescriptor { /// macOS X transparent works with winit out of the box, so this issue might be related to: /// Windows 11 is related to pub transparent: bool, - /// The "html canvas" element selector. - /// - /// 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). - /// - /// This value has no effect on non-web platforms. - pub canvas: Option, + #[cfg(target_arch = "wasm32")] + pub canvas: Option, /// 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 @@ -921,6 +1078,7 @@ pub struct WindowDescriptor { /// feature, ensure the parent's size is not affected by its children. /// /// This value has no effect on non-web platforms. + #[cfg(target_arch = "wasm32")] pub fit_canvas_to_parent: bool, } @@ -941,8 +1099,33 @@ impl Default for WindowDescriptor { cursor_visible: true, mode: WindowMode::Windowed, transparent: false, + #[cfg(target_arch = "wasm32")] canvas: None, + #[cfg(target_arch = "wasm32")] fit_canvas_to_parent: false, } } } + +#[cfg(target_arch = "wasm32")] +impl WindowDescriptor { + pub fn set_canvas_from_selector( + &mut self, + selector: &str, + ) -> Result { + Ok(web_sys::window() + .unwrap() + .document() + .unwrap() + .query_selector(selector)? + .and_then(|element| element.dyn_into().ok()) + .map(|canvas| { + self.canvas = Some(SendSyncCanvas(canvas)); + true + }) + .unwrap_or(false)) + } + pub fn set_canvas(&mut self, canvas: HtmlCanvasElement) { + self.canvas = Some(SendSyncCanvas(canvas)); + } +} diff --git a/crates/bevy_winit/src/lib.rs b/crates/bevy_winit/src/lib.rs index 0107a83e58e67..a25a8a4ac1b15 100644 --- a/crates/bevy_winit/src/lib.rs +++ b/crates/bevy_winit/src/lib.rs @@ -23,6 +23,8 @@ use bevy_utils::{ tracing::{error, info, trace, warn}, Instant, }; +#[cfg(target_arch = "wasm32")] +use bevy_window::SendSyncCanvas; use bevy_window::{ CreateWindow, CursorEntered, CursorLeft, CursorMoved, FileDragAndDrop, ModifiesWindows, ReceivedCharacter, RequestRedraw, WindowBackendScaleFactorChanged, WindowCloseRequested, @@ -686,6 +688,22 @@ fn handle_create_window_events( #[cfg(not(any(target_os = "windows", target_feature = "x11")))] let mut window_resized_events = world.resource_mut::>(); for create_window_event in create_window_event_reader.iter(&create_window_events) { + #[cfg(target_arch = "wasm32")] + let window = if let Some(SendSyncCanvas(canvas)) = &create_window_event.descriptor.canvas { + winit_windows.create_window_with_canvas( + event_loop, + create_window_event.id, + &create_window_event.descriptor, + canvas.clone(), + ) + } else { + winit_windows.create_window( + event_loop, + create_window_event.id, + &create_window_event.descriptor, + ) + }; + #[cfg(not(target_arch = "wasm32"))] let window = winit_windows.create_window( event_loop, create_window_event.id, @@ -709,12 +727,16 @@ fn handle_create_window_events( { let channel = world.resource_mut::(); if create_window_event.descriptor.fit_canvas_to_parent { - let selector = if let Some(selector) = &create_window_event.descriptor.canvas { - selector + if let Some(SendSyncCanvas(canvas)) = &create_window_event.descriptor.canvas { + // PROBLEM: this path is unreachable, because we're always creating the window + // based on the raw window handle above. + channel.listen_to_element(create_window_event.id, canvas.clone()); } else { - web_resize::WINIT_CANVAS_SELECTOR - }; - channel.listen_to_selector(create_window_event.id, selector); + channel.listen_to_selector( + create_window_event.id, + web_resize::WINIT_CANVAS_SELECTOR, + ); + } } } } diff --git a/crates/bevy_winit/src/web_resize.rs b/crates/bevy_winit/src/web_resize.rs index 7e289afdfc675..baf3ca739221e 100644 --- a/crates/bevy_winit/src/web_resize.rs +++ b/crates/bevy_winit/src/web_resize.rs @@ -58,6 +58,15 @@ impl Default for CanvasParentResizeEventChannel { } } +fn get_size_element(element: &web_sys::HtmlCanvasElement) -> Option> { + 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, + )); +} + impl CanvasParentResizeEventChannel { pub(crate) fn listen_to_selector(&self, window_id: WindowId, selector: &str) { let sender = self.sender.clone(); @@ -81,4 +90,30 @@ impl CanvasParentResizeEventChannel { .unwrap(); closure.forget(); } + + pub(crate) fn listen_to_element( + &self, + window_id: WindowId, + element: web_sys::HtmlCanvasElement, + ) { + let sender = self.sender.clone(); + let resize = move || { + if let Some(size) = get_size_element(&element) { + sender.send(ResizeEvent { size, window_id }).unwrap(); + } + }; + + // ensure resize happens on startup + resize(); + + let closure = wasm_bindgen::closure::Closure::wrap(Box::new(move |_: web_sys::Event| { + resize(); + }) as Box); + let window = web_sys::window().unwrap(); + + window + .add_event_listener_with_callback("resize", closure.as_ref().unchecked_ref()) + .unwrap(); + closure.forget(); + } } diff --git a/crates/bevy_winit/src/winit_windows.rs b/crates/bevy_winit/src/winit_windows.rs index 47423957cfbc5..9c7b0d0c265f2 100644 --- a/crates/bevy_winit/src/winit_windows.rs +++ b/crates/bevy_winit/src/winit_windows.rs @@ -24,6 +24,50 @@ impl WinitWindows { event_loop: &winit::event_loop::EventLoopWindowTarget<()>, window_id: WindowId, window_descriptor: &WindowDescriptor, + ) -> Window { + #[cfg(target_arch = "wasm32")] + { + self.create_window_internal(event_loop, window_id, window_descriptor, None) + } + #[cfg(not(target_arch = "wasm32"))] + { + self.create_window_internal(event_loop, window_id, window_descriptor) + } + } + #[cfg(target_arch = "wasm32")] + pub fn create_window_with_canvas( + &mut self, + event_loop: &winit::event_loop::EventLoopWindowTarget<()>, + window_id: WindowId, + window_descriptor: &WindowDescriptor, + canvas: web_sys::HtmlCanvasElement, + ) -> Window { + self.create_window_internal(event_loop, window_id, window_descriptor, Some(canvas)) + } + #[cfg(target_arch = "wasm32")] + pub fn create_window_with_selector( + &mut self, + event_loop: &winit::event_loop::EventLoopWindowTarget<()>, + window_id: WindowId, + window_descriptor: &WindowDescriptor, + selector: &str, + ) -> Result, wasm_bindgen::JsValue> { + use wasm_bindgen::JsCast; + + let window = web_sys::window().unwrap(); + let document = window.document().unwrap(); + let canvas = document.query_selector(selector)?; + Ok(canvas.map(|canvas| { + let canvas = canvas.dyn_into::().ok(); + self.create_window_internal(event_loop, window_id, window_descriptor, canvas) + })) + } + fn create_window_internal( + &mut self, + event_loop: &winit::event_loop::EventLoopWindowTarget<()>, + window_id: WindowId, + window_descriptor: &WindowDescriptor, + #[cfg(target_arch = "wasm32")] canvas: Option, ) -> Window { let mut winit_window_builder = winit::window::WindowBuilder::new(); @@ -94,23 +138,9 @@ impl WinitWindows { let mut winit_window_builder = winit_window_builder.with_title(&window_descriptor.title); #[cfg(target_arch = "wasm32")] - { - use wasm_bindgen::JsCast; + if let Some(canvas) = canvas { use winit::platform::web::WindowBuilderExtWebSys; - - if let Some(selector) = &window_descriptor.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); - } - } + winit_window_builder = winit_window_builder.with_canvas(Some(canvas)); } let winit_window = winit_window_builder.build(event_loop).unwrap(); @@ -174,16 +204,14 @@ impl WinitWindows { { use winit::platform::web::WindowExtWebSys; - if window_descriptor.canvas.is_none() { - let canvas = winit_window.canvas(); + let canvas = winit_window.canvas(); - let window = web_sys::window().unwrap(); - let document = window.document().unwrap(); - let body = document.body().unwrap(); + let window = web_sys::window().unwrap(); + let document = window.document().unwrap(); + let body = document.body().unwrap(); - body.append_child(&canvas) - .expect("Append canvas to HTML body."); - } + body.append_child(&canvas) + .expect("Append canvas to HTML body."); } let position = winit_window @@ -201,7 +229,7 @@ impl WinitWindows { inner_size.height, scale_factor, position, - Some(raw_window_handle), + raw_window_handle, ) } diff --git a/examples/window/virtual.rs b/examples/window/virtual.rs new file mode 100644 index 0000000000000..99ecd3a1ea68f --- /dev/null +++ b/examples/window/virtual.rs @@ -0,0 +1,211 @@ +//! Uses two windows to visualize a 3D model from different angles. + +use std::f32::consts::PI; + +use bevy::{ + core_pipeline::clear_color::ClearColorConfig, + prelude::*, + render::{ + camera::RenderTarget, + extract_resource::{ExtractResource, ExtractResourcePlugin}, + render_asset::{PrepareAssetLabel, RenderAssets}, + render_resource::{ + Extent3d, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, + }, + view::{ExtractedWindows, RenderLayers, WindowSystem}, + RenderApp, RenderStage, + }, + window::{PresentMode, WindowId}, +}; + +#[derive(Clone, Resource)] +struct WindowTexture { + window_id: WindowId, + render_texture: Handle, +} + +impl ExtractResource for WindowTexture { + type Source = WindowTexture; + + fn extract_resource(source: &WindowTexture) -> Self { + source.clone() + } +} + +fn main() { + let mut app = App::new(); + app.add_plugins(DefaultPlugins) + .add_startup_system(setup) + .add_system(bevy::window::close_on_esc) + .add_plugin(ExtractResourcePlugin::::default()); + if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { + render_app.add_system_to_stage( + RenderStage::Prepare, + prepare_window_texture + .after(PrepareAssetLabel::AssetPrepare) + .before(WindowSystem::Prepare), + ); + } + app.run(); +} + +fn prepare_window_texture( + window_texture: Res, + gpu_images: Res>, + mut extracted_windows: ResMut, +) { + if let Some(window) = extracted_windows.get_mut(&window_texture.window_id) { + window.swap_chain_texture = Some( + gpu_images + .get(&window_texture.render_texture) + .unwrap() + .texture_view + .clone(), + ); + } +} + +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, + mut images: ResMut>, + mut windows: ResMut, +) { + let window_id = WindowId::new(); + windows.add(Window::new_virtual( + window_id, + &WindowDescriptor { + width: 800., + height: 600., + present_mode: PresentMode::AutoNoVsync, + title: "Second window".to_string(), + ..default() + }, + 800, + 600, + 1.0, + None, + )); + + let size = Extent3d { + width: 800, + height: 600, + ..default() + }; + + // This is the texture that will be rendered to. + let mut image = Image { + texture_descriptor: TextureDescriptor { + label: None, + size, + dimension: TextureDimension::D2, + format: TextureFormat::Bgra8UnormSrgb, + mip_level_count: 1, + sample_count: 1, + usage: TextureUsages::TEXTURE_BINDING + | TextureUsages::COPY_DST + | TextureUsages::RENDER_ATTACHMENT, + }, + ..default() + }; + + // fill image.data with zeroes + image.resize(size); + + let image_handle = images.add(image); + commands.insert_resource(WindowTexture { + window_id, + render_texture: image_handle.clone(), + }); + + let cube_handle = meshes.add(Mesh::from(shape::Cube { size: 4.0 })); + let cube_material_handle = materials.add(StandardMaterial { + base_color: Color::rgb(0.8, 0.7, 0.6), + reflectance: 0.02, + unlit: false, + ..default() + }); + + // This specifies the layer used for the first pass, which will be attached to the first pass camera and cube. + let first_pass_layer = RenderLayers::layer(1); + + // The cube that will be rendered to the texture. + commands.spawn(( + PbrBundle { + mesh: cube_handle, + material: cube_material_handle, + transform: Transform::from_translation(Vec3::new(0.0, 0.0, 1.0)), + ..default() + }, + first_pass_layer, + )); + + // Light + // NOTE: Currently lights are shared between passes - see https://github.com/bevyengine/bevy/issues/3462 + commands.spawn(PointLightBundle { + transform: Transform::from_translation(Vec3::new(0.0, 0.0, 10.0)), + ..default() + }); + + commands.spawn(( + Camera3dBundle { + camera_3d: Camera3d { + clear_color: ClearColorConfig::Custom(Color::WHITE), + ..default() + }, + camera: Camera { + // render before the "main pass" camera + priority: -1, + target: RenderTarget::Image(image_handle.clone()), + ..default() + }, + transform: Transform::from_translation(Vec3::new(0.0, 0.0, 15.0)) + .looking_at(Vec3::ZERO, Vec3::Y), + ..default() + }, + first_pass_layer, + )); + + let cube_size = 4.0; + let cube_handle = meshes.add(Mesh::from(shape::Box::new(cube_size, cube_size, cube_size))); + + // This material has the texture that has been rendered. + let material_handle = materials.add(StandardMaterial { + base_color_texture: Some(image_handle), + reflectance: 0.02, + unlit: false, + ..default() + }); + + // Main pass cube, with material containing the rendered first pass texture. + commands.spawn(PbrBundle { + mesh: cube_handle, + material: material_handle, + transform: Transform::from_xyz(0.0, 0.0, 1.5) + .with_rotation(Quat::from_rotation_x(-PI / 5.0)), + ..default() + }); + + // The main pass camera. + commands.spawn(Camera3dBundle { + transform: Transform::from_xyz(0.0, 0.0, 15.0).looking_at(Vec3::ZERO, Vec3::Y), + ..default() + }); + + let window_id = WindowId::new(); + windows.add(Window::new_virtual( + window_id, + &WindowDescriptor { + width: 800., + height: 600., + present_mode: PresentMode::AutoNoVsync, + title: "Second window".to_string(), + ..default() + }, + 800, + 600, + 1.0, + None, + )); +}