diff --git a/crates/bevy_core_pipeline/src/auto_exposure/buffers.rs b/crates/bevy_core_pipeline/src/auto_exposure/buffers.rs index 836c18b3a7..4991a0925b 100644 --- a/crates/bevy_core_pipeline/src/auto_exposure/buffers.rs +++ b/crates/bevy_core_pipeline/src/auto_exposure/buffers.rs @@ -2,6 +2,7 @@ use bevy_ecs::prelude::*; use bevy_render::{ render_resource::{StorageBuffer, UniformBuffer}, renderer::{RenderDevice, RenderQueue}, + world_sync::RenderEntity, Extract, }; use bevy_utils::{Entry, HashMap}; @@ -26,13 +27,13 @@ pub(super) struct ExtractedStateBuffers { pub(super) fn extract_buffers( mut commands: Commands, - changed: Extract>>, + changed: Extract>>, mut removed: Extract>, ) { commands.insert_resource(ExtractedStateBuffers { changed: changed .iter() - .map(|(entity, settings)| (entity, settings.clone())) + .map(|(entity, settings)| (entity.id(), settings.clone())) .collect(), removed: removed.read().collect(), }); diff --git a/crates/bevy_core_pipeline/src/core_2d/camera_2d.rs b/crates/bevy_core_pipeline/src/core_2d/camera_2d.rs index 682c15cd03..03ddf5c073 100644 --- a/crates/bevy_core_pipeline/src/core_2d/camera_2d.rs +++ b/crates/bevy_core_pipeline/src/core_2d/camera_2d.rs @@ -4,6 +4,7 @@ use crate::{ }; use bevy_ecs::prelude::*; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use bevy_render::world_sync::SyncToRenderWorld; use bevy_render::{ camera::{ Camera, CameraMainTextureUsages, CameraProjection, CameraRenderGraph, @@ -35,6 +36,8 @@ pub struct Camera2dBundle { pub deband_dither: DebandDither, pub main_texture_usages: CameraMainTextureUsages, pub msaa: Msaa, + /// Marker component that indicates that its entity needs to be synchronized to the render world + pub sync: SyncToRenderWorld, } impl Default for Camera2dBundle { @@ -55,6 +58,7 @@ impl Default for Camera2dBundle { deband_dither: DebandDither::Disabled, main_texture_usages: Default::default(), msaa: Default::default(), + sync: Default::default(), } } } @@ -88,6 +92,7 @@ impl Camera2dBundle { deband_dither: DebandDither::Disabled, main_texture_usages: Default::default(), msaa: Default::default(), + sync: Default::default(), } } } diff --git a/crates/bevy_core_pipeline/src/core_2d/mod.rs b/crates/bevy_core_pipeline/src/core_2d/mod.rs index 6bd3b3e324..9f07f19bf8 100644 --- a/crates/bevy_core_pipeline/src/core_2d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_2d/mod.rs @@ -57,6 +57,7 @@ use bevy_render::{ renderer::RenderDevice, texture::TextureCache, view::{Msaa, ViewDepthTexture}, + world_sync::RenderEntity, Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; @@ -357,11 +358,10 @@ impl CachedRenderPipelinePhaseItem for Transparent2d { } pub fn extract_core_2d_camera_phases( - mut commands: Commands, mut transparent_2d_phases: ResMut>, mut opaque_2d_phases: ResMut>, mut alpha_mask_2d_phases: ResMut>, - cameras_2d: Extract>>, + cameras_2d: Extract>>, mut live_entities: Local, ) { live_entities.clear(); @@ -370,8 +370,7 @@ pub fn extract_core_2d_camera_phases( if !camera.is_active { continue; } - - commands.get_or_spawn(entity); + let entity = entity.id(); transparent_2d_phases.insert_or_clear(entity); opaque_2d_phases.insert_or_clear(entity); alpha_mask_2d_phases.insert_or_clear(entity); diff --git a/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs b/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs index ed748c52bc..454892a306 100644 --- a/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs +++ b/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs @@ -10,6 +10,7 @@ use bevy_render::{ primitives::Frustum, render_resource::{LoadOp, TextureUsages}, view::{ColorGrading, Msaa, VisibleEntities}, + world_sync::SyncToRenderWorld, }; use bevy_transform::prelude::{GlobalTransform, Transform}; use serde::{Deserialize, Serialize}; @@ -153,6 +154,8 @@ pub struct Camera3dBundle { pub exposure: Exposure, pub main_texture_usages: CameraMainTextureUsages, pub msaa: Msaa, + /// Marker component that indicates that its entity needs to be synchronized to the render world + pub sync: SyncToRenderWorld, } // NOTE: ideally Perspective and Orthographic defaults can share the same impl, but sadly it breaks rust's type inference @@ -173,6 +176,7 @@ impl Default for Camera3dBundle { main_texture_usages: Default::default(), deband_dither: DebandDither::Enabled, msaa: Default::default(), + sync: Default::default(), } } } diff --git a/crates/bevy_core_pipeline/src/core_3d/mod.rs b/crates/bevy_core_pipeline/src/core_3d/mod.rs index fc066e2d9e..71f1f03197 100644 --- a/crates/bevy_core_pipeline/src/core_3d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_3d/mod.rs @@ -91,6 +91,7 @@ use bevy_render::{ renderer::RenderDevice, texture::{BevyDefault, ColorAttachment, Image, TextureCache}, view::{ExtractedView, ViewDepthTexture, ViewTarget}, + world_sync::RenderEntity, Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; use bevy_utils::{tracing::warn, HashMap}; @@ -504,23 +505,21 @@ impl CachedRenderPipelinePhaseItem for Transparent3d { } pub fn extract_core_3d_camera_phases( - mut commands: Commands, mut opaque_3d_phases: ResMut>, mut alpha_mask_3d_phases: ResMut>, mut transmissive_3d_phases: ResMut>, mut transparent_3d_phases: ResMut>, - cameras_3d: Extract>>, + cameras_3d: Extract>>, mut live_entities: Local, ) { live_entities.clear(); - for (entity, camera) in &cameras_3d { + for (render_entity, camera) in &cameras_3d { if !camera.is_active { continue; } - commands.get_or_spawn(entity); - + let entity = render_entity.id(); opaque_3d_phases.insert_or_clear(entity); alpha_mask_3d_phases.insert_or_clear(entity); transmissive_3d_phases.insert_or_clear(entity); @@ -545,7 +544,7 @@ pub fn extract_camera_prepass_phase( cameras_3d: Extract< Query< ( - Entity, + &RenderEntity, &Camera, Has, Has, @@ -559,13 +558,20 @@ pub fn extract_camera_prepass_phase( ) { live_entities.clear(); - for (entity, camera, depth_prepass, normal_prepass, motion_vector_prepass, deferred_prepass) in - cameras_3d.iter() + for ( + render_entity, + camera, + depth_prepass, + normal_prepass, + motion_vector_prepass, + deferred_prepass, + ) in cameras_3d.iter() { if !camera.is_active { continue; } + let entity = render_entity.id(); if depth_prepass || normal_prepass || motion_vector_prepass { opaque_3d_prepass_phases.insert_or_clear(entity); alpha_mask_3d_prepass_phases.insert_or_clear(entity); @@ -581,7 +587,6 @@ pub fn extract_camera_prepass_phase( opaque_3d_deferred_phases.remove(&entity); alpha_mask_3d_deferred_phases.remove(&entity); } - live_entities.insert(entity); commands diff --git a/crates/bevy_core_pipeline/src/dof/mod.rs b/crates/bevy_core_pipeline/src/dof/mod.rs index ba31dfb279..100b7f20ff 100644 --- a/crates/bevy_core_pipeline/src/dof/mod.rs +++ b/crates/bevy_core_pipeline/src/dof/mod.rs @@ -51,6 +51,7 @@ use bevy_render::{ prepare_view_targets, ExtractedView, Msaa, ViewDepthTexture, ViewTarget, ViewUniform, ViewUniformOffset, ViewUniforms, }, + world_sync::RenderEntity, Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; use bevy_utils::{info_once, prelude::default, warn_once}; @@ -809,7 +810,7 @@ impl SpecializedRenderPipeline for DepthOfFieldPipeline { /// Extracts all [`DepthOfField`] components into the render world. fn extract_depth_of_field_settings( mut commands: Commands, - mut query: Extract>, + mut query: Extract>, ) { if !DEPTH_TEXTURE_SAMPLING_SUPPORTED { info_once!( @@ -819,6 +820,7 @@ fn extract_depth_of_field_settings( } for (entity, depth_of_field, projection) in query.iter_mut() { + let entity = entity.id(); // Depth of field is nonsensical without a perspective projection. let Projection::Perspective(ref perspective_projection) = *projection else { continue; diff --git a/crates/bevy_core_pipeline/src/taa/mod.rs b/crates/bevy_core_pipeline/src/taa/mod.rs index 56dcd6a69b..3ca33a21d6 100644 --- a/crates/bevy_core_pipeline/src/taa/mod.rs +++ b/crates/bevy_core_pipeline/src/taa/mod.rs @@ -32,6 +32,7 @@ use bevy_render::{ renderer::{RenderContext, RenderDevice}, texture::{BevyDefault, CachedTexture, TextureCache}, view::{ExtractedView, Msaa, ViewTarget}, + world_sync::RenderEntity, ExtractSchedule, MainWorld, Render, RenderApp, RenderSet, }; use bevy_utils::tracing::warn; @@ -351,20 +352,26 @@ impl SpecializedRenderPipeline for TaaPipeline { } fn extract_taa_settings(mut commands: Commands, mut main_world: ResMut) { - let mut cameras_3d = main_world - .query_filtered::<(Entity, &Camera, &Projection, &mut TemporalAntiAliasing), ( - With, - With, - With, - With, - )>(); + let mut cameras_3d = main_world.query_filtered::<( + &RenderEntity, + &Camera, + &Projection, + &mut TemporalAntiAliasing, + ), ( + With, + With, + With, + With, + )>(); for (entity, camera, camera_projection, mut taa_settings) in cameras_3d.iter_mut(&mut main_world) { let has_perspective_projection = matches!(camera_projection, Projection::Perspective(_)); if camera.is_active && has_perspective_projection { - commands.get_or_spawn(entity).insert(taa_settings.clone()); + commands + .get_or_spawn(entity.id()) + .insert(taa_settings.clone()); taa_settings.reset = false; } } diff --git a/crates/bevy_gizmos/src/lib.rs b/crates/bevy_gizmos/src/lib.rs index 0c3bd81d67..0fdf881bb1 100644 --- a/crates/bevy_gizmos/src/lib.rs +++ b/crates/bevy_gizmos/src/lib.rs @@ -103,6 +103,7 @@ use { ShaderStages, ShaderType, VertexFormat, }, renderer::RenderDevice, + world_sync::TemporaryRenderEntity, Extract, ExtractSchedule, Render, RenderApp, RenderSet, }, bytemuck::cast_slice, @@ -113,7 +114,6 @@ use { any(feature = "bevy_pbr", feature = "bevy_sprite"), ))] use bevy_render::render_resource::{VertexAttribute, VertexBufferLayout, VertexStepMode}; - use bevy_time::Fixed; use bevy_utils::TypeIdMap; use config::{ @@ -459,6 +459,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/bundle.rs b/crates/bevy_pbr/src/bundle.rs index 7c6b9fef9f..d5ffebf981 100644 --- a/crates/bevy_pbr/src/bundle.rs +++ b/crates/bevy_pbr/src/bundle.rs @@ -15,6 +15,7 @@ use bevy_render::{ mesh::Mesh, primitives::{CascadesFrusta, CubemapFrusta, Frustum}, view::{InheritedVisibility, ViewVisibility, Visibility}, + world_sync::SyncToRenderWorld, }; use bevy_transform::components::{GlobalTransform, Transform}; @@ -108,6 +109,8 @@ pub struct PointLightBundle { pub inherited_visibility: InheritedVisibility, /// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering pub view_visibility: ViewVisibility, + /// Marker component that indicates that its entity needs to be synchronized to the render world + pub sync: SyncToRenderWorld, } /// A component bundle for spot light entities @@ -124,6 +127,8 @@ pub struct SpotLightBundle { pub inherited_visibility: InheritedVisibility, /// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering pub view_visibility: ViewVisibility, + /// Marker component that indicates that its entity needs to be synchronized to the render world + pub sync: SyncToRenderWorld, } /// A component bundle for [`DirectionalLight`] entities. @@ -142,4 +147,6 @@ pub struct DirectionalLightBundle { pub inherited_visibility: InheritedVisibility, /// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering pub view_visibility: ViewVisibility, + /// Marker component that indicates that its entity needs to be synchronized to the render world + pub sync: SyncToRenderWorld, } diff --git a/crates/bevy_pbr/src/cluster/mod.rs b/crates/bevy_pbr/src/cluster/mod.rs index b0933a3fd2..9c77a02149 100644 --- a/crates/bevy_pbr/src/cluster/mod.rs +++ b/crates/bevy_pbr/src/cluster/mod.rs @@ -20,6 +20,7 @@ use bevy_render::{ UniformBuffer, }, renderer::{RenderDevice, RenderQueue}, + world_sync::RenderEntity, Extract, }; use bevy_utils::{hashbrown::HashSet, tracing::warn}; @@ -525,7 +526,8 @@ pub(crate) fn clusterable_object_order( /// Extracts clusters from the main world from the render world. pub fn extract_clusters( mut commands: Commands, - views: Extract>, + views: Extract>, + mapper: Extract>, ) { for (entity, clusters, camera) in &views { if !camera.is_active { @@ -544,13 +546,15 @@ pub fn extract_clusters( cluster_objects.spot_light_count as u32, )); for clusterable_entity in &cluster_objects.entities { - data.push(ExtractedClusterableObjectElement::ClusterableObjectEntity( - *clusterable_entity, - )); + if let Ok(entity) = mapper.get(*clusterable_entity) { + data.push(ExtractedClusterableObjectElement::ClusterableObjectEntity( + entity.id(), + )); + } } } - commands.get_or_spawn(entity).insert(( + commands.get_or_spawn(entity.id()).insert(( ExtractedClusterableObjects { data }, ExtractedClusterConfig { near: clusters.near, diff --git a/crates/bevy_pbr/src/lib.rs b/crates/bevy_pbr/src/lib.rs index feb6b3d4fc..c48601b889 100644 --- a/crates/bevy_pbr/src/lib.rs +++ b/crates/bevy_pbr/src/lib.rs @@ -439,6 +439,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/light_probe/mod.rs b/crates/bevy_pbr/src/light_probe/mod.rs index f110dc6f82..eb1b4ccec4 100644 --- a/crates/bevy_pbr/src/light_probe/mod.rs +++ b/crates/bevy_pbr/src/light_probe/mod.rs @@ -23,6 +23,7 @@ use bevy_render::{ settings::WgpuFeatures, texture::{FallbackImage, GpuImage, Image}, view::ExtractedView, + world_sync::RenderEntity, Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; use bevy_transform::{components::Transform, prelude::GlobalTransform}; @@ -371,7 +372,7 @@ impl Plugin for LightProbePlugin { /// Compared to the `ExtractComponentPlugin`, this implementation will create a default instance /// if one does not already exist. fn gather_environment_map_uniform( - view_query: Extract), With>>, + view_query: Extract), With>>, mut commands: Commands, ) { for (view_entity, environment_map_light) in view_query.iter() { @@ -385,7 +386,7 @@ fn gather_environment_map_uniform( EnvironmentMapUniform::default() }; commands - .get_or_spawn(view_entity) + .get_or_spawn(view_entity.id()) .insert(environment_map_uniform); } } @@ -395,7 +396,9 @@ fn gather_environment_map_uniform( fn gather_light_probes( image_assets: Res>, light_probe_query: Extract>>, - view_query: Extract), With>>, + view_query: Extract< + Query<(&RenderEntity, &GlobalTransform, &Frustum, Option<&C>), With>, + >, mut reflection_probes: Local>>, mut view_reflection_probes: Local>>, mut commands: Commands, @@ -433,14 +436,15 @@ fn gather_light_probes( // Gather up the light probes in the list. render_view_light_probes.maybe_gather_light_probes(&view_reflection_probes); + let entity = view_entity.id(); // Record the per-view light probes. if render_view_light_probes.is_empty() { commands - .get_or_spawn(view_entity) + .get_or_spawn(entity) .remove::>(); } else { commands - .get_or_spawn(view_entity) + .get_or_spawn(entity) .insert(render_view_light_probes); } } diff --git a/crates/bevy_pbr/src/meshlet/instance_manager.rs b/crates/bevy_pbr/src/meshlet/instance_manager.rs index 161cbf7f8b..4c609848a6 100644 --- a/crates/bevy_pbr/src/meshlet/instance_manager.rs +++ b/crates/bevy_pbr/src/meshlet/instance_manager.rs @@ -178,21 +178,21 @@ pub fn extract_meshlet_mesh_entities( Res, ResMut>, EventReader>, - &Entities, )>, >, >, + render_entities: &Entities, ) { // Get instances query if system_state.is_none() { *system_state = Some(SystemState::new(&mut main_world)); } let system_state = system_state.as_mut().unwrap(); - let (instances_query, asset_server, mut assets, mut asset_events, entities) = + let (instances_query, asset_server, mut assets, mut asset_events) = system_state.get_mut(&mut main_world); // Reset per-frame data - instance_manager.reset(entities); + instance_manager.reset(render_entities); // Free GPU buffer space for any modified or dropped MeshletMesh assets for asset_event in asset_events.read() { diff --git a/crates/bevy_pbr/src/prepass/mod.rs b/crates/bevy_pbr/src/prepass/mod.rs index 6cf3d8f641..166ef0c2cc 100644 --- a/crates/bevy_pbr/src/prepass/mod.rs +++ b/crates/bevy_pbr/src/prepass/mod.rs @@ -4,6 +4,7 @@ use bevy_render::{ mesh::{MeshVertexBufferLayoutRef, RenderMesh}, render_resource::binding_types::uniform_buffer, view::WithMesh, + world_sync::RenderEntity, }; pub use prepass_bindings::*; @@ -580,10 +581,11 @@ where // Extract the render phases for the prepass pub fn extract_camera_previous_view_data( mut commands: Commands, - cameras_3d: Extract), With>>, + cameras_3d: Extract), With>>, ) { for (entity, camera, maybe_previous_view_data) in cameras_3d.iter() { if camera.is_active { + let entity = entity.id(); let entity = commands.get_or_spawn(entity); if let Some(previous_view_data) = maybe_previous_view_data { diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index b3f83eb5a5..212737e058 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -1,12 +1,14 @@ 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::{EntityHashMap, EntityHashSet}, prelude::*, system::lifetimeless::Read, }; use bevy_math::{ops, Mat4, UVec4, Vec2, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles}; +use bevy_render::world_sync::RenderEntity; use bevy_render::{ diagnostic::RecordDiagnostics, mesh::RenderMesh, @@ -192,6 +194,7 @@ pub fn extract_lights( global_point_lights: Extract>, point_lights: Extract< Query<( + &RenderEntity, &PointLight, &CubemapVisibleEntities, &GlobalTransform, @@ -202,6 +205,7 @@ pub fn extract_lights( >, spot_lights: Extract< Query<( + &RenderEntity, &SpotLight, &VisibleMeshEntities, &GlobalTransform, @@ -213,7 +217,7 @@ pub fn extract_lights( directional_lights: Extract< Query< ( - Entity, + &RenderEntity, &DirectionalLight, &CascadesVisibleEntities, &Cascades, @@ -227,6 +231,7 @@ pub fn extract_lights( Without, >, >, + mapper: Extract>, mut previous_point_lights_len: Local, mut previous_spot_lights_len: Local, ) { @@ -250,6 +255,7 @@ pub fn extract_lights( let mut point_lights_values = Vec::with_capacity(*previous_point_lights_len); for entity in global_point_lights.iter().copied() { let Ok(( + render_entity, point_light, cubemap_visible_entities, transform, @@ -287,7 +293,7 @@ pub fn extract_lights( volumetric: volumetric_light.is_some(), }; point_lights_values.push(( - entity, + render_entity.id(), ( extracted_point_light, render_cubemap_visible_entities, @@ -301,6 +307,7 @@ pub fn extract_lights( let mut spot_lights_values = Vec::with_capacity(*previous_spot_lights_len); for entity in global_point_lights.iter().copied() { if let Ok(( + render_entity, spot_light, visible_entities, transform, @@ -319,7 +326,7 @@ pub fn extract_lights( 2.0 * ops::tan(spot_light.outer_angle) / directional_light_shadow_map.size as f32; spot_lights_values.push(( - entity, + render_entity.id(), ( ExtractedPointLight { color: spot_light.color.into(), @@ -370,9 +377,33 @@ pub fn extract_lights( continue; } - // TODO: As above - let render_visible_entities = visible_entities.clone(); - commands.get_or_spawn(entity).insert(( + // TODO: update in place instead of reinserting. + let mut extracted_cascades = EntityHashMap::default(); + let mut extracted_frusta = EntityHashMap::default(); + let mut cascade_visible_entities = EntityHashMap::default(); + for (e, v) in cascades.cascades.iter() { + if let Ok(entity) = mapper.get(*e) { + extracted_cascades.insert(entity.id(), v.clone()); + } else { + break; + } + } + for (e, v) in frusta.frusta.iter() { + if let Ok(entity) = mapper.get(*e) { + extracted_frusta.insert(entity.id(), v.clone()); + } else { + break; + } + } + for (e, v) in visible_entities.entities.iter() { + if let Ok(entity) = mapper.get(*e) { + cascade_visible_entities.insert(entity.id(), v.clone()); + } else { + break; + } + } + + commands.get_or_spawn(entity.id()).insert(( ExtractedDirectionalLight { color: directional_light.color.into(), illuminance: directional_light.illuminance, @@ -385,15 +416,44 @@ pub fn extract_lights( shadow_normal_bias: directional_light.shadow_normal_bias * core::f32::consts::SQRT_2, cascade_shadow_config: cascade_config.clone(), - cascades: cascades.cascades.clone(), - frusta: frusta.frusta.clone(), + cascades: extracted_cascades, + frusta: extracted_frusta, render_layers: maybe_layers.unwrap_or_default().clone(), }, - render_visible_entities, + CascadesVisibleEntities { + entities: cascade_visible_entities, + }, )); } } +#[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) struct CubeMapFace { pub(crate) target: Vec3, pub(crate) up: Vec3, @@ -564,14 +624,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(); @@ -862,8 +925,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 { @@ -949,15 +1013,25 @@ 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()); + let Ok(mut light_entities) = light_view_entities.get_mut(light_entity) else { + continue; + }; + + // for each face of a cube and each view we spawn a light entity + while light_entities.len() < 6 * (offset + 1) { + light_entities.push(commands.spawn_empty().id()); + } + let cube_face_projection = Mat4::perspective_infinite_reverse_rh( core::f32::consts::FRAC_PI_2, 1.0, light.shadow_map_near_z, ); - for (face_index, (view_rotation, frustum)) in cube_face_rotations + 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 = @@ -974,36 +1048,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); @@ -1021,6 +1094,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, light.shadow_map_near_z); @@ -1039,29 +1116,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); @@ -1079,6 +1160,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; @@ -1102,9 +1186,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] = @@ -1134,37 +1231,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_pbr/src/ssao/mod.rs b/crates/bevy_pbr/src/ssao/mod.rs index ffbd758458..7495960391 100644 --- a/crates/bevy_pbr/src/ssao/mod.rs +++ b/crates/bevy_pbr/src/ssao/mod.rs @@ -30,6 +30,7 @@ use bevy_render::{ renderer::{RenderAdapter, RenderContext, RenderDevice, RenderQueue}, texture::{CachedTexture, TextureCache}, view::{Msaa, ViewUniform, ViewUniformOffset, ViewUniforms}, + world_sync::RenderEntity, Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; use bevy_utils::{ @@ -488,7 +489,7 @@ fn extract_ssao_settings( mut commands: Commands, cameras: Extract< Query< - (Entity, &Camera, &ScreenSpaceAmbientOcclusion, &Msaa), + (&RenderEntity, &Camera, &ScreenSpaceAmbientOcclusion, &Msaa), (With, With, With), >, >, @@ -501,9 +502,10 @@ fn extract_ssao_settings( ); return; } - if camera.is_active { - commands.get_or_spawn(entity).insert(ssao_settings.clone()); + commands + .get_or_spawn(entity.id()) + .insert(ssao_settings.clone()); } } } diff --git a/crates/bevy_pbr/src/volumetric_fog/render.rs b/crates/bevy_pbr/src/volumetric_fog/render.rs index 126d2fddf7..b833197d4a 100644 --- a/crates/bevy_pbr/src/volumetric_fog/render.rs +++ b/crates/bevy_pbr/src/volumetric_fog/render.rs @@ -38,6 +38,7 @@ use bevy_render::{ renderer::{RenderContext, RenderDevice, RenderQueue}, texture::{BevyDefault as _, GpuImage, Image}, view::{ExtractedView, Msaa, ViewDepthTexture, ViewTarget, ViewUniformOffset}, + world_sync::RenderEntity, Extract, }; use bevy_transform::components::GlobalTransform; @@ -270,27 +271,27 @@ impl FromWorld for VolumetricFogPipeline { /// from the main world to the render world. pub fn extract_volumetric_fog( mut commands: Commands, - view_targets: Extract>, - fog_volumes: Extract>, - volumetric_lights: Extract>, + view_targets: Extract>, + fog_volumes: Extract>, + volumetric_lights: Extract>, ) { if volumetric_lights.is_empty() { return; } for (entity, volumetric_fog) in view_targets.iter() { - commands.get_or_spawn(entity).insert(*volumetric_fog); + commands.get_or_spawn(entity.id()).insert(*volumetric_fog); } for (entity, fog_volume, fog_transform) in fog_volumes.iter() { commands - .get_or_spawn(entity) + .get_or_spawn(entity.id()) .insert((*fog_volume).clone()) .insert(*fog_transform); } for (entity, volumetric_light) in volumetric_lights.iter() { - commands.get_or_spawn(entity).insert(*volumetric_light); + commands.get_or_spawn(entity.id()).insert(*volumetric_light); } } diff --git a/crates/bevy_render/src/camera/camera.rs b/crates/bevy_render/src/camera/camera.rs index 3fd905bdcf..1cd6238a50 100644 --- a/crates/bevy_render/src/camera/camera.rs +++ b/crates/bevy_render/src/camera/camera.rs @@ -10,6 +10,7 @@ use crate::{ view::{ ColorGrading, ExtractedView, ExtractedWindows, GpuCulling, RenderLayers, VisibleEntities, }, + world_sync::RenderEntity, Extract, }; use bevy_asset::{AssetEvent, AssetId, Assets, Handle}; @@ -935,7 +936,7 @@ pub fn extract_cameras( mut commands: Commands, query: Extract< Query<( - Entity, + &RenderEntity, &Camera, &CameraRenderGraph, &GlobalTransform, @@ -954,7 +955,7 @@ pub fn extract_cameras( ) { let primary_window = primary_window.iter().next(); for ( - entity, + render_entity, camera, camera_render_graph, transform, @@ -968,11 +969,10 @@ pub fn extract_cameras( gpu_culling, ) in query.iter() { - let color_grading = color_grading.unwrap_or(&ColorGrading::default()).clone(); - if !camera.is_active { continue; } + let color_grading = color_grading.unwrap_or(&ColorGrading::default()).clone(); if let ( Some(URect { @@ -990,7 +990,8 @@ pub fn extract_cameras( continue; } - let mut commands = commands.get_or_spawn(entity).insert(( + let mut commands = commands.entity(render_entity.id()); + commands = commands.insert(( ExtractedCamera { target: camera.target.normalize(primary_window), viewport: camera.viewport.clone(), @@ -1036,7 +1037,6 @@ pub fn extract_cameras( if let Some(perspective) = projection { commands = commands.insert(perspective.clone()); } - if gpu_culling { if *gpu_preprocessing_support == GpuPreprocessingSupport::Culling { commands.insert(GpuCulling); @@ -1046,7 +1046,7 @@ pub fn extract_cameras( ); } } - } + }; } } diff --git a/crates/bevy_render/src/extract_component.rs b/crates/bevy_render/src/extract_component.rs index 01b6b06cea..cfbdaa4f1d 100644 --- a/crates/bevy_render/src/extract_component.rs +++ b/crates/bevy_render/src/extract_component.rs @@ -2,6 +2,7 @@ use crate::{ render_resource::{encase::internal::WriteInto, DynamicUniformBuffer, ShaderType}, renderer::{RenderDevice, RenderQueue}, view::ViewVisibility, + world_sync::{RenderEntity, SyncToRenderWorld}, Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; use bevy_app::{App, Plugin}; @@ -11,6 +12,7 @@ use bevy_ecs::{ prelude::*, query::{QueryFilter, QueryItem, ReadOnlyQueryData}, system::lifetimeless::Read, + world::OnAdd, }; use core::{marker::PhantomData, ops::Deref}; @@ -155,10 +157,18 @@ fn prepare_uniform_components( commands.insert_or_spawn_batch(entities); } -/// This plugin extracts the components into the "render world". +/// This plugin extracts the components into the render world for synced entities. /// -/// Therefore it sets up the [`ExtractSchedule`] step -/// for the specified [`ExtractComponent`]. +/// To do so, it sets up the [`ExtractSchedule`] step for the specified [`ExtractComponent`]. +/// +/// # Warning +/// +/// Be careful when removing the [`ExtractComponent`] from an entity. When an [`ExtractComponent`] +/// is added to an entity, that entity is automatically synced with the render world (see also +/// [`WorldSyncPlugin`](crate::world_sync::WorldSyncPlugin)). When removing the entity in the main +/// world, the synced entity also gets removed. However, if only the [`ExtractComponent`] is removed +/// this *doesn't* happen, and the synced entity stays around with the old extracted data. +/// We recommend despawning the entire entity, instead of only removing [`ExtractComponent`]. pub struct ExtractComponentPlugin { only_extract_visible: bool, marker: PhantomData (C, F)>, @@ -184,6 +194,10 @@ impl ExtractComponentPlugin { impl Plugin for ExtractComponentPlugin { fn build(&self, app: &mut App) { + // TODO: use required components + app.observe(|trigger: Trigger, mut commands: Commands| { + commands.entity(trigger.entity()).insert(SyncToRenderWorld); + }); if let Some(render_app) = app.get_sub_app_mut(RenderApp) { if self.only_extract_visible { render_app.add_systems(ExtractSchedule, extract_visible_components::); @@ -205,33 +219,33 @@ impl ExtractComponent for Handle { } } -/// This system extracts all components of the corresponding [`ExtractComponent`] type. +/// This system extracts all components of the corresponding [`ExtractComponent`], for entities that are synced via [`SyncToRenderWorld`]. fn extract_components( mut commands: Commands, mut previous_len: Local, - query: Extract>, + query: Extract>, ) { let mut values = Vec::with_capacity(*previous_len); for (entity, query_item) in &query { if let Some(component) = C::extract_component(query_item) { - values.push((entity, component)); + values.push((entity.id(), component)); } } *previous_len = values.len(); commands.insert_or_spawn_batch(values); } -/// This system extracts all visible components of the corresponding [`ExtractComponent`] type. +/// This system extracts all components of the corresponding [`ExtractComponent`], for entities that are visible and synced via [`SyncToRenderWorld`]. fn extract_visible_components( mut commands: Commands, mut previous_len: Local, - query: Extract>, + query: Extract>, ) { let mut values = Vec::with_capacity(*previous_len); for (entity, view_visibility, query_item) in &query { if view_visibility.get() { if let Some(component) = C::extract_component(query_item) { - values.push((entity, component)); + values.push((entity.id(), component)); } } } diff --git a/crates/bevy_render/src/lib.rs b/crates/bevy_render/src/lib.rs index c69ddac032..51bd01c612 100644 --- a/crates/bevy_render/src/lib.rs +++ b/crates/bevy_render/src/lib.rs @@ -40,6 +40,7 @@ mod spatial_bundle; pub mod storage; pub mod texture; pub mod view; +pub mod world_sync; /// The render prelude. /// @@ -73,6 +74,9 @@ use extract_resource::ExtractResourcePlugin; use globals::GlobalsPlugin; use render_asset::RenderAssetBytesPerFrame; use renderer::{RenderAdapter, RenderAdapterInfo, RenderDevice, RenderQueue}; +use world_sync::{ + despawn_temporary_render_entities, entity_sync_system, SyncToRenderWorld, WorldSyncPlugin, +}; use crate::gpu_readback::GpuReadbackPlugin; use crate::{ @@ -364,6 +368,7 @@ impl Plugin for RenderPlugin { GlobalsPlugin, MorphPlugin, BatchingPlugin, + WorldSyncPlugin, StoragePlugin, GpuReadbackPlugin::default(), )); @@ -377,7 +382,8 @@ impl Plugin for RenderPlugin { .register_type::() .register_type::() .register_type::() - .register_type::(); + .register_type::() + .register_type::(); } fn ready(&self, app: &App) -> bool { @@ -484,35 +490,15 @@ unsafe fn initialize_render_app(app: &mut App) { render_system, ) .in_set(RenderSet::Render), - World::clear_entities.in_set(RenderSet::PostCleanup), + despawn_temporary_render_entities.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(); + entity_sync_system(main_world, render_world); } // run extract schedule diff --git a/crates/bevy_render/src/pipelined_rendering.rs b/crates/bevy_render/src/pipelined_rendering.rs index 7abca12845..f17209665b 100644 --- a/crates/bevy_render/src/pipelined_rendering.rs +++ b/crates/bevy_render/src/pipelined_rendering.rs @@ -84,13 +84,15 @@ impl Drop for RenderAppChannels { /// A single frame of execution looks something like below /// /// ```text -/// |--------------------------------------------------------------------| -/// | | RenderExtractApp schedule | winit events | main schedule | -/// | extract |----------------------------------------------------------| -/// | | extract commands | rendering schedule | -/// |--------------------------------------------------------------------| +/// |---------------------------------------------------------------------------| +/// | | | RenderExtractApp schedule | winit events | main schedule | +/// | sync | extract |----------------------------------------------------------| +/// | | | extract commands | rendering schedule | +/// |---------------------------------------------------------------------------| /// ``` /// +/// - `sync` is the step where the entity-entity mapping between the main and render world is updated. +/// This is run on the main app's thread. For more information checkout [`WorldSyncPlugin`]. /// - `extract` is the step where data is copied from the main world to the render world. /// This is run on the main app's thread. /// - On the render thread, we first apply the `extract commands`. This is not run during extract, so the @@ -101,6 +103,8 @@ impl Drop for RenderAppChannels { /// - Next all the `winit events` are processed. /// - And finally the `main app schedule` is run. /// - Once both the `main app schedule` and the `render schedule` are finished running, `extract` is run again. +/// +/// [`WorldSyncPlugin`]: crate::world_sync::WorldSyncPlugin #[derive(Default)] pub struct PipelinedRenderingPlugin; diff --git a/crates/bevy_render/src/world_sync.rs b/crates/bevy_render/src/world_sync.rs new file mode 100644 index 0000000000..cec1ce748c --- /dev/null +++ b/crates/bevy_render/src/world_sync.rs @@ -0,0 +1,268 @@ +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_reflect::Reflect; + +/// A plugin that synchronizes entities with [`SyncToRenderWorld`] between the main world and the render world. +/// +/// Bevy's renderer is architected independently from the main app. +/// It operates in its own separate ECS [`World`], so the renderer logic can run in parallel with the main world logic. +/// This is called "Pipelined Rendering", see [`PipelinedRenderingPlugin`] for more information. +/// +/// [`WorldSyncPlugin`] is the first thing that runs every frame and it maintains an entity-to-entity mapping +/// between the main world and the render world. +/// It does so by spawning and despawning entities in the render world, to match spawned and despawned entities in the main world. +/// The link between synced entities is maintained by the [`RenderEntity`] and [`MainEntity`] components. +/// The [`RenderEntity`] contains the corresponding render world entity of a main world entity, while [`MainEntity`] contains +/// the corresponding main world entity of a render world entity. +/// The entities can be accessed by calling `.id()` on either component. +/// +/// Synchronization is necessary preparation for extraction ([`ExtractSchedule`](crate::ExtractSchedule)), which copies over component data from the main +/// to the render world for these entities. +/// +/// ```text +/// |--------------------------------------------------------------------| +/// | | | Main world update | +/// | sync | extract |---------------------------------------------------| +/// | | | Render world update | +/// |--------------------------------------------------------------------| +/// ``` +/// +/// An example for synchronized main entities 1v1 and 18v1 +/// +/// ```text +/// |---------------------------Main World------------------------------| +/// | Entity | Component | +/// |-------------------------------------------------------------------| +/// | ID: 1v1 | PointLight | RenderEntity(ID: 3V1) | SyncToRenderWorld | +/// | ID: 18v1 | PointLight | RenderEntity(ID: 5V1) | SyncToRenderWorld | +/// |-------------------------------------------------------------------| +/// +/// |----------Render World-----------| +/// | Entity | Component | +/// |---------------------------------| +/// | ID: 3v1 | MainEntity(ID: 1V1) | +/// | ID: 5v1 | MainEntity(ID: 18V1) | +/// |---------------------------------| +/// +/// ``` +/// +/// Note that this effectively establishes a link between the main world entity and the render world entity. +/// Not every entity needs to be synchronized, however; only entities with the [`SyncToRenderWorld`] component are synced. +/// Adding [`SyncToRenderWorld`] to a main world component will establish such a link. +/// Once a synchronized main entity is despawned, its corresponding render entity will be automatically +/// despawned in the next `sync`. +/// +/// The sync step does not copy any of component data between worlds, since its often not necessary to transfer over all +/// the components of a main world entity. +/// The render world probably cares about a `Position` component, but not a `Velocity` component. +/// The extraction happens in its own step, independently from, and after synchronization. +/// +/// Moreover, [`WorldSyncPlugin`] only synchronizes *entities*. [`RenderAsset`](crate::render_asset::RenderAsset)s like meshes and textures are handled +/// differently. +/// +/// [`PipelinedRenderingPlugin`]: crate::pipelined_rendering::PipelinedRenderingPlugin +#[derive(Default)] +pub struct WorldSyncPlugin; + +impl Plugin for WorldSyncPlugin { + fn build(&self, app: &mut bevy_app::App) { + app.init_resource::(); + app.observe( + |trigger: Trigger, mut pending: ResMut| { + pending.push(EntityRecord::Added(trigger.entity())); + }, + ); + app.observe( + |trigger: Trigger, + mut pending: ResMut, + query: Query<&RenderEntity>| { + if let Ok(e) = query.get(trigger.entity()) { + pending.push(EntityRecord::Removed(e.id())); + }; + }, + ); + } +} +/// 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 SyncToRenderWorld; + +/// Component added on the main world entities that are synced to the Render World in order to keep track of the corresponding render world entity +#[derive(Component, Deref, Clone, Debug, Copy)] +pub struct RenderEntity(Entity); +impl RenderEntity { + #[inline] + pub fn id(&self) -> Entity { + self.0 + } +} + +/// Component added on the render world entities to keep track of the corresponding main world entity +#[derive(Component, Deref, Clone, Debug)] +pub struct MainEntity(Entity); +impl MainEntity { + #[inline] + pub fn id(&self) -> Entity { + self.0 + } +} + +/// Marker component that indicates that its entity needs to be despawned at the end of the frame. +#[derive(Component, Clone, Debug, Default, Reflect)] +#[component(storage = "SparseSet")] +pub struct TemporaryRenderEntity; + +/// A record enum to what entities with [`SyncToRenderWorld`] have been added or removed. +pub(crate) enum EntityRecord { + /// When an entity is spawned on the main world, notify the render world so that it can spawn a corresponding + /// entity. This contains the main world entity. + Added(Entity), + /// When an entity is despawned on the main world, notify the render world so that the corresponding entity can be + /// despawned. This contains the render world entity. + Removed(Entity), +} + +// Entity Record in MainWorld pending to Sync +#[derive(Resource, Default, Deref, DerefMut)] +pub(crate) struct PendingSyncEntity { + records: Vec, +} + +pub(crate) fn entity_sync_system(main_world: &mut World, render_world: &mut World) { + main_world.resource_scope(|world, mut pending: Mut| { + // TODO : batching record + for record in pending.drain(..) { + match record { + EntityRecord::Added(e) => { + if let Some(mut entity) = world.get_entity_mut(e) { + match entity.entry::() { + bevy_ecs::world::Entry::Occupied(_) => { + panic!("Attempting to synchronize an entity that has already been synchronized!"); + } + bevy_ecs::world::Entry::Vacant(entry) => { + let id = render_world.spawn(MainEntity(e)).id(); + + entry.insert(RenderEntity(id)); + } + }; + } + } + EntityRecord::Removed(e) => { + if let Some(ec) = render_world.get_entity_mut(e) { + ec.despawn(); + }; + } + } + } + }); +} + +pub(crate) fn despawn_temporary_render_entities( + 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); + } +} + +#[cfg(test)] +mod tests { + use bevy_ecs::{ + component::Component, + entity::Entity, + observer::Trigger, + query::With, + system::{Query, ResMut}, + world::{OnAdd, OnRemove, World}, + }; + + use super::{ + entity_sync_system, EntityRecord, MainEntity, PendingSyncEntity, RenderEntity, + SyncToRenderWorld, + }; + + #[derive(Component)] + struct RenderDataComponent; + + #[test] + fn world_sync() { + let mut main_world = World::new(); + let mut render_world = World::new(); + main_world.init_resource::(); + + main_world.observe( + |trigger: Trigger, mut pending: ResMut| { + pending.push(EntityRecord::Added(trigger.entity())); + }, + ); + main_world.observe( + |trigger: Trigger, + mut pending: ResMut, + query: Query<&RenderEntity>| { + if let Ok(e) = query.get(trigger.entity()) { + pending.push(EntityRecord::Removed(e.id())); + }; + }, + ); + + // spawn some empty entities for test + for _ in 0..99 { + main_world.spawn_empty(); + } + + // spawn + let main_entity = main_world + .spawn(RenderDataComponent) + // indicates that its entity needs to be synchronized to the render world + .insert(SyncToRenderWorld) + .id(); + + entity_sync_system(&mut main_world, &mut render_world); + + let mut q = render_world.query_filtered::>(); + + // Only one synchronized entity + assert!(q.iter(&render_world).count() == 1); + + let render_entity = q.get_single(&render_world).unwrap(); + let render_entity_component = main_world.get::(main_entity).unwrap(); + + assert!(render_entity_component.id() == render_entity); + + let main_entity_component = render_world + .get::(render_entity_component.id()) + .unwrap(); + + assert!(main_entity_component.id() == main_entity); + + // despawn + main_world.despawn(main_entity); + + entity_sync_system(&mut main_world, &mut render_world); + + // Only one synchronized entity + assert!(q.iter(&render_world).count() == 0); + } +} diff --git a/crates/bevy_sprite/src/bundle.rs b/crates/bevy_sprite/src/bundle.rs index ca962c40b1..df9e99e622 100644 --- a/crates/bevy_sprite/src/bundle.rs +++ b/crates/bevy_sprite/src/bundle.rs @@ -4,6 +4,7 @@ use bevy_ecs::bundle::Bundle; use bevy_render::{ texture::Image, view::{InheritedVisibility, ViewVisibility, Visibility}, + world_sync::SyncToRenderWorld, }; use bevy_transform::components::{GlobalTransform, Transform}; @@ -30,4 +31,6 @@ pub struct SpriteBundle { pub inherited_visibility: InheritedVisibility, /// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering pub view_visibility: ViewVisibility, + /// Marker component that indicates that its entity needs to be synchronized to the render world + pub sync: SyncToRenderWorld, } diff --git a/crates/bevy_sprite/src/mesh2d/mesh.rs b/crates/bevy_sprite/src/mesh2d/mesh.rs index 0f70232afc..a0e6229a6a 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 1efa866ec7..63b025d5a6 100644 --- a/crates/bevy_sprite/src/render/mod.rs +++ b/crates/bevy_sprite/src/render/mod.rs @@ -39,6 +39,7 @@ use bevy_render::{ ExtractedView, Msaa, ViewTarget, ViewUniform, ViewUniformOffset, ViewUniforms, ViewVisibility, VisibleEntities, }, + world_sync::{RenderEntity, TemporaryRenderEntity}, Extract, }; use bevy_transform::components::GlobalTransform; @@ -372,6 +373,7 @@ pub fn extract_sprites( sprite_query: Extract< Query<( Entity, + &RenderEntity, &ViewVisibility, &Sprite, &GlobalTransform, @@ -382,7 +384,9 @@ pub fn extract_sprites( >, ) { extracted_sprites.sprites.clear(); - for (entity, view_visibility, sprite, transform, handle, sheet, slices) in sprite_query.iter() { + for (original_entity, entity, view_visibility, sprite, transform, handle, sheet, slices) in + sprite_query.iter() + { if !view_visibility.get() { continue; } @@ -390,8 +394,8 @@ pub fn extract_sprites( if let Some(slices) = slices { extracted_sprites.sprites.extend( slices - .extract_sprites(transform, entity, sprite, handle) - .map(|e| (commands.spawn_empty().id(), e)), + .extract_sprites(transform, original_entity, sprite, handle) + .map(|e| (commands.spawn(TemporaryRenderEntity).id(), e)), ); } else { let atlas_rect = @@ -410,7 +414,7 @@ pub fn extract_sprites( // PERF: we don't check in this function that the `Image` asset is ready, since it should be in most cases and hashing the handle is expensive extracted_sprites.sprites.insert( - entity, + entity.id(), ExtractedSprite { color: sprite.color.into(), transform: *transform, @@ -421,7 +425,7 @@ pub fn extract_sprites( flip_y: sprite.flip_y, image_handle_id: handle.id(), anchor: sprite.anchor.as_vec(), - original_entity: None, + original_entity: Some(original_entity), }, ); } diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index 0026ecbe23..75cf7b771b 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -33,6 +33,7 @@ use bevy_render::{ render_phase::{PhaseItem, PhaseItemExtraIndex}, texture::GpuImage, view::ViewVisibility, + world_sync::{RenderEntity, TemporaryRenderEntity}, ExtractSchedule, Render, }; use bevy_sprite::TextureAtlasLayout; @@ -188,12 +189,13 @@ pub struct ExtractedUiNodes { pub uinodes: EntityHashMap, } +#[allow(clippy::too_many_arguments)] pub fn extract_uinode_background_colors( + mut commands: Commands, mut extracted_uinodes: ResMut, default_ui_camera: Extract, uinode_query: Extract< Query<( - Entity, &Node, &GlobalTransform, &ViewVisibility, @@ -202,22 +204,25 @@ pub fn extract_uinode_background_colors( &BackgroundColor, )>, >, + mapping: Extract>, ) { - for (entity, uinode, transform, view_visibility, clip, camera, background_color) in - &uinode_query - { + for (uinode, transform, view_visibility, clip, camera, background_color) in &uinode_query { let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get()) else { continue; }; + let Ok(&camera_entity) = mapping.get(camera_entity) else { + continue; + }; + // Skip invisible backgrounds if !view_visibility.get() || background_color.0.is_fully_transparent() { continue; } extracted_uinodes.uinodes.insert( - entity, + commands.spawn(TemporaryRenderEntity).id(), ExtractedUiNode { stack_index: uinode.stack_index, transform: transform.compute_matrix(), @@ -231,7 +236,7 @@ pub fn extract_uinode_background_colors( atlas_scaling: None, flip_x: false, flip_y: false, - camera_entity, + camera_entity: camera_entity.id(), border: uinode.border(), border_radius: uinode.border_radius(), node_type: NodeType::Rect, @@ -260,6 +265,7 @@ pub fn extract_uinode_images( Without, >, >, + mapping: Extract>, ) { for (uinode, transform, view_visibility, clip, camera, image, atlas) in &uinode_query { let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get()) @@ -267,6 +273,10 @@ pub fn extract_uinode_images( continue; }; + let Ok(render_camera_entity) = mapping.get(camera_entity) else { + continue; + }; + // Skip invisible images if !view_visibility.get() || image.color.is_fully_transparent() @@ -303,7 +313,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(), @@ -314,7 +324,7 @@ pub fn extract_uinode_images( atlas_scaling, flip_x: image.flip_x, flip_y: image.flip_y, - camera_entity, + camera_entity: render_camera_entity.id(), border: uinode.border, border_radius: uinode.border_radius, node_type: NodeType::Rect, @@ -337,6 +347,7 @@ pub fn extract_uinode_borders( AnyOf<(&BorderColor, &Outline)>, )>, >, + mapping: Extract>, ) { let image = AssetId::::default(); @@ -356,6 +367,10 @@ pub fn extract_uinode_borders( continue; }; + let Ok(&camera_entity) = mapping.get(camera_entity) else { + continue; + }; + // Skip invisible borders if !view_visibility.get() || maybe_border_color.is_some_and(|border_color| border_color.0.is_fully_transparent()) @@ -368,7 +383,7 @@ pub fn extract_uinode_borders( if !uinode.is_empty() && uinode.border() != BorderRect::ZERO { if let Some(border_color) = maybe_border_color { extracted_uinodes.uinodes.insert( - commands.spawn_empty().id(), + commands.spawn(TemporaryRenderEntity).id(), ExtractedUiNode { stack_index: uinode.stack_index, transform: global_transform.compute_matrix(), @@ -382,7 +397,7 @@ pub fn extract_uinode_borders( clip: maybe_clip.map(|clip| clip.clip), flip_x: false, flip_y: false, - camera_entity, + camera_entity: camera_entity.id(), border_radius: uinode.border_radius(), border: uinode.border(), node_type: NodeType::Border, @@ -408,7 +423,7 @@ pub fn extract_uinode_borders( clip: maybe_clip.map(|clip| clip.clip), flip_x: false, flip_y: false, - camera_entity, + camera_entity: camera_entity.id(), border: BorderRect::square(uinode.outline_width()), border_radius: uinode.outline_radius(), node_type: NodeType::Border, @@ -438,7 +453,7 @@ pub fn extract_default_ui_camera_view( mut transparent_render_phases: ResMut>, ui_scale: Extract>, query: Extract< - Query<(Entity, &Camera, Option<&UiAntiAlias>), Or<(With, With)>>, + Query<(&RenderEntity, &Camera, Option<&UiAntiAlias>), Or<(With, With)>>, >, mut live_entities: Local, ) { @@ -463,6 +478,7 @@ pub fn extract_default_ui_camera_view( camera.physical_viewport_rect(), camera.physical_viewport_size(), ) { + let entity = entity.id(); // use a projection matrix with the origin in the top left instead of the bottom left that comes with OrthographicProjection let projection_matrix = Mat4::orthographic_rh( 0.0, @@ -473,23 +489,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(); let entity_commands = commands .get_or_spawn(entity) @@ -507,10 +526,11 @@ pub fn extract_default_ui_camera_view( } #[cfg(feature = "bevy_text")] +#[allow(clippy::too_many_arguments)] 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>, @@ -525,12 +545,13 @@ pub fn extract_uinode_text( &TextLayoutInfo, )>, >, + mapping: Extract>, ) { + 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; }; @@ -542,11 +563,14 @@ 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(); + let Ok(&camera_entity) = mapping.get(camera_entity) else { + continue; + }; // Align the text to the nearest physical pixel: // * Translate by minus the text node's half-size // (The transform translates to the center of the node but the text coordinates are relative to the node's top left corner) @@ -581,8 +605,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 @@ -594,7 +619,7 @@ pub fn extract_uinode_text( clip: clip.map(|clip| clip.clip), flip_x: false, flip_y: false, - camera_entity, + camera_entity: camera_entity.id(), border: BorderRect::ZERO, border_radius: ResolvedBorderRadius::ZERO, node_type: NodeType::Rect, diff --git a/crates/bevy_ui/src/render/ui_material_pipeline.rs b/crates/bevy_ui/src/render/ui_material_pipeline.rs index 9c814ab843..71cbb2dcb4 100644 --- a/crates/bevy_ui/src/render/ui_material_pipeline.rs +++ b/crates/bevy_ui/src/render/ui_material_pipeline.rs @@ -20,6 +20,7 @@ use bevy_render::{ renderer::{RenderDevice, RenderQueue}, texture::BevyDefault, view::*, + world_sync::{RenderEntity, TemporaryRenderEntity}, Extract, ExtractSchedule, Render, RenderSet, }; use bevy_transform::prelude::GlobalTransform; @@ -354,13 +355,13 @@ impl Default for ExtractedUiMaterialNodes { } pub fn extract_ui_material_nodes( + mut commands: Commands, mut extracted_uinodes: ResMut>, materials: Extract>>, default_ui_camera: Extract, uinode_query: Extract< Query< ( - Entity, &Node, &GlobalTransform, &Handle, @@ -371,15 +372,20 @@ pub fn extract_ui_material_nodes( Without, >, >, + render_entity_lookup: Extract>, ) { // If there is only one camera, we use it as default let default_single_camera = default_ui_camera.get(); - for (entity, uinode, transform, handle, view_visibility, clip, camera) in uinode_query.iter() { + for (uinode, transform, handle, view_visibility, clip, camera) in uinode_query.iter() { let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_single_camera) else { continue; }; + let Ok(&camera_entity) = render_entity_lookup.get(camera_entity) else { + continue; + }; + // skip invisible nodes if !view_visibility.get() { continue; @@ -398,7 +404,7 @@ pub fn extract_ui_material_nodes( ]; extracted_uinodes.uinodes.insert( - entity, + commands.spawn(TemporaryRenderEntity).id(), ExtractedUiMaterialNode { stack_index: uinode.stack_index, transform: transform.compute_matrix(), @@ -409,7 +415,7 @@ pub fn extract_ui_material_nodes( }, border, clip: clip.map(|clip| clip.clip), - camera_entity, + camera_entity: camera_entity.id(), }, ); } diff --git a/examples/3d/fog_volumes.rs b/examples/3d/fog_volumes.rs index 551d719e68..8ebc923332 100644 --- a/examples/3d/fog_volumes.rs +++ b/examples/3d/fog_volumes.rs @@ -9,6 +9,7 @@ use bevy::{ math::vec3, pbr::{FogVolume, VolumetricFog, VolumetricLight}, prelude::*, + render::world_sync::SyncToRenderWorld, }; /// Entry point. @@ -43,7 +44,9 @@ fn setup(mut commands: Commands, asset_server: Res) { // up. scattering: 1.0, ..default() - }); + }) + // indicates that this fog volume needs to be Synchronized to the render world + .insert(SyncToRenderWorld); // Spawn a bright directional light that illuminates the fog well. commands