diff --git a/crates/bevy_picking/src/input.rs b/crates/bevy_picking/src/input.rs index d751e07c94196..d1d2e90e40566 100644 --- a/crates/bevy_picking/src/input.rs +++ b/crates/bevy_picking/src/input.rs @@ -23,7 +23,7 @@ use bevy_math::Vec2; use bevy_platform::collections::{HashMap, HashSet}; use bevy_reflect::prelude::*; use bevy_render::camera::RenderTarget; -use bevy_window::{PrimaryWindow, WindowEvent, WindowRef}; +use bevy_window::{PrimaryWindow, WindowEvent, WindowPlugin, WindowRef}; use tracing::debug; use crate::pointer::{ @@ -80,6 +80,14 @@ impl Plugin for PointerInputPlugin { app.insert_resource(*self) .add_systems(Startup, spawn_mouse_pointer) .add_systems( + Last, + deactivate_touch_pointers.run_if(PointerInputPlugin::is_touch_enabled), + ) + .register_type::() + .register_type::(); + + if app.is_plugin_added::() { + app.add_systems( First, ( mouse_pick_events.run_if(PointerInputPlugin::is_mouse_enabled), @@ -87,13 +95,8 @@ impl Plugin for PointerInputPlugin { ) .chain() .in_set(PickingSystems::Input), - ) - .add_systems( - Last, - deactivate_touch_pointers.run_if(PointerInputPlugin::is_touch_enabled), - ) - .register_type::() - .register_type::(); + ); + } } } diff --git a/crates/bevy_render/src/camera/camera_driver_node.rs b/crates/bevy_render/src/camera/camera_driver_node.rs index 8be5a345b4c4a..6ff4cb5bf1db6 100644 --- a/crates/bevy_render/src/camera/camera_driver_node.rs +++ b/crates/bevy_render/src/camera/camera_driver_node.rs @@ -31,7 +31,6 @@ impl Node for CameraDriverNode { world: &World, ) -> Result<(), NodeRunError> { let sorted_cameras = world.resource::(); - let windows = world.resource::(); let mut camera_windows = >::default(); for sorted_camera in &sorted_cameras.0 { let Ok(camera) = self.cameras.get_manual(world, sorted_camera.entity) else { @@ -41,15 +40,22 @@ impl Node for CameraDriverNode { let mut run_graph = true; if let Some(NormalizedRenderTarget::Window(window_ref)) = camera.target { let window_entity = window_ref.entity(); - if windows - .windows - .get(&window_entity) - .is_some_and(|w| w.physical_width > 0 && w.physical_height > 0) - { - camera_windows.insert(window_entity); + if let Some(windows) = world.get_resource::() { + if windows + .windows + .get(&window_entity) + .is_some_and(|w| w.physical_width > 0 && w.physical_height > 0) + { + camera_windows.insert(window_entity); + } else { + // The window doesn't exist anymore or zero-sized so we don't need to run the graph + run_graph = false; + } } else { - // The window doesn't exist anymore or zero-sized so we don't need to run the graph - run_graph = false; + tracing::warn!( + "Camera is targeting Window {}, but ExtractedWindows is not available.", + window_entity + ); } } if run_graph { @@ -61,35 +67,37 @@ impl Node for CameraDriverNode { // wgpu (and some backends) require doing work for swap chains if you call `get_current_texture()` and `present()` // This ensures that Bevy doesn't crash, even when there are no cameras (and therefore no work submitted). - for (id, window) in world.resource::().iter() { - if camera_windows.contains(id) { - continue; - } + if let Some(windows) = world.get_resource::() { + for (id, window) in windows.iter() { + if camera_windows.contains(id) { + continue; + } - let Some(swap_chain_texture) = &window.swap_chain_texture_view else { - continue; - }; + let Some(swap_chain_texture) = &window.swap_chain_texture_view else { + continue; + }; - #[cfg(feature = "trace")] - let _span = tracing::info_span!("no_camera_clear_pass").entered(); - let pass_descriptor = RenderPassDescriptor { - label: Some("no_camera_clear_pass"), - color_attachments: &[Some(RenderPassColorAttachment { - view: swap_chain_texture, - resolve_target: None, - ops: Operations { - load: LoadOp::Clear(clear_color_global.to_linear().into()), - store: StoreOp::Store, - }, - })], - depth_stencil_attachment: None, - timestamp_writes: None, - occlusion_query_set: None, - }; + #[cfg(feature = "trace")] + let _span = tracing::info_span!("no_camera_clear_pass").entered(); + let pass_descriptor = RenderPassDescriptor { + label: Some("no_camera_clear_pass"), + color_attachments: &[Some(RenderPassColorAttachment { + view: swap_chain_texture, + resolve_target: None, + ops: Operations { + load: LoadOp::Clear(clear_color_global.to_linear().into()), + store: StoreOp::Store, + }, + })], + depth_stencil_attachment: None, + timestamp_writes: None, + occlusion_query_set: None, + }; - render_context - .command_encoder() - .begin_render_pass(&pass_descriptor); + render_context + .command_encoder() + .begin_render_pass(&pass_descriptor); + } } Ok(()) diff --git a/crates/bevy_render/src/camera/projection.rs b/crates/bevy_render/src/camera/projection.rs index a7796a1d1ad07..ce1c2baa5b21c 100644 --- a/crates/bevy_render/src/camera/projection.rs +++ b/crates/bevy_render/src/camera/projection.rs @@ -8,6 +8,7 @@ use bevy_ecs::prelude::*; use bevy_math::{ops, AspectRatio, Mat4, Rect, Vec2, Vec3A, Vec4}; use bevy_reflect::{std_traits::ReflectDefault, Reflect, ReflectDeserialize, ReflectSerialize}; use bevy_transform::{components::GlobalTransform, TransformSystems}; +use bevy_window::WindowPlugin; use derive_more::derive::From; use serde::{Deserialize, Serialize}; @@ -24,21 +25,25 @@ impl Plugin for CameraProjectionPlugin { .register_type::() .register_type::() .add_systems( + PostUpdate, + crate::view::update_frusta + .in_set(VisibilitySystems::UpdateFrusta) + .after(crate::camera::camera_system) + .after(TransformSystems::Propagate), + ); + + if app.is_plugin_added::() { + app.add_systems( PostStartup, crate::camera::camera_system.in_set(CameraUpdateSystems), ) .add_systems( PostUpdate, - ( - crate::camera::camera_system - .in_set(CameraUpdateSystems) - .before(AssetEventSystems), - crate::view::update_frusta - .in_set(VisibilitySystems::UpdateFrusta) - .after(crate::camera::camera_system) - .after(TransformSystems::Propagate), - ), + crate::camera::camera_system + .in_set(CameraUpdateSystems) + .before(AssetEventSystems), ); + } } } diff --git a/crates/bevy_render/src/lib.rs b/crates/bevy_render/src/lib.rs index b4c9011c7887e..4b11cd31132da 100644 --- a/crates/bevy_render/src/lib.rs +++ b/crates/bevy_render/src/lib.rs @@ -76,7 +76,7 @@ use bevy_ecs::schedule::ScheduleBuildSettings; use bevy_utils::prelude::default; pub use extract_param::Extract; -use bevy_window::{PrimaryWindow, RawHandleWrapperHolder}; +use bevy_window::{PrimaryWindow, RawHandleWrapperHolder, WindowPlugin}; use experimental::occlusion_culling::OcclusionCullingPlugin; use globals::GlobalsPlugin; use render_asset::{ @@ -396,8 +396,10 @@ impl Plugin for RenderPlugin { } }; + if app.is_plugin_added::() { + app.add_plugins(WindowRenderPlugin); + } app.add_plugins(( - WindowRenderPlugin, CameraPlugin, ViewPlugin, MeshPlugin, diff --git a/crates/bevy_render/src/renderer/mod.rs b/crates/bevy_render/src/renderer/mod.rs index 1691911c2cbbe..262f862bccbf8 100644 --- a/crates/bevy_render/src/renderer/mod.rs +++ b/crates/bevy_render/src/renderer/mod.rs @@ -85,14 +85,15 @@ pub fn render_system(world: &mut World, state: &mut SystemState(); } - let mut windows = world.resource_mut::(); - for window in windows.values_mut() { - if let Some(surface_texture) = window.swap_chain_texture.take() { - // TODO(clean): winit docs recommends calling pre_present_notify before this. - // though `present()` doesn't present the frame, it schedules it to be presented - // by wgpu. - // https://docs.rs/winit/0.29.9/wasm32-unknown-unknown/winit/window/struct.Window.html#method.pre_present_notify - surface_texture.present(); + if let Some(mut windows) = world.get_resource_mut::() { + for window in windows.values_mut() { + if let Some(surface_texture) = window.swap_chain_texture.take() { + // TODO(clean): winit docs recommends calling pre_present_notify before this. + // though `present()` doesn't present the frame, it schedules it to be presented + // by wgpu. + // https://docs.rs/winit/0.29.9/wasm32-unknown-unknown/winit/window/struct.Window.html#method.pre_present_notify + surface_texture.present(); + } } } diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index 2f80e5f94bdb6..66f1a9cfa098e 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -120,6 +120,8 @@ impl Plugin for ViewPlugin { VisibilityRangePlugin, )); + let has_windows_render_plugin = app.is_plugin_added::(); + if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app.add_systems( Render, @@ -128,18 +130,26 @@ impl Plugin for ViewPlugin { clear_view_attachments .in_set(RenderSystems::ManageViews) .before(create_surfaces), - prepare_view_attachments - .in_set(RenderSystems::ManageViews) - .before(prepare_view_targets) - .after(prepare_windows), - prepare_view_targets - .in_set(RenderSystems::ManageViews) - .after(prepare_windows) - .after(crate::render_asset::prepare_assets::) - .ambiguous_with(crate::camera::sort_cameras), // doesn't use `sorted_camera_index_for_target` prepare_view_uniforms.in_set(RenderSystems::PrepareResources), ), ); + + if has_windows_render_plugin { + render_app.add_systems( + Render, + ( + prepare_view_attachments + .in_set(RenderSystems::ManageViews) + .before(prepare_view_targets) + .after(prepare_windows), + prepare_view_targets + .in_set(RenderSystems::ManageViews) + .after(prepare_windows) + .after(crate::render_asset::prepare_assets::) + .ambiguous_with(crate::camera::sort_cameras), // doesn't use `sorted_camera_index_for_target` + ), + ); + } } } diff --git a/crates/bevy_render/src/view/window/screenshot.rs b/crates/bevy_render/src/view/window/screenshot.rs index 854a6bc064bbf..dc9777fe6f69d 100644 --- a/crates/bevy_render/src/view/window/screenshot.rs +++ b/crates/bevy_render/src/view/window/screenshot.rs @@ -495,84 +495,85 @@ impl SpecializedRenderPipeline for ScreenshotToScreenPipeline { } pub(crate) fn submit_screenshot_commands(world: &World, encoder: &mut CommandEncoder) { - let targets = world.resource::(); - let prepared = world.resource::(); - let pipelines = world.resource::(); - let gpu_images = world.resource::>(); - let windows = world.resource::(); - let manual_texture_views = world.resource::(); - - for (entity, render_target) in targets.iter() { - match render_target { - NormalizedRenderTarget::Window(window) => { - let window = window.entity(); - let Some(window) = windows.get(&window) else { - continue; - }; - let width = window.physical_width; - let height = window.physical_height; - let Some(texture_format) = window.swap_chain_texture_format else { - continue; - }; - let Some(swap_chain_texture) = window.swap_chain_texture.as_ref() else { - continue; - }; - let texture_view = swap_chain_texture.texture.create_view(&Default::default()); - render_screenshot( - encoder, - prepared, - pipelines, - entity, - width, - height, - texture_format, - &texture_view, - ); - } - NormalizedRenderTarget::Image(image) => { - let Some(gpu_image) = gpu_images.get(&image.handle) else { - warn!("Unknown image for screenshot, skipping: {:?}", image); - continue; - }; - let width = gpu_image.size.width; - let height = gpu_image.size.height; - let texture_format = gpu_image.texture_format; - let texture_view = gpu_image.texture_view.deref(); - render_screenshot( - encoder, - prepared, - pipelines, - entity, - width, - height, - texture_format, - texture_view, - ); - } - NormalizedRenderTarget::TextureView(texture_view) => { - let Some(texture_view) = manual_texture_views.get(texture_view) else { - warn!( - "Unknown manual texture view for screenshot, skipping: {:?}", - texture_view + if let Some(targets) = world.get_resource::() { + let prepared = world.resource::(); + let pipelines = world.resource::(); + let gpu_images = world.resource::>(); + let windows = world.resource::(); + let manual_texture_views = world.resource::(); + + for (entity, render_target) in targets.iter() { + match render_target { + NormalizedRenderTarget::Window(window) => { + let window = window.entity(); + let Some(window) = windows.get(&window) else { + continue; + }; + let width = window.physical_width; + let height = window.physical_height; + let Some(texture_format) = window.swap_chain_texture_format else { + continue; + }; + let Some(swap_chain_texture) = window.swap_chain_texture.as_ref() else { + continue; + }; + let texture_view = swap_chain_texture.texture.create_view(&Default::default()); + render_screenshot( + encoder, + prepared, + pipelines, + entity, + width, + height, + texture_format, + &texture_view, ); - continue; - }; - let width = texture_view.size.x; - let height = texture_view.size.y; - let texture_format = texture_view.format; - let texture_view = texture_view.texture_view.deref(); - render_screenshot( - encoder, - prepared, - pipelines, - entity, - width, - height, - texture_format, - texture_view, - ); - } - }; + } + NormalizedRenderTarget::Image(image) => { + let Some(gpu_image) = gpu_images.get(&image.handle) else { + warn!("Unknown image for screenshot, skipping: {:?}", image); + continue; + }; + let width = gpu_image.size.width; + let height = gpu_image.size.height; + let texture_format = gpu_image.texture_format; + let texture_view = gpu_image.texture_view.deref(); + render_screenshot( + encoder, + prepared, + pipelines, + entity, + width, + height, + texture_format, + texture_view, + ); + } + NormalizedRenderTarget::TextureView(texture_view) => { + let Some(texture_view) = manual_texture_views.get(texture_view) else { + warn!( + "Unknown manual texture view for screenshot, skipping: {:?}", + texture_view + ); + continue; + }; + let width = texture_view.size.x; + let height = texture_view.size.y; + let texture_format = texture_view.format; + let texture_view = texture_view.texture_view.deref(); + render_screenshot( + encoder, + prepared, + pipelines, + entity, + width, + height, + texture_format, + texture_view, + ); + } + }; + } } } @@ -627,70 +628,74 @@ pub(crate) fn collect_screenshots(world: &mut World) { #[cfg(feature = "trace")] let _span = tracing::info_span!("collect_screenshots").entered(); - let sender = world.resource::().deref().clone(); - let prepared = world.resource::(); - - for (entity, prepared) in prepared.iter() { - let entity = *entity; - let sender = sender.clone(); - let width = prepared.size.width; - let height = prepared.size.height; - let texture_format = prepared.texture.format(); - let pixel_size = texture_format.pixel_size(); - let buffer = prepared.buffer.clone(); - - let finish = async move { - let (tx, rx) = async_channel::bounded(1); - let buffer_slice = buffer.slice(..); - // The polling for this map call is done every frame when the command queue is submitted. - buffer_slice.map_async(wgpu::MapMode::Read, move |result| { - let err = result.err(); - if err.is_some() { - panic!("{}", err.unwrap().to_string()); - } - tx.try_send(()).unwrap(); - }); - rx.recv().await.unwrap(); - let data = buffer_slice.get_mapped_range(); - // we immediately move the data to CPU memory to avoid holding the mapped view for long - let mut result = Vec::from(&*data); - drop(data); - - if result.len() != ((width * height) as usize * pixel_size) { - // Our buffer has been padded because we needed to align to a multiple of 256. - // We remove this padding here - let initial_row_bytes = width as usize * pixel_size; - let buffered_row_bytes = - gpu_readback::align_byte_size(width * pixel_size as u32) as usize; - - let mut take_offset = buffered_row_bytes; - let mut place_offset = initial_row_bytes; - for _ in 1..height { - result.copy_within(take_offset..take_offset + buffered_row_bytes, place_offset); - take_offset += buffered_row_bytes; - place_offset += initial_row_bytes; + if let Some(prepared) = world.get_resource::() { + let sender = world.resource::().deref().clone(); + + for (entity, prepared) in prepared.iter() { + let entity = *entity; + let sender = sender.clone(); + let width = prepared.size.width; + let height = prepared.size.height; + let texture_format = prepared.texture.format(); + let pixel_size = texture_format.pixel_size(); + let buffer = prepared.buffer.clone(); + + let finish = async move { + let (tx, rx) = async_channel::bounded(1); + let buffer_slice = buffer.slice(..); + // The polling for this map call is done every frame when the command queue is submitted. + buffer_slice.map_async(wgpu::MapMode::Read, move |result| { + let err = result.err(); + if err.is_some() { + panic!("{}", err.unwrap().to_string()); + } + tx.try_send(()).unwrap(); + }); + rx.recv().await.unwrap(); + let data = buffer_slice.get_mapped_range(); + // we immediately move the data to CPU memory to avoid holding the mapped view for long + let mut result = Vec::from(&*data); + drop(data); + + if result.len() != ((width * height) as usize * pixel_size) { + // Our buffer has been padded because we needed to align to a multiple of 256. + // We remove this padding here + let initial_row_bytes = width as usize * pixel_size; + let buffered_row_bytes = + gpu_readback::align_byte_size(width * pixel_size as u32) as usize; + + let mut take_offset = buffered_row_bytes; + let mut place_offset = initial_row_bytes; + for _ in 1..height { + result.copy_within( + take_offset..take_offset + buffered_row_bytes, + place_offset, + ); + take_offset += buffered_row_bytes; + place_offset += initial_row_bytes; + } + result.truncate(initial_row_bytes * height as usize); } - result.truncate(initial_row_bytes * height as usize); - } - if let Err(e) = sender.send(( - entity, - Image::new( - Extent3d { - width, - height, - depth_or_array_layers: 1, - }, - wgpu::TextureDimension::D2, - result, - texture_format, - RenderAssetUsages::RENDER_WORLD, - ), - )) { - error!("Failed to send screenshot: {}", e); - } - }; + if let Err(e) = sender.send(( + entity, + Image::new( + Extent3d { + width, + height, + depth_or_array_layers: 1, + }, + wgpu::TextureDimension::D2, + result, + texture_format, + RenderAssetUsages::RENDER_WORLD, + ), + )) { + error!("Failed to send screenshot: {}", e); + } + }; - AsyncComputeTaskPool::get().spawn(finish).detach(); + AsyncComputeTaskPool::get().spawn(finish).detach(); + } } } diff --git a/crates/bevy_winit/src/lib.rs b/crates/bevy_winit/src/lib.rs index d7c880a9b930d..bddee0d3b5335 100644 --- a/crates/bevy_winit/src/lib.rs +++ b/crates/bevy_winit/src/lib.rs @@ -17,7 +17,7 @@ extern crate alloc; use bevy_derive::Deref; use bevy_reflect::prelude::ReflectDefault; use bevy_reflect::Reflect; -use bevy_window::{RawHandleWrapperHolder, WindowEvent}; +use bevy_window::{RawHandleWrapperHolder, WindowEvent, WindowPlugin}; use core::cell::RefCell; use core::marker::PhantomData; use winit::{event_loop::EventLoop, window::WindowId}; @@ -133,8 +133,12 @@ impl Plugin for WinitPlugin { .init_resource::() .insert_resource(DisplayHandleWrapper(event_loop.owned_display_handle())) .add_event::() - .set_runner(|app| winit_runner(app, event_loop)) - .add_systems( + .set_runner(|app| winit_runner(app, event_loop)); + + app.add_plugins(cursor::CursorPlugin); + + if app.is_plugin_added::() { + app.add_systems( Last, ( // `exit_on_all_closed` only checks if windows exist but doesn't access data, @@ -145,9 +149,8 @@ impl Plugin for WinitPlugin { ) .chain(), ); - - app.add_plugins(AccessKitPlugin); - app.add_plugins(cursor::CursorPlugin); + app.add_plugins(AccessKitPlugin); + } } } diff --git a/crates/bevy_winit/src/state.rs b/crates/bevy_winit/src/state.rs index 1855539cc5f62..4070311bead97 100644 --- a/crates/bevy_winit/src/state.rs +++ b/crates/bevy_winit/src/state.rs @@ -18,7 +18,7 @@ use bevy_input::{ }; #[cfg(any(not(target_arch = "wasm32"), feature = "custom_cursor"))] use bevy_log::error; -use bevy_log::{trace, warn}; +use bevy_log::{once, trace, warn}; #[cfg(feature = "custom_cursor")] use bevy_math::URect; use bevy_math::{ivec2, DVec2, Vec2}; @@ -41,7 +41,7 @@ use winit::{ use bevy_window::{ AppLifecycle, CursorEntered, CursorLeft, CursorMoved, FileDragAndDrop, Ime, RequestRedraw, - Window, WindowBackendScaleFactorChanged, WindowCloseRequested, WindowDestroyed, + Window, WindowBackendScaleFactorChanged, WindowCloseRequested, WindowCreated, WindowDestroyed, WindowEvent as BevyWindowEvent, WindowFocused, WindowMoved, WindowOccluded, WindowResized, WindowScaleFactorChanged, WindowThemeChanged, }; @@ -506,8 +506,14 @@ impl ApplicationHandler for WinitAppRunnerState { SystemState::>>::from_world(self.world_mut()); create_monitors(event_loop, create_monitor.get_mut(self.world_mut())); create_monitor.apply(self.world_mut()); - create_windows(event_loop, create_window.get_mut(self.world_mut())); - create_window.apply(self.world_mut()); + if self + .world_mut() + .get_resource::>() + .is_some() + { + create_windows(event_loop, create_window.get_mut(self.world_mut())); + create_window.apply(self.world_mut()); + } // TODO: This is a workaround for https://github.com/bevyengine/bevy/issues/17488 // while preserving the iOS fix in https://github.com/bevyengine/bevy/pull/11245 @@ -876,9 +882,13 @@ impl WinitAppRunnerState { } if !buffered_events.is_empty() { - world - .resource_mut::>() - .send_batch(buffered_events); + if let Some(mut events) = world.get_resource_mut::>() { + events.send_batch(buffered_events); + } else { + once!(warn!( + "Lost windows events because `Events` is not available." + )); + } } }