diff --git a/crates/bevy_core/src/lib.rs b/crates/bevy_core/src/lib.rs index ffd9ce813f9eb..94a6be7f08331 100644 --- a/crates/bevy_core/src/lib.rs +++ b/crates/bevy_core/src/lib.rs @@ -14,7 +14,8 @@ pub mod prelude { //! The Bevy Core Prelude. #[doc(hidden)] pub use crate::{ - DebugName, FrameCountPlugin, Name, TaskPoolOptions, TaskPoolPlugin, TypeRegistrationPlugin, + Cached, DebugName, FrameCountPlugin, Name, Static, TaskPoolOptions, TaskPoolPlugin, + TypeRegistrationPlugin, }; } @@ -26,7 +27,7 @@ use bevy_utils::{Duration, HashSet, Instant, Uuid}; use std::borrow::Cow; use std::ffi::OsString; use std::marker::PhantomData; -use std::ops::Range; +use std::ops::{Deref, Range}; use std::path::{Path, PathBuf}; #[cfg(not(target_arch = "wasm32"))] @@ -139,6 +140,91 @@ fn tick_global_task_pools(_main_thread_marker: Option>) { tick_global_task_pools_on_main_thread(); } +/// Static is a special marker component. Bevy assumes that entities with a Static component do not change +/// during play. +/// +/// * They do not move. When [`Transform`] changes, [`GlobalTransform`] is not propagated. +/// * They do not change apperence. Data is cached for render and not updated. +/// +/// The [`Cached`] component is added to static entities after they are rendered for the first time (when using +/// the [`StaticPlugin`]). +/// +/// # Examples +/// +/// These sorts of entities should be marked with a Static component. +/// +/// * Terrain. +/// * Background titles (in a 2d game). +/// * Large props that never move, like buildings. +/// * Everything with properties that don't change during play. +/// +/// These sorts of entities should not be marked with a Static component. +/// +/// * Dynamic physics bodies. +/// * Animated characters. +/// * Things that change color, texture, or material. +/// * Anything with properties you expect to change during play. +/// +#[derive(Component, Default, Debug, Clone, Copy)] +#[component(storage = "Table")] +pub struct Static; + +/// Cached is a special marker component closely related to [`Static`]. +/// +/// Systems in the [`PostUpdate`] schedule that query for cached entities should run after [`refresh_cached`] +/// to avoid a possible one frame lag when [`Static`] is removed. +#[derive(Component, Default, Debug, Clone, Copy)] +#[component(storage = "Table")] +pub struct Cached; + +/// The vector of entities that have had [`Cached`] removed since the last frame was drawn. +#[derive(Debug, Default, Resource, Clone)] +pub struct Invalidated { + entities: Vec, +} + +impl Deref for Invalidated { + type Target = [Entity]; + + fn deref(&self) -> &Self::Target { + &self.entities + } +} + +/// Removes [`Cached`] from entities that are no longer [`Static`]. This is run in the [`PostUpdate`] schedule, so it +/// executes between the `Update` schedule and start of render-world extraction. +pub fn remove_cached_dynamics( + cached: Query, Without)>, + mut commands: Commands, +) { + // FIXME: This should eventually become an unsafe exclusive system operating directly on architypes. + for entity in cached.iter() { + commands.entity(entity).remove::(); + } +} + +/// Writes a vec of entities to be droped from caches in the render world. This is necessary because +/// extraction systems can't mutate the main world, and so cannot read component removal events. +pub fn write_cache_invalidation( + mut removed: RemovedComponents, + mut invalidated: ResMut, +) { + invalidated.entities.clear(); + invalidated.entities.extend(removed.read()); +} + +/// Adds static entities and caching to Apps. See [`Static`] and [`Cached`] for details. +#[derive(Default)] +pub struct StaticPlugin; + +impl Plugin for StaticPlugin { + fn build(&self, app: &mut App) { + app.init_resource::() + .add_systems(PostUpdate, remove_cached_dynamics) + .add_systems(Last, write_cache_invalidation); + } +} + /// Maintains a count of frames rendered since the start of the application. /// /// [`FrameCount`] is incremented during [`Last`], providing predictable diff --git a/crates/bevy_internal/src/default_plugins.rs b/crates/bevy_internal/src/default_plugins.rs index ccf992cd02e5b..10ca30ee84545 100644 --- a/crates/bevy_internal/src/default_plugins.rs +++ b/crates/bevy_internal/src/default_plugins.rs @@ -45,6 +45,7 @@ impl PluginGroup for DefaultPlugins { .add(bevy_core::TaskPoolPlugin::default()) .add(bevy_core::TypeRegistrationPlugin) .add(bevy_core::FrameCountPlugin) + .add(bevy_core::StaticPlugin) .add(bevy_time::TimePlugin) .add(bevy_transform::TransformPlugin) .add(bevy_hierarchy::HierarchyPlugin) diff --git a/crates/bevy_pbr/Cargo.toml b/crates/bevy_pbr/Cargo.toml index 3c5584ecbeda4..9e47fab5ca63b 100644 --- a/crates/bevy_pbr/Cargo.toml +++ b/crates/bevy_pbr/Cargo.toml @@ -18,6 +18,7 @@ pbr_transmission_textures = [] # bevy bevy_app = { path = "../bevy_app", version = "0.12.0" } bevy_asset = { path = "../bevy_asset", version = "0.12.0" } +bevy_core = { path = "../bevy_core", version = "0.12.0" } bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.12.0" } bevy_ecs = { path = "../bevy_ecs", version = "0.12.0" } bevy_math = { path = "../bevy_math", version = "0.12.0" } diff --git a/crates/bevy_pbr/src/material.rs b/crates/bevy_pbr/src/material.rs index a4a8eb8401e8a..01fa48aa19244 100644 --- a/crates/bevy_pbr/src/material.rs +++ b/crates/bevy_pbr/src/material.rs @@ -1,6 +1,7 @@ use crate::*; use bevy_app::{App, Plugin}; use bevy_asset::{Asset, AssetApp, AssetEvent, AssetId, AssetServer, Assets, Handle}; +use bevy_core::Cached; use bevy_core_pipeline::{ core_3d::{ AlphaMask3d, Camera3d, Opaque3d, ScreenSpaceTransmissionQuality, Transmissive3d, diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index bf9d8bbb81afe..70b2401a258bc 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -5,13 +5,14 @@ use crate::{ }; use bevy_app::{Plugin, PostUpdate}; use bevy_asset::{load_internal_asset, AssetId, Handle}; +use bevy_core::{Cached, Invalidated}; use bevy_core_pipeline::{ core_3d::{AlphaMask3d, Opaque3d, Transmissive3d, Transparent3d, CORE_3D_DEPTH_FORMAT}, deferred::{AlphaMask3dDeferred, Opaque3dDeferred}, }; use bevy_derive::{Deref, DerefMut}; -use bevy_ecs::entity::EntityHashMap; use bevy_ecs::{ + entity::EntityHashMap, prelude::*, query::ROQueryItem, system::{lifetimeless::*, SystemParamItem, SystemState}, @@ -267,17 +268,20 @@ pub fn extract_meshes( mut render_mesh_instances: ResMut, mut thread_local_queues: Local>>>, meshes_query: Extract< - Query<( - Entity, - &ViewVisibility, - &GlobalTransform, - Option<&PreviousGlobalTransform>, - &Handle, - Has, - Has, - Has, - Has, - )>, + Query< + ( + Entity, + &ViewVisibility, + &GlobalTransform, + Option<&PreviousGlobalTransform>, + &Handle, + Has, + Has, + Has, + Has, + ), + Without, + >, >, ) { meshes_query.par_iter().for_each( @@ -313,6 +317,7 @@ pub fn extract_meshes( previous_transform: (&previous_transform).into(), flags: flags.bits(), }; + let tls = thread_local_queues.get_or_default(); let mut queue = tls.take(); queue.push(( @@ -329,7 +334,6 @@ pub fn extract_meshes( }, ); - render_mesh_instances.clear(); for queue in thread_local_queues.iter_mut() { render_mesh_instances.extend(queue.get_mut().drain(..)); } diff --git a/crates/bevy_render/src/view/visibility/mod.rs b/crates/bevy_render/src/view/visibility/mod.rs index e060864617100..3f1b19f09e815 100644 --- a/crates/bevy_render/src/view/visibility/mod.rs +++ b/crates/bevy_render/src/view/visibility/mod.rs @@ -1,9 +1,10 @@ mod render_layers; +use bevy_core::{Cached, Static}; use bevy_derive::Deref; pub use render_layers::*; -use bevy_app::{Plugin, PostUpdate}; +use bevy_app::{First, Plugin, PostUpdate}; use bevy_asset::{Assets, Handle}; use bevy_ecs::prelude::*; use bevy_hierarchy::{Children, Parent}; @@ -212,7 +213,7 @@ impl Plugin for VisibilityPlugin { fn build(&self, app: &mut bevy_app::App) { use VisibilitySystems::*; - app.add_systems( + app.add_systems(First, cache_viewed).add_systems( PostUpdate, ( calculate_bounds.in_set(CalculateBounds), @@ -455,6 +456,18 @@ pub fn check_visibility( } } +/// Sets [`Static`] entities that were visible in the previous frame to [`Cached`]. +fn cache_viewed( + visibilities: Query<(Entity, &ViewVisibility), (With, Without)>, + mut commands: Commands, +) { + for (entity, vis) in visibilities.iter() { + if vis.get() { + commands.entity(entity).insert(Cached); + } + } +} + #[cfg(test)] mod test { use bevy_app::prelude::*; diff --git a/crates/bevy_transform/Cargo.toml b/crates/bevy_transform/Cargo.toml index 8e5e937187d64..826556cc9ca31 100644 --- a/crates/bevy_transform/Cargo.toml +++ b/crates/bevy_transform/Cargo.toml @@ -11,6 +11,7 @@ keywords = ["bevy"] [dependencies] # bevy bevy_app = { path = "../bevy_app", version = "0.12.0" } +bevy_core = { path = "../bevy_core", version = "0.12.0" } bevy_ecs = { path = "../bevy_ecs", version = "0.12.0", features = [ "bevy_reflect", ] } diff --git a/crates/bevy_transform/src/lib.rs b/crates/bevy_transform/src/lib.rs index c44c120b0146a..7ef5a486cc189 100644 --- a/crates/bevy_transform/src/lib.rs +++ b/crates/bevy_transform/src/lib.rs @@ -17,6 +17,7 @@ pub mod prelude { } use bevy_app::prelude::*; +use bevy_core::remove_cached_dynamics; use bevy_ecs::prelude::*; use bevy_hierarchy::ValidParentCheckPlugin; use bevy_math::{Affine3A, Mat4, Vec3}; @@ -119,7 +120,9 @@ impl Plugin for TransformPlugin { ) .configure_sets( PostUpdate, - PropagateTransformsSet.in_set(TransformSystem::TransformPropagate), + PropagateTransformsSet + .in_set(TransformSystem::TransformPropagate) + .after(remove_cached_dynamics), ) .add_systems( PostUpdate, diff --git a/crates/bevy_transform/src/systems.rs b/crates/bevy_transform/src/systems.rs index 8f6bac916a739..d693749975e42 100644 --- a/crates/bevy_transform/src/systems.rs +++ b/crates/bevy_transform/src/systems.rs @@ -1,4 +1,5 @@ use crate::components::{GlobalTransform, Transform}; +use bevy_core::Cached; use bevy_ecs::{ change_detection::Ref, prelude::{Changed, DetectChanges, Entity, Query, With, Without}, @@ -8,7 +9,9 @@ use bevy_ecs::{ }; use bevy_hierarchy::{Children, Parent}; -/// Update [`GlobalTransform`] component of entities that aren't in the hierarchy +/// Update [`GlobalTransform`] component of entities that aren't in the hierarchy. +/// +/// Ignores entities marked as [`Cached`]. /// /// Third party plugins should ensure that this is used in concert with [`propagate_transforms`]. pub fn sync_simple_transforms( @@ -19,9 +22,13 @@ pub fn sync_simple_transforms( Or<(Changed, Added)>, Without, Without, + Without, ), >, - Query<(Ref, &mut GlobalTransform), (Without, Without)>, + Query< + (Ref, &mut GlobalTransform), + (Without, Without, Without), + >, )>, mut orphaned: RemovedComponents, ) { @@ -45,14 +52,20 @@ pub fn sync_simple_transforms( /// Update [`GlobalTransform`] component of entities based on entity hierarchy and /// [`Transform`] component. /// +/// This function will not propagate transforms to entities marked as [`Cached`], but it will visit them +/// in parallel to propagate the transforms of their children. +/// /// Third party plugins should ensure that this is used in concert with [`sync_simple_transforms`]. pub fn propagate_transforms( mut root_query: Query< (Entity, &Children, Ref, &mut GlobalTransform), - Without, + Or<(Without, With)>, >, mut orphaned: RemovedComponents, - transform_query: Query<(Ref, &mut GlobalTransform, Option<&Children>), With>, + transform_query: Query< + (Ref, &mut GlobalTransform, Option<&Children>), + (With, Without), + >, parent_query: Query<(Entity, Ref)>, mut orphaned_entities: Local>, ) { @@ -110,7 +123,7 @@ unsafe fn propagate_recursive( parent: &GlobalTransform, transform_query: &Query< (Ref, &mut GlobalTransform, Option<&Children>), - With, + (With, Without), >, parent_query: &Query<(Entity, Ref)>, entity: Entity, @@ -145,7 +158,7 @@ unsafe fn propagate_recursive( // Even if these A and B start two separate tasks running in parallel, one of them will panic before attempting // to mutably access E. (unsafe { transform_query.get_unchecked(entity) }) else { - return; + return; // This happens when `entity` is marked as `Cached`. }; changed |= transform.is_changed() || global_transform.is_added(); diff --git a/examples/stress_tests/many_cubes.rs b/examples/stress_tests/many_cubes.rs index 2b95053fa82f8..01b26c6eaa540 100644 --- a/examples/stress_tests/many_cubes.rs +++ b/examples/stress_tests/many_cubes.rs @@ -131,12 +131,15 @@ fn setup( let spherical_polar_theta_phi = fibonacci_spiral_on_sphere(golden_ratio, i, N_POINTS); let unit_sphere_p = spherical_polar_to_cartesian(spherical_polar_theta_phi); - commands.spawn(PbrBundle { - mesh: mesh.clone(), - material: materials.choose(&mut material_rng).unwrap().clone(), - transform: Transform::from_translation((radius * unit_sphere_p).as_vec3()), - ..default() - }); + commands.spawn(( + PbrBundle { + mesh: mesh.clone(), + material: materials.choose(&mut material_rng).unwrap().clone(), + transform: Transform::from_translation((radius * unit_sphere_p).as_vec3()), + ..default() + }, + Static, + )); } // camera @@ -152,34 +155,46 @@ fn setup( continue; } // cube - commands.spawn(PbrBundle { - mesh: mesh.clone(), - material: materials.choose(&mut material_rng).unwrap().clone(), - transform: Transform::from_xyz((x as f32) * 2.5, (y as f32) * 2.5, 0.0), - ..default() - }); - commands.spawn(PbrBundle { - mesh: mesh.clone(), - material: materials.choose(&mut material_rng).unwrap().clone(), - transform: Transform::from_xyz( - (x as f32) * 2.5, - HEIGHT as f32 * 2.5, - (y as f32) * 2.5, - ), - ..default() - }); - commands.spawn(PbrBundle { - mesh: mesh.clone(), - material: materials.choose(&mut material_rng).unwrap().clone(), - transform: Transform::from_xyz((x as f32) * 2.5, 0.0, (y as f32) * 2.5), - ..default() - }); - commands.spawn(PbrBundle { - mesh: mesh.clone(), - material: materials.choose(&mut material_rng).unwrap().clone(), - transform: Transform::from_xyz(0.0, (x as f32) * 2.5, (y as f32) * 2.5), - ..default() - }); + commands.spawn(( + PbrBundle { + mesh: mesh.clone(), + material: materials.choose(&mut material_rng).unwrap().clone(), + transform: Transform::from_xyz((x as f32) * 2.5, (y as f32) * 2.5, 0.0), + ..default() + }, + Static, + )); + commands.spawn(( + PbrBundle { + mesh: mesh.clone(), + material: materials.choose(&mut material_rng).unwrap().clone(), + transform: Transform::from_xyz( + (x as f32) * 2.5, + HEIGHT as f32 * 2.5, + (y as f32) * 2.5, + ), + ..default() + }, + Static, + )); + commands.spawn(( + PbrBundle { + mesh: mesh.clone(), + material: materials.choose(&mut material_rng).unwrap().clone(), + transform: Transform::from_xyz((x as f32) * 2.5, 0.0, (y as f32) * 2.5), + ..default() + }, + Static, + )); + commands.spawn(( + PbrBundle { + mesh: mesh.clone(), + material: materials.choose(&mut material_rng).unwrap().clone(), + transform: Transform::from_xyz(0.0, (x as f32) * 2.5, (y as f32) * 2.5), + ..default() + }, + Static, + )); } } // camera @@ -190,7 +205,7 @@ fn setup( } } - commands.spawn(DirectionalLightBundle { ..default() }); + commands.spawn((DirectionalLightBundle { ..default() }, Static)); } fn init_textures(args: &Args, images: &mut Assets) -> Vec> {