diff --git a/crates/bevy_app/src/sub_app.rs b/crates/bevy_app/src/sub_app.rs index 48ffe9738830e..3b9aa5f319eaa 100644 --- a/crates/bevy_app/src/sub_app.rs +++ b/crates/bevy_app/src/sub_app.rs @@ -102,6 +102,21 @@ impl SubApp { Self::default() } + /// Returns a default, empty [`SubApp`] using an entity alloc mask. + pub fn new_with_entity_alloc_mask(mask: u32) -> Self { + let mut world = World::new_with_entity_alloc_mask(mask); + world.init_resource::(); + Self { + world, + plugin_registry: Vec::default(), + plugin_names: HashSet::default(), + plugin_build_depth: 0, + plugins_state: PluginsState::Adding, + update_schedule: None, + extract: None, + } + } + /// This method is a workaround. Each [`SubApp`] can have its own plugins, but [`Plugin`] /// works on an [`App`] as a whole. fn run_as_app(&mut self, f: F) diff --git a/crates/bevy_ecs/src/entity/mod.rs b/crates/bevy_ecs/src/entity/mod.rs index cca781d06b652..c4dae29f01e65 100644 --- a/crates/bevy_ecs/src/entity/mod.rs +++ b/crates/bevy_ecs/src/entity/mod.rs @@ -59,7 +59,9 @@ use crate::{ }; #[cfg(feature = "serialize")] use serde::{Deserialize, Serialize}; -use std::{fmt, hash::Hash, mem, num::NonZeroU32, sync::atomic::Ordering}; +use std::{ + fmt, hash::Hash, iter::StepBy, mem, num::NonZeroU32, ops::Range, sync::atomic::Ordering, +}; #[cfg(target_has_atomic = "64")] use std::sync::atomic::AtomicI64 as AtomicIdCursor; @@ -453,7 +455,8 @@ pub struct ReserveEntitiesIterator<'a> { index_iter: std::slice::Iter<'a, u32>, // New Entity indices to hand out, outside the range of meta.len(). - index_range: std::ops::Range, + index_range: StepBy>, + mask: u32, } impl<'a> Iterator for ReserveEntitiesIterator<'a> { @@ -465,7 +468,11 @@ impl<'a> Iterator for ReserveEntitiesIterator<'a> { .map(|&index| { Entity::from_raw_and_generation(index, self.meta[index as usize].generation) }) - .or_else(|| self.index_range.next().map(Entity::from_raw)) + .or_else(|| { + self.index_range + .next() + .map(|i| Entity::from_raw(i | self.mask)) + }) } fn size_hint(&self) -> (usize, Option) { @@ -477,6 +484,10 @@ impl<'a> Iterator for ReserveEntitiesIterator<'a> { impl<'a> ExactSizeIterator for ReserveEntitiesIterator<'a> {} impl<'a> core::iter::FusedIterator for ReserveEntitiesIterator<'a> {} +pub const RESERVED_BITS: usize = 1; +pub const ENTITY_ALLOC_STEP: usize = 1 << RESERVED_BITS; +pub const INDEX_HIGH_MASK: u32 = u32::MAX << RESERVED_BITS; + /// A [`World`]'s internal metadata store on all of its entities. /// /// Contains metadata on: @@ -533,6 +544,7 @@ pub struct Entities { free_cursor: AtomicIdCursor, /// Stores the number of free entities for [`len`](Entities::len) len: u32, + mask: u32, } impl Entities { @@ -542,6 +554,18 @@ impl Entities { pending: Vec::new(), free_cursor: AtomicIdCursor::new(0), len: 0, + mask: 0, + } + } + + pub(crate) fn new_with_mask(mask: u32) -> Self { + assert!((mask as usize) < ENTITY_ALLOC_STEP); + Entities { + meta: Vec::new(), + pending: Vec::new(), + free_cursor: AtomicIdCursor::new(0), + len: 0, + mask, } } @@ -577,7 +601,8 @@ impl Entities { // to go, yielding `meta.len()+0 .. meta.len()+3`. let base = self.meta.len() as IdCursor; - let new_id_end = u32::try_from(base - range_start).expect("too many entities"); + let new_id_end = u32::try_from(base - (range_start * ENTITY_ALLOC_STEP as i64)) + .expect("too many entities"); // `new_id_end` is in range, so no need to check `start`. let new_id_start = (base - range_end.min(0)) as u32; @@ -588,7 +613,8 @@ impl Entities { ReserveEntitiesIterator { meta: &self.meta[..], index_iter: self.pending[freelist_range].iter(), - index_range: new_id_start..new_id_end, + index_range: (new_id_start..new_id_end).step_by(ENTITY_ALLOC_STEP), + mask: self.mask, } } @@ -608,7 +634,9 @@ impl Entities { // As `self.free_cursor` goes more and more negative, we return IDs farther // and farther beyond `meta.len()`. Entity::from_raw( - u32::try_from(self.meta.len() as IdCursor - n).expect("too many entities"), + u32::try_from(self.meta.len() as IdCursor - (n * ENTITY_ALLOC_STEP as i64)) + .expect("too many entities") + | self.mask, ) } } @@ -631,43 +659,10 @@ impl Entities { Entity::from_raw_and_generation(index, self.meta[index as usize].generation) } else { let index = u32::try_from(self.meta.len()).expect("too many entities"); - self.meta.push(EntityMeta::EMPTY); - Entity::from_raw(index) - } - } - - /// Allocate a specific entity ID, overwriting its generation. - /// - /// Returns the location of the entity currently using the given ID, if any. Location should be - /// written immediately. - pub fn alloc_at(&mut self, entity: Entity) -> Option { - self.verify_flushed(); - - let loc = if entity.index() as usize >= self.meta.len() { - self.pending - .extend((self.meta.len() as u32)..entity.index()); - let new_free_cursor = self.pending.len() as IdCursor; - *self.free_cursor.get_mut() = new_free_cursor; self.meta - .resize(entity.index() as usize + 1, EntityMeta::EMPTY); - self.len += 1; - None - } else if let Some(index) = self.pending.iter().position(|item| *item == entity.index()) { - self.pending.swap_remove(index); - let new_free_cursor = self.pending.len() as IdCursor; - *self.free_cursor.get_mut() = new_free_cursor; - self.len += 1; - None - } else { - Some(mem::replace( - &mut self.meta[entity.index() as usize].location, - EntityMeta::EMPTY.location, - )) - }; - - self.meta[entity.index() as usize].generation = entity.generation; - - loc + .extend((0..ENTITY_ALLOC_STEP).map(|_| EntityMeta::EMPTY)); + Entity::from_raw(index | self.mask) + } } /// Allocate a specific entity ID, overwriting its generation. @@ -680,12 +675,13 @@ impl Entities { self.verify_flushed(); let result = if entity.index() as usize >= self.meta.len() { - self.pending - .extend((self.meta.len() as u32)..entity.index()); + let padding = (entity.index() + 1).next_multiple_of(ENTITY_ALLOC_STEP as u32); + self.pending.extend( + (self.meta.len() as u32..padding).filter(|v| (v & !INDEX_HIGH_MASK) == self.mask), + ); let new_free_cursor = self.pending.len() as IdCursor; *self.free_cursor.get_mut() = new_free_cursor; - self.meta - .resize(entity.index() as usize + 1, EntityMeta::EMPTY); + self.meta.resize(padding as usize, EntityMeta::EMPTY); self.len += 1; AllocAtWithoutReplacement::DidNotExist } else if let Some(index) = self.pending.iter().position(|item| *item == entity.index()) { @@ -731,11 +727,13 @@ impl Entities { let loc = mem::replace(&mut meta.location, EntityMeta::EMPTY.location); - self.pending.push(entity.index()); + if entity.index() & !INDEX_HIGH_MASK == self.mask { + self.pending.push(entity.index()); - let new_free_cursor = self.pending.len() as IdCursor; - *self.free_cursor.get_mut() = new_free_cursor; - self.len -= 1; + let new_free_cursor = self.pending.len() as IdCursor; + *self.free_cursor.get_mut() = new_free_cursor; + self.len -= 1; + } Some(loc) } @@ -834,7 +832,8 @@ impl Entities { // If this entity was manually created, then free_cursor might be positive // Returning None handles that case correctly let num_pending = usize::try_from(-free_cursor).ok()?; - (idu < self.meta.len() + num_pending).then_some(Entity::from_raw(index)) + (idu < self.meta.len() + num_pending * ENTITY_ALLOC_STEP) + .then_some(Entity::from_raw(index)) } } @@ -860,7 +859,7 @@ impl Entities { current_free_cursor as usize } else { let old_meta_len = self.meta.len(); - let new_meta_len = old_meta_len + -current_free_cursor as usize; + let new_meta_len = old_meta_len + (-current_free_cursor as usize) * ENTITY_ALLOC_STEP; self.meta.resize(new_meta_len, EntityMeta::EMPTY); self.len += -current_free_cursor as u32; for (index, meta) in self.meta.iter_mut().enumerate().skip(old_meta_len) { @@ -902,6 +901,7 @@ impl Entities { pub unsafe fn flush_and_reserve_invalid_assuming_no_entities(&mut self, count: usize) { let free_cursor = self.free_cursor.get_mut(); *free_cursor = 0; + let count = count.next_multiple_of(ENTITY_ALLOC_STEP); self.meta.reserve(count); // SAFETY: The EntityMeta struct only contains integers, and it is valid to have all bytes set to u8::MAX unsafe { diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 6f1f1a1214178..a56c63cb6f754 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -187,6 +187,35 @@ impl World { World::default() } + /// Creates a new empty [`World`]. + /// + /// # Panics + /// + /// If [`usize::MAX`] [`World`]s have been created. + /// This guarantee allows System Parameters to safely uniquely identify a [`World`], + /// since its [`WorldId`] is unique + pub fn new_with_entity_alloc_mask(mask: u32) -> Self { + let mut world = Self { + id: WorldId::new().expect("More `bevy` `World`s have been created than is supported"), + entities: Entities::new_with_mask(mask), + components: Default::default(), + archetypes: Archetypes::new(), + storages: Default::default(), + bundles: Default::default(), + observers: Observers::default(), + removed_components: Default::default(), + // Default value is `1`, and `last_change_tick`s default to `0`, such that changes + // are detected on first system runs and for direct world queries. + change_tick: AtomicU32::new(1), + last_change_tick: Tick::new(0), + last_check_tick: Tick::new(0), + last_trigger_id: 0, + command_queue: RawCommandQueue::new(), + }; + world.bootstrap(); + world + } + /// Retrieves this [`World`]'s unique ID #[inline] pub fn id(&self) -> WorldId { diff --git a/crates/bevy_gizmos/src/lib.rs b/crates/bevy_gizmos/src/lib.rs index 3e28516b608b7..0d80e74b25816 100644 --- a/crates/bevy_gizmos/src/lib.rs +++ b/crates/bevy_gizmos/src/lib.rs @@ -105,6 +105,7 @@ use bevy_render::{ ShaderType, VertexFormat, }, renderer::RenderDevice, + world_sync::TemporaryRenderEntity, Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; @@ -452,6 +453,7 @@ fn extract_gizmo_data( (*handle).clone_weak(), #[cfg(any(feature = "bevy_pbr", feature = "bevy_sprite"))] config::GizmoMeshConfig::from(config), + TemporaryRenderEntity, )); } } diff --git a/crates/bevy_pbr/src/lib.rs b/crates/bevy_pbr/src/lib.rs index ea84c49f18709..2dda2aedb18e1 100644 --- a/crates/bevy_pbr/src/lib.rs +++ b/crates/bevy_pbr/src/lib.rs @@ -420,6 +420,9 @@ impl Plugin for PbrPlugin { ) .init_resource::(); + render_app.world_mut().observe(add_light_view_entities); + render_app.world_mut().observe(remove_light_view_entities); + let shadow_pass_node = ShadowPassNode::new(render_app.world_mut()); let mut graph = render_app.world_mut().resource_mut::(); let draw_3d_graph = graph.get_sub_graph_mut(Core3d).unwrap(); diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index ae509c8739c3c..7e3d7e54bc707 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -1,6 +1,7 @@ use bevy_asset::UntypedAssetId; use bevy_color::ColorToComponents; use bevy_core_pipeline::core_3d::{Camera3d, CORE_3D_DEPTH_FORMAT}; +use bevy_derive::{Deref, DerefMut}; use bevy_ecs::entity::EntityHashSet; use bevy_ecs::prelude::*; use bevy_ecs::{entity::EntityHashMap, system::lifetimeless::Read}; @@ -356,6 +357,33 @@ pub fn extract_lights( } } +#[derive(Component, Default, Deref, DerefMut)] +pub struct LightViewEntities(Vec); + +// TODO: using required component +pub(crate) fn add_light_view_entities( + trigger: Trigger, + mut commands: Commands, +) { + commands + .get_entity(trigger.entity()) + .map(|v| v.insert(LightViewEntities::default())); +} + +pub(crate) fn remove_light_view_entities( + trigger: Trigger, + query: Query<&LightViewEntities>, + mut commands: Commands, +) { + if let Ok(entities) = query.get(trigger.entity()) { + for e in entities.0.iter().copied() { + if let Some(v) = commands.get_entity(e) { + v.despawn(); + } + } + } +} + pub(crate) const POINT_LIGHT_NEAR_Z: f32 = 0.1f32; pub(crate) struct CubeMapFace { @@ -528,14 +556,17 @@ pub fn prepare_lights( point_light_shadow_map: Res, directional_light_shadow_map: Res, mut shadow_render_phases: ResMut>, - mut max_directional_lights_warning_emitted: Local, - mut max_cascades_per_light_warning_emitted: Local, + (mut max_directional_lights_warning_emitted, mut max_cascades_per_light_warning_emitted): ( + Local, + Local, + ), point_lights: Query<( Entity, &ExtractedPointLight, AnyOf<(&CubemapFrusta, &Frustum)>, )>, directional_lights: Query<(Entity, &ExtractedDirectionalLight)>, + mut light_view_entities: Query<&mut LightViewEntities>, mut live_shadow_mapping_lights: Local, ) { let views_iter = views.iter(); @@ -791,8 +822,9 @@ pub fn prepare_lights( live_shadow_mapping_lights.clear(); + let mut dir_light_view_offset = 0; // set up light data for each view - for (entity, extracted_view, clusters, maybe_layers) in &views { + for (offset, (entity, extracted_view, clusters, maybe_layers)) in views.iter().enumerate() { let point_light_depth_texture = texture_cache.get( &render_device, TextureDescriptor { @@ -878,9 +910,18 @@ pub fn prepare_lights( // and ignore rotation because we want the shadow map projections to align with the axes let view_translation = GlobalTransform::from_translation(light.transform.translation()); - for (face_index, (view_rotation, frustum)) in cube_face_rotations + let Ok(mut light_entities) = light_view_entities.get_mut(light_entity) else { + continue; + }; + + while light_entities.len() < 6 * (offset + 1) { + light_entities.push(commands.spawn_empty().id()); + } + + for (face_index, ((view_rotation, frustum), view_light_entity)) in cube_face_rotations .iter() .zip(&point_light_frusta.unwrap().frusta) + .zip(light_entities.iter().skip(6 * offset).copied()) .enumerate() { let depth_texture_view = @@ -897,36 +938,35 @@ pub fn prepare_lights( array_layer_count: Some(1u32), }); - let view_light_entity = commands - .spawn(( - ShadowView { - depth_attachment: DepthAttachment::new(depth_texture_view, Some(0.0)), - pass_name: format!( - "shadow pass point light {} {}", - light_index, - face_index_to_name(face_index) - ), - }, - ExtractedView { - viewport: UVec4::new( - 0, - 0, - point_light_shadow_map.size as u32, - point_light_shadow_map.size as u32, - ), - world_from_view: view_translation * *view_rotation, - clip_from_world: None, - clip_from_view: cube_face_projection, - hdr: false, - color_grading: Default::default(), - }, - *frustum, - LightEntity::Point { - light_entity, - face_index, - }, - )) - .id(); + commands.entity(view_light_entity).insert(( + ShadowView { + depth_attachment: DepthAttachment::new(depth_texture_view, Some(0.0)), + pass_name: format!( + "shadow pass point light {} {}", + light_index, + face_index_to_name(face_index) + ), + }, + ExtractedView { + viewport: UVec4::new( + 0, + 0, + point_light_shadow_map.size as u32, + point_light_shadow_map.size as u32, + ), + world_from_view: view_translation * *view_rotation, + clip_from_world: None, + clip_from_view: cube_face_projection, + hdr: false, + color_grading: Default::default(), + }, + *frustum, + LightEntity::Point { + light_entity, + face_index, + }, + )); + view_lights.push(view_light_entity); shadow_render_phases.insert_or_clear(view_light_entity); @@ -944,6 +984,10 @@ pub fn prepare_lights( let spot_world_from_view = spot_light_world_from_view(&light.transform); let spot_world_from_view = spot_world_from_view.into(); + let Ok(mut light_view_entities) = light_view_entities.get_mut(light_entity) else { + continue; + }; + let angle = light.spot_light_angles.expect("lights should be sorted so that \ [point_light_count..point_light_count + spot_light_shadow_maps_count] are spot lights").1; let spot_projection = spot_light_clip_from_view(angle); @@ -962,29 +1006,33 @@ pub fn prepare_lights( array_layer_count: Some(1u32), }); - let view_light_entity = commands - .spawn(( - ShadowView { - depth_attachment: DepthAttachment::new(depth_texture_view, Some(0.0)), - pass_name: format!("shadow pass spot light {light_index}"), - }, - ExtractedView { - viewport: UVec4::new( - 0, - 0, - directional_light_shadow_map.size as u32, - directional_light_shadow_map.size as u32, - ), - world_from_view: spot_world_from_view, - clip_from_view: spot_projection, - clip_from_world: None, - hdr: false, - color_grading: Default::default(), - }, - *spot_light_frustum.unwrap(), - LightEntity::Spot { light_entity }, - )) - .id(); + while light_view_entities.len() < offset + 1 { + light_view_entities.push(commands.spawn_empty().id()); + } + + let view_light_entity = light_view_entities[offset]; + + commands.entity(view_light_entity).insert(( + ShadowView { + depth_attachment: DepthAttachment::new(depth_texture_view, Some(0.0)), + pass_name: format!("shadow pass spot light {light_index}"), + }, + ExtractedView { + viewport: UVec4::new( + 0, + 0, + directional_light_shadow_map.size as u32, + directional_light_shadow_map.size as u32, + ), + world_from_view: spot_world_from_view, + clip_from_view: spot_projection, + clip_from_world: None, + hdr: false, + color_grading: Default::default(), + }, + *spot_light_frustum.unwrap(), + LightEntity::Spot { light_entity }, + )); view_lights.push(view_light_entity); @@ -1002,6 +1050,9 @@ pub fn prepare_lights( { let gpu_light = &mut gpu_lights.directional_lights[light_index]; + let Ok(mut light_view_entities) = light_view_entities.get_mut(light_entity) else { + continue; + }; // Check if the light intersects with the view. if !view_layers.intersects(&light.render_layers) { gpu_light.skip = 1u32; @@ -1025,9 +1076,22 @@ pub fn prepare_lights( .unwrap() .iter() .take(MAX_CASCADES_PER_LIGHT); - for (cascade_index, ((cascade, frustum), bound)) in cascades + + let iter = cascades .zip(frusta) - .zip(&light.cascade_shadow_config.bounds) + .zip(&light.cascade_shadow_config.bounds); + + while light_view_entities.len() < dir_light_view_offset + iter.len() { + light_view_entities.push(commands.spawn_empty().id()); + } + + for (cascade_index, (((cascade, frustum), bound), view_light_entity)) in iter + .zip( + light_view_entities + .iter() + .skip(dir_light_view_offset) + .copied(), + ) .enumerate() { gpu_lights.directional_lights[light_index].cascades[cascade_index] = @@ -1057,37 +1121,37 @@ pub fn prepare_lights( frustum.half_spaces[4] = HalfSpace::new(frustum.half_spaces[4].normal().extend(f32::INFINITY)); - let view_light_entity = commands - .spawn(( - ShadowView { - depth_attachment: DepthAttachment::new(depth_texture_view, Some(0.0)), - pass_name: format!( - "shadow pass directional light {light_index} cascade {cascade_index}"), - }, - ExtractedView { - viewport: UVec4::new( - 0, - 0, - directional_light_shadow_map.size as u32, - directional_light_shadow_map.size as u32, - ), - world_from_view: GlobalTransform::from(cascade.world_from_cascade), - clip_from_view: cascade.clip_from_cascade, - clip_from_world: Some(cascade.clip_from_world), - hdr: false, - color_grading: Default::default(), - }, - frustum, - LightEntity::Directional { - light_entity, - cascade_index, - }, - )) - .id(); + commands.entity(view_light_entity).insert(( + ShadowView { + depth_attachment: DepthAttachment::new(depth_texture_view, Some(0.0)), + pass_name: format!( + "shadow pass directional light {light_index} cascade {cascade_index}" + ), + }, + ExtractedView { + viewport: UVec4::new( + 0, + 0, + directional_light_shadow_map.size as u32, + directional_light_shadow_map.size as u32, + ), + world_from_view: GlobalTransform::from(cascade.world_from_cascade), + clip_from_view: cascade.clip_from_cascade, + clip_from_world: Some(cascade.clip_from_world), + hdr: false, + color_grading: Default::default(), + }, + frustum, + LightEntity::Directional { + light_entity, + cascade_index, + }, + )); view_lights.push(view_light_entity); shadow_render_phases.insert_or_clear(view_light_entity); live_shadow_mapping_lights.insert(view_light_entity); + dir_light_view_offset += 1; } } diff --git a/crates/bevy_render/src/lib.rs b/crates/bevy_render/src/lib.rs index 32dff45d1df7c..9fb7ce7ca7948 100644 --- a/crates/bevy_render/src/lib.rs +++ b/crates/bevy_render/src/lib.rs @@ -37,6 +37,7 @@ pub mod settings; mod spatial_bundle; pub mod texture; pub mod view; +pub mod world_sync; pub mod prelude { #[doc(hidden)] pub use crate::{ @@ -65,6 +66,7 @@ use extract_resource::ExtractResourcePlugin; use globals::GlobalsPlugin; use render_asset::RenderAssetBytesPerFrame; use renderer::{RenderAdapter, RenderAdapterInfo, RenderDevice, RenderQueue}; +use world_sync::{despawn_temporary_render_entity, SyncRenderWorld}; use crate::mesh::RenderMesh; use crate::renderer::WgpuWrapper; @@ -367,7 +369,8 @@ impl Plugin for RenderPlugin { .register_type::() .register_type::() .register_type::() - .register_type::(); + .register_type::() + .register_type::(); } fn ready(&self, app: &App) -> bool { @@ -444,7 +447,7 @@ fn extract(main_world: &mut World, render_world: &mut World) { unsafe fn initialize_render_app(app: &mut App) { app.init_resource::(); - let mut render_app = SubApp::new(); + let mut render_app = SubApp::new_with_entity_alloc_mask(1); render_app.update_schedule = Some(Render.intern()); let mut extract_schedule = Schedule::new(ExtractSchedule); @@ -473,35 +476,18 @@ unsafe fn initialize_render_app(app: &mut App) { render_system, ) .in_set(RenderSet::Render), - World::clear_entities.in_set(RenderSet::PostCleanup), + despawn_temporary_render_entity.in_set(RenderSet::PostCleanup), ), ); render_app.set_extract(|main_world, render_world| { - #[cfg(feature = "trace")] - let _render_span = bevy_utils::tracing::info_span!("extract main app to render subapp").entered(); { #[cfg(feature = "trace")] - let _stage_span = - bevy_utils::tracing::info_span!("reserve_and_flush") - .entered(); - - // reserve all existing main world entities for use in render_app - // they can only be spawned using `get_or_spawn()` - let total_count = main_world.entities().total_count(); - - assert_eq!( - render_world.entities().len(), - 0, - "An entity was spawned after the entity list was cleared last frame and before the extract schedule began. This is not supported", - ); - - // SAFETY: This is safe given the clear_entities call in the past frame and the assert above - unsafe { - render_world - .entities_mut() - .flush_and_reserve_invalid_assuming_no_entities(total_count); - } + let _stage_span = bevy_utils::tracing::info_span!("entity_sync").entered(); + // println!( + // "render world count:{}", + // render_world.entities().total_count() + // ); } // run extract schedule diff --git a/crates/bevy_render/src/world_sync.rs b/crates/bevy_render/src/world_sync.rs new file mode 100644 index 0000000000000..354b09a11d88d --- /dev/null +++ b/crates/bevy_render/src/world_sync.rs @@ -0,0 +1,45 @@ +use bevy_app::Plugin; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{ + component::Component, + entity::Entity, + observer::Trigger, + query::With, + reflect::ReflectComponent, + system::{Local, Query, ResMut, Resource, SystemState}, + world::{Mut, OnAdd, OnRemove, World}, +}; +use bevy_hierarchy::DespawnRecursiveExt; +use bevy_reflect::Reflect; +use bevy_utils::tracing::warn; + +/// Marker component that indicates that its entity needs to be Synchronized to the render world +/// +/// NOTE: This component should persist throughout the entity's entire lifecycle. +/// If this component is removed from its entity, the entity will be despawned. +#[derive(Component, Clone, Debug, Default, Reflect)] +#[reflect[Component]] +#[component(storage = "SparseSet")] +pub struct SyncRenderWorld; + +// marker component that indicates that its entity needs to be despawned at the end of every frame. +#[derive(Component, Clone, Debug, Default, Reflect)] +#[component(storage = "SparseSet")] +pub struct TemporaryRenderEntity; + +// TODO: directly remove matched archetype for performance +pub(crate) fn despawn_temporary_render_entity( + world: &mut World, + state: &mut SystemState>>, + mut local: Local>, +) { + let query = state.get(world); + + local.extend(query.iter()); + + // ensure next frame allocation keeps order + local.sort_unstable_by_key(|e| e.index()); + for e in local.drain(..).rev() { + world.despawn(e); + } +} diff --git a/crates/bevy_sprite/src/mesh2d/mesh.rs b/crates/bevy_sprite/src/mesh2d/mesh.rs index 8bce6f1cb6529..2cb571f50bc1f 100644 --- a/crates/bevy_sprite/src/mesh2d/mesh.rs +++ b/crates/bevy_sprite/src/mesh2d/mesh.rs @@ -221,8 +221,6 @@ pub struct RenderMesh2dInstances(EntityHashMap); pub struct Mesh2d; pub fn extract_mesh2d( - mut commands: Commands, - mut previous_len: Local, mut render_mesh_instances: ResMut, query: Extract< Query<( @@ -235,15 +233,11 @@ pub fn extract_mesh2d( >, ) { render_mesh_instances.clear(); - let mut entities = Vec::with_capacity(*previous_len); for (entity, view_visibility, transform, handle, no_automatic_batching) in &query { if !view_visibility.get() { continue; } - // FIXME: Remove this - it is just a workaround to enable rendering to work as - // render commands require an entity to exist at the moment. - entities.push((entity, Mesh2d)); render_mesh_instances.insert( entity, RenderMesh2dInstance { @@ -257,8 +251,6 @@ pub fn extract_mesh2d( }, ); } - *previous_len = entities.len(); - commands.insert_or_spawn_batch(entities); } #[derive(Resource, Clone)] diff --git a/crates/bevy_sprite/src/render/mod.rs b/crates/bevy_sprite/src/render/mod.rs index 60c64131fe12b..30a25eef13320 100644 --- a/crates/bevy_sprite/src/render/mod.rs +++ b/crates/bevy_sprite/src/render/mod.rs @@ -38,6 +38,7 @@ use bevy_render::{ ExtractedView, Msaa, ViewTarget, ViewUniform, ViewUniformOffset, ViewUniforms, ViewVisibility, VisibleEntities, }, + world_sync::TemporaryRenderEntity, Extract, }; use bevy_transform::components::GlobalTransform; @@ -390,7 +391,7 @@ pub fn extract_sprites( extracted_sprites.sprites.extend( slices .extract_sprites(transform, entity, sprite, handle) - .map(|e| (commands.spawn_empty().id(), e)), + .map(|e| (commands.spawn(TemporaryRenderEntity).id(), e)), ); } else { let atlas_rect = diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index e1f25482cfaa6..2ce2e01fbeb3c 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -9,6 +9,7 @@ use bevy_core_pipeline::{core_2d::Camera2d, core_3d::Camera3d}; use bevy_hierarchy::Parent; use bevy_render::render_phase::ViewSortedRenderPhases; use bevy_render::texture::TRANSPARENT_IMAGE_HANDLE; +use bevy_render::world_sync::TemporaryRenderEntity; use bevy_render::{ render_phase::{PhaseItem, PhaseItemExtraIndex}, texture::GpuImage, @@ -21,10 +22,10 @@ pub use render_pass::*; pub use ui_material_pipeline::*; use crate::graph::{NodeUi, SubGraphUi}; +use crate::DefaultUiCamera; use crate::{ texture_slice::ComputedTextureSlices, BackgroundColor, BorderColor, BorderRadius, - CalculatedClip, ContentSize, DefaultUiCamera, Node, Outline, Style, TargetCamera, UiImage, - UiScale, Val, + CalculatedClip, ContentSize, Node, Outline, Style, TargetCamera, UiImage, UiScale, Val, }; use bevy_app::prelude::*; @@ -187,7 +188,7 @@ pub struct ExtractedUiNodes { pub fn extract_uinode_background_colors( mut extracted_uinodes: ResMut, - camera_query: Extract>, + camera_query: Extract>, default_ui_camera: Extract, ui_scale: Extract>, uinode_query: Extract< @@ -206,6 +207,7 @@ pub fn extract_uinode_background_colors( >, node_query: Extract>, ) { + let default_ui_camera = default_ui_camera.get(); for ( entity, uinode, @@ -219,8 +221,7 @@ pub fn extract_uinode_background_colors( parent, ) in &uinode_query { - let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get()) - else { + let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera) else { continue; }; @@ -232,7 +233,7 @@ pub fn extract_uinode_background_colors( let ui_logical_viewport_size = camera_query .get(camera_entity) .ok() - .and_then(|(_, c)| c.logical_viewport_size()) + .and_then(Camera::logical_viewport_size) .unwrap_or(Vec2::ZERO) // The logical window resolution returned by `Window` only takes into account the window scale factor and not `UiScale`, // so we have to divide by `UiScale` to get the size of the UI viewport. @@ -294,7 +295,7 @@ pub fn extract_uinode_background_colors( pub fn extract_uinode_images( mut commands: Commands, mut extracted_uinodes: ResMut, - camera_query: Extract>, + camera_query: Extract>, texture_atlases: Extract>>, ui_scale: Extract>, default_ui_camera: Extract, @@ -315,6 +316,7 @@ pub fn extract_uinode_images( >, node_query: Extract>, ) { + let default_ui_camera = default_ui_camera.get(); for ( uinode, transform, @@ -329,8 +331,7 @@ pub fn extract_uinode_images( style, ) in &uinode_query { - let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get()) - else { + let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera) else { continue; }; @@ -346,7 +347,7 @@ pub fn extract_uinode_images( extracted_uinodes.uinodes.extend( slices .extract_ui_nodes(transform, uinode, image, clip, camera_entity) - .map(|e| (commands.spawn_empty().id(), e)), + .map(|e| (commands.spawn(TemporaryRenderEntity).id(), e)), ); continue; } @@ -373,13 +374,13 @@ pub fn extract_uinode_images( }; let ui_logical_viewport_size = camera_query - .get(camera_entity) - .ok() - .and_then(|(_, c)| c.logical_viewport_size()) - .unwrap_or(Vec2::ZERO) - // The logical window resolution returned by `Window` only takes into account the window scale factor and not `UiScale`, - // so we have to divide by `UiScale` to get the size of the UI viewport. - / ui_scale.0; + .get(camera_entity) + .ok() + .and_then(|c| c.logical_viewport_size()) + .unwrap_or(Vec2::ZERO) + // The logical window resolution returned by `Window` only takes into account the window scale factor and not `UiScale`, + // so we have to divide by `UiScale` to get the size of the UI viewport. + / ui_scale.0; // Both vertical and horizontal percentage border values are calculated based on the width of the parent node // @@ -410,7 +411,7 @@ pub fn extract_uinode_images( }; extracted_uinodes.uinodes.insert( - commands.spawn_empty().id(), + commands.spawn(TemporaryRenderEntity).id(), ExtractedUiNode { stack_index: uinode.stack_index, transform: transform.compute_matrix(), @@ -494,7 +495,7 @@ fn clamp_radius( pub fn extract_uinode_borders( mut commands: Commands, mut extracted_uinodes: ResMut, - camera_query: Extract>, + camera_query: Extract>, default_ui_camera: Extract, ui_scale: Extract>, uinode_query: Extract< @@ -516,6 +517,7 @@ pub fn extract_uinode_borders( node_query: Extract>, ) { let image = AssetId::::default(); + let default_ui_camera = default_ui_camera.get(); for ( node, @@ -529,11 +531,9 @@ pub fn extract_uinode_borders( border_radius, ) in &uinode_query { - let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get()) - else { + let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera) else { continue; }; - // Skip invisible borders if !view_visibility.get() || border_color.0.is_fully_transparent() @@ -546,7 +546,7 @@ pub fn extract_uinode_borders( let ui_logical_viewport_size = camera_query .get(camera_entity) .ok() - .and_then(|(_, c)| c.logical_viewport_size()) + .and_then(Camera::logical_viewport_size) .unwrap_or(Vec2::ZERO) // The logical window resolution returned by `Window` only takes into account the window scale factor and not `UiScale`, // so we have to divide by `UiScale` to get the size of the UI viewport. @@ -585,7 +585,7 @@ pub fn extract_uinode_borders( let transform = global_transform.compute_matrix(); extracted_uinodes.uinodes.insert( - commands.spawn_empty().id(), + commands.spawn(TemporaryRenderEntity).id(), ExtractedUiNode { stack_index: node.stack_index, // This translates the uinode's transform to the center of the current border rectangle @@ -624,10 +624,10 @@ pub fn extract_uinode_outlines( )>, >, ) { + let default_ui_camera = default_ui_camera.get(); let image = AssetId::::default(); for (node, global_transform, view_visibility, maybe_clip, camera, outline) in &uinode_query { - let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get()) - else { + let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera) else { continue; }; @@ -677,7 +677,7 @@ pub fn extract_uinode_outlines( for edge in outline_edges { if edge.min.x < edge.max.x && edge.min.y < edge.max.y { extracted_uinodes.uinodes.insert( - commands.spawn_empty().id(), + commands.spawn(TemporaryRenderEntity).id(), ExtractedUiNode { stack_index: node.stack_index, // This translates the uinode's transform to the center of the current border rectangle @@ -757,23 +757,26 @@ pub fn extract_default_ui_camera_view( UI_CAMERA_FAR, ); let default_camera_view = commands - .spawn(ExtractedView { - clip_from_view: projection_matrix, - world_from_view: GlobalTransform::from_xyz( - 0.0, - 0.0, - UI_CAMERA_FAR + UI_CAMERA_TRANSFORM_OFFSET, - ), - clip_from_world: None, - hdr: camera.hdr, - viewport: UVec4::new( - physical_origin.x, - physical_origin.y, - physical_size.x, - physical_size.y, - ), - color_grading: Default::default(), - }) + .spawn(( + ExtractedView { + clip_from_view: projection_matrix, + world_from_view: GlobalTransform::from_xyz( + 0.0, + 0.0, + UI_CAMERA_FAR + UI_CAMERA_TRANSFORM_OFFSET, + ), + clip_from_world: None, + hdr: camera.hdr, + viewport: UVec4::new( + physical_origin.x, + physical_origin.y, + physical_size.x, + physical_size.y, + ), + color_grading: Default::default(), + }, + TemporaryRenderEntity, + )) .id(); commands .get_or_spawn(entity) @@ -791,7 +794,7 @@ pub fn extract_default_ui_camera_view( pub fn extract_uinode_text( mut commands: Commands, mut extracted_uinodes: ResMut, - camera_query: Extract>, + camera_query: Extract>, default_ui_camera: Extract, texture_atlases: Extract>>, ui_scale: Extract>, @@ -807,11 +810,11 @@ pub fn extract_uinode_text( )>, >, ) { + let default_ui_camera = default_ui_camera.get(); for (uinode, global_transform, view_visibility, clip, camera, text, text_layout_info) in &uinode_query { - let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get()) - else { + let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera) else { continue; }; @@ -823,7 +826,7 @@ pub fn extract_uinode_text( let scale_factor = camera_query .get(camera_entity) .ok() - .and_then(|(_, c)| c.target_scaling_factor()) + .and_then(Camera::target_scaling_factor) .unwrap_or(1.0) * ui_scale.0; let inverse_scale_factor = scale_factor.recip(); @@ -862,8 +865,9 @@ pub fn extract_uinode_text( let mut rect = atlas.textures[atlas_info.location.glyph_index].as_rect(); rect.min *= inverse_scale_factor; rect.max *= inverse_scale_factor; + let id = commands.spawn(TemporaryRenderEntity).id(); extracted_uinodes.uinodes.insert( - commands.spawn_empty().id(), + id, ExtractedUiNode { stack_index: uinode.stack_index, transform: transform