diff --git a/Cargo.toml b/Cargo.toml index f8f26109910da..ad564188424e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1577,6 +1577,17 @@ path = "tests/window/minimising.rs" [package.metadata.example.minimising] hidden = true +[[example]] +name = "virtual" +path = "examples/window/virtual.rs" + +[package.metadata.example.virtual] +name = "Virtual Window" +description = "Demonstrates creating virtual windows and manually setting their render target" +category = "Window" +wasm = 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 59b5cc5b462b8..6b1e837609c8e 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 { @@ -142,17 +143,21 @@ 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_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| { + match window.window_handle() { + AbstractWindowHandle::RawWindowHandle(handle) => unsafe { + // SAFETY: This is only run on the main thread. Also the caller of `RawHandleWrapper::new` has + // ensured that the window was created on the main thread. + Some(instance.create_surface(&handle.get_handle())) + }, + AbstractWindowHandle::Virtual => None, + } }); - + 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 627c96e8c9de8..a012c6adb2f7a 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, RawHandleWrapper, WindowClosed, WindowId, Windows, + AbstractWindowHandle, CompositeAlphaMode, PresentMode, WindowClosed, WindowId, Windows, }; use std::ops::{Deref, DerefMut}; use wgpu::TextureFormat; @@ -41,7 +41,7 @@ impl Plugin for WindowRenderPlugin { pub struct ExtractedWindow { pub id: WindowId, - pub raw_handle: Option, + pub handle: AbstractWindowHandle, pub physical_width: u32, pub physical_height: u32, pub present_mode: PresentMode, @@ -88,7 +88,7 @@ fn extract_windows( .entry(window.id()) .or_insert(ExtractedWindow { id: window.id(), - raw_handle: window.raw_handle(), + handle: window.window_handle(), physical_width: new_width, physical_height: new_height, present_mode: window.present_mode(), @@ -144,6 +144,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 @@ -173,31 +175,28 @@ pub fn prepare_windows( render_instance: Res, render_adapter: Res, ) { - for window in windows - .windows - .values_mut() - // value of raw_handle is only None in synthetic tests - .filter(|x| x.raw_handle.is_some()) - { + for window in windows.windows.values_mut() { let window_surfaces = window_surfaces.deref_mut(); - let surface_data = window_surfaces - .surfaces - .entry(window.id) - .or_insert_with(|| unsafe { - // NOTE: On some OSes this MUST be called from the main thread. - let surface = render_instance - .create_surface(&window.raw_handle.as_ref().unwrap().get_handle()); - let format = *surface - .get_supported_formats(&render_adapter) - .get(0) - .unwrap_or_else(|| { - panic!( - "No supported formats found for surface {:?} on adapter {:?}", - surface, render_adapter - ) - }); - SurfaceData { surface, format } - }); + let surface_data = 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. + let surface = render_instance.create_surface(&handle.get_handle()); + let format = *surface + .get_supported_formats(&render_adapter) + .get(0) + .unwrap_or_else(|| { + panic!( + "No supported formats found for surface {:?} on adapter {:?}", + surface, render_adapter + ) + }); + SurfaceData { surface, format } + }), + AbstractWindowHandle::Virtual => continue, + }; let surface_configuration = wgpu::SurfaceConfiguration { format: surface_data.format, diff --git a/crates/bevy_window/src/raw_handle.rs b/crates/bevy_window/src/raw_handle.rs index 0e495d2c138b5..bed16c30ddcb5 100644 --- a/crates/bevy_window/src/raw_handle.rs +++ b/crates/bevy_window/src/raw_handle.rs @@ -9,11 +9,24 @@ use raw_window_handle::{ /// thread-safe. #[derive(Debug, Clone)] pub struct RawHandleWrapper { - pub window_handle: RawWindowHandle, - pub display_handle: RawDisplayHandle, + window_handle: RawWindowHandle, + display_handle: RawDisplayHandle, } impl RawHandleWrapper { + /// Create a new `RawHandleWrapper` from its components + /// + /// # Safety + /// + /// If the particular underlying handle can only be used on the same thread, the caller should ensure that the window + /// was created on the main thread. + pub unsafe fn new(window_handle: RawWindowHandle, display_handle: RawDisplayHandle) -> Self { + RawHandleWrapper { + window_handle, + display_handle, + } + } + /// Returns a [`HasRawWindowHandle`] + [`HasRawDisplayHandle`] impl, which exposes [`RawWindowHandle`] and [`RawDisplayHandle`]. /// /// # Safety @@ -23,14 +36,6 @@ impl RawHandleWrapper { pub unsafe fn get_handle(&self) -> ThreadLockedRawWindowHandleWrapper { ThreadLockedRawWindowHandleWrapper(self.clone()) } - - pub fn get_display_handle(&self) -> RawDisplayHandle { - self.display_handle - } - - pub fn get_window_handle(&self) -> RawWindowHandle { - self.window_handle - } } // SAFETY: [`RawHandleWrapper`] is just a normal "raw pointer", which doesn't impl Send/Sync. However the pointer is only @@ -58,7 +63,7 @@ pub struct ThreadLockedRawWindowHandleWrapper(RawHandleWrapper); // and so exposing a safe method to get a [`RawWindowHandle`] directly would be UB. unsafe impl HasRawWindowHandle for ThreadLockedRawWindowHandleWrapper { fn raw_window_handle(&self) -> RawWindowHandle { - self.0.get_window_handle() + self.0.window_handle } } @@ -70,6 +75,6 @@ unsafe impl HasRawWindowHandle for ThreadLockedRawWindowHandleWrapper { // and so exposing a safe method to get a [`RawDisplayHandle`] directly would be UB. unsafe impl HasRawDisplayHandle for ThreadLockedRawWindowHandleWrapper { fn raw_display_handle(&self) -> RawDisplayHandle { - self.0.get_display_handle() + self.0.display_handle } } diff --git a/crates/bevy_window/src/window.rs b/crates/bevy_window/src/window.rs index 1c7416b2c96a2..6d2e6831d295b 100644 --- a/crates/bevy_window/src/window.rs +++ b/crates/bevy_window/src/window.rs @@ -179,7 +179,22 @@ 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(RawHandleWrapper), + /// 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, +} + +/// An operating system or virtual window that can present content and receive user input. /// /// To create a window, use a [`EventWriter`](`crate::CreateWindow`). /// @@ -241,13 +256,13 @@ impl WindowResizeConstraints { /// resize_constraints, /// ..default() /// }; -/// let mut window = Window::new( +/// let mut window = Window::new_virtual( /// WindowId::new(), /// &window_descriptor, /// 100, // physical_width /// 100, // physical_height /// 1.0, // scale_factor -/// None, None); +/// None); /// /// let area = compute_window_area(&window); /// assert_eq!(area, 100.0 * 100.0); @@ -287,7 +302,7 @@ pub struct Window { cursor_visible: bool, cursor_grab_mode: CursorGrabMode, physical_cursor_position: Option, - raw_handle: Option, + window_handle: AbstractWindowHandle, focused: bool, mode: WindowMode, canvas: Option, @@ -411,7 +426,46 @@ impl Window { physical_height: u32, scale_factor: f64, position: Option, - raw_handle: Option, + raw_handle: RawHandleWrapper, + ) -> 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_grab_mode: window_descriptor.cursor_grab_mode, + cursor_icon: CursorIcon::Default, + physical_cursor_position: None, + window_handle: AbstractWindowHandle::RawWindowHandle(raw_handle), + focused: true, + mode: window_descriptor.mode, + canvas: window_descriptor.canvas.clone(), + fit_canvas_to_parent: window_descriptor.fit_canvas_to_parent, + command_queue: Vec::new(), + alpha_mode: window_descriptor.alpha_mode, + } + } + + /// 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, @@ -431,7 +485,7 @@ impl Window { cursor_grab_mode: window_descriptor.cursor_grab_mode, cursor_icon: CursorIcon::Default, physical_cursor_position: None, - raw_handle, + window_handle: AbstractWindowHandle::Virtual, focused: true, mode: window_descriptor.mode, canvas: window_descriptor.canvas.clone(), @@ -440,6 +494,7 @@ impl Window { alpha_mode: window_descriptor.alpha_mode, } } + /// Get the window's [`WindowId`]. #[inline] pub fn id(&self) -> WindowId { @@ -822,11 +877,10 @@ impl Window { pub fn is_focused(&self) -> bool { self.focused } - /// Get the [`RawHandleWrapper`] 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_handle(&self) -> Option { - self.raw_handle.as_ref().cloned() + + /// Get the [`AbstractWindowHandle`] corresponding to this window. + pub fn window_handle(&self) -> AbstractWindowHandle { + self.window_handle.clone() } /// The "html canvas" element selector. diff --git a/crates/bevy_winit/src/lib.rs b/crates/bevy_winit/src/lib.rs index 831020323a45d..b4362a202b5d2 100644 --- a/crates/bevy_winit/src/lib.rs +++ b/crates/bevy_winit/src/lib.rs @@ -685,11 +685,14 @@ 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) { - let window = winit_windows.create_window( - event_loop, - create_window_event.id, - &create_window_event.descriptor, - ); + let window = unsafe { + // SAFETY: This is only called from the main thread + winit_windows.create_window( + event_loop, + create_window_event.id, + &create_window_event.descriptor, + ) + }; // This event is already sent on windows, x11, and xwayland. // TODO: we aren't yet sure about native wayland, so we might be able to exclude it, // but sending a duplicate event isn't problematic, as windows already does this. diff --git a/crates/bevy_winit/src/winit_windows.rs b/crates/bevy_winit/src/winit_windows.rs index 5d803c944b2bc..69a51a482c3c3 100644 --- a/crates/bevy_winit/src/winit_windows.rs +++ b/crates/bevy_winit/src/winit_windows.rs @@ -23,7 +23,10 @@ pub struct WinitWindows { } impl WinitWindows { - pub fn create_window( + /// # SAFETY + /// + /// This should only be called from the main thread, as it might create windows that cannot be used across threads + pub unsafe fn create_window( &mut self, event_loop: &winit::event_loop::EventLoopWindowTarget<()>, window_id: WindowId, @@ -199,9 +202,12 @@ impl WinitWindows { .map(|position| IVec2::new(position.x, position.y)); let inner_size = winit_window.inner_size(); let scale_factor = winit_window.scale_factor(); - let raw_handle = RawHandleWrapper { - window_handle: winit_window.raw_window_handle(), - display_handle: winit_window.raw_display_handle(), + let raw_handle = unsafe { + // SAFETY: Caller ensured that this is run on the main thread + RawHandleWrapper::new( + winit_window.raw_window_handle(), + winit_window.raw_display_handle(), + ) }; self.windows.insert(winit_window.id(), winit_window); Window::new( @@ -211,7 +217,7 @@ impl WinitWindows { inner_size.height, scale_factor, position, - Some(raw_handle), + raw_handle, ) } diff --git a/examples/README.md b/examples/README.md index 31b986abdbe05..3fead4b5a183f 100644 --- a/examples/README.md +++ b/examples/README.md @@ -328,6 +328,7 @@ Example | Description [Multiple Windows](../examples/window/multiple_windows.rs) | Demonstrates creating multiple windows, and rendering to them [Scale Factor Override](../examples/window/scale_factor_override.rs) | Illustrates how to customize the default window settings [Transparent Window](../examples/window/transparent_window.rs) | Illustrates making the window transparent and hiding the window decoration +[Virtual Window](../examples/window/virtual.rs) | Demonstrates creating virtual windows and manually setting their render target [Window Resizing](../examples/window/window_resizing.rs) | Demonstrates resizing and responding to resizing a window [Window Settings](../examples/window/window_settings.rs) | Demonstrates customizing default window settings 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, + )); +}