diff --git a/crates/bevy_render/src/lib.rs b/crates/bevy_render/src/lib.rs index 5d7127c0ce7b1..e39e2c2ad2dee 100644 --- a/crates/bevy_render/src/lib.rs +++ b/crates/bevy_render/src/lib.rs @@ -1,5 +1,3 @@ -extern crate core; - pub mod camera; pub mod color; pub mod mesh; @@ -47,6 +45,7 @@ use bevy_app::{App, AppLabel, Plugin}; use bevy_asset::{AddAsset, AssetServer}; use bevy_ecs::prelude::*; use std::ops::{Deref, DerefMut}; +use wgpu::Surface; /// Contains the default Bevy rendering backend based on wgpu. #[derive(Default)] @@ -124,14 +123,7 @@ impl Plugin for RenderPlugin { if let Some(backends) = options.backends { let instance = wgpu::Instance::new(backends); - let surface = { - let windows = app.world.resource_mut::(); - let raw_handle = windows.get_primary().map(|window| unsafe { - let handle = window.raw_window_handle().get_handle(); - instance.create_surface(&handle) - }); - raw_handle - }; + let surface = try_create_surface(app, &instance); let request_adapter_options = wgpu::RequestAdapterOptions { power_preference: options.power_preference, compatible_surface: surface.as_ref(), @@ -289,6 +281,19 @@ impl Plugin for RenderPlugin { } } +fn try_create_surface(app: &mut App, wgpu_instance: &wgpu::Instance) -> Option { + let windows = app + .world + .get_resource_mut::() + .unwrap(); + windows.get_primary().and_then(|window| unsafe { + window.raw_window_handle().map(|handle_wrapper| { + let window_handle = handle_wrapper.get_handle(); + wgpu_instance.create_surface(&window_handle) + }) + }) +} + /// Executes the [`Extract`](RenderStage::Extract) stage of the renderer. /// This updates the render world with the extracted ECS data of the current frame. fn extract(app_world: &mut World, render_app: &mut App) { diff --git a/crates/bevy_render/src/view/window.rs b/crates/bevy_render/src/view/window.rs index c06776c54a878..096c4dedbc99b 100644 --- a/crates/bevy_render/src/view/window.rs +++ b/crates/bevy_render/src/view/window.rs @@ -40,7 +40,7 @@ impl Plugin for WindowRenderPlugin { pub struct ExtractedWindow { pub id: WindowId, - pub handle: RawWindowHandleWrapper, + pub handle: Option, pub physical_width: u32, pub physical_height: u32, pub present_mode: PresentMode, @@ -125,42 +125,44 @@ pub fn prepare_windows( ) { let window_surfaces = window_surfaces.deref_mut(); for window in windows.windows.values_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.handle.get_handle()) - }); - - let swap_chain_descriptor = wgpu::SurfaceConfiguration { - format: TextureFormat::bevy_default(), - width: window.physical_width, - height: window.physical_height, - usage: wgpu::TextureUsages::RENDER_ATTACHMENT, - present_mode: match window.present_mode { - PresentMode::Fifo => wgpu::PresentMode::Fifo, - PresentMode::Mailbox => wgpu::PresentMode::Mailbox, - PresentMode::Immediate => wgpu::PresentMode::Immediate, - }, - }; - - // Do the initial surface configuration if it hasn't been configured yet - if window_surfaces.configured_windows.insert(window.id) || window.size_changed { - render_device.configure_surface(surface, &swap_chain_descriptor); - } + if let Some(window_handle_wrapper) = &window.handle { + 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_handle_wrapper.get_handle()) + }); - let frame = match surface.get_current_texture() { - Ok(swap_chain_frame) => swap_chain_frame, - Err(wgpu::SurfaceError::Outdated) => { + let swap_chain_descriptor = wgpu::SurfaceConfiguration { + format: TextureFormat::bevy_default(), + width: window.physical_width, + height: window.physical_height, + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + present_mode: match window.present_mode { + PresentMode::Fifo => wgpu::PresentMode::Fifo, + PresentMode::Mailbox => wgpu::PresentMode::Mailbox, + PresentMode::Immediate => wgpu::PresentMode::Immediate, + }, + }; + + // Do the initial surface configuration if it hasn't been configured yet + if window_surfaces.configured_windows.insert(window.id) || window.size_changed { render_device.configure_surface(surface, &swap_chain_descriptor); - surface - .get_current_texture() - .expect("Error reconfiguring surface") } - err => err.expect("Failed to acquire next swap chain texture!"), - }; - window.swap_chain_texture = Some(TextureView::from(frame)); + let frame = match surface.get_current_texture() { + Ok(swap_chain_frame) => swap_chain_frame, + Err(wgpu::SurfaceError::Outdated) => { + render_device.configure_surface(surface, &swap_chain_descriptor); + surface + .get_current_texture() + .expect("Error reconfiguring surface") + } + err => err.expect("Failed to acquire next swap chain texture!"), + }; + + window.swap_chain_texture = Some(TextureView::from(frame)); + } } } diff --git a/crates/bevy_window/src/window.rs b/crates/bevy_window/src/window.rs index 47c03d085ce7e..00910276615c2 100644 --- a/crates/bevy_window/src/window.rs +++ b/crates/bevy_window/src/window.rs @@ -143,6 +143,9 @@ impl WindowResizeConstraints { /// requested size due to operating system limits on the window size, or the /// quantization of the logical size when converting the physical size to the /// logical size through the scaling factor. +/// +/// ## Headless Testing +/// To run tests without needing to create an actual window, set `raw_window_handle` to `None`. #[derive(Debug)] pub struct Window { id: WindowId, @@ -162,7 +165,7 @@ pub struct Window { cursor_visible: bool, cursor_locked: bool, physical_cursor_position: Option, - raw_window_handle: RawWindowHandleWrapper, + raw_window_handle: Option, focused: bool, mode: WindowMode, #[cfg(target_arch = "wasm32")] @@ -243,7 +246,7 @@ impl Window { physical_height: u32, scale_factor: f64, position: Option, - raw_window_handle: RawWindowHandle, + raw_window_handle: Option, ) -> Self { Window { id, @@ -263,7 +266,7 @@ impl Window { cursor_locked: window_descriptor.cursor_locked, cursor_icon: CursorIcon::Default, physical_cursor_position: None, - raw_window_handle: RawWindowHandleWrapper::new(raw_window_handle), + raw_window_handle: raw_window_handle.map(RawWindowHandleWrapper::new), focused: true, mode: window_descriptor.mode, #[cfg(target_arch = "wasm32")] @@ -581,7 +584,7 @@ impl Window { self.focused } - pub fn raw_window_handle(&self) -> RawWindowHandleWrapper { + pub fn raw_window_handle(&self) -> Option { self.raw_window_handle.clone() } } diff --git a/crates/bevy_winit/src/winit_windows.rs b/crates/bevy_winit/src/winit_windows.rs index 65354e80882f8..b4fcc47a2786b 100644 --- a/crates/bevy_winit/src/winit_windows.rs +++ b/crates/bevy_winit/src/winit_windows.rs @@ -168,7 +168,7 @@ impl WinitWindows { inner_size.height, scale_factor, position, - raw_window_handle, + Some(raw_window_handle), ) } diff --git a/tests/how_to_test_ui.rs b/tests/how_to_test_ui.rs new file mode 100644 index 0000000000000..8025cb2539aa2 --- /dev/null +++ b/tests/how_to_test_ui.rs @@ -0,0 +1,91 @@ +use bevy::prelude::*; +use bevy_internal::asset::AssetPlugin; +use bevy_internal::core_pipeline::CorePipelinePlugin; +use bevy_internal::input::InputPlugin; +use bevy_internal::render::settings::WgpuSettings; +use bevy_internal::render::RenderPlugin; +use bevy_internal::sprite::SpritePlugin; +use bevy_internal::text::TextPlugin; +use bevy_internal::ui::UiPlugin; +use bevy_internal::window::{WindowId, WindowPlugin}; + +const WINDOW_WIDTH: u32 = 200; +const WINDOW_HEIGHT: u32 = 100; + +struct HeadlessUiPlugin; + +impl Plugin for HeadlessUiPlugin { + fn build(&self, app: &mut App) { + // These tests are meant to be ran on systems without gpu, or display. + // To make this work, we tell bevy not to look for any rendering backends. + app.insert_resource(WgpuSettings { + backends: None, + ..Default::default() + }) + // To test the positioning of UI elements, + // we first need a window to position these elements in. + .insert_resource({ + let mut windows = Windows::default(); + windows.add(Window::new( + // At the moment, all ui elements are placed in the primary window. + WindowId::primary(), + &WindowDescriptor::default(), + WINDOW_WIDTH, + WINDOW_HEIGHT, + 1.0, + None, + // Because this test is running without a real window, we pass `None` here. + None, + )); + windows + }) + .add_plugins(MinimalPlugins) + .add_plugin(TransformPlugin) + .add_plugin(WindowPlugin::default()) + .add_plugin(InputPlugin) + .add_plugin(AssetPlugin) + .add_plugin(RenderPlugin) + .add_plugin(CorePipelinePlugin) + .add_plugin(SpritePlugin) + .add_plugin(TextPlugin) + .add_plugin(UiPlugin); + } +} + +#[test] +fn test_button_translation() { + let mut app = App::new(); + app.add_plugin(HeadlessUiPlugin) + .add_startup_system(setup_button_test); + + // First call to `update` also runs the startup systems. + app.update(); + + let mut query = app.world.query_filtered::>(); + let button = *query.iter(&app.world).collect::>().first().unwrap(); + + // The button's translation got updated because the UI system had a window to place it in. + // If we hadn't added a window, the button's translation would at this point be all zeros. + let button_transform = app.world.entity(button).get::().unwrap(); + assert_eq!( + button_transform.translation.x.floor() as u32, + WINDOW_WIDTH / 2 + ); + assert_eq!( + button_transform.translation.y.floor() as u32, + WINDOW_HEIGHT / 2 + ); +} + +fn setup_button_test(mut commands: Commands) { + commands.spawn_bundle(UiCameraBundle::default()); + commands.spawn_bundle(ButtonBundle { + style: Style { + size: Size::new(Val::Px(150.0), Val::Px(65.0)), + // Center this button in the middle of the window. + margin: Rect::all(Val::Auto), + ..Default::default() + }, + ..Default::default() + }); +}