Skip to content

Add option to create headless windows #3835

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

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 15 additions & 10 deletions crates/bevy_render/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
extern crate core;

pub mod camera;
pub mod color;
pub mod mesh;
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -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::<bevy_window::Windows>();
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(),
Expand Down Expand Up @@ -289,6 +281,19 @@ impl Plugin for RenderPlugin {
}
}

fn try_create_surface(app: &mut App, wgpu_instance: &wgpu::Instance) -> Option<Surface> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should return a Result, not an Option IMO.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can see why that would make sense. Then the caller would know why they didn't get a surface (no window resource for example). On the other hand, we don't actually use the reason for not getting a surface anywhere (yet?).
If this returned an error, currently we'd have to convert it back into an Option to pass it into RequestAdapterOptions

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, that's fair. Since this isn't pub that's probably fine for now.

let windows = app
.world
.get_resource_mut::<bevy_window::Windows>()
.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) {
Expand Down
70 changes: 36 additions & 34 deletions crates/bevy_render/src/view/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ impl Plugin for WindowRenderPlugin {

pub struct ExtractedWindow {
pub id: WindowId,
pub handle: RawWindowHandleWrapper,
pub handle: Option<RawWindowHandleWrapper>,
pub physical_width: u32,
pub physical_height: u32,
pub present_mode: PresentMode,
Expand Down Expand Up @@ -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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing safety comment.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. As far as I can tell, it has never had a safety comment. I am also not experienced enough with the codebase to know why this unsafe is OK here (or maybe it even isn't, and we haven't noticed yet).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright. @cart / @superdump if you can quickly explain why this is safe during review that would be appreciated, but it shouldn't block this PR.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from https://docs.rs/wgpu/latest/wgpu/struct.Instance.html#safety-2:

Raw Window Handle must be a valid object to create a surface upon and must remain valid for the lifetime of the returned surface.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These safety requirements have always been slightly spooky. That is, imo a reasonable safety comment here is ¯\_(ツ)_/¯ :).

To my knowledge, it's safe, but it's very difficult to prove that. I don't think winit documents that this requirement is met, so any use of this is mostly best effort.

// 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));
}
}
}
11 changes: 7 additions & 4 deletions crates/bevy_window/src/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -162,7 +165,7 @@ pub struct Window {
cursor_visible: bool,
cursor_locked: bool,
physical_cursor_position: Option<DVec2>,
raw_window_handle: RawWindowHandleWrapper,
raw_window_handle: Option<RawWindowHandleWrapper>,
focused: bool,
mode: WindowMode,
#[cfg(target_arch = "wasm32")]
Expand Down Expand Up @@ -243,7 +246,7 @@ impl Window {
physical_height: u32,
scale_factor: f64,
position: Option<IVec2>,
raw_window_handle: RawWindowHandle,
raw_window_handle: Option<RawWindowHandle>,
) -> Self {
Window {
id,
Expand All @@ -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")]
Expand Down Expand Up @@ -581,7 +584,7 @@ impl Window {
self.focused
}

pub fn raw_window_handle(&self) -> RawWindowHandleWrapper {
pub fn raw_window_handle(&self) -> Option<RawWindowHandleWrapper> {
self.raw_window_handle.clone()
}
}
Expand Down
2 changes: 1 addition & 1 deletion crates/bevy_winit/src/winit_windows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ impl WinitWindows {
inner_size.height,
scale_factor,
position,
raw_window_handle,
Some(raw_window_handle),
)
}

Expand Down
91 changes: 91 additions & 0 deletions tests/how_to_test_ui.rs
Original file line number Diff line number Diff line change
@@ -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::<Entity, With<Button>>();
let button = *query.iter(&app.world).collect::<Vec<_>>().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::<Transform>().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()
});
}