diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 057280cc60..43e44f537d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -292,11 +292,10 @@ There are three main places you can check for things to review: 2. Pull requests on [bevy](https://github.com/bevyengine/bevy/pulls) and the [bevy-website](https://github.com/bevyengine/bevy-website/pulls) repos. 3. [RFCs](https://github.com/bevyengine/rfcs), which need extensive thoughtful community input on their design. -Official focus areas and work done by @cart go through this review process as well. -Not even our project lead is exempt from reviews and RFCs! +Not even our Project Leads and Maintainers are exempt from reviews and RFCs! By giving feedback on this work (and related supporting work), you can help us make sure our releases are both high-quality and timely. -Finally, if nothing brings you more satisfaction than seeing every last issue labeled and all resolved issues closed, feel free to message @cart for a Bevy org role to help us keep things tidy. +Finally, if nothing brings you more satisfaction than seeing every last issue labeled and all resolved issues closed, feel free to message the Project Lead (currently @cart) for a Bevy org role to help us keep things tidy. As discussed in our [*Bevy Organization doc*](/docs/the_bevy_organization.md), this role only requires good faith and a basic understanding of our development process. ### How to adopt pull requests @@ -340,8 +339,8 @@ To locally lint your files using the same workflow as our CI: 1. Install [markdownlint-cli](https://github.com/igorshubovych/markdownlint-cli). 2. Run `markdownlint -f -c .github/linters/.markdown-lint.yml .` in the root directory of the Bevy project. 5. Push your changes to your fork on Github and open a Pull Request. -6. Respond to any CI failures or review feedback. While CI failures must be fixed before we can merge your PR, you do not need to *agree* with all feedback from your reviews, merely acknowledge that it was given. If you cannot come to an agreement, leave the thread open and defer to @cart's final judgement. -7. When your PR is ready to merge, @cart will review it and suggest final changes. If those changes are minimal he may even apply them directly to speed up merging. +6. Respond to any CI failures or review feedback. While CI failures must be fixed before we can merge your PR, you do not need to *agree* with all feedback from your reviews, merely acknowledge that it was given. If you cannot come to an agreement, leave the thread open and defer to a Maintainer or Project Lead's final judgement. +7. When your PR is ready to merge, a Maintainer or Project Lead will review it and suggest final changes. If those changes are minimal they may even apply them directly to speed up merging. If you end up adding a new official Bevy crate to the `bevy` repo: diff --git a/Cargo.toml b/Cargo.toml index 80d7509280..24372c2768 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,7 @@ default = [ "vorbis", "x11", "filesystem_watcher", + "bevy_gizmos", "android_shared_stdcxx", "tonemapping_luts", ] @@ -66,7 +67,7 @@ bevy_asset = ["bevy_internal/bevy_asset"] bevy_audio = ["bevy_internal/bevy_audio"] # Provides cameras and other basic render pipeline features -bevy_core_pipeline = ["bevy_internal/bevy_core_pipeline"] +bevy_core_pipeline = ["bevy_internal/bevy_core_pipeline", "bevy_asset", "bevy_render"] # Plugin for dynamic loading (using [libloading](https://crates.io/crates/libloading)) bevy_dynamic_plugin = ["bevy_internal/bevy_dynamic_plugin"] @@ -75,29 +76,32 @@ bevy_dynamic_plugin = ["bevy_internal/bevy_dynamic_plugin"] bevy_gilrs = ["bevy_internal/bevy_gilrs"] # [glTF](https://www.khronos.org/gltf/) support -bevy_gltf = ["bevy_internal/bevy_gltf"] +bevy_gltf = ["bevy_internal/bevy_gltf", "bevy_asset", "bevy_scene", "bevy_pbr"] # Adds PBR rendering -bevy_pbr = ["bevy_internal/bevy_pbr"] +bevy_pbr = ["bevy_internal/bevy_pbr", "bevy_asset", "bevy_render", "bevy_core_pipeline"] # Provides rendering functionality bevy_render = ["bevy_internal/bevy_render"] # Provides scene functionality -bevy_scene = ["bevy_internal/bevy_scene"] +bevy_scene = ["bevy_internal/bevy_scene", "bevy_asset"] # Provides sprite functionality -bevy_sprite = ["bevy_internal/bevy_sprite"] +bevy_sprite = ["bevy_internal/bevy_sprite", "bevy_render", "bevy_core_pipeline"] # Provides text functionality bevy_text = ["bevy_internal/bevy_text"] # A custom ECS-driven UI framework -bevy_ui = ["bevy_internal/bevy_ui"] +bevy_ui = ["bevy_internal/bevy_ui", "bevy_core_pipeline", "bevy_text", "bevy_sprite"] # winit window and input backend bevy_winit = ["bevy_internal/bevy_winit"] +# Adds support for rendering gizmos +bevy_gizmos = ["bevy_internal/bevy_gizmos"] + # Tracing support, saving a file in Chrome Tracing format trace_chrome = ["trace", "bevy_internal/trace_chrome"] @@ -309,6 +313,16 @@ description = "Renders a rectangle, circle, and hexagon" category = "2D Rendering" wasm = true +[[example]] +name = "2d_gizmos" +path = "examples/2d/2d_gizmos.rs" + +[package.metadata.example.2d_gizmos] +name = "2D Gizmos" +description = "A scene showcasing 2D gizmos" +category = "2D Rendering" +wasm = true + [[example]] name = "sprite" path = "examples/2d/sprite.rs" @@ -400,6 +414,16 @@ description = "A scene showcasing the built-in 3D shapes" category = "3D Rendering" wasm = true +[[example]] +name = "3d_gizmos" +path = "examples/3d/3d_gizmos.rs" + +[package.metadata.example.3d_gizmos] +name = "3D Gizmos" +description = "A scene showcasing 3D gizmos" +category = "3D Rendering" +wasm = true + [[example]] name = "atmospheric_fog" path = "examples/3d/atmospheric_fog.rs" @@ -1395,11 +1419,21 @@ name = "post_processing" path = "examples/shader/post_processing.rs" [package.metadata.example.post_processing] -name = "Post Processing" +name = "Post Processing - Render To Texture" description = "A custom post processing effect, using two cameras, with one reusing the render texture of the first one" category = "Shaders" wasm = true +[[example]] +name = "post_process_pass" +path = "examples/shader/post_process_pass.rs" + +[package.metadata.example.post_process_pass] +name = "Post Processing - Custom Render Pass" +description = "A custom post processing effect, using a custom render pass that runs after the main pass" +category = "Shaders" +wasm = true + [[example]] name = "shader_defs" path = "examples/shader/shader_defs.rs" @@ -1553,6 +1587,16 @@ description = "Simple benchmark to test per-entity draw overhead. Run with the ` category = "Stress Tests" wasm = true +[[example]] +name = "many_gizmos" +path = "examples/stress_tests/many_gizmos.rs" + +[package.metadata.example.many_gizmos] +name = "Many Gizmos" +description = "Test rendering of many gizmos" +category = "Stress Tests" +wasm = true + [[example]] name = "many_foxes" path = "examples/stress_tests/many_foxes.rs" @@ -1563,6 +1607,16 @@ description = "Loads an animated fox model and spawns lots of them. Good for tes category = "Stress Tests" wasm = true +[[example]] +name = "many_glyphs" +path = "examples/stress_tests/many_glyphs.rs" + +[package.metadata.example.many_glyphs] +name = "Many Glyphs" +description = "Simple benchmark to test text rendering." +category = "Stress Tests" +wasm = true + [[example]] name = "many_lights" path = "examples/stress_tests/many_lights.rs" @@ -1736,12 +1790,12 @@ category = "UI (User Interface)" wasm = true [[example]] -name = "text_layout" -path = "examples/ui/text_layout.rs" +name = "flex_layout" +path = "examples/ui/flex_layout.rs" -[package.metadata.example.text_layout] -name = "Text Layout" -description = "Demonstrates how the AlignItems and JustifyContent properties can be composed to layout text" +[package.metadata.example.flex_layout] +name = "Flex Layout" +description = "Demonstrates how the AlignItems and JustifyContent properties can be composed to layout nodes and position text" category = "UI (User Interface)" wasm = false diff --git a/assets/scenes/load_scene_example.scn.ron b/assets/scenes/load_scene_example.scn.ron index de4d3ce928..4c03d1c53c 100644 --- a/assets/scenes/load_scene_example.scn.ron +++ b/assets/scenes/load_scene_example.scn.ron @@ -1,4 +1,9 @@ ( + resources: { + "scene::ResourceA": ( + score: 2, + ), + }, entities: { 0: ( components: { diff --git a/assets/shaders/post_process_pass.wgsl b/assets/shaders/post_process_pass.wgsl new file mode 100644 index 0000000000..b25b5788cc --- /dev/null +++ b/assets/shaders/post_process_pass.wgsl @@ -0,0 +1,48 @@ +// This shader computes the chromatic aberration effect + +#import bevy_pbr::utils + +// Since post processing is a fullscreen effect, we use the fullscreen vertex shader provided by bevy. +// This will import a vertex shader that renders a single fullscreen triangle. +// +// A fullscreen triangle is a single triangle that covers the entire screen. +// The box in the top left in that diagram is the screen. The 4 x are the corner of the screen +// +// Y axis +// 1 | x-----x...... +// 0 | | s | . ´ +// -1 | x_____x´ +// -2 | : .´ +// -3 | :´ +// +--------------- X axis +// -1 0 1 2 3 +// +// As you can see, the triangle ends up bigger than the screen. +// +// You don't need to worry about this too much since bevy will compute the correct UVs for you. +#import bevy_core_pipeline::fullscreen_vertex_shader + +@group(0) @binding(0) +var screen_texture: texture_2d; +@group(0) @binding(1) +var texture_sampler: sampler; +struct PostProcessSettings { + intensity: f32, +} +@group(0) @binding(2) +var settings: PostProcessSettings; + +@fragment +fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4 { + // Chromatic aberration strength + let offset_strength = settings.intensity; + + // Sample each color channel with an arbitrary shift + return vec4( + textureSample(screen_texture, texture_sampler, in.uv + vec2(offset_strength, -offset_strength)).r, + textureSample(screen_texture, texture_sampler, in.uv + vec2(-offset_strength, 0.0)).g, + textureSample(screen_texture, texture_sampler, in.uv + vec2(0.0, offset_strength)).b, + 1.0 + ); +} + diff --git a/benches/benches/bevy_ecs/components/archetype_updates.rs b/benches/benches/bevy_ecs/components/archetype_updates.rs index 53cc22f6a1..30f8ca6fcc 100644 --- a/benches/benches/bevy_ecs/components/archetype_updates.rs +++ b/benches/benches/bevy_ecs/components/archetype_updates.rs @@ -9,7 +9,7 @@ fn setup(system_count: usize) -> (World, Schedule) { fn empty() {} let mut schedule = Schedule::new(); for _ in 0..system_count { - schedule.add_system(empty); + schedule.add_systems(empty); } schedule.run(&mut world); (world, schedule) diff --git a/benches/benches/bevy_ecs/empty_archetypes.rs b/benches/benches/bevy_ecs/empty_archetypes.rs index 0d01bdcc32..315a2d2ce2 100644 --- a/benches/benches/bevy_ecs/empty_archetypes.rs +++ b/benches/benches/bevy_ecs/empty_archetypes.rs @@ -154,7 +154,7 @@ fn empty_archetypes(criterion: &mut Criterion) { let mut group = criterion.benchmark_group("empty_archetypes"); for archetype_count in [10, 100, 500, 1000, 2000, 5000, 10000] { let (mut world, mut schedule) = setup(true, |schedule| { - schedule.add_system(iter); + schedule.add_systems(iter); }); add_archetypes(&mut world, archetype_count); world.clear_entities(); @@ -185,7 +185,7 @@ fn empty_archetypes(criterion: &mut Criterion) { } for archetype_count in [10, 100, 500, 1000, 2000, 5000, 10000] { let (mut world, mut schedule) = setup(true, |schedule| { - schedule.add_system(for_each); + schedule.add_systems(for_each); }); add_archetypes(&mut world, archetype_count); world.clear_entities(); @@ -216,7 +216,7 @@ fn empty_archetypes(criterion: &mut Criterion) { } for archetype_count in [10, 100, 500, 1000, 2000, 5000, 10000] { let (mut world, mut schedule) = setup(true, |schedule| { - schedule.add_system(par_for_each); + schedule.add_systems(par_for_each); }); add_archetypes(&mut world, archetype_count); world.clear_entities(); diff --git a/benches/benches/bevy_ecs/scheduling/run_condition.rs b/benches/benches/bevy_ecs/scheduling/run_condition.rs index be6f25a4c7..a18bdd8c97 100644 --- a/benches/benches/bevy_ecs/scheduling/run_condition.rs +++ b/benches/benches/bevy_ecs/scheduling/run_condition.rs @@ -19,14 +19,9 @@ pub fn run_condition_yes(criterion: &mut Criterion) { fn empty() {} for amount in 0..21 { let mut schedule = Schedule::new(); - schedule.add_system(empty.run_if(yes)); + schedule.add_systems(empty.run_if(yes)); for _ in 0..amount { - schedule - .add_system(empty.run_if(yes)) - .add_system(empty.run_if(yes)) - .add_system(empty.run_if(yes)) - .add_system(empty.run_if(yes)) - .add_system(empty.run_if(yes)); + schedule.add_systems((empty, empty, empty, empty, empty).distributive_run_if(yes)); } // run once to initialize systems schedule.run(&mut world); @@ -47,14 +42,9 @@ pub fn run_condition_no(criterion: &mut Criterion) { fn empty() {} for amount in 0..21 { let mut schedule = Schedule::new(); - schedule.add_system(empty.run_if(no)); + schedule.add_systems(empty.run_if(no)); for _ in 0..amount { - schedule - .add_system(empty.run_if(no)) - .add_system(empty.run_if(no)) - .add_system(empty.run_if(no)) - .add_system(empty.run_if(no)) - .add_system(empty.run_if(no)); + schedule.add_systems((empty, empty, empty, empty, empty).distributive_run_if(no)); } // run once to initialize systems schedule.run(&mut world); @@ -82,14 +72,11 @@ pub fn run_condition_yes_with_query(criterion: &mut Criterion) { } for amount in 0..21 { let mut schedule = Schedule::new(); - schedule.add_system(empty.run_if(yes_with_query)); + schedule.add_systems(empty.run_if(yes_with_query)); for _ in 0..amount { - schedule - .add_system(empty.run_if(yes_with_query)) - .add_system(empty.run_if(yes_with_query)) - .add_system(empty.run_if(yes_with_query)) - .add_system(empty.run_if(yes_with_query)) - .add_system(empty.run_if(yes_with_query)); + schedule.add_systems( + (empty, empty, empty, empty, empty).distributive_run_if(yes_with_query), + ); } // run once to initialize systems schedule.run(&mut world); @@ -114,14 +101,11 @@ pub fn run_condition_yes_with_resource(criterion: &mut Criterion) { } for amount in 0..21 { let mut schedule = Schedule::new(); - schedule.add_system(empty.run_if(yes_with_resource)); + schedule.add_systems(empty.run_if(yes_with_resource)); for _ in 0..amount { - schedule - .add_system(empty.run_if(yes_with_resource)) - .add_system(empty.run_if(yes_with_resource)) - .add_system(empty.run_if(yes_with_resource)) - .add_system(empty.run_if(yes_with_resource)) - .add_system(empty.run_if(yes_with_resource)); + schedule.add_systems( + (empty, empty, empty, empty, empty).distributive_run_if(yes_with_resource), + ); } // run once to initialize systems schedule.run(&mut world); diff --git a/benches/benches/bevy_ecs/scheduling/running_systems.rs b/benches/benches/bevy_ecs/scheduling/running_systems.rs index 9206bd285f..151b96d4a6 100644 --- a/benches/benches/bevy_ecs/scheduling/running_systems.rs +++ b/benches/benches/bevy_ecs/scheduling/running_systems.rs @@ -23,7 +23,7 @@ pub fn empty_systems(criterion: &mut Criterion) { for amount in 0..5 { let mut schedule = Schedule::new(); for _ in 0..amount { - schedule.add_system(empty); + schedule.add_systems(empty); } schedule.run(&mut world); group.bench_function(&format!("{:03}_systems", amount), |bencher| { @@ -35,12 +35,7 @@ pub fn empty_systems(criterion: &mut Criterion) { for amount in 1..21 { let mut schedule = Schedule::new(); for _ in 0..amount { - schedule - .add_system(empty) - .add_system(empty) - .add_system(empty) - .add_system(empty) - .add_system(empty); + schedule.add_systems((empty, empty, empty, empty, empty)); } schedule.run(&mut world); group.bench_function(&format!("{:03}_systems", 5 * amount), |bencher| { @@ -79,9 +74,9 @@ pub fn busy_systems(criterion: &mut Criterion) { world.spawn_batch((0..ENTITY_BUNCH).map(|_| (A(0.0), B(0.0), C(0.0), E(0.0)))); for system_amount in 0..5 { let mut schedule = Schedule::new(); - schedule.add_system(ab).add_system(cd).add_system(ce); + schedule.add_systems((ab, cd, ce)); for _ in 0..system_amount { - schedule.add_system(ab).add_system(cd).add_system(ce); + schedule.add_systems((ab, cd, ce)); } schedule.run(&mut world); group.bench_function( @@ -130,9 +125,9 @@ pub fn contrived(criterion: &mut Criterion) { world.spawn_batch((0..ENTITY_BUNCH).map(|_| (C(0.0), D(0.0)))); for system_amount in 0..5 { let mut schedule = Schedule::new(); - schedule.add_system(s_0).add_system(s_1).add_system(s_2); + schedule.add_systems((s_0, s_1, s_2)); for _ in 0..system_amount { - schedule.add_system(s_0).add_system(s_1).add_system(s_2); + schedule.add_systems((s_0, s_1, s_2)); } schedule.run(&mut world); group.bench_function( diff --git a/benches/benches/bevy_ecs/scheduling/schedule.rs b/benches/benches/bevy_ecs/scheduling/schedule.rs index 3117c0fff2..e670c23d74 100644 --- a/benches/benches/bevy_ecs/scheduling/schedule.rs +++ b/benches/benches/bevy_ecs/scheduling/schedule.rs @@ -1,4 +1,4 @@ -use bevy_app::App; +use bevy_app::{App, Update}; use bevy_ecs::prelude::*; use criterion::Criterion; @@ -47,9 +47,7 @@ pub fn schedule(c: &mut Criterion) { world.spawn_batch((0..10000).map(|_| (A(0.0), B(0.0), C(0.0), E(0.0)))); let mut schedule = Schedule::new(); - schedule.add_system(ab); - schedule.add_system(cd); - schedule.add_system(ce); + schedule.add_systems((ab, cd, ce)); schedule.run(&mut world); b.iter(move || schedule.run(&mut world)); @@ -74,7 +72,7 @@ pub fn build_schedule(criterion: &mut Criterion) { group.measurement_time(std::time::Duration::from_secs(15)); // Method: generate a set of `graph_size` systems which have a One True Ordering. - // Add system to the schedule with full constraints. Hopefully this should be maximimally + // Add system to the schedule with full constraints. Hopefully this should be maximally // difficult for bevy to figure out. let labels: Vec<_> = (0..1000).map(|i| NumSet(i)).collect(); @@ -85,7 +83,7 @@ pub fn build_schedule(criterion: &mut Criterion) { bencher.iter(|| { let mut app = App::new(); for _ in 0..graph_size { - app.add_system(empty_system); + app.add_systems(Update, empty_system); } app.update(); }); @@ -95,7 +93,7 @@ pub fn build_schedule(criterion: &mut Criterion) { group.bench_function(format!("{graph_size}_schedule"), |bencher| { bencher.iter(|| { let mut app = App::new(); - app.add_system(empty_system.in_set(DummySet)); + app.add_systems(Update, empty_system.in_set(DummySet)); // Build a fully-connected dependency graph describing the One True Ordering. // Not particularly realistic but this can be refined later. @@ -107,7 +105,7 @@ pub fn build_schedule(criterion: &mut Criterion) { for label in &labels[i + 1..graph_size] { sys = sys.before(*label); } - app.add_system(sys); + app.add_systems(Update, sys); } // Run the app for a single frame. // This is necessary since dependency resolution does not occur until the game runs. diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index 7312a5fcf6..0e9534d63b 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -5,7 +5,7 @@ use std::ops::Deref; use std::time::Duration; -use bevy_app::{App, CoreSet, Plugin}; +use bevy_app::{App, Plugin, PostUpdate}; use bevy_asset::{AddAsset, Assets, Handle}; use bevy_core::Name; use bevy_ecs::prelude::*; @@ -550,10 +550,9 @@ impl Plugin for AnimationPlugin { app.add_asset::() .register_asset_reflect::() .register_type::() - .add_system( - animation_player - .in_base_set(CoreSet::PostUpdate) - .before(TransformSystem::TransformPropagate), + .add_systems( + PostUpdate, + animation_player.before(TransformSystem::TransformPropagate), ); } } diff --git a/crates/bevy_app/src/app.rs b/crates/bevy_app/src/app.rs index d70669be5a..d7a4da63cd 100644 --- a/crates/bevy_app/src/app.rs +++ b/crates/bevy_app/src/app.rs @@ -1,18 +1,20 @@ use crate::{ - CoreSchedule, CoreSet, IntoSystemAppConfig, IntoSystemAppConfigs, Plugin, PluginGroup, - StartupSet, SystemAppConfig, + First, Main, MainSchedulePlugin, Plugin, PluginGroup, Startup, StateTransition, Update, }; pub use bevy_derive::AppLabel; use bevy_ecs::{ prelude::*, schedule::{ apply_state_transition, common_conditions::run_once as run_once_condition, - run_enter_schedule, BoxedScheduleLabel, IntoSystemConfig, IntoSystemSetConfigs, + run_enter_schedule, BoxedScheduleLabel, IntoSystemConfigs, IntoSystemSetConfigs, ScheduleLabel, }, }; use bevy_utils::{tracing::debug, HashMap, HashSet}; -use std::fmt::Debug; +use std::{ + fmt::Debug, + panic::{catch_unwind, resume_unwind, AssertUnwindSafe}, +}; #[cfg(feature = "trace")] use bevy_utils::tracing::info_span; @@ -50,7 +52,7 @@ pub(crate) enum AppError { /// # /// fn main() { /// App::new() -/// .add_system(hello_world_system) +/// .add_systems(Update, hello_world_system) /// .run(); /// } /// @@ -71,17 +73,15 @@ pub struct App { pub runner: Box, // Send bound is required to make App Send /// The schedule that systems are added to by default. /// - /// This is initially set to [`CoreSchedule::Main`]. - pub default_schedule_label: BoxedScheduleLabel, - /// The schedule that controls the outer loop of schedule execution. + /// The schedule that runs the main loop of schedule execution. /// - /// This is initially set to [`CoreSchedule::Outer`]. - pub outer_schedule_label: BoxedScheduleLabel, + /// This is initially set to [`Main`]. + pub main_schedule_label: BoxedScheduleLabel, sub_apps: HashMap, plugin_registry: Vec>, plugin_name_added: HashSet, - /// A private marker to prevent incorrect calls to `App::run()` from `Plugin::build()` - is_building_plugin: bool, + /// A private counter to prevent incorrect calls to `App::run()` from `Plugin::build()` + building_plugin_depth: usize, } impl Debug for App { @@ -101,7 +101,7 @@ impl Debug for App { /// # Example /// /// ```rust -/// # use bevy_app::{App, AppLabel, SubApp, CoreSchedule}; +/// # use bevy_app::{App, AppLabel, SubApp, Main}; /// # use bevy_ecs::prelude::*; /// # use bevy_ecs::schedule::ScheduleLabel; /// @@ -119,12 +119,10 @@ impl Debug for App { /// // create a app with a resource and a single schedule /// let mut sub_app = App::empty(); /// // add an outer schedule that runs the main schedule -/// sub_app.add_simple_outer_schedule(); /// sub_app.insert_resource(Val(100)); /// /// // initialize main schedule -/// sub_app.init_schedule(CoreSchedule::Main); -/// sub_app.add_system(|counter: Res| { +/// sub_app.add_systems(Main, |counter: Res| { /// // since we assigned the value from the main world in extract /// // we see that value instead of 100 /// assert_eq!(counter.0, 10); @@ -166,7 +164,7 @@ impl SubApp { pub fn run(&mut self) { self.app .world - .run_schedule_ref(&*self.app.outer_schedule_label); + .run_schedule_ref(&*self.app.main_schedule_label); self.app.world.clear_trackers(); } @@ -192,8 +190,7 @@ impl Default for App { #[cfg(feature = "bevy_reflect")] app.init_resource::(); - app.add_default_schedules(); - + app.add_plugin(MainSchedulePlugin); app.add_event::(); #[cfg(feature = "bevy_ci_testing")] @@ -208,8 +205,6 @@ impl Default for App { impl App { /// Creates a new [`App`] with some default structure to enable core engine features. /// This is the preferred constructor for most use cases. - /// - /// This calls [`App::add_default_schedules`]. pub fn new() -> App { App::default() } @@ -226,9 +221,8 @@ impl App { sub_apps: HashMap::default(), plugin_registry: Vec::default(), plugin_name_added: Default::default(), - default_schedule_label: Box::new(CoreSchedule::Main), - outer_schedule_label: Box::new(CoreSchedule::Outer), - is_building_plugin: false, + main_schedule_label: Box::new(Main), + building_plugin_depth: 0, } } @@ -237,9 +231,8 @@ impl App { /// This method also updates sub apps. /// See [`insert_sub_app`](Self::insert_sub_app) for more details. /// - /// The schedule run by this method is determined by the [`outer_schedule_label`](App) field. - /// In normal usage, this is [`CoreSchedule::Outer`], which will run [`CoreSchedule::Startup`] - /// the first time the app is run, then [`CoreSchedule::Main`] on every call of this method. + /// The schedule run by this method is determined by the [`main_schedule_label`](App) field. + /// By default this is [`Main`]. /// /// # Panics /// @@ -248,7 +241,7 @@ impl App { { #[cfg(feature = "trace")] let _bevy_frame_update_span = info_span!("main app").entered(); - self.world.run_schedule_ref(&*self.outer_schedule_label); + self.world.run_schedule_ref(&*self.main_schedule_label); } for (_label, sub_app) in self.sub_apps.iter_mut() { #[cfg(feature = "trace")] @@ -291,8 +284,8 @@ impl App { let _bevy_app_run_span = info_span!("bevy_app").entered(); let mut app = std::mem::replace(self, App::empty()); - if app.is_building_plugin { - panic!("App::run() was called from within Plugin::Build(), which is not allowed."); + if app.building_plugin_depth > 0 { + panic!("App::run() was called from within Plugin::build(), which is not allowed."); } Self::setup(&mut app); @@ -313,14 +306,14 @@ impl App { } /// Adds [`State`] and [`NextState`] resources, [`OnEnter`] and [`OnExit`] schedules - /// for each state variant, an instance of [`apply_state_transition::`] in - /// [`CoreSet::StateTransitions`] so that transitions happen before [`CoreSet::Update`] and - /// a instance of [`run_enter_schedule::`] in [`CoreSet::StateTransitions`] with a + /// for each state variant (if they don't already exist), an instance of [`apply_state_transition::`] in + /// [`StateTransition`] so that transitions happen before [`Update`] and + /// a instance of [`run_enter_schedule::`] in [`StateTransition`] with a /// [`run_once`](`run_once_condition`) condition to run the on enter schedule of the /// initial state. /// /// This also adds an [`OnUpdate`] system set for each state variant, - /// which runs during [`CoreSet::Update`] after the transitions are applied. + /// which runs during [`Update`] after the transitions are applied. /// These system sets only run if the [`State`] resource matches the respective state variant. /// /// If you would like to control how other systems run based on the current state, @@ -329,38 +322,24 @@ impl App { /// Note that you can also apply state transitions at other points in the schedule /// by adding the [`apply_state_transition`] system manually. pub fn add_state(&mut self) -> &mut Self { - self.init_resource::>(); - self.init_resource::>(); - - let mut schedules = self.world.resource_mut::(); - - let Some(default_schedule) = schedules.get_mut(&*self.default_schedule_label) else { - let schedule_label = &self.default_schedule_label; - panic!("Default schedule {schedule_label:?} does not exist.") - }; - - default_schedule.add_systems( - ( - run_enter_schedule::.run_if(run_once_condition()), - apply_state_transition::, - ) - .chain() - .in_base_set(CoreSet::StateTransitions), - ); - - for variant in S::variants() { - default_schedule.configure_set( - OnUpdate(variant.clone()) - .in_base_set(CoreSet::Update) - .run_if(in_state(variant)), + self.init_resource::>() + .init_resource::>() + .add_systems( + StateTransition, + ( + run_enter_schedule::.run_if(run_once_condition()), + apply_state_transition::, + ) + .chain(), ); + + for variant in S::variants() { + self.configure_set(Update, OnUpdate(variant.clone()).run_if(in_state(variant))); } - // These are different for loops to avoid conflicting access to self - for variant in S::variants() { - self.add_schedule(OnEnter(variant.clone()), Schedule::new()); - self.add_schedule(OnExit(variant), Schedule::new()); - } + // The OnEnter, OnExit, and OnTransition schedules are lazily initialized + // (i.e. when the first system is added to them), and World::try_run_schedule is used to fail + // gracefully if they aren't present. self } @@ -381,28 +360,15 @@ impl App { /// # /// app.add_system(my_system); /// ``` - pub fn add_system(&mut self, system: impl IntoSystemAppConfig) -> &mut Self { - let mut schedules = self.world.resource_mut::(); - - let SystemAppConfig { system, schedule } = system.into_app_config(); - - if let Some(schedule_label) = schedule { - if let Some(schedule) = schedules.get_mut(&*schedule_label) { - schedule.add_system(system); - } else { - panic!("Schedule {schedule_label:?} does not exist.") - } - } else if let Some(default_schedule) = schedules.get_mut(&*self.default_schedule_label) { - default_schedule.add_system(system); - } else { - let schedule_label = &self.default_schedule_label; - panic!("Default schedule {schedule_label:?} does not exist.") - } - - self + #[deprecated( + since = "0.11.0", + note = "Please use `add_systems` instead. If you didn't change the default base set, you should use `add_systems(Update, your_system).`" + )] + pub fn add_system(&mut self, system: impl IntoSystemConfigs) -> &mut Self { + self.add_systems(Update, system) } - /// Adds a system to the default system set and schedule of the app's [`Schedules`]. + /// Adds a system to the given schedule in this app's [`Schedules`]. /// /// # Examples /// @@ -415,36 +381,27 @@ impl App { /// # fn system_b() {} /// # fn system_c() {} /// # - /// app.add_systems((system_a, system_b, system_c)); + /// app.add_systems(Update, (system_a, system_b, system_c)); /// ``` - pub fn add_systems(&mut self, systems: impl IntoSystemAppConfigs) -> &mut Self { + pub fn add_systems( + &mut self, + schedule: impl ScheduleLabel, + systems: impl IntoSystemConfigs, + ) -> &mut Self { let mut schedules = self.world.resource_mut::(); - match systems.into_app_configs().0 { - crate::InnerConfigs::Blanket { systems, schedule } => { - let schedule = if let Some(label) = schedule { - schedules - .get_mut(&*label) - .unwrap_or_else(|| panic!("Schedule '{label:?}' does not exist.")) - } else { - let label = &*self.default_schedule_label; - schedules - .get_mut(label) - .unwrap_or_else(|| panic!("Default schedule '{label:?}' does not exist.")) - }; - schedule.add_systems(systems); - } - crate::InnerConfigs::Granular(systems) => { - for system in systems { - self.add_system(system); - } - } + if let Some(schedule) = schedules.get_mut(&schedule) { + schedule.add_systems(systems); + } else { + let mut new_schedule = Schedule::new(); + new_schedule.add_systems(systems); + schedules.insert(schedule, new_schedule); } self } - /// Adds a system to [`CoreSchedule::Startup`]. + /// Adds a system to [`Startup`]. /// /// These systems will run exactly once, at the start of the [`App`]'s lifecycle. /// To add a system that runs every frame, see [`add_system`](Self::add_system). @@ -460,13 +417,17 @@ impl App { /// } /// /// App::new() - /// .add_startup_system(my_startup_system); + /// .add_systems(Startup, my_startup_system); /// ``` - pub fn add_startup_system(&mut self, system: impl IntoSystemConfig) -> &mut Self { - self.add_system(system.in_schedule(CoreSchedule::Startup)) + #[deprecated( + since = "0.11.0", + note = "Please use `add_systems` instead. If you didn't change the default base set, you should use `add_systems(Startup, your_system).`" + )] + pub fn add_startup_system(&mut self, system: impl IntoSystemConfigs) -> &mut Self { + self.add_systems(Startup, system) } - /// Adds a collection of systems to [`CoreSchedule::Startup`]. + /// Adds a collection of systems to [`Startup`]. /// /// # Examples /// @@ -479,88 +440,58 @@ impl App { /// # fn startup_system_b() {} /// # fn startup_system_c() {} /// # - /// app.add_startup_systems(( + /// app.add_systems(Startup, ( /// startup_system_a, /// startup_system_b, /// startup_system_c, /// )); /// ``` + #[deprecated( + since = "0.11.0", + note = "Please use `add_systems` instead. If you didn't change the default base set, you should use `add_systems(Startup, your_system).`" + )] pub fn add_startup_systems(&mut self, systems: impl IntoSystemConfigs) -> &mut Self { - self.add_systems(systems.into_configs().in_schedule(CoreSchedule::Startup)) + self.add_systems(Startup, systems.into_configs()) } /// Configures a system set in the default schedule, adding the set if it does not exist. - pub fn configure_set(&mut self, set: impl IntoSystemSetConfig) -> &mut Self { - self.world - .resource_mut::() - .get_mut(&*self.default_schedule_label) - .unwrap() - .configure_set(set); + pub fn configure_set( + &mut self, + schedule: impl ScheduleLabel, + set: impl IntoSystemSetConfig, + ) -> &mut Self { + let mut schedules = self.world.resource_mut::(); + if let Some(schedule) = schedules.get_mut(&schedule) { + schedule.configure_set(set); + } else { + let mut new_schedule = Schedule::new(); + new_schedule.configure_set(set); + schedules.insert(schedule, new_schedule); + } self } /// Configures a collection of system sets in the default schedule, adding any sets that do not exist. - pub fn configure_sets(&mut self, sets: impl IntoSystemSetConfigs) -> &mut Self { - self.world - .resource_mut::() - .get_mut(&*self.default_schedule_label) - .unwrap() - .configure_sets(sets); - self - } - - /// Adds standardized schedules and labels to an [`App`]. - /// - /// Adding these schedules is necessary to make almost all core engine features work. - /// This is typically done implicitly by calling `App::default`, which is in turn called by - /// [`App::new`]. - /// - /// The schedules added are defined in the [`CoreSchedule`] enum, - /// and have a starting configuration defined by: - /// - /// - [`CoreSchedule::Outer`]: uses [`CoreSchedule::outer_schedule`] - /// - [`CoreSchedule::Startup`]: uses [`StartupSet::base_schedule`] - /// - [`CoreSchedule::Main`]: uses [`CoreSet::base_schedule`] - /// - [`CoreSchedule::FixedUpdate`]: no starting configuration - /// - /// # Examples - /// - /// ``` - /// use bevy_app::App; - /// use bevy_ecs::schedule::Schedules; - /// - /// let app = App::empty() - /// .init_resource::() - /// .add_default_schedules() - /// .update(); - /// ``` - pub fn add_default_schedules(&mut self) -> &mut Self { - self.add_schedule(CoreSchedule::Outer, CoreSchedule::outer_schedule()); - self.add_schedule(CoreSchedule::Startup, StartupSet::base_schedule()); - self.add_schedule(CoreSchedule::Main, CoreSet::base_schedule()); - self.init_schedule(CoreSchedule::FixedUpdate); - - self - } - - /// adds a single threaded outer schedule to the [`App`] that just runs the main schedule - pub fn add_simple_outer_schedule(&mut self) -> &mut Self { - fn run_main_schedule(world: &mut World) { - world.run_schedule(CoreSchedule::Main); + pub fn configure_sets( + &mut self, + schedule: impl ScheduleLabel, + sets: impl IntoSystemSetConfigs, + ) -> &mut Self { + let mut schedules = self.world.resource_mut::(); + if let Some(schedule) = schedules.get_mut(&schedule) { + schedule.configure_sets(sets); + } else { + let mut new_schedule = Schedule::new(); + new_schedule.configure_sets(sets); + schedules.insert(schedule, new_schedule); } - - self.edit_schedule(CoreSchedule::Outer, |schedule| { - schedule.set_executor_kind(bevy_ecs::schedule::ExecutorKind::SingleThreaded); - schedule.add_system(run_main_schedule); - }); - self } /// Setup the application to manage events of type `T`. /// /// This is done by adding a [`Resource`] of type [`Events::`], - /// and inserting an [`update_system`](Events::update_system) into [`CoreSet::First`]. + /// and inserting an [`update_system`](Events::update_system) into [`First`]. /// /// See [`Events`] for defining events. /// @@ -581,7 +512,7 @@ impl App { { if !self.world.contains_resource::>() { self.init_resource::>() - .add_system(Events::::update_system.in_base_set(CoreSet::First)); + .add_systems(First, Events::::update_system); } self } @@ -760,9 +691,12 @@ impl App { plugin_name: plugin.name().to_string(), })?; } - self.is_building_plugin = true; - plugin.build(self); - self.is_building_plugin = false; + self.building_plugin_depth += 1; + let result = catch_unwind(AssertUnwindSafe(|| plugin.build(self))); + self.building_plugin_depth -= 1; + if let Err(payload) = result { + resume_unwind(payload); + } self.plugin_registry.push(plugin); Ok(self) } @@ -1014,6 +948,11 @@ pub struct AppExit; #[cfg(test)] mod tests { + use bevy_ecs::{ + schedule::{OnEnter, States}, + system::Commands, + }; + use crate::{App, Plugin}; struct PluginA; @@ -1061,11 +1000,48 @@ mod tests { #[should_panic] fn cant_call_app_run_from_plugin_build() { struct PluginRun; + struct InnerPlugin; + impl Plugin for InnerPlugin { + fn build(&self, _: &mut crate::App) {} + } impl Plugin for PluginRun { fn build(&self, app: &mut crate::App) { - app.run(); + app.add_plugin(InnerPlugin).run(); } } App::new().add_plugin(PluginRun); } + + #[derive(States, PartialEq, Eq, Debug, Default, Hash, Clone)] + enum AppState { + #[default] + MainMenu, + } + fn bar(mut commands: Commands) { + commands.spawn_empty(); + } + + fn foo(mut commands: Commands) { + commands.spawn_empty(); + } + + #[test] + fn add_systems_should_create_schedule_if_it_does_not_exist() { + let mut app = App::new(); + app.add_state::() + .add_systems(OnEnter(AppState::MainMenu), (foo, bar)); + + app.world.run_schedule(OnEnter(AppState::MainMenu)); + assert_eq!(app.world.entities().len(), 2); + } + + #[test] + fn add_systems_should_create_schedule_if_it_does_not_exist2() { + let mut app = App::new(); + app.add_systems(OnEnter(AppState::MainMenu), (foo, bar)) + .add_state::(); + + app.world.run_schedule(OnEnter(AppState::MainMenu)); + assert_eq!(app.world.entities().len(), 2); + } } diff --git a/crates/bevy_app/src/ci_testing.rs b/crates/bevy_app/src/ci_testing.rs index 17d8d929d6..f662a350a4 100644 --- a/crates/bevy_app/src/ci_testing.rs +++ b/crates/bevy_app/src/ci_testing.rs @@ -1,4 +1,4 @@ -use crate::{app::AppExit, App}; +use crate::{app::AppExit, App, Update}; use serde::Deserialize; use bevy_ecs::prelude::Resource; @@ -47,7 +47,7 @@ pub(crate) fn setup_app(app: &mut App) -> &mut App { }; app.insert_resource(config) - .add_system(ci_testing_exit_after); + .add_systems(Update, ci_testing_exit_after); app } diff --git a/crates/bevy_app/src/config.rs b/crates/bevy_app/src/config.rs deleted file mode 100644 index 3a5f30a504..0000000000 --- a/crates/bevy_app/src/config.rs +++ /dev/null @@ -1,291 +0,0 @@ -use bevy_ecs::{ - all_tuples, - schedule::{ - BaseSystemSet, BoxedScheduleLabel, Condition, FreeSystemSet, IntoSystemConfig, - IntoSystemSet, ScheduleLabel, SystemConfig, SystemConfigs, - }, -}; - -use crate::CoreSchedule; - -/// A [`System`] with [`App`]-aware scheduling metadata. -/// -/// [`System`]: bevy_ecs::prelude::System -/// [`App`]: crate::App -pub struct SystemAppConfig { - pub(crate) system: SystemConfig, - pub(crate) schedule: Option, -} - -/// Types that can be converted into a [`SystemAppConfig`]. -/// -/// This has been implemented for all `System` trait objects -/// and all functions that convert into such. -pub trait IntoSystemAppConfig: Sized { - /// Converts into a [`SystemAppConfig`]. - fn into_app_config(self) -> SystemAppConfig; - - /// Adds the system to the provided `schedule`. - /// - /// If a schedule is not specified, it will be added to the [`App`]'s default schedule. - /// - /// [`App`]: crate::App - /// - /// # Panics - /// - /// If the system has already been assigned to a schedule. - #[track_caller] - fn in_schedule(self, schedule: impl ScheduleLabel) -> SystemAppConfig { - let mut config = self.into_app_config(); - if let Some(old_schedule) = &config.schedule { - panic!( - "Cannot add system to schedule '{schedule:?}': it is already in '{old_schedule:?}'." - ); - } - config.schedule = Some(Box::new(schedule)); - - config - } - - /// Adds the system to [`CoreSchedule::Startup`]. - /// This is a shorthand for `self.in_schedule(CoreSchedule::Startup)`. - /// - /// Systems in this schedule will run exactly once, at the start of the [`App`]'s lifecycle. - /// - /// [`App`]: crate::App - /// - /// # Examples - /// - /// ``` - /// # use bevy_app::prelude::*; - /// # use bevy_ecs::prelude::*; - /// # - /// fn my_startup_system(_commands: Commands) { - /// println!("My startup system"); - /// } - /// - /// App::new() - /// .add_system(my_startup_system.on_startup()) - /// .run(); - /// ``` - /// - /// # Panics - /// - /// If the system has already been assigned to a schedule. - #[inline] - fn on_startup(self) -> SystemAppConfig { - self.in_schedule(CoreSchedule::Startup) - } -} - -impl IntoSystemConfig<(), Self> for SystemAppConfig { - fn into_config(self) -> Self { - self - } - - #[track_caller] - fn in_set(self, set: impl FreeSystemSet) -> Self { - let Self { system, schedule } = self; - Self { - system: system.in_set(set), - schedule, - } - } - - #[track_caller] - fn in_base_set(self, set: impl BaseSystemSet) -> Self { - let Self { system, schedule } = self; - Self { - system: system.in_base_set(set), - schedule, - } - } - - fn no_default_base_set(self) -> Self { - let Self { system, schedule } = self; - Self { - system: system.no_default_base_set(), - schedule, - } - } - - fn before(self, set: impl IntoSystemSet) -> Self { - let Self { system, schedule } = self; - Self { - system: system.before(set), - schedule, - } - } - - fn after(self, set: impl IntoSystemSet) -> Self { - let Self { system, schedule } = self; - Self { - system: system.after(set), - schedule, - } - } - - fn run_if

(self, condition: impl Condition

) -> Self { - let Self { system, schedule } = self; - Self { - system: system.run_if(condition), - schedule, - } - } - - fn ambiguous_with(self, set: impl IntoSystemSet) -> Self { - let Self { system, schedule } = self; - Self { - system: system.ambiguous_with(set), - schedule, - } - } - - fn ambiguous_with_all(self) -> Self { - let Self { system, schedule } = self; - Self { - system: system.ambiguous_with_all(), - schedule, - } - } -} - -impl IntoSystemAppConfig<()> for SystemAppConfig { - fn into_app_config(self) -> SystemAppConfig { - self - } -} - -impl IntoSystemAppConfig for T -where - T: IntoSystemConfig, -{ - fn into_app_config(self) -> SystemAppConfig { - SystemAppConfig { - system: self.into_config(), - schedule: None, - } - } -} - -/// A collection of [`SystemAppConfig`]s. -pub struct SystemAppConfigs(pub(crate) InnerConfigs); - -pub(crate) enum InnerConfigs { - /// This came from an instance of `SystemConfigs`. - /// All systems are in the same schedule. - Blanket { - systems: SystemConfigs, - schedule: Option, - }, - /// This came from several separate instances of `SystemAppConfig`. - /// Each system gets its own schedule. - Granular(Vec), -} - -/// Types that can convert into [`SystemAppConfigs`]. -pub trait IntoSystemAppConfigs: Sized { - /// Converts to [`SystemAppConfigs`]. - fn into_app_configs(self) -> SystemAppConfigs; - - /// Adds the systems to the provided `schedule`. - /// - /// If a schedule is not specified, they will be added to the [`App`]'s default schedule. - /// - /// [`App`]: crate::App - /// - /// # Panics - /// - /// If any of the systems have already been assigned to a schedule. - #[track_caller] - fn in_schedule(self, label: impl ScheduleLabel) -> SystemAppConfigs { - let mut configs = self.into_app_configs(); - - match &mut configs.0 { - InnerConfigs::Blanket { schedule, .. } => { - if schedule.is_some() { - panic!( - "Cannot add systems to the schedule '{label:?}: they are already in '{schedule:?}'" - ); - } - *schedule = Some(Box::new(label)); - } - InnerConfigs::Granular(configs) => { - for SystemAppConfig { schedule, .. } in configs { - if schedule.is_some() { - panic!( - "Cannot add system to the schedule '{label:?}': it is already in '{schedule:?}'." - ); - } - *schedule = Some(label.dyn_clone()); - } - } - } - - configs - } - - /// Adds the systems to [`CoreSchedule::Startup`]. - /// This is a shorthand for `self.in_schedule(CoreSchedule::Startup)`. - /// - /// # Examples - /// - /// ``` - /// # use bevy_app::prelude::*; - /// # use bevy_ecs::prelude::*; - /// # - /// # let mut app = App::new(); - /// # fn startup_system_a() {} - /// # fn startup_system_b() {} - /// # fn startup_system_c() {} - /// # - /// app.add_systems( - /// ( - /// startup_system_a, - /// startup_system_b, - /// startup_system_c, - /// ) - /// .on_startup() - /// ); - /// ``` - /// - /// # Panics - /// - /// If any of the systems have already been assigned to a schedule. - #[track_caller] - fn on_startup(self) -> SystemAppConfigs { - self.in_schedule(CoreSchedule::Startup) - } -} - -impl IntoSystemAppConfigs<()> for SystemAppConfigs { - fn into_app_configs(self) -> SystemAppConfigs { - self - } -} - -impl IntoSystemAppConfigs<()> for SystemConfigs { - fn into_app_configs(self) -> SystemAppConfigs { - SystemAppConfigs(InnerConfigs::Blanket { - systems: self, - schedule: None, - }) - } -} - -macro_rules! impl_system_collection { - ($(($param: ident, $sys: ident)),*) => { - impl<$($param, $sys),*> IntoSystemAppConfigs<($($param,)*)> for ($($sys,)*) - where - $($sys: IntoSystemAppConfig<$param>),* - { - #[allow(non_snake_case)] - fn into_app_configs(self) -> SystemAppConfigs { - let ($($sys,)*) = self; - SystemAppConfigs(InnerConfigs::Granular(vec![$($sys.into_app_config(),)*])) - } - } - } -} - -all_tuples!(impl_system_collection, 0, 15, P, S); diff --git a/crates/bevy_app/src/lib.rs b/crates/bevy_app/src/lib.rs index 8348a7c16e..8094d0458e 100644 --- a/crates/bevy_app/src/lib.rs +++ b/crates/bevy_app/src/lib.rs @@ -3,7 +3,7 @@ #![warn(missing_docs)] mod app; -mod config; +mod main_schedule; mod plugin; mod plugin_group; mod schedule_runner; @@ -13,7 +13,7 @@ mod ci_testing; pub use app::*; pub use bevy_derive::DynamicPlugin; -pub use config::*; +pub use main_schedule::*; pub use plugin::*; pub use plugin_group::*; pub use schedule_runner::*; @@ -26,185 +26,10 @@ pub mod prelude { #[doc(hidden)] pub use crate::{ app::App, - config::{IntoSystemAppConfig, IntoSystemAppConfigs}, - CoreSchedule, CoreSet, DynamicPlugin, Plugin, PluginGroup, StartupSet, + main_schedule::{ + First, FixedUpdate, Last, Main, PostStartup, PostUpdate, PreStartup, PreUpdate, + Startup, StateTransition, Update, + }, + DynamicPlugin, Plugin, PluginGroup, }; } - -use bevy_ecs::{ - schedule::{ - apply_system_buffers, IntoSystemConfig, IntoSystemSetConfig, IntoSystemSetConfigs, - Schedule, ScheduleLabel, SystemSet, - }, - system::Local, - world::World, -}; - -/// The names of the default [`App`] schedules. -/// -/// The corresponding [`Schedule`](bevy_ecs::schedule::Schedule) objects are added by [`App::add_default_schedules`]. -#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] -pub enum CoreSchedule { - /// The schedule that runs once when the app starts. - Startup, - /// The schedule that contains the app logic that is evaluated each tick of [`App::update()`]. - Main, - /// The schedule that controls which schedules run. - /// - /// This is typically created using the [`CoreSchedule::outer_schedule`] method, - /// and does not need to manipulated during ordinary use. - Outer, - /// The schedule that contains systems which only run after a fixed period of time has elapsed. - /// - /// The exclusive `run_fixed_update_schedule` system runs this schedule during the [`CoreSet::FixedUpdate`] system set. - FixedUpdate, -} - -impl CoreSchedule { - /// An exclusive system that controls which schedule should be running. - /// - /// [`CoreSchedule::Main`] is always run. - /// - /// If this is the first time this system has been run, [`CoreSchedule::Startup`] will run before [`CoreSchedule::Main`]. - pub fn outer_loop(world: &mut World, mut run_at_least_once: Local) { - if !*run_at_least_once { - world.run_schedule(CoreSchedule::Startup); - *run_at_least_once = true; - } - - world.run_schedule(CoreSchedule::Main); - } - - /// Initializes a single threaded schedule for [`CoreSchedule::Outer`] that contains the [`outer_loop`](CoreSchedule::outer_loop) system. - pub fn outer_schedule() -> Schedule { - let mut schedule = Schedule::new(); - schedule.set_executor_kind(bevy_ecs::schedule::ExecutorKind::SingleThreaded); - schedule.add_system(Self::outer_loop); - schedule - } -} - -/// The names of the default [`App`] system sets. -/// -/// These are ordered in the same order they are listed. -/// -/// The corresponding [`SystemSets`](bevy_ecs::schedule::SystemSet) are added by [`App::add_default_schedules`]. -/// -/// The `*Flush` sets are assigned to the copy of [`apply_system_buffers`] -/// that runs immediately after the matching system set. -/// These can be useful for ordering, but you almost never want to add your systems to these sets. -#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] -#[system_set(base)] -pub enum CoreSet { - /// Runs before all other members of this set. - First, - /// The copy of [`apply_system_buffers`] that runs immediately after `First`. - FirstFlush, - /// Runs before [`CoreSet::Update`]. - PreUpdate, - /// The copy of [`apply_system_buffers`] that runs immediately after `PreUpdate`. - PreUpdateFlush, - /// Applies [`State`](bevy_ecs::schedule::State) transitions - StateTransitions, - /// Runs systems that should only occur after a fixed period of time. - /// - /// The `run_fixed_update_schedule` system runs the [`CoreSchedule::FixedUpdate`] system in this system set. - FixedUpdate, - /// Responsible for doing most app logic. Systems should be registered here by default. - Update, - /// The copy of [`apply_system_buffers`] that runs immediately after `Update`. - UpdateFlush, - /// Runs after [`CoreSet::Update`]. - PostUpdate, - /// The copy of [`apply_system_buffers`] that runs immediately after `PostUpdate`. - PostUpdateFlush, - /// Runs after all other members of this set. - Last, - /// The copy of [`apply_system_buffers`] that runs immediately after `Last`. - LastFlush, -} - -impl CoreSet { - /// Sets up the base structure of [`CoreSchedule::Main`]. - /// - /// The sets defined in this enum are configured to run in order, - /// and a copy of [`apply_system_buffers`] is inserted into each `*Flush` set. - pub fn base_schedule() -> Schedule { - use CoreSet::*; - let mut schedule = Schedule::new(); - - // Create "stage-like" structure using buffer flushes + ordering - schedule - .set_default_base_set(Update) - .add_system(apply_system_buffers.in_base_set(FirstFlush)) - .add_system(apply_system_buffers.in_base_set(PreUpdateFlush)) - .add_system(apply_system_buffers.in_base_set(UpdateFlush)) - .add_system(apply_system_buffers.in_base_set(PostUpdateFlush)) - .add_system(apply_system_buffers.in_base_set(LastFlush)) - .configure_sets( - ( - First, - FirstFlush, - PreUpdate, - PreUpdateFlush, - StateTransitions, - FixedUpdate, - Update, - UpdateFlush, - PostUpdate, - PostUpdateFlush, - Last, - LastFlush, - ) - .chain(), - ); - schedule - } -} - -/// The names of the default [`App`] startup sets, which live in [`CoreSchedule::Startup`]. -/// -/// The corresponding [`SystemSets`](bevy_ecs::schedule::SystemSet) are added by [`App::add_default_schedules`]. -/// -/// The `*Flush` sets are assigned to the copy of [`apply_system_buffers`] -/// that runs immediately after the matching system set. -/// These can be useful for ordering, but you almost never want to add your systems to these sets. -#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] -#[system_set(base)] -pub enum StartupSet { - /// Runs once before [`StartupSet::Startup`]. - PreStartup, - /// The copy of [`apply_system_buffers`] that runs immediately after `PreStartup`. - PreStartupFlush, - /// Runs once when an [`App`] starts up. - Startup, - /// The copy of [`apply_system_buffers`] that runs immediately after `Startup`. - StartupFlush, - /// Runs once after [`StartupSet::Startup`]. - PostStartup, - /// The copy of [`apply_system_buffers`] that runs immediately after `PostStartup`. - PostStartupFlush, -} - -impl StartupSet { - /// Sets up the base structure of [`CoreSchedule::Startup`]. - /// - /// The sets defined in this enum are configured to run in order, - /// and a copy of [`apply_system_buffers`] is inserted into each `*Flush` set. - pub fn base_schedule() -> Schedule { - use StartupSet::*; - let mut schedule = Schedule::new(); - schedule.set_default_base_set(Startup); - - // Create "stage-like" structure using buffer flushes + ordering - schedule.add_system(apply_system_buffers.in_base_set(PreStartupFlush)); - schedule.add_system(apply_system_buffers.in_base_set(StartupFlush)); - schedule.add_system(apply_system_buffers.in_base_set(PostStartupFlush)); - - schedule.configure_set(PreStartup.before(PreStartupFlush)); - schedule.configure_set(Startup.after(PreStartupFlush).before(StartupFlush)); - schedule.configure_set(PostStartup.after(StartupFlush).before(PostStartupFlush)); - - schedule - } -} diff --git a/crates/bevy_app/src/main_schedule.rs b/crates/bevy_app/src/main_schedule.rs new file mode 100644 index 0000000000..2042b9e01f --- /dev/null +++ b/crates/bevy_app/src/main_schedule.rs @@ -0,0 +1,168 @@ +use crate::{App, Plugin}; +use bevy_ecs::{ + schedule::{ExecutorKind, Schedule, ScheduleLabel}, + system::{Local, Resource}, + world::{Mut, World}, +}; + +/// The schedule that contains the app logic that is evaluated each tick of [`App::update()`]. +/// +/// By default, it will run the following schedules in the given order: +/// +/// On the first run of the schedule (and only on the first run), it will run: +/// * [`PreStartup`] +/// * [`Startup`] +/// * [`PostStartup`] +/// +/// Then it will run: +/// * [`First`] +/// * [`PreUpdate`] +/// * [`StateTransition`] +/// * [`RunFixedUpdateLoop`] +/// * This will run [`FixedUpdate`] zero to many times, based on how much time has elapsed. +/// * [`Update`] +/// * [`PostUpdate`] +/// * [`Last`] +#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] +pub struct Main; + +/// The schedule that runs before [`Startup`]. +/// This is run by the [`Main`] schedule. +#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] +pub struct PreStartup; + +/// The schedule that runs once when the app starts. +/// This is run by the [`Main`] schedule. +#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] +pub struct Startup; + +/// The schedule that runs once after [`Startup`]. +/// This is run by the [`Main`] schedule. +#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] +pub struct PostStartup; + +/// Runs first in the schedule. +/// This is run by the [`Main`] schedule. +#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] +pub struct First; + +/// The schedule that contains logic that must run before [`Update`]. For example, a system that reads raw keyboard +/// input OS events into an `Events` resource. This enables systems in [`Update`] to consume the events from the `Events` +/// resource without actually knowing about (or taking a direct scheduler dependency on) the "os-level keyboard event sytsem". +/// +/// [`PreUpdate`] exists to do "engine/plugin preparation work" that ensures the APIs consumed in [`Update`] are "ready". +/// [`PreUpdate`] abstracts out "pre work implementation details". +/// +/// This is run by the [`Main`] schedule. +#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] +pub struct PreUpdate; + +/// Runs [state transitions](bevy_ecs::schedule::States). +/// This is run by the [`Main`] schedule. +#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] +pub struct StateTransition; + +/// Runs the [`FixedUpdate`] schedule in a loop according until all relevant elapsed time has been "consumed". +/// This is run by the [`Main`] schedule. +#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] +pub struct RunFixedUpdateLoop; + +/// The schedule that contains systems which only run after a fixed period of time has elapsed. +/// +/// The exclusive `run_fixed_update_schedule` system runs this schedule. +/// This is run by the [`RunFixedUpdateLoop`] schedule. +#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] +pub struct FixedUpdate; + +/// The schedule that contains app logic. +/// This is run by the [`Main`] schedule. +#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] +pub struct Update; + +/// The schedule that contains logic that must run after [`Update`]. For example, synchronizing "local transforms" in a hierarchy +/// to "global" absolute transforms. This enables the [`PostUpdate`] transform-sync system to react to "local transform" changes in +/// [`Update`] without the [`Update`] systems needing to know about (or add scheduler dependencies for) the "global transform sync system". +/// +/// [`PostUpdate`] exists to do "engine/plugin response work" to things that happened in [`Update`]. +/// [`PostUpdate`] abstracts out "implementation details" from users defining systems in [`Update`]. +/// +/// This is run by the [`Main`] schedule. +#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] +pub struct PostUpdate; + +/// Runs last in the schedule. +/// This is run by the [`Main`] schedule. +#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] +pub struct Last; + +/// Defines the schedules to be run for the [`Main`] schedule, including +/// their order. +#[derive(Resource, Debug)] +pub struct MainScheduleOrder { + /// The labels to run for the [`Main`] schedule (in the order they will be run). + pub labels: Vec>, +} + +impl Default for MainScheduleOrder { + fn default() -> Self { + Self { + labels: vec![ + Box::new(First), + Box::new(PreUpdate), + Box::new(StateTransition), + Box::new(RunFixedUpdateLoop), + Box::new(Update), + Box::new(PostUpdate), + Box::new(Last), + ], + } + } +} + +impl MainScheduleOrder { + /// Adds the given `schedule` after the `after` schedule + pub fn insert_after(&mut self, after: impl ScheduleLabel, schedule: impl ScheduleLabel) { + let index = self + .labels + .iter() + .position(|current| (**current).eq(&after)) + .unwrap_or_else(|| panic!("Expected {after:?} to exist")); + self.labels.insert(index + 1, Box::new(schedule)); + } +} + +impl Main { + /// A system that runs the "main schedule" + pub fn run_main(world: &mut World, mut run_at_least_once: Local) { + if !*run_at_least_once { + let _ = world.try_run_schedule(PreStartup); + let _ = world.try_run_schedule(Startup); + let _ = world.try_run_schedule(PostStartup); + *run_at_least_once = true; + } + + world.resource_scope(|world, order: Mut| { + for label in &order.labels { + let _ = world.try_run_schedule_ref(&**label); + } + }); + } +} + +/// Initializes the [`Main`] schedule, sub schedules, and resources for a given [`App`]. +pub struct MainSchedulePlugin; + +impl Plugin for MainSchedulePlugin { + fn build(&self, app: &mut App) { + // simple "facilitator" schedules benefit from simpler single threaded scheduling + let mut main_schedule = Schedule::new(); + main_schedule.set_executor_kind(ExecutorKind::SingleThreaded); + let mut fixed_update_loop_schedule = Schedule::new(); + fixed_update_loop_schedule.set_executor_kind(ExecutorKind::SingleThreaded); + + app.add_schedule(Main, main_schedule) + .add_schedule(RunFixedUpdateLoop, fixed_update_loop_schedule) + .init_resource::() + .add_systems(Main, Main::run_main); + } +} diff --git a/crates/bevy_asset/src/asset_server.rs b/crates/bevy_asset/src/asset_server.rs index c7eea2916c..e456c7eb44 100644 --- a/crates/bevy_asset/src/asset_server.rs +++ b/crates/bevy_asset/src/asset_server.rs @@ -644,7 +644,7 @@ pub fn free_unused_assets_system(asset_server: Res) { mod test { use super::*; use crate::{loader::LoadedAsset, update_asset_storage_system}; - use bevy_app::App; + use bevy_app::{App, Update}; use bevy_ecs::prelude::*; use bevy_reflect::TypeUuid; use bevy_utils::BoxedFuture; @@ -852,8 +852,13 @@ mod test { let mut app = App::new(); app.insert_resource(assets); app.insert_resource(asset_server); - app.add_system(free_unused_assets_system.in_set(FreeUnusedAssets)); - app.add_system(update_asset_storage_system::.after(FreeUnusedAssets)); + app.add_systems( + Update, + ( + free_unused_assets_system.in_set(FreeUnusedAssets), + update_asset_storage_system::.after(FreeUnusedAssets), + ), + ); fn load_asset(path: AssetPath, world: &World) -> HandleUntyped { let asset_server = world.resource::(); diff --git a/crates/bevy_asset/src/assets.rs b/crates/bevy_asset/src/assets.rs index f0f1a035f6..3fffa1d413 100644 --- a/crates/bevy_asset/src/assets.rs +++ b/crates/bevy_asset/src/assets.rs @@ -1,6 +1,6 @@ use crate::{ - update_asset_storage_system, Asset, AssetLoader, AssetServer, AssetSet, Handle, HandleId, - RefChange, ReflectAsset, ReflectHandle, + update_asset_storage_system, Asset, AssetEvents, AssetLoader, AssetServer, Handle, HandleId, + LoadAssets, RefChange, ReflectAsset, ReflectHandle, }; use bevy_app::{App, AppTypeRegistry}; use bevy_ecs::prelude::*; @@ -331,8 +331,8 @@ impl AddAsset for App { }; self.insert_resource(assets) - .add_system(Assets::::asset_event_system.in_base_set(AssetSet::AssetEvents)) - .add_system(update_asset_storage_system::.in_base_set(AssetSet::LoadAssets)) + .add_systems(LoadAssets, update_asset_storage_system::) + .add_systems(AssetEvents, Assets::::asset_event_system) .register_type::>() .add_event::>() } @@ -360,9 +360,9 @@ impl AddAsset for App { { #[cfg(feature = "debug_asset_server")] { - self.add_system( - crate::debug_asset_server::sync_debug_assets:: - .in_base_set(bevy_app::CoreSet::Update), + self.add_systems( + bevy_app::Update, + crate::debug_asset_server::sync_debug_assets::, ); let mut app = self .world diff --git a/crates/bevy_asset/src/debug_asset_server.rs b/crates/bevy_asset/src/debug_asset_server.rs index 7b506faeed..955fadb80f 100644 --- a/crates/bevy_asset/src/debug_asset_server.rs +++ b/crates/bevy_asset/src/debug_asset_server.rs @@ -2,7 +2,7 @@ //! //! Internal assets (e.g. shaders) are bundled directly into an application and can't be hot //! reloaded using the conventional API. -use bevy_app::{App, Plugin}; +use bevy_app::{App, Plugin, Update}; use bevy_ecs::{prelude::*, system::SystemState}; use bevy_tasks::{IoTaskPool, TaskPoolBuilder}; use bevy_utils::HashMap; @@ -75,7 +75,7 @@ impl Plugin for DebugAssetServerPlugin { watch_for_changes: true, }); app.insert_non_send_resource(DebugAssetApp(debug_asset_app)); - app.add_system(run_debug_asset_app); + app.add_systems(Update, run_debug_asset_app); } } diff --git a/crates/bevy_asset/src/diagnostic/asset_count_diagnostics_plugin.rs b/crates/bevy_asset/src/diagnostic/asset_count_diagnostics_plugin.rs index d36cd2969d..a5ce2546db 100644 --- a/crates/bevy_asset/src/diagnostic/asset_count_diagnostics_plugin.rs +++ b/crates/bevy_asset/src/diagnostic/asset_count_diagnostics_plugin.rs @@ -18,8 +18,8 @@ impl Default for AssetCountDiagnosticsPlugin { impl Plugin for AssetCountDiagnosticsPlugin { fn build(&self, app: &mut App) { - app.add_startup_system(Self::setup_system) - .add_system(Self::diagnostic_system); + app.add_systems(Startup, Self::setup_system) + .add_systems(Update, Self::diagnostic_system); } } diff --git a/crates/bevy_asset/src/io/file_asset_io.rs b/crates/bevy_asset/src/io/file_asset_io.rs index 93c6878c31..61003e2249 100644 --- a/crates/bevy_asset/src/io/file_asset_io.rs +++ b/crates/bevy_asset/src/io/file_asset_io.rs @@ -127,6 +127,7 @@ impl AssetIo for FileAssetIo { to_watch: &Path, to_reload: Option, ) -> Result<(), AssetIoError> { + #![allow(unused_variables)] #[cfg(feature = "filesystem_watcher")] { let to_reload = to_reload.unwrap_or_else(|| to_watch.to_owned()); diff --git a/crates/bevy_asset/src/lib.rs b/crates/bevy_asset/src/lib.rs index 59b0b2cb32..3265690165 100644 --- a/crates/bevy_asset/src/lib.rs +++ b/crates/bevy_asset/src/lib.rs @@ -46,18 +46,15 @@ pub use loader::*; pub use path::*; pub use reflect::*; -use bevy_app::prelude::*; -use bevy_ecs::prelude::*; +use bevy_app::{prelude::*, MainScheduleOrder}; +use bevy_ecs::schedule::ScheduleLabel; -/// [`SystemSet`]s for asset loading in an [`App`] schedule. -#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] -#[system_set(base)] -pub enum AssetSet { - /// Asset storages are updated. - LoadAssets, - /// Asset events are generated. - AssetEvents, -} +/// Asset storages are updated. +#[derive(Debug, Hash, PartialEq, Eq, Clone, ScheduleLabel)] +pub struct LoadAssets; +/// Asset events are generated. +#[derive(Debug, Hash, PartialEq, Eq, Clone, ScheduleLabel)] +pub struct AssetEvents; /// Adds support for [`Assets`] to an App. /// @@ -106,24 +103,19 @@ impl Plugin for AssetPlugin { app.insert_resource(asset_server); } - app.register_type::(); - - app.configure_set( - AssetSet::LoadAssets - .before(CoreSet::PreUpdate) - .after(CoreSet::First), - ) - .configure_set( - AssetSet::AssetEvents - .after(CoreSet::PostUpdate) - .before(CoreSet::Last), - ) - .add_system(asset_server::free_unused_assets_system.in_base_set(CoreSet::PreUpdate)); + app.register_type::() + .add_systems(PreUpdate, asset_server::free_unused_assets_system); + app.init_schedule(LoadAssets); + app.init_schedule(AssetEvents); #[cfg(all( feature = "filesystem_watcher", all(not(target_arch = "wasm32"), not(target_os = "android")) ))] - app.add_system(io::filesystem_watcher_system.in_base_set(AssetSet::LoadAssets)); + app.add_systems(LoadAssets, io::filesystem_watcher_system); + + let mut order = app.world.resource_mut::(); + order.insert_after(First, LoadAssets); + order.insert_after(PostUpdate, AssetEvents); } } diff --git a/crates/bevy_audio/src/lib.rs b/crates/bevy_audio/src/lib.rs index af751a8364..468ff3d622 100644 --- a/crates/bevy_audio/src/lib.rs +++ b/crates/bevy_audio/src/lib.rs @@ -4,13 +4,13 @@ //! # use bevy_ecs::{system::Res, event::EventWriter}; //! # use bevy_audio::{Audio, AudioPlugin}; //! # use bevy_asset::{AssetPlugin, AssetServer}; -//! # use bevy_app::{App, AppExit, NoopPluginGroup as MinimalPlugins}; +//! # use bevy_app::{App, AppExit, NoopPluginGroup as MinimalPlugins, Startup}; //! fn main() { //! App::new() //! .add_plugins(MinimalPlugins) //! .add_plugin(AssetPlugin::default()) //! .add_plugin(AudioPlugin) -//! .add_startup_system(play_background_audio) +//! .add_systems(Startup, play_background_audio) //! .run(); //! } //! @@ -47,7 +47,6 @@ pub use sinks::*; use bevy_app::prelude::*; use bevy_asset::{AddAsset, Asset}; -use bevy_ecs::prelude::*; /// Adds support for audio playback to a Bevy Application /// @@ -62,7 +61,7 @@ impl Plugin for AudioPlugin { .add_asset::() .add_asset::() .init_resource::>() - .add_system(play_queued_audio_system::.in_base_set(CoreSet::PostUpdate)); + .add_systems(PostUpdate, play_queued_audio_system::); #[cfg(any(feature = "mp3", feature = "flac", feature = "wav", feature = "vorbis"))] app.init_asset_loader::(); @@ -78,6 +77,6 @@ impl AddAudioSource for App { self.add_asset::() .init_resource::>() .init_resource::>() - .add_system(play_queued_audio_system::.in_base_set(CoreSet::PostUpdate)) + .add_systems(PostUpdate, play_queued_audio_system::) } } diff --git a/crates/bevy_core/src/lib.rs b/crates/bevy_core/src/lib.rs index 7e58685c9f..daf43cf86e 100644 --- a/crates/bevy_core/src/lib.rs +++ b/crates/bevy_core/src/lib.rs @@ -102,12 +102,12 @@ pub struct TaskPoolPlugin { } impl Plugin for TaskPoolPlugin { - fn build(&self, app: &mut App) { + fn build(&self, _app: &mut App) { // Setup the default bevy task pools self.task_pool_options.create_default_pools(); #[cfg(not(target_arch = "wasm32"))] - app.add_system(tick_global_task_pools.in_base_set(bevy_app::CoreSet::Last)); + _app.add_systems(Last, tick_global_task_pools); } } /// A dummy type that is [`!Send`](Send), to force systems to run on the main thread. @@ -124,7 +124,7 @@ fn tick_global_task_pools(_main_thread_marker: Option>) { /// Maintains a count of frames rendered since the start of the application. /// -/// [`FrameCount`] is incremented during [`CoreSet::Last`], providing predictable +/// [`FrameCount`] is incremented during [`Last`], providing predictable /// behaviour: it will be 0 during the first update, 1 during the next, and so forth. /// /// # Overflows @@ -142,7 +142,7 @@ pub struct FrameCountPlugin; impl Plugin for FrameCountPlugin { fn build(&self, app: &mut App) { app.init_resource::(); - app.add_system(update_frame_count.in_base_set(CoreSet::Last)); + app.add_systems(Last, update_frame_count); } } diff --git a/crates/bevy_core_pipeline/src/bloom/mod.rs b/crates/bevy_core_pipeline/src/bloom/mod.rs index f1c90ac05b..d6d6e0f98c 100644 --- a/crates/bevy_core_pipeline/src/bloom/mod.rs +++ b/crates/bevy_core_pipeline/src/bloom/mod.rs @@ -7,13 +7,7 @@ pub use settings::{BloomCompositeMode, BloomPrefilterSettings, BloomSettings}; use crate::{core_2d, core_3d}; use bevy_app::{App, Plugin}; use bevy_asset::{load_internal_asset, HandleUntyped}; -use bevy_ecs::{ - prelude::{Component, Entity}, - query::{QueryState, With}, - schedule::IntoSystemConfig, - system::{Commands, Query, Res, ResMut}, - world::World, -}; +use bevy_ecs::prelude::*; use bevy_math::UVec2; use bevy_reflect::TypeUuid; use bevy_render::{ @@ -22,12 +16,12 @@ use bevy_render::{ ComponentUniforms, DynamicUniformIndex, ExtractComponentPlugin, UniformComponentPlugin, }, prelude::Color, - render_graph::{Node, NodeRunError, RenderGraph, RenderGraphContext, SlotInfo, SlotType}, + render_graph::{Node, NodeRunError, RenderGraph, RenderGraphContext}, render_resource::*, renderer::{RenderContext, RenderDevice}, texture::{CachedTexture, TextureCache}, view::ViewTarget, - RenderApp, RenderSet, + Render, RenderApp, RenderSet, }; #[cfg(feature = "trace")] use bevy_utils::tracing::info_span; @@ -71,10 +65,15 @@ impl Plugin for BloomPlugin { .init_resource::() .init_resource::>() .init_resource::>() - .add_system(prepare_bloom_textures.in_set(RenderSet::Prepare)) - .add_system(prepare_downsampling_pipeline.in_set(RenderSet::Prepare)) - .add_system(prepare_upsampling_pipeline.in_set(RenderSet::Prepare)) - .add_system(queue_bloom_bind_groups.in_set(RenderSet::Queue)); + .add_systems( + Render, + ( + prepare_bloom_textures.in_set(RenderSet::Prepare), + prepare_downsampling_pipeline.in_set(RenderSet::Prepare), + prepare_upsampling_pipeline.in_set(RenderSet::Prepare), + queue_bloom_bind_groups.in_set(RenderSet::Queue), + ), + ); // Add bloom to the 3d render graph { @@ -84,12 +83,6 @@ impl Plugin for BloomPlugin { .get_sub_graph_mut(crate::core_3d::graph::NAME) .unwrap(); draw_3d_graph.add_node(core_3d::graph::node::BLOOM, bloom_node); - draw_3d_graph.add_slot_edge( - draw_3d_graph.input_node().id, - crate::core_3d::graph::input::VIEW_ENTITY, - core_3d::graph::node::BLOOM, - BloomNode::IN_VIEW, - ); // MAIN_PASS -> BLOOM -> TONEMAPPING draw_3d_graph.add_node_edge( crate::core_3d::graph::node::MAIN_PASS, @@ -109,12 +102,6 @@ impl Plugin for BloomPlugin { .get_sub_graph_mut(crate::core_2d::graph::NAME) .unwrap(); draw_2d_graph.add_node(core_2d::graph::node::BLOOM, bloom_node); - draw_2d_graph.add_slot_edge( - draw_2d_graph.input_node().id, - crate::core_2d::graph::input::VIEW_ENTITY, - core_2d::graph::node::BLOOM, - BloomNode::IN_VIEW, - ); // MAIN_PASS -> BLOOM -> TONEMAPPING draw_2d_graph.add_node_edge( crate::core_2d::graph::node::MAIN_PASS, @@ -142,8 +129,6 @@ pub struct BloomNode { } impl BloomNode { - pub const IN_VIEW: &'static str = "view"; - pub fn new(world: &mut World) -> Self { Self { view_query: QueryState::new(world), @@ -152,10 +137,6 @@ impl BloomNode { } impl Node for BloomNode { - fn input(&self) -> Vec { - vec![SlotInfo::new(Self::IN_VIEW, SlotType::Entity)] - } - fn update(&mut self, world: &mut World) { self.view_query.update_archetypes(world); } @@ -175,7 +156,7 @@ impl Node for BloomNode { let downsampling_pipeline_res = world.resource::(); let pipeline_cache = world.resource::(); let uniforms = world.resource::>(); - let view_entity = graph.get_input_entity(Self::IN_VIEW)?; + let view_entity = graph.view_entity(); let Ok(( camera, view_target, diff --git a/crates/bevy_core_pipeline/src/bloom/settings.rs b/crates/bevy_core_pipeline/src/bloom/settings.rs index 32cfab0702..edf8ea493f 100644 --- a/crates/bevy_core_pipeline/src/bloom/settings.rs +++ b/crates/bevy_core_pipeline/src/bloom/settings.rs @@ -49,7 +49,7 @@ pub struct BloomSettings { /// /// In this configuration: /// * 0.0 means no bloom - /// * > 0.0 means a proportionate amount of scattered light is added + /// * Greater than 0.0 means a proportionate amount of scattered light is added pub intensity: f32, /// Low frequency contribution boost. @@ -69,7 +69,7 @@ pub struct BloomSettings { /// /// In this configuration: /// * 0.0 means no bloom - /// * > 0.0 means a proportionate amount of scattered light is added + /// * Greater than 0.0 means a proportionate amount of scattered light is added pub low_frequency_boost: f32, /// Low frequency contribution boost curve. diff --git a/crates/bevy_core_pipeline/src/core_2d/main_pass_2d_node.rs b/crates/bevy_core_pipeline/src/core_2d/main_pass_2d_node.rs index b5660c4c0a..dd40ffdecb 100644 --- a/crates/bevy_core_pipeline/src/core_2d/main_pass_2d_node.rs +++ b/crates/bevy_core_pipeline/src/core_2d/main_pass_2d_node.rs @@ -5,7 +5,7 @@ use crate::{ use bevy_ecs::prelude::*; use bevy_render::{ camera::ExtractedCamera, - render_graph::{Node, NodeRunError, RenderGraphContext, SlotInfo, SlotType}, + render_graph::{Node, NodeRunError, RenderGraphContext}, render_phase::RenderPhase, render_resource::{LoadOp, Operations, RenderPassDescriptor}, renderer::RenderContext, @@ -27,8 +27,6 @@ pub struct MainPass2dNode { } impl MainPass2dNode { - pub const IN_VIEW: &'static str = "view"; - pub fn new(world: &mut World) -> Self { Self { query: world.query_filtered(), @@ -37,10 +35,6 @@ impl MainPass2dNode { } impl Node for MainPass2dNode { - fn input(&self) -> Vec { - vec![SlotInfo::new(MainPass2dNode::IN_VIEW, SlotType::Entity)] - } - fn update(&mut self, world: &mut World) { self.query.update_archetypes(world); } @@ -51,7 +45,7 @@ impl Node for MainPass2dNode { render_context: &mut RenderContext, world: &World, ) -> Result<(), NodeRunError> { - let view_entity = graph.get_input_entity(Self::IN_VIEW)?; + let view_entity = graph.view_entity(); let (camera, transparent_phase, target, camera_2d) = if let Ok(result) = self.query.get_manual(world, view_entity) { result diff --git a/crates/bevy_core_pipeline/src/core_2d/mod.rs b/crates/bevy_core_pipeline/src/core_2d/mod.rs index 573bf0d5a0..5f866715fb 100644 --- a/crates/bevy_core_pipeline/src/core_2d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_2d/mod.rs @@ -20,18 +20,18 @@ pub mod graph { pub use camera_2d::*; pub use main_pass_2d_node::*; -use bevy_app::{App, IntoSystemAppConfig, Plugin}; +use bevy_app::{App, Plugin}; use bevy_ecs::prelude::*; use bevy_render::{ camera::Camera, extract_component::ExtractComponentPlugin, - render_graph::{EmptyNode, RenderGraph, SlotInfo, SlotType}, + render_graph::{EmptyNode, RenderGraph}, render_phase::{ batch_phase_system, sort_phase_system, BatchedPhaseItem, CachedRenderPipelinePhaseItem, DrawFunctionId, DrawFunctions, PhaseItem, RenderPhase, }, render_resource::CachedRenderPipelineId, - Extract, ExtractSchedule, RenderApp, RenderSet, + Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; use bevy_utils::FloatOrd; use std::ops::Range; @@ -52,12 +52,15 @@ impl Plugin for Core2dPlugin { render_app .init_resource::>() - .add_system(extract_core_2d_camera_phases.in_schedule(ExtractSchedule)) - .add_system(sort_phase_system::.in_set(RenderSet::PhaseSort)) - .add_system( - batch_phase_system:: - .after(sort_phase_system::) - .in_set(RenderSet::PhaseSort), + .add_systems(ExtractSchedule, extract_core_2d_camera_phases) + .add_systems( + Render, + ( + sort_phase_system::.in_set(RenderSet::PhaseSort), + batch_phase_system:: + .after(sort_phase_system::) + .in_set(RenderSet::PhaseSort), + ), ); let pass_node_2d = MainPass2dNode::new(&mut render_app.world); @@ -70,28 +73,6 @@ impl Plugin for Core2dPlugin { draw_2d_graph.add_node(graph::node::TONEMAPPING, tonemapping); draw_2d_graph.add_node(graph::node::END_MAIN_PASS_POST_PROCESSING, EmptyNode); draw_2d_graph.add_node(graph::node::UPSCALING, upscaling); - let input_node_id = draw_2d_graph.set_input(vec![SlotInfo::new( - graph::input::VIEW_ENTITY, - SlotType::Entity, - )]); - draw_2d_graph.add_slot_edge( - input_node_id, - graph::input::VIEW_ENTITY, - graph::node::MAIN_PASS, - MainPass2dNode::IN_VIEW, - ); - draw_2d_graph.add_slot_edge( - input_node_id, - graph::input::VIEW_ENTITY, - graph::node::TONEMAPPING, - TonemappingNode::IN_VIEW, - ); - draw_2d_graph.add_slot_edge( - input_node_id, - graph::input::VIEW_ENTITY, - graph::node::UPSCALING, - UpscalingNode::IN_VIEW, - ); draw_2d_graph.add_node_edge(graph::node::MAIN_PASS, graph::node::TONEMAPPING); draw_2d_graph.add_node_edge( graph::node::TONEMAPPING, diff --git a/crates/bevy_core_pipeline/src/core_3d/main_pass_3d_node.rs b/crates/bevy_core_pipeline/src/core_3d/main_pass_3d_node.rs index 7cf9f3b465..ec2e3b9bbd 100644 --- a/crates/bevy_core_pipeline/src/core_3d/main_pass_3d_node.rs +++ b/crates/bevy_core_pipeline/src/core_3d/main_pass_3d_node.rs @@ -6,7 +6,7 @@ use crate::{ use bevy_ecs::prelude::*; use bevy_render::{ camera::ExtractedCamera, - render_graph::{Node, NodeRunError, RenderGraphContext, SlotInfo, SlotType}, + render_graph::{Node, NodeRunError, RenderGraphContext}, render_phase::RenderPhase, render_resource::{ Extent3d, LoadOp, Operations, RenderPassDepthStencilAttachment, RenderPassDescriptor, @@ -38,8 +38,6 @@ pub struct MainPass3dNode { } impl MainPass3dNode { - pub const IN_VIEW: &'static str = "view"; - pub fn new(world: &mut World) -> Self { Self { query: world.query_filtered(), @@ -48,10 +46,6 @@ impl MainPass3dNode { } impl Node for MainPass3dNode { - fn input(&self) -> Vec { - vec![SlotInfo::new(MainPass3dNode::IN_VIEW, SlotType::Entity)] - } - fn update(&mut self, world: &mut World) { self.query.update_archetypes(world); } @@ -62,7 +56,7 @@ impl Node for MainPass3dNode { render_context: &mut RenderContext, world: &World, ) -> Result<(), NodeRunError> { - let view_entity = graph.get_input_entity(Self::IN_VIEW)?; + let view_entity = graph.view_entity(); let Ok(( camera, opaque_phase, diff --git a/crates/bevy_core_pipeline/src/core_3d/mod.rs b/crates/bevy_core_pipeline/src/core_3d/mod.rs index 1021847d73..0e30b9bf6f 100644 --- a/crates/bevy_core_pipeline/src/core_3d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_3d/mod.rs @@ -23,13 +23,13 @@ use std::cmp::Reverse; pub use camera_3d::*; pub use main_pass_3d_node::*; -use bevy_app::{App, IntoSystemAppConfig, Plugin}; +use bevy_app::{App, Plugin}; use bevy_ecs::prelude::*; use bevy_render::{ camera::{Camera, ExtractedCamera}, extract_component::ExtractComponentPlugin, prelude::Msaa, - render_graph::{EmptyNode, RenderGraph, SlotInfo, SlotType}, + render_graph::{EmptyNode, RenderGraph}, render_phase::{ sort_phase_system, CachedRenderPipelinePhaseItem, DrawFunctionId, DrawFunctions, PhaseItem, RenderPhase, @@ -41,7 +41,7 @@ use bevy_render::{ renderer::RenderDevice, texture::{BevyDefault, TextureCache}, view::{ExtractedView, ViewDepthTexture, ViewTarget}, - Extract, ExtractSchedule, RenderApp, RenderSet, + Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; use bevy_utils::{FloatOrd, HashMap}; @@ -68,20 +68,21 @@ impl Plugin for Core3dPlugin { .init_resource::>() .init_resource::>() .init_resource::>() - .add_system(extract_core_3d_camera_phases.in_schedule(ExtractSchedule)) - .add_system( - prepare_core_3d_depth_textures - .in_set(RenderSet::Prepare) - .after(bevy_render::view::prepare_windows), - ) - .add_system( - prepare_core_3d_transmission_textures - .in_set(RenderSet::Prepare) - .after(bevy_render::view::prepare_windows), - ) - .add_system(sort_phase_system::.in_set(RenderSet::PhaseSort)) - .add_system(sort_phase_system::.in_set(RenderSet::PhaseSort)) - .add_system(sort_phase_system::.in_set(RenderSet::PhaseSort)); + .add_systems(ExtractSchedule, extract_core_3d_camera_phases) + .add_systems( + Render, + ( + prepare_core_3d_depth_textures + .in_set(RenderSet::Prepare) + .after(bevy_render::view::prepare_windows), + prepare_core_3d_transmission_textures + .in_set(RenderSet::Prepare) + .after(bevy_render::view::prepare_windows), + sort_phase_system::.in_set(RenderSet::PhaseSort), + sort_phase_system::.in_set(RenderSet::PhaseSort), + sort_phase_system::.in_set(RenderSet::PhaseSort), + ), + ); let prepass_node = PrepassNode::new(&mut render_app.world); let pass_node_3d = MainPass3dNode::new(&mut render_app.world); @@ -96,34 +97,6 @@ impl Plugin for Core3dPlugin { draw_3d_graph.add_node(graph::node::END_MAIN_PASS_POST_PROCESSING, EmptyNode); draw_3d_graph.add_node(graph::node::UPSCALING, upscaling); - let input_node_id = draw_3d_graph.set_input(vec![SlotInfo::new( - graph::input::VIEW_ENTITY, - SlotType::Entity, - )]); - draw_3d_graph.add_slot_edge( - input_node_id, - graph::input::VIEW_ENTITY, - graph::node::PREPASS, - PrepassNode::IN_VIEW, - ); - draw_3d_graph.add_slot_edge( - input_node_id, - graph::input::VIEW_ENTITY, - graph::node::MAIN_PASS, - MainPass3dNode::IN_VIEW, - ); - draw_3d_graph.add_slot_edge( - input_node_id, - graph::input::VIEW_ENTITY, - graph::node::TONEMAPPING, - TonemappingNode::IN_VIEW, - ); - draw_3d_graph.add_slot_edge( - input_node_id, - graph::input::VIEW_ENTITY, - graph::node::UPSCALING, - UpscalingNode::IN_VIEW, - ); draw_3d_graph.add_node_edge(graph::node::PREPASS, graph::node::MAIN_PASS); draw_3d_graph.add_node_edge(graph::node::MAIN_PASS, graph::node::TONEMAPPING); draw_3d_graph.add_node_edge( diff --git a/crates/bevy_core_pipeline/src/fxaa/mod.rs b/crates/bevy_core_pipeline/src/fxaa/mod.rs index 0892299f24..6c4181d1f4 100644 --- a/crates/bevy_core_pipeline/src/fxaa/mod.rs +++ b/crates/bevy_core_pipeline/src/fxaa/mod.rs @@ -14,7 +14,7 @@ use bevy_render::{ renderer::RenderDevice, texture::BevyDefault, view::{ExtractedView, ViewTarget}, - RenderApp, RenderSet, + Render, RenderApp, RenderSet, }; mod node; @@ -90,7 +90,7 @@ impl Plugin for FxaaPlugin { render_app .init_resource::() .init_resource::>() - .add_system(prepare_fxaa_pipelines.in_set(RenderSet::Prepare)); + .add_systems(Render, prepare_fxaa_pipelines.in_set(RenderSet::Prepare)); { let fxaa_node = FxaaNode::new(&mut render_app.world); @@ -99,13 +99,6 @@ impl Plugin for FxaaPlugin { graph.add_node(core_3d::graph::node::FXAA, fxaa_node); - graph.add_slot_edge( - graph.input_node().id, - core_3d::graph::input::VIEW_ENTITY, - core_3d::graph::node::FXAA, - FxaaNode::IN_VIEW, - ); - graph.add_node_edge( core_3d::graph::node::TONEMAPPING, core_3d::graph::node::FXAA, @@ -122,13 +115,6 @@ impl Plugin for FxaaPlugin { graph.add_node(core_2d::graph::node::FXAA, fxaa_node); - graph.add_slot_edge( - graph.input_node().id, - core_2d::graph::input::VIEW_ENTITY, - core_2d::graph::node::FXAA, - FxaaNode::IN_VIEW, - ); - graph.add_node_edge( core_2d::graph::node::TONEMAPPING, core_2d::graph::node::FXAA, diff --git a/crates/bevy_core_pipeline/src/fxaa/node.rs b/crates/bevy_core_pipeline/src/fxaa/node.rs index 5050e3c4b3..71d66ca27a 100644 --- a/crates/bevy_core_pipeline/src/fxaa/node.rs +++ b/crates/bevy_core_pipeline/src/fxaa/node.rs @@ -4,7 +4,7 @@ use crate::fxaa::{CameraFxaaPipeline, Fxaa, FxaaPipeline}; use bevy_ecs::prelude::*; use bevy_ecs::query::QueryState; use bevy_render::{ - render_graph::{Node, NodeRunError, RenderGraphContext, SlotInfo, SlotType}, + render_graph::{Node, NodeRunError, RenderGraphContext}, render_resource::{ BindGroup, BindGroupDescriptor, BindGroupEntry, BindingResource, FilterMode, Operations, PipelineCache, RenderPassColorAttachment, RenderPassDescriptor, SamplerDescriptor, @@ -28,8 +28,6 @@ pub struct FxaaNode { } impl FxaaNode { - pub const IN_VIEW: &'static str = "view"; - pub fn new(world: &mut World) -> Self { Self { query: QueryState::new(world), @@ -39,10 +37,6 @@ impl FxaaNode { } impl Node for FxaaNode { - fn input(&self) -> Vec { - vec![SlotInfo::new(FxaaNode::IN_VIEW, SlotType::Entity)] - } - fn update(&mut self, world: &mut World) { self.query.update_archetypes(world); } @@ -53,7 +47,7 @@ impl Node for FxaaNode { render_context: &mut RenderContext, world: &World, ) -> Result<(), NodeRunError> { - let view_entity = graph.get_input_entity(Self::IN_VIEW)?; + let view_entity = graph.view_entity(); let pipeline_cache = world.resource::(); let fxaa_pipeline = world.resource::(); diff --git a/crates/bevy_core_pipeline/src/msaa_writeback.rs b/crates/bevy_core_pipeline/src/msaa_writeback.rs index 2f8122d193..43401ec568 100644 --- a/crates/bevy_core_pipeline/src/msaa_writeback.rs +++ b/crates/bevy_core_pipeline/src/msaa_writeback.rs @@ -3,10 +3,10 @@ use bevy_app::{App, Plugin}; use bevy_ecs::prelude::*; use bevy_render::{ camera::ExtractedCamera, - render_graph::{Node, NodeRunError, RenderGraph, RenderGraphContext, SlotInfo, SlotType}, + render_graph::{Node, NodeRunError, RenderGraph, RenderGraphContext}, renderer::RenderContext, view::{Msaa, ViewTarget}, - RenderSet, + Render, RenderSet, }; use bevy_render::{render_resource::*, RenderApp}; @@ -20,12 +20,14 @@ impl Plugin for MsaaWritebackPlugin { return }; - render_app.add_system(queue_msaa_writeback_pipelines.in_set(RenderSet::Queue)); + render_app.add_systems( + Render, + queue_msaa_writeback_pipelines.in_set(RenderSet::Queue), + ); let msaa_writeback_2d = MsaaWritebackNode::new(&mut render_app.world); let msaa_writeback_3d = MsaaWritebackNode::new(&mut render_app.world); let mut graph = render_app.world.resource_mut::(); if let Some(core_2d) = graph.get_sub_graph_mut(crate::core_2d::graph::NAME) { - let input_node = core_2d.input_node().id; core_2d.add_node( crate::core_2d::graph::node::MSAA_WRITEBACK, msaa_writeback_2d, @@ -34,16 +36,9 @@ impl Plugin for MsaaWritebackPlugin { crate::core_2d::graph::node::MSAA_WRITEBACK, crate::core_2d::graph::node::MAIN_PASS, ); - core_2d.add_slot_edge( - input_node, - crate::core_2d::graph::input::VIEW_ENTITY, - crate::core_2d::graph::node::MSAA_WRITEBACK, - MsaaWritebackNode::IN_VIEW, - ); } if let Some(core_3d) = graph.get_sub_graph_mut(crate::core_3d::graph::NAME) { - let input_node = core_3d.input_node().id; core_3d.add_node( crate::core_3d::graph::node::MSAA_WRITEBACK, msaa_writeback_3d, @@ -52,12 +47,6 @@ impl Plugin for MsaaWritebackPlugin { crate::core_3d::graph::node::MSAA_WRITEBACK, crate::core_3d::graph::node::MAIN_PASS, ); - core_3d.add_slot_edge( - input_node, - crate::core_3d::graph::input::VIEW_ENTITY, - crate::core_3d::graph::node::MSAA_WRITEBACK, - MsaaWritebackNode::IN_VIEW, - ); } } } @@ -67,8 +56,6 @@ pub struct MsaaWritebackNode { } impl MsaaWritebackNode { - pub const IN_VIEW: &'static str = "view"; - pub fn new(world: &mut World) -> Self { Self { cameras: world.query(), @@ -77,9 +64,6 @@ impl MsaaWritebackNode { } impl Node for MsaaWritebackNode { - fn input(&self) -> Vec { - vec![SlotInfo::new(Self::IN_VIEW, SlotType::Entity)] - } fn update(&mut self, world: &mut World) { self.cameras.update_archetypes(world); } @@ -89,7 +73,7 @@ impl Node for MsaaWritebackNode { render_context: &mut RenderContext, world: &World, ) -> Result<(), NodeRunError> { - let view_entity = graph.get_input_entity(Self::IN_VIEW)?; + let view_entity = graph.view_entity(); if let Ok((target, blit_pipeline_id)) = self.cameras.get_manual(world, view_entity) { let blit_pipeline = world.resource::(); let pipeline_cache = world.resource::(); diff --git a/crates/bevy_core_pipeline/src/prepass/node.rs b/crates/bevy_core_pipeline/src/prepass/node.rs index 2687b925c0..0e4cb17f84 100644 --- a/crates/bevy_core_pipeline/src/prepass/node.rs +++ b/crates/bevy_core_pipeline/src/prepass/node.rs @@ -3,7 +3,7 @@ use bevy_ecs::query::QueryState; use bevy_render::{ camera::ExtractedCamera, prelude::Color, - render_graph::{Node, NodeRunError, RenderGraphContext, SlotInfo, SlotType}, + render_graph::{Node, NodeRunError, RenderGraphContext}, render_phase::RenderPhase, render_resource::{ LoadOp, Operations, RenderPassColorAttachment, RenderPassDepthStencilAttachment, @@ -34,8 +34,6 @@ pub struct PrepassNode { } impl PrepassNode { - pub const IN_VIEW: &'static str = "view"; - pub fn new(world: &mut World) -> Self { Self { main_view_query: QueryState::new(world), @@ -44,10 +42,6 @@ impl PrepassNode { } impl Node for PrepassNode { - fn input(&self) -> Vec { - vec![SlotInfo::new(Self::IN_VIEW, SlotType::Entity)] - } - fn update(&mut self, world: &mut World) { self.main_view_query.update_archetypes(world); } @@ -58,7 +52,7 @@ impl Node for PrepassNode { render_context: &mut RenderContext, world: &World, ) -> Result<(), NodeRunError> { - let view_entity = graph.get_input_entity(Self::IN_VIEW)?; + let view_entity = graph.view_entity(); let Ok(( camera, opaque_prepass_phase, diff --git a/crates/bevy_core_pipeline/src/tonemapping/mod.rs b/crates/bevy_core_pipeline/src/tonemapping/mod.rs index d6c2f3a062..31ecd12177 100644 --- a/crates/bevy_core_pipeline/src/tonemapping/mod.rs +++ b/crates/bevy_core_pipeline/src/tonemapping/mod.rs @@ -10,7 +10,7 @@ use bevy_render::render_asset::RenderAssets; use bevy_render::renderer::RenderDevice; use bevy_render::texture::{CompressedImageFormats, Image, ImageSampler, ImageType}; use bevy_render::view::{ViewTarget, ViewUniform}; -use bevy_render::{render_resource::*, RenderApp, RenderSet}; +use bevy_render::{render_resource::*, Render, RenderApp, RenderSet}; mod node; @@ -94,7 +94,10 @@ impl Plugin for TonemappingPlugin { render_app .init_resource::() .init_resource::>() - .add_system(queue_view_tonemapping_pipelines.in_set(RenderSet::Queue)); + .add_systems( + Render, + queue_view_tonemapping_pipelines.in_set(RenderSet::Queue), + ); } } } diff --git a/crates/bevy_core_pipeline/src/tonemapping/node.rs b/crates/bevy_core_pipeline/src/tonemapping/node.rs index 357822fc73..eed9013ba3 100644 --- a/crates/bevy_core_pipeline/src/tonemapping/node.rs +++ b/crates/bevy_core_pipeline/src/tonemapping/node.rs @@ -6,7 +6,7 @@ use bevy_ecs::prelude::*; use bevy_ecs::query::QueryState; use bevy_render::{ render_asset::RenderAssets, - render_graph::{Node, NodeRunError, RenderGraphContext, SlotInfo, SlotType}, + render_graph::{Node, NodeRunError, RenderGraphContext}, render_resource::{ BindGroup, BindGroupDescriptor, BindGroupEntry, BindingResource, BufferId, LoadOp, Operations, PipelineCache, RenderPassColorAttachment, RenderPassDescriptor, @@ -34,8 +34,6 @@ pub struct TonemappingNode { } impl TonemappingNode { - pub const IN_VIEW: &'static str = "view"; - pub fn new(world: &mut World) -> Self { Self { query: QueryState::new(world), @@ -46,10 +44,6 @@ impl TonemappingNode { } impl Node for TonemappingNode { - fn input(&self) -> Vec { - vec![SlotInfo::new(TonemappingNode::IN_VIEW, SlotType::Entity)] - } - fn update(&mut self, world: &mut World) { self.query.update_archetypes(world); } @@ -60,7 +54,7 @@ impl Node for TonemappingNode { render_context: &mut RenderContext, world: &World, ) -> Result<(), NodeRunError> { - let view_entity = graph.get_input_entity(Self::IN_VIEW)?; + let view_entity = graph.view_entity(); let pipeline_cache = world.resource::(); let tonemapping_pipeline = world.resource::(); let gpu_images = world.get_resource::>().unwrap(); diff --git a/crates/bevy_core_pipeline/src/upscaling/mod.rs b/crates/bevy_core_pipeline/src/upscaling/mod.rs index 441f9f7775..f3594397d5 100644 --- a/crates/bevy_core_pipeline/src/upscaling/mod.rs +++ b/crates/bevy_core_pipeline/src/upscaling/mod.rs @@ -3,7 +3,7 @@ use bevy_app::prelude::*; use bevy_ecs::prelude::*; use bevy_render::camera::{CameraOutputMode, ExtractedCamera}; use bevy_render::view::ViewTarget; -use bevy_render::{render_resource::*, RenderApp, RenderSet}; +use bevy_render::{render_resource::*, Render, RenderApp, RenderSet}; mod node; @@ -14,7 +14,10 @@ pub struct UpscalingPlugin; impl Plugin for UpscalingPlugin { fn build(&self, app: &mut App) { if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { - render_app.add_system(queue_view_upscaling_pipelines.in_set(RenderSet::Queue)); + render_app.add_systems( + Render, + queue_view_upscaling_pipelines.in_set(RenderSet::Queue), + ); } } } diff --git a/crates/bevy_core_pipeline/src/upscaling/node.rs b/crates/bevy_core_pipeline/src/upscaling/node.rs index 718901bca7..244b45b820 100644 --- a/crates/bevy_core_pipeline/src/upscaling/node.rs +++ b/crates/bevy_core_pipeline/src/upscaling/node.rs @@ -3,7 +3,7 @@ use bevy_ecs::prelude::*; use bevy_ecs::query::QueryState; use bevy_render::{ camera::{CameraOutputMode, ExtractedCamera}, - render_graph::{Node, NodeRunError, RenderGraphContext, SlotInfo, SlotType}, + render_graph::{Node, NodeRunError, RenderGraphContext}, render_resource::{ BindGroup, BindGroupDescriptor, BindGroupEntry, BindingResource, LoadOp, Operations, PipelineCache, RenderPassColorAttachment, RenderPassDescriptor, SamplerDescriptor, @@ -27,8 +27,6 @@ pub struct UpscalingNode { } impl UpscalingNode { - pub const IN_VIEW: &'static str = "view"; - pub fn new(world: &mut World) -> Self { Self { query: QueryState::new(world), @@ -38,10 +36,6 @@ impl UpscalingNode { } impl Node for UpscalingNode { - fn input(&self) -> Vec { - vec![SlotInfo::new(UpscalingNode::IN_VIEW, SlotType::Entity)] - } - fn update(&mut self, world: &mut World) { self.query.update_archetypes(world); } @@ -52,7 +46,7 @@ impl Node for UpscalingNode { render_context: &mut RenderContext, world: &World, ) -> Result<(), NodeRunError> { - let view_entity = graph.get_input_entity(Self::IN_VIEW)?; + let view_entity = graph.view_entity(); let pipeline_cache = world.get_resource::().unwrap(); let blit_pipeline = world.get_resource::().unwrap(); diff --git a/crates/bevy_diagnostic/src/entity_count_diagnostics_plugin.rs b/crates/bevy_diagnostic/src/entity_count_diagnostics_plugin.rs index fd6b21117c..5c4415c09b 100644 --- a/crates/bevy_diagnostic/src/entity_count_diagnostics_plugin.rs +++ b/crates/bevy_diagnostic/src/entity_count_diagnostics_plugin.rs @@ -9,8 +9,8 @@ pub struct EntityCountDiagnosticsPlugin; impl Plugin for EntityCountDiagnosticsPlugin { fn build(&self, app: &mut App) { - app.add_startup_system(Self::setup_system) - .add_system(Self::diagnostic_system); + app.add_systems(Startup, Self::setup_system) + .add_systems(Update, Self::diagnostic_system); } } diff --git a/crates/bevy_diagnostic/src/frame_time_diagnostics_plugin.rs b/crates/bevy_diagnostic/src/frame_time_diagnostics_plugin.rs index 8e26025bf2..4dbc3c4cfa 100644 --- a/crates/bevy_diagnostic/src/frame_time_diagnostics_plugin.rs +++ b/crates/bevy_diagnostic/src/frame_time_diagnostics_plugin.rs @@ -10,8 +10,8 @@ pub struct FrameTimeDiagnosticsPlugin; impl Plugin for FrameTimeDiagnosticsPlugin { fn build(&self, app: &mut bevy_app::App) { - app.add_startup_system(Self::setup_system) - .add_system(Self::diagnostic_system); + app.add_systems(Startup, Self::setup_system) + .add_systems(Update, Self::diagnostic_system); } } diff --git a/crates/bevy_diagnostic/src/lib.rs b/crates/bevy_diagnostic/src/lib.rs index b2d127f115..854f6e74cd 100644 --- a/crates/bevy_diagnostic/src/lib.rs +++ b/crates/bevy_diagnostic/src/lib.rs @@ -17,8 +17,10 @@ pub struct DiagnosticsPlugin; impl Plugin for DiagnosticsPlugin { fn build(&self, app: &mut App) { - app.init_resource::() - .add_startup_system(system_information_diagnostics_plugin::internal::log_system_info); + app.init_resource::().add_systems( + Startup, + system_information_diagnostics_plugin::internal::log_system_info, + ); } } diff --git a/crates/bevy_diagnostic/src/log_diagnostics_plugin.rs b/crates/bevy_diagnostic/src/log_diagnostics_plugin.rs index dd80c8c8f2..65f212dcc6 100644 --- a/crates/bevy_diagnostic/src/log_diagnostics_plugin.rs +++ b/crates/bevy_diagnostic/src/log_diagnostics_plugin.rs @@ -37,9 +37,9 @@ impl Plugin for LogDiagnosticsPlugin { }); if self.debug { - app.add_system(Self::log_diagnostics_debug_system.in_base_set(CoreSet::PostUpdate)); + app.add_systems(PostUpdate, Self::log_diagnostics_debug_system); } else { - app.add_system(Self::log_diagnostics_system.in_base_set(CoreSet::PostUpdate)); + app.add_systems(PostUpdate, Self::log_diagnostics_system); } } } diff --git a/crates/bevy_diagnostic/src/system_information_diagnostics_plugin.rs b/crates/bevy_diagnostic/src/system_information_diagnostics_plugin.rs index 096c9b13a6..b33cba5ca4 100644 --- a/crates/bevy_diagnostic/src/system_information_diagnostics_plugin.rs +++ b/crates/bevy_diagnostic/src/system_information_diagnostics_plugin.rs @@ -14,8 +14,8 @@ use bevy_app::prelude::*; pub struct SystemInformationDiagnosticsPlugin; impl Plugin for SystemInformationDiagnosticsPlugin { fn build(&self, app: &mut App) { - app.add_startup_system(internal::setup_system) - .add_system(internal::diagnostic_system); + app.add_systems(Startup, internal::setup_system) + .add_systems(Update, internal::diagnostic_system); } } diff --git a/crates/bevy_ecs/Cargo.toml b/crates/bevy_ecs/Cargo.toml index 9112683c22..cfcea53b36 100644 --- a/crates/bevy_ecs/Cargo.toml +++ b/crates/bevy_ecs/Cargo.toml @@ -27,6 +27,7 @@ fixedbitset = "0.4.2" rustc-hash = "1.1" downcast-rs = "1.2" serde = { version = "1", features = ["derive"] } +thiserror = "1.0" [dev-dependencies] rand = "0.8" diff --git a/crates/bevy_ecs/README.md b/crates/bevy_ecs/README.md index ee9126e1b5..457f386e89 100644 --- a/crates/bevy_ecs/README.md +++ b/crates/bevy_ecs/README.md @@ -150,7 +150,7 @@ fn main() { let mut schedule = Schedule::default(); // Add our system to the schedule - schedule.add_system(movement); + schedule.add_systems(movement); // Run the schedule once. If your app has a "loop", you would run this once per loop schedule.run(&mut world); diff --git a/crates/bevy_ecs/examples/change_detection.rs b/crates/bevy_ecs/examples/change_detection.rs index 03a9793318..8bfa18d735 100644 --- a/crates/bevy_ecs/examples/change_detection.rs +++ b/crates/bevy_ecs/examples/change_detection.rs @@ -1,4 +1,4 @@ -use bevy_ecs::{prelude::*, schedule::IntoSystemConfig}; +use bevy_ecs::prelude::*; use rand::Rng; use std::ops::Deref; @@ -21,11 +21,13 @@ fn main() { // Add systems to the Schedule to execute our app logic // We can label our systems to force a specific run-order between some of them - schedule.add_system(spawn_entities.in_set(SimulationSystem::Spawn)); - schedule.add_system(print_counter_when_changed.after(SimulationSystem::Spawn)); - schedule.add_system(age_all_entities.in_set(SimulationSystem::Age)); - schedule.add_system(remove_old_entities.after(SimulationSystem::Age)); - schedule.add_system(print_changed_entities.after(SimulationSystem::Age)); + schedule.add_systems(( + spawn_entities.in_set(SimulationSet::Spawn), + print_counter_when_changed.after(SimulationSet::Spawn), + age_all_entities.in_set(SimulationSet::Age), + remove_old_entities.after(SimulationSet::Age), + print_changed_entities.after(SimulationSet::Age), + )); // Simulate 10 frames in our world for iteration in 1..=10 { @@ -48,7 +50,7 @@ struct Age { // System sets can be used to group systems and configured to control relative ordering #[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)] -enum SimulationSystem { +enum SimulationSet { Spawn, Age, } diff --git a/crates/bevy_ecs/examples/events.rs b/crates/bevy_ecs/examples/events.rs index b0f96e39a1..27bd13b5f6 100644 --- a/crates/bevy_ecs/examples/events.rs +++ b/crates/bevy_ecs/examples/events.rs @@ -16,11 +16,13 @@ fn main() { #[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)] pub struct FlushEvents; - schedule.add_system(Events::::update_system.in_set(FlushEvents)); + schedule.add_systems(Events::::update_system.in_set(FlushEvents)); // Add systems sending and receiving events after the events are flushed. - schedule.add_system(sending_system.after(FlushEvents)); - schedule.add_system(receiving_system.after(sending_system)); + schedule.add_systems(( + sending_system.after(FlushEvents), + receiving_system.after(sending_system), + )); // Simulate 10 frames of our world for iteration in 1..=10 { diff --git a/crates/bevy_ecs/examples/resources.rs b/crates/bevy_ecs/examples/resources.rs index a3fd26fc29..4030b5fee6 100644 --- a/crates/bevy_ecs/examples/resources.rs +++ b/crates/bevy_ecs/examples/resources.rs @@ -15,8 +15,7 @@ fn main() { let mut schedule = Schedule::default(); // Add systems to increase the counter and to print out the current value - schedule.add_system(increase_counter); - schedule.add_system(print_counter.after(increase_counter)); + schedule.add_systems((increase_counter, print_counter).chain()); for iteration in 1..=10 { println!("Simulating frame {iteration}/10"); diff --git a/crates/bevy_ecs/macros/src/fetch.rs b/crates/bevy_ecs/macros/src/fetch.rs index 418c5741e7..a72b4d10f3 100644 --- a/crates/bevy_ecs/macros/src/fetch.rs +++ b/crates/bevy_ecs/macros/src/fetch.rs @@ -1,9 +1,10 @@ +use bevy_macro_utils::ensure_no_collision; use proc_macro::TokenStream; use proc_macro2::{Ident, Span}; use quote::{quote, ToTokens}; use syn::{ parse::{Parse, ParseStream}, - parse_quote, + parse_macro_input, parse_quote, punctuated::Punctuated, Attribute, Data, DataStruct, DeriveInput, Field, Fields, }; @@ -25,7 +26,10 @@ mod field_attr_keywords { pub static WORLD_QUERY_ATTRIBUTE_NAME: &str = "world_query"; -pub fn derive_world_query_impl(ast: DeriveInput) -> TokenStream { +pub fn derive_world_query_impl(input: TokenStream) -> TokenStream { + let tokens = input.clone(); + + let ast = parse_macro_input!(input as DeriveInput); let visibility = ast.vis; let mut fetch_struct_attributes = FetchStructAttributes::default(); @@ -104,13 +108,18 @@ pub fn derive_world_query_impl(ast: DeriveInput) -> TokenStream { }; let fetch_struct_name = Ident::new(&format!("{struct_name}Fetch"), Span::call_site()); + let fetch_struct_name = ensure_no_collision(fetch_struct_name, tokens.clone()); let read_only_fetch_struct_name = if fetch_struct_attributes.is_mutable { - Ident::new(&format!("{struct_name}ReadOnlyFetch"), Span::call_site()) + let new_ident = Ident::new(&format!("{struct_name}ReadOnlyFetch"), Span::call_site()); + ensure_no_collision(new_ident, tokens.clone()) } else { fetch_struct_name.clone() }; + // Generate a name for the state struct that doesn't conflict + // with the struct definition. let state_struct_name = Ident::new(&format!("{struct_name}State"), Span::call_site()); + let state_struct_name = ensure_no_collision(state_struct_name, tokens); let fields = match &ast.data { Data::Struct(DataStruct { @@ -176,7 +185,7 @@ pub fn derive_world_query_impl(ast: DeriveInput) -> TokenStream { &field_types }; - quote! { + let item_struct = quote! { #derive_macro_call #[doc = "Automatically generated [`WorldQuery`] item type for [`"] #[doc = stringify!(#struct_name)] @@ -186,7 +195,9 @@ pub fn derive_world_query_impl(ast: DeriveInput) -> TokenStream { #(#(#field_attrs)* #field_visibilities #field_idents: <#field_types as #path::query::WorldQuery>::Item<'__w>,)* #(#(#ignored_field_attrs)* #ignored_field_visibilities #ignored_field_idents: #ignored_field_types,)* } + }; + let query_impl = quote! { #[doc(hidden)] #[doc = "Automatically generated internal [`WorldQuery`] fetch type for [`"] #[doc = stringify!(#struct_name)] @@ -222,16 +233,16 @@ pub fn derive_world_query_impl(ast: DeriveInput) -> TokenStream { unsafe fn init_fetch<'__w>( _world: &'__w #path::world::World, state: &Self::State, - _last_change_tick: u32, - _change_tick: u32 + _last_run: #path::component::Tick, + _this_run: #path::component::Tick, ) -> ::Fetch<'__w> { #fetch_struct_name { #(#field_idents: <#field_types>::init_fetch( _world, &state.#field_idents, - _last_change_tick, - _change_tick + _last_run, + _this_run, ), )* #(#ignored_field_idents: Default::default(),)* @@ -324,26 +335,28 @@ pub fn derive_world_query_impl(ast: DeriveInput) -> TokenStream { true #(&& <#field_types>::matches_component_set(&state.#field_idents, _set_contains_id))* } } - } + }; + (item_struct, query_impl) }; - let mutable_impl = impl_fetch(false); - let readonly_impl = if fetch_struct_attributes.is_mutable { - let world_query_impl = impl_fetch(true); - quote! { + let (mutable_struct, mutable_impl) = impl_fetch(false); + let (read_only_struct, read_only_impl) = if fetch_struct_attributes.is_mutable { + let (readonly_state, read_only_impl) = impl_fetch(true); + let read_only_structs = quote! { #[doc = "Automatically generated [`WorldQuery`] type for a read-only variant of [`"] #[doc = stringify!(#struct_name)] #[doc = "`]."] #[automatically_derived] #visibility struct #read_only_struct_name #user_impl_generics #user_where_clauses { - #( #field_idents: #read_only_field_types, )* + #( #field_visibilities #field_idents: #read_only_field_types, )* #(#(#ignored_field_attrs)* #ignored_field_visibilities #ignored_field_idents: #ignored_field_types,)* } - #world_query_impl - } + #readonly_state + }; + (read_only_structs, read_only_impl) } else { - quote! {} + (quote! {}, quote! {}) }; let read_only_asserts = if fetch_struct_attributes.is_mutable { @@ -367,24 +380,30 @@ pub fn derive_world_query_impl(ast: DeriveInput) -> TokenStream { }; TokenStream::from(quote! { - #mutable_impl + #mutable_struct - #readonly_impl - - #[doc(hidden)] - #[doc = "Automatically generated internal [`WorldQuery`] state type for [`"] - #[doc = stringify!(#struct_name)] - #[doc = "`], used for caching."] - #[automatically_derived] - #visibility struct #state_struct_name #user_impl_generics #user_where_clauses { - #(#field_idents: <#field_types as #path::query::WorldQuery>::State,)* - #(#ignored_field_idents: #ignored_field_types,)* - } + #read_only_struct /// SAFETY: we assert fields are readonly below unsafe impl #user_impl_generics #path::query::ReadOnlyWorldQuery for #read_only_struct_name #user_ty_generics #user_where_clauses {} + const _: () = { + #[doc(hidden)] + #[doc = "Automatically generated internal [`WorldQuery`] state type for [`"] + #[doc = stringify!(#struct_name)] + #[doc = "`], used for caching."] + #[automatically_derived] + #visibility struct #state_struct_name #user_impl_generics #user_where_clauses { + #(#field_idents: <#field_types as #path::query::WorldQuery>::State,)* + #(#ignored_field_idents: #ignored_field_types,)* + } + + #mutable_impl + + #read_only_impl + }; + #[allow(dead_code)] const _: () = { fn assert_readonly() @@ -412,7 +431,6 @@ pub fn derive_world_query_impl(ast: DeriveInput) -> TokenStream { #(q.#ignored_field_idents;)* #(q2.#field_idents;)* #(q2.#ignored_field_idents;)* - } }; }) diff --git a/crates/bevy_ecs/macros/src/lib.rs b/crates/bevy_ecs/macros/src/lib.rs index b4dde955dd..3866cea315 100644 --- a/crates/bevy_ecs/macros/src/lib.rs +++ b/crates/bevy_ecs/macros/src/lib.rs @@ -6,7 +6,9 @@ mod set; mod states; use crate::{fetch::derive_world_query_impl, set::derive_set}; -use bevy_macro_utils::{derive_boxed_label, get_named_struct_fields, BevyManifest}; +use bevy_macro_utils::{ + derive_boxed_label, ensure_no_collision, get_named_struct_fields, BevyManifest, +}; use proc_macro::TokenStream; use proc_macro2::Span; use quote::{format_ident, quote}; @@ -126,7 +128,9 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream { #(#field_from_components)* } } + } + impl #impl_generics #ecs_path::bundle::DynamicBundle for #struct_name #ty_generics #where_clause { #[allow(unused_variables)] #[inline] fn get_components( @@ -227,7 +231,7 @@ pub fn impl_param_set(_input: TokenStream) -> TokenStream { state: &'s mut Self::State, system_meta: &SystemMeta, world: &'w World, - change_tick: u32, + change_tick: Tick, ) -> Self::Item<'w, 's> { ParamSet { param_states: state, @@ -258,6 +262,7 @@ static SYSTEM_PARAM_ATTRIBUTE_NAME: &str = "system_param"; /// Implement `SystemParam` to use a struct as a parameter in a system #[proc_macro_derive(SystemParam, attributes(system_param))] pub fn derive_system_param(input: TokenStream) -> TokenStream { + let token_stream = input.clone(); let ast = parse_macro_input!(input as DeriveInput); let syn::Data::Struct(syn::DataStruct { fields: field_definitions, ..}) = ast.data else { return syn::Error::new(ast.span(), "Invalid `SystemParam` type: expected a `struct`") @@ -392,6 +397,7 @@ pub fn derive_system_param(input: TokenStream) -> TokenStream { let struct_name = &ast.ident; let state_struct_visibility = &ast.vis; + let state_struct_name = ensure_no_collision(format_ident!("FetchState"), token_stream); TokenStream::from(quote! { // We define the FetchState struct in an anonymous scope to avoid polluting the user namespace. @@ -399,7 +405,7 @@ pub fn derive_system_param(input: TokenStream) -> TokenStream { // as SystemParam>::State const _: () = { #[doc(hidden)] - #state_struct_visibility struct FetchState <'w, 's, #(#lifetimeless_generics,)*> + #state_struct_visibility struct #state_struct_name <'w, 's, #(#lifetimeless_generics,)*> #where_clause { state: (#(<#tuple_types as #path::system::SystemParam>::State,)*), marker: std::marker::PhantomData<( @@ -409,11 +415,11 @@ pub fn derive_system_param(input: TokenStream) -> TokenStream { } unsafe impl<'w, 's, #punctuated_generics> #path::system::SystemParam for #struct_name #ty_generics #where_clause { - type State = FetchState<'static, 'static, #punctuated_generic_idents>; + type State = #state_struct_name<'static, 'static, #punctuated_generic_idents>; type Item<'_w, '_s> = #struct_name <#(#shadowed_lifetimes,)* #punctuated_generic_idents>; fn init_state(world: &mut #path::world::World, system_meta: &mut #path::system::SystemMeta) -> Self::State { - FetchState { + #state_struct_name { state: <(#(#tuple_types,)*) as #path::system::SystemParam>::init_state(world, system_meta), marker: std::marker::PhantomData, } @@ -431,7 +437,7 @@ pub fn derive_system_param(input: TokenStream) -> TokenStream { state: &'s2 mut Self::State, system_meta: &#path::system::SystemMeta, world: &'w2 #path::world::World, - change_tick: u32, + change_tick: #path::component::Tick, ) -> Self::Item<'w2, 's2> { let (#(#tuple_patterns,)*) = < (#(#tuple_types,)*) as #path::system::SystemParam @@ -452,8 +458,7 @@ pub fn derive_system_param(input: TokenStream) -> TokenStream { /// Implement `WorldQuery` to use a struct as a parameter in a query #[proc_macro_derive(WorldQuery, attributes(world_query))] pub fn derive_world_query(input: TokenStream) -> TokenStream { - let ast = parse_macro_input!(input as DeriveInput); - derive_world_query_impl(ast) + derive_world_query_impl(input) } /// Derive macro generating an impl of the trait `ScheduleLabel`. @@ -469,7 +474,7 @@ pub fn derive_schedule_label(input: TokenStream) -> TokenStream { } /// Derive macro generating an impl of the trait `SystemSet`. -#[proc_macro_derive(SystemSet, attributes(system_set))] +#[proc_macro_derive(SystemSet)] pub fn derive_system_set(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); let mut trait_path = bevy_ecs_path(); diff --git a/crates/bevy_ecs/macros/src/set.rs b/crates/bevy_ecs/macros/src/set.rs index 054735c858..66bfb601ec 100644 --- a/crates/bevy_ecs/macros/src/set.rs +++ b/crates/bevy_ecs/macros/src/set.rs @@ -1,9 +1,5 @@ use proc_macro::TokenStream; -use quote::{format_ident, quote, ToTokens}; -use syn::parse::{Parse, ParseStream}; - -pub static SYSTEM_SET_ATTRIBUTE_NAME: &str = "system_set"; -pub static BASE_ATTRIBUTE_NAME: &str = "base"; +use quote::quote; /// Derive a set trait /// @@ -12,55 +8,8 @@ pub static BASE_ATTRIBUTE_NAME: &str = "base"; /// - `input`: The [`syn::DeriveInput`] for the struct that we want to derive the set trait for /// - `trait_path`: The [`syn::Path`] to the set trait pub fn derive_set(input: syn::DeriveInput, trait_path: &syn::Path) -> TokenStream { - let mut base_trait_path = trait_path.clone(); - let ident = &mut base_trait_path.segments.last_mut().unwrap().ident; - *ident = format_ident!("Base{ident}"); - - let mut free_trait_path = trait_path.clone(); - let ident = &mut free_trait_path.segments.last_mut().unwrap().ident; - *ident = format_ident!("Free{ident}"); - let ident = input.ident; - let mut is_base = false; - for attr in &input.attrs { - if !attr - .path - .get_ident() - .map_or(false, |ident| ident == SYSTEM_SET_ATTRIBUTE_NAME) - { - continue; - } - - attr.parse_args_with(|input: ParseStream| { - let meta = input.parse_terminated::(syn::Meta::parse)?; - for meta in meta { - let ident = meta.path().get_ident().unwrap_or_else(|| { - panic!( - "Unrecognized attribute: `{}`", - meta.path().to_token_stream() - ) - }); - if ident == BASE_ATTRIBUTE_NAME { - if let syn::Meta::Path(_) = meta { - is_base = true; - } else { - panic!( - "The `{BASE_ATTRIBUTE_NAME}` attribute is expected to have no value or arguments", - ); - } - } else { - panic!( - "Unrecognized attribute: `{}`", - meta.path().to_token_stream() - ); - } - } - Ok(()) - }) - .unwrap_or_else(|_| panic!("Invalid `{SYSTEM_SET_ATTRIBUTE_NAME}` attribute format")); - } - let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); let mut where_clause = where_clause.cloned().unwrap_or_else(|| syn::WhereClause { where_token: Default::default(), @@ -73,28 +22,12 @@ pub fn derive_set(input: syn::DeriveInput, trait_path: &syn::Path) -> TokenStrea .unwrap(), ); - let marker_impl = if is_base { - quote! { - impl #impl_generics #base_trait_path for #ident #ty_generics #where_clause {} - } - } else { - quote! { - impl #impl_generics #free_trait_path for #ident #ty_generics #where_clause {} - } - }; - (quote! { impl #impl_generics #trait_path for #ident #ty_generics #where_clause { - fn is_base(&self) -> bool { - #is_base - } - fn dyn_clone(&self) -> std::boxed::Box { std::boxed::Box::new(std::clone::Clone::clone(self)) } } - - #marker_impl }) .into() } diff --git a/crates/bevy_ecs/src/archetype.rs b/crates/bevy_ecs/src/archetype.rs index a2215630a8..d106a83a4f 100644 --- a/crates/bevy_ecs/src/archetype.rs +++ b/crates/bevy_ecs/src/archetype.rs @@ -648,6 +648,9 @@ impl Archetypes { self.archetypes.get(id.index()) } + /// # Panics + /// + /// Panics if `a` and `b` are equal. #[inline] pub(crate) fn get_2_mut( &mut self, diff --git a/crates/bevy_ecs/src/bundle.rs b/crates/bevy_ecs/src/bundle.rs index ff4ceb03af..c86ef4bad1 100644 --- a/crates/bevy_ecs/src/bundle.rs +++ b/crates/bevy_ecs/src/bundle.rs @@ -3,7 +3,7 @@ //! This module contains the [`Bundle`] trait and some other helper types. pub use bevy_ecs_macros::Bundle; -use bevy_utils::HashSet; +use bevy_utils::{HashMap, HashSet}; use crate::{ archetype::{ @@ -12,6 +12,7 @@ use crate::{ }, component::{Component, ComponentId, ComponentStorage, Components, StorageType, Tick}, entity::{Entities, Entity, EntityLocation}, + query::DebugCheckedUnwrap, storage::{SparseSetIndex, SparseSets, Storages, Table, TableRow}, TypeIdMap, }; @@ -135,10 +136,10 @@ use std::any::TypeId; /// [`Query`]: crate::system::Query // Some safety points: // - [`Bundle::component_ids`] must return the [`ComponentId`] for each component type in the -// bundle, in the _exact_ order that [`Bundle::get_components`] is called. +// bundle, in the _exact_ order that [`DynamicBundle::get_components`] is called. // - [`Bundle::from_components`] must call `func` exactly once for each [`ComponentId`] returned by // [`Bundle::component_ids`]. -pub unsafe trait Bundle: Send + Sync + 'static { +pub unsafe trait Bundle: DynamicBundle + Send + Sync + 'static { /// Gets this [`Bundle`]'s component ids, in the order of this bundle's [`Component`]s #[doc(hidden)] fn component_ids( @@ -159,7 +160,10 @@ pub unsafe trait Bundle: Send + Sync + 'static { // Ensure that the `OwningPtr` is used correctly F: for<'a> FnMut(&'a mut T) -> OwningPtr<'a>, Self: Sized; +} +/// The parts from [`Bundle`] that don't require statically knowing the components of the bundle. +pub trait DynamicBundle { // SAFETY: // The `StorageType` argument passed into [`Bundle::get_components`] must be correct for the // component being fetched. @@ -192,7 +196,9 @@ unsafe impl Bundle for C { // Safety: The id given in `component_ids` is for `Self` func(ctx).read() } +} +impl DynamicBundle for C { #[inline] fn get_components(self, func: &mut impl FnMut(StorageType, OwningPtr<'_>)) { OwningPtr::make(self, |ptr| func(C::Storage::STORAGE_TYPE, ptr)); @@ -203,7 +209,7 @@ macro_rules! tuple_impl { ($($name: ident),*) => { // SAFETY: // - `Bundle::component_ids` calls `ids` for each component type in the - // bundle, in the exact order that `Bundle::get_components` is called. + // bundle, in the exact order that `DynamicBundle::get_components` is called. // - `Bundle::from_components` calls `func` exactly once for each `ComponentId` returned by `Bundle::component_ids`. // - `Bundle::get_components` is called exactly once for each member. Relies on the above implementation to pass the correct // `StorageType` into the callback. @@ -223,7 +229,9 @@ macro_rules! tuple_impl { // https://doc.rust-lang.org/reference/expressions.html#evaluation-order-of-operands ($(<$name as Bundle>::from_components(ctx, func),)*) } + } + impl<$($name: Bundle),*> DynamicBundle for ($($name,)*) { #[allow(unused_variables, unused_mut)] #[inline(always)] fn get_components(self, func: &mut impl FnMut(StorageType, OwningPtr<'_>)) { @@ -261,13 +269,62 @@ impl SparseSetIndex for BundleId { } pub struct BundleInfo { - pub(crate) id: BundleId, - pub(crate) component_ids: Vec, + id: BundleId, + // SAFETY: Every ID in this list must be valid within the World that owns the BundleInfo, + // must have its storage initialized (i.e. columns created in tables, sparse set created), + // and must be in the same order as the source bundle type writes its components in. + component_ids: Vec, } impl BundleInfo { + /// Create a new [`BundleInfo`]. + /// + /// # Safety + /// + // Every ID in `component_ids` must be valid within the World that owns the BundleInfo, + // must have its storage initialized (i.e. columns created in tables, sparse set created), + // and must be in the same order as the source bundle type writes its components in. + unsafe fn new( + bundle_type_name: &'static str, + components: &Components, + component_ids: Vec, + id: BundleId, + ) -> BundleInfo { + let mut deduped = component_ids.clone(); + deduped.sort(); + deduped.dedup(); + + if deduped.len() != component_ids.len() { + // TODO: Replace with `Vec::partition_dedup` once https://github.com/rust-lang/rust/issues/54279 is stabilized + let mut seen = HashSet::new(); + let mut dups = Vec::new(); + for id in component_ids { + if !seen.insert(id) { + dups.push(id); + } + } + + let names = dups + .into_iter() + .map(|id| { + // SAFETY: the caller ensures component_id is valid. + unsafe { components.get_info_unchecked(id).name() } + }) + .collect::>() + .join(", "); + + panic!("Bundle {bundle_type_name} has duplicate components: {names}"); + } + + // SAFETY: The caller ensures that component_ids: + // - is valid for the associated world + // - has had its storage initialized + // - is in the same order as the source bundle type + BundleInfo { id, component_ids } + } + #[inline] - pub fn id(&self) -> BundleId { + pub const fn id(&self) -> BundleId { self.id } @@ -283,7 +340,7 @@ impl BundleInfo { components: &mut Components, storages: &'a mut Storages, archetype_id: ArchetypeId, - change_tick: u32, + change_tick: Tick, ) -> BundleInserter<'a, 'b> { let new_archetype_id = self.add_bundle_to_archetype(archetypes, storages, components, archetype_id); @@ -342,7 +399,7 @@ impl BundleInfo { archetypes: &'a mut Archetypes, components: &mut Components, storages: &'a mut Storages, - change_tick: u32, + change_tick: Tick, ) -> BundleSpawner<'a, 'b> { let new_archetype_id = self.add_bundle_to_archetype(archetypes, storages, components, ArchetypeId::EMPTY); @@ -376,14 +433,14 @@ impl BundleInfo { /// `entity`, `bundle` must match this [`BundleInfo`]'s type #[inline] #[allow(clippy::too_many_arguments)] - unsafe fn write_components( + unsafe fn write_components( &self, table: &mut Table, sparse_sets: &mut SparseSets, bundle_component_status: &S, entity: Entity, table_row: TableRow, - change_tick: u32, + change_tick: Tick, bundle: T, ) { // NOTE: get_components calls this closure on each component in "bundle order". @@ -393,11 +450,14 @@ impl BundleInfo { let component_id = *self.component_ids.get_unchecked(bundle_component); match storage_type { StorageType::Table => { - let column = table.get_column_mut(component_id).unwrap(); + let column = + // SAFETY: If component_id is in self.component_ids, BundleInfo::new requires that + // the target table contains the component. + unsafe { table.get_column_mut(component_id).debug_checked_unwrap() }; // SAFETY: bundle_component is a valid index for this bundle match bundle_component_status.get_status(bundle_component) { ComponentStatus::Added => { - column.initialize(table_row, component_ptr, Tick::new(change_tick)); + column.initialize(table_row, component_ptr, change_tick); } ComponentStatus::Mutated => { column.replace(table_row, component_ptr, change_tick); @@ -405,11 +465,11 @@ impl BundleInfo { } } StorageType::SparseSet => { - sparse_sets.get_mut(component_id).unwrap().insert( - entity, - component_ptr, - change_tick, - ); + let sparse_set = + // SAFETY: If component_id is in self.component_ids, BundleInfo::new requires that + // a sparse set exists for the component. + unsafe { sparse_sets.get_mut(component_id).debug_checked_unwrap() }; + sparse_set.insert(entity, component_ptr, change_tick); } } bundle_component += 1; @@ -508,7 +568,7 @@ pub(crate) struct BundleInserter<'a, 'b> { sparse_sets: &'a mut SparseSets, result: InsertBundleResult<'a>, archetypes_ptr: *mut Archetype, - change_tick: u32, + change_tick: Tick, } pub(crate) enum InsertBundleResult<'a> { @@ -527,7 +587,7 @@ impl<'a, 'b> BundleInserter<'a, 'b> { /// `entity` must currently exist in the source archetype for this inserter. `archetype_row` /// must be `entity`'s location in the archetype. `T` must match this [`BundleInfo`]'s type #[inline] - pub unsafe fn insert( + pub unsafe fn insert( &mut self, entity: Entity, location: EntityLocation, @@ -536,11 +596,13 @@ impl<'a, 'b> BundleInserter<'a, 'b> { match &mut self.result { InsertBundleResult::SameArchetype => { // PERF: this could be looked up during Inserter construction and stored (but borrowing makes this nasty) - let add_bundle = self - .archetype - .edges() - .get_add_bundle_internal(self.bundle_info.id) - .unwrap(); + // SAFETY: The edge is assured to be initialized when creating the BundleInserter + let add_bundle = unsafe { + self.archetype + .edges() + .get_add_bundle_internal(self.bundle_info.id) + .debug_checked_unwrap() + }; self.bundle_info.write_components( self.table, self.sparse_sets, @@ -555,7 +617,9 @@ impl<'a, 'b> BundleInserter<'a, 'b> { InsertBundleResult::NewArchetypeSameTable { new_archetype } => { let result = self.archetype.swap_remove(location.archetype_row); if let Some(swapped_entity) = result.swapped_entity { - let swapped_location = self.entities.get(swapped_entity).unwrap(); + let swapped_location = + // SAFETY: If the swap was successful, swapped_entity must be valid. + unsafe { self.entities.get(swapped_entity).debug_checked_unwrap() }; self.entities.set( swapped_entity.index(), EntityLocation { @@ -570,11 +634,13 @@ impl<'a, 'b> BundleInserter<'a, 'b> { self.entities.set(entity.index(), new_location); // PERF: this could be looked up during Inserter construction and stored (but borrowing makes this nasty) - let add_bundle = self - .archetype - .edges() - .get_add_bundle_internal(self.bundle_info.id) - .unwrap(); + // SAFETY: The edge is assured to be initialized when creating the BundleInserter + let add_bundle = unsafe { + self.archetype + .edges() + .get_add_bundle_internal(self.bundle_info.id) + .debug_checked_unwrap() + }; self.bundle_info.write_components( self.table, self.sparse_sets, @@ -592,7 +658,9 @@ impl<'a, 'b> BundleInserter<'a, 'b> { } => { let result = self.archetype.swap_remove(location.archetype_row); if let Some(swapped_entity) = result.swapped_entity { - let swapped_location = self.entities.get(swapped_entity).unwrap(); + let swapped_location = + // SAFETY: If the swap was successful, swapped_entity must be valid. + unsafe { self.entities.get(swapped_entity).debug_checked_unwrap() }; self.entities.set( swapped_entity.index(), EntityLocation { @@ -613,7 +681,9 @@ impl<'a, 'b> BundleInserter<'a, 'b> { // if an entity was moved into this entity's table spot, update its table row if let Some(swapped_entity) = move_result.swapped_entity { - let swapped_location = self.entities.get(swapped_entity).unwrap(); + let swapped_location = + // SAFETY: If the swap was successful, swapped_entity must be valid. + unsafe { self.entities.get(swapped_entity).debug_checked_unwrap() }; let swapped_archetype = if self.archetype.id() == swapped_location.archetype_id { &mut *self.archetype @@ -640,11 +710,13 @@ impl<'a, 'b> BundleInserter<'a, 'b> { } // PERF: this could be looked up during Inserter construction and stored (but borrowing makes this nasty) - let add_bundle = self - .archetype - .edges() - .get_add_bundle_internal(self.bundle_info.id) - .unwrap(); + // SAFETY: The edge is assured to be initialized when creating the BundleInserter + let add_bundle = unsafe { + self.archetype + .edges() + .get_add_bundle_internal(self.bundle_info.id) + .debug_checked_unwrap() + }; self.bundle_info.write_components( new_table, self.sparse_sets, @@ -666,7 +738,7 @@ pub(crate) struct BundleSpawner<'a, 'b> { bundle_info: &'b BundleInfo, table: &'a mut Table, sparse_sets: &'a mut SparseSets, - change_tick: u32, + change_tick: Tick, } impl<'a, 'b> BundleSpawner<'a, 'b> { @@ -677,7 +749,7 @@ impl<'a, 'b> BundleSpawner<'a, 'b> { /// # Safety /// `entity` must be allocated (but non-existent), `T` must match this [`BundleInfo`]'s type #[inline] - pub unsafe fn spawn_non_existent( + pub unsafe fn spawn_non_existent( &mut self, entity: Entity, bundle: T, @@ -712,7 +784,12 @@ impl<'a, 'b> BundleSpawner<'a, 'b> { #[derive(Default)] pub struct Bundles { bundle_infos: Vec, + /// Cache static [`BundleId`] bundle_ids: TypeIdMap, + /// Cache dynamic [`BundleId`] with multiple components + dynamic_bundle_ids: HashMap, (BundleId, Vec)>, + /// Cache optimized dynamic [`BundleId`] with single component + dynamic_component_bundle_ids: HashMap, } impl Bundles { @@ -726,6 +803,7 @@ impl Bundles { self.bundle_ids.get(&type_id).cloned() } + /// Initializes a new [`BundleInfo`] for a statically known type. pub(crate) fn init_info<'a, T: Bundle>( &'a mut self, components: &mut Components, @@ -737,50 +815,98 @@ impl Bundles { T::component_ids(components, storages, &mut |id| component_ids.push(id)); let id = BundleId(bundle_infos.len()); let bundle_info = - // SAFETY: T::component_id ensures info was created - unsafe { initialize_bundle(std::any::type_name::(), components, component_ids, id) }; + // SAFETY: T::component_id ensures its: + // - info was created + // - appropriate storage for it has been initialized. + // - was created in the same order as the components in T + unsafe { BundleInfo::new(std::any::type_name::(), components, component_ids, id) }; bundle_infos.push(bundle_info); id }); // SAFETY: index either exists, or was initialized unsafe { self.bundle_infos.get_unchecked(id.0) } } -} -/// # Safety -/// -/// `component_id` must be valid [`ComponentId`]'s -unsafe fn initialize_bundle( - bundle_type_name: &'static str, - components: &Components, - component_ids: Vec, - id: BundleId, -) -> BundleInfo { - let mut deduped = component_ids.clone(); - deduped.sort(); - deduped.dedup(); + /// Initializes a new [`BundleInfo`] for a dynamic [`Bundle`]. + /// + /// # Panics + /// + /// Panics if any of the provided [`ComponentId`]s do not exist in the + /// provided [`Components`]. + pub(crate) fn init_dynamic_info( + &mut self, + components: &mut Components, + component_ids: &[ComponentId], + ) -> (&BundleInfo, &Vec) { + let bundle_infos = &mut self.bundle_infos; - if deduped.len() != component_ids.len() { - // TODO: Replace with `Vec::partition_dedup` once https://github.com/rust-lang/rust/issues/54279 is stabilized - let mut seen = HashSet::new(); - let mut dups = Vec::new(); - for id in component_ids { - if !seen.insert(id) { - dups.push(id); - } - } + // Use `raw_entry_mut` to avoid cloning `component_ids` to access `Entry` + let (_, (bundle_id, storage_types)) = self + .dynamic_bundle_ids + .raw_entry_mut() + .from_key(component_ids) + .or_insert_with(|| { + ( + Vec::from(component_ids), + initialize_dynamic_bundle(bundle_infos, components, Vec::from(component_ids)), + ) + }); - let names = dups - .into_iter() - .map(|id| { - // SAFETY: component_id exists and is therefore valid - unsafe { components.get_info_unchecked(id).name() } - }) - .collect::>() - .join(", "); + // SAFETY: index either exists, or was initialized + let bundle_info = unsafe { bundle_infos.get_unchecked(bundle_id.0) }; - panic!("Bundle {bundle_type_name} has duplicate components: {names}"); + (bundle_info, storage_types) } - BundleInfo { id, component_ids } + /// Initializes a new [`BundleInfo`] for a dynamic [`Bundle`] with single component. + /// + /// # Panics + /// + /// Panics if the provided [`ComponentId`] does not exist in the provided [`Components`]. + pub(crate) fn init_component_info( + &mut self, + components: &mut Components, + component_id: ComponentId, + ) -> (&BundleInfo, StorageType) { + let bundle_infos = &mut self.bundle_infos; + let (bundle_id, storage_types) = self + .dynamic_component_bundle_ids + .entry(component_id) + .or_insert_with(|| { + let (id, storage_type) = + initialize_dynamic_bundle(bundle_infos, components, vec![component_id]); + // SAFETY: `storage_type` guaranteed to have length 1 + (id, storage_type[0]) + }); + + // SAFETY: index either exists, or was initialized + let bundle_info = unsafe { bundle_infos.get_unchecked(bundle_id.0) }; + + (bundle_info, *storage_types) + } +} + +/// Asserts that all components are part of [`Components`] +/// and initializes a [`BundleInfo`]. +fn initialize_dynamic_bundle( + bundle_infos: &mut Vec, + components: &Components, + component_ids: Vec, +) -> (BundleId, Vec) { + // Assert component existence + let storage_types = component_ids.iter().map(|&id| { + components.get_info(id).unwrap_or_else(|| { + panic!( + "init_dynamic_info called with component id {id:?} which doesn't exist in this world" + ) + }).storage_type() + }).collect(); + + let id = BundleId(bundle_infos.len()); + let bundle_info = + // SAFETY: `component_ids` are valid as they were just checked + unsafe { BundleInfo::new("", components, component_ids, id) }; + bundle_infos.push(bundle_info); + + (id, storage_types) } diff --git a/crates/bevy_ecs/src/change_detection.rs b/crates/bevy_ecs/src/change_detection.rs index ad55766578..273b9b119d 100644 --- a/crates/bevy_ecs/src/change_detection.rs +++ b/crates/bevy_ecs/src/change_detection.rs @@ -56,7 +56,7 @@ pub trait DetectChanges { /// For comparison, the previous change tick of a system can be read using the /// [`SystemChangeTick`](crate::system::SystemChangeTick) /// [`SystemParam`](crate::system::SystemParam). - fn last_changed(&self) -> u32; + fn last_changed(&self) -> Tick; } /// Types that implement reliable change detection. @@ -109,7 +109,7 @@ pub trait DetectChangesMut: DetectChanges { /// This is a complex and error-prone operation, primarily intended for use with rollback networking strategies. /// If you merely want to flag this data as changed, use [`set_changed`](DetectChangesMut::set_changed) instead. /// If you want to avoid triggering change detection, use [`bypass_change_detection`](DetectChangesMut::bypass_change_detection) instead. - fn set_last_changed(&mut self, last_change_tick: u32); + fn set_last_changed(&mut self, last_changed: Tick); /// Manually bypasses change detection, allowing you to mutate the underlying value without updating the change tick. /// @@ -145,19 +145,19 @@ macro_rules! change_detection_impl { fn is_added(&self) -> bool { self.ticks .added - .is_newer_than(self.ticks.last_change_tick, self.ticks.change_tick) + .is_newer_than(self.ticks.last_run, self.ticks.this_run) } #[inline] fn is_changed(&self) -> bool { self.ticks .changed - .is_newer_than(self.ticks.last_change_tick, self.ticks.change_tick) + .is_newer_than(self.ticks.last_run, self.ticks.this_run) } #[inline] - fn last_changed(&self) -> u32 { - self.ticks.changed.tick + fn last_changed(&self) -> Tick { + *self.ticks.changed } } @@ -186,16 +186,12 @@ macro_rules! change_detection_mut_impl { #[inline] fn set_changed(&mut self) { - self.ticks - .changed - .set_changed(self.ticks.change_tick); + *self.ticks.changed = self.ticks.this_run; } #[inline] - fn set_last_changed(&mut self, last_changed: u32) { - self.ticks - .changed - .set_changed(last_changed); + fn set_last_changed(&mut self, last_changed: Tick) { + *self.ticks.changed = last_changed; } #[inline] @@ -242,8 +238,8 @@ macro_rules! impl_methods { ticks: TicksMut { added: self.ticks.added, changed: self.ticks.changed, - last_change_tick: self.ticks.last_change_tick, - change_tick: self.ticks.change_tick, + last_run: self.ticks.last_run, + this_run: self.ticks.this_run, } } } @@ -299,8 +295,8 @@ macro_rules! impl_debug { pub(crate) struct Ticks<'a> { pub(crate) added: &'a Tick, pub(crate) changed: &'a Tick, - pub(crate) last_change_tick: u32, - pub(crate) change_tick: u32, + pub(crate) last_run: Tick, + pub(crate) this_run: Tick, } impl<'a> Ticks<'a> { @@ -309,14 +305,14 @@ impl<'a> Ticks<'a> { #[inline] pub(crate) unsafe fn from_tick_cells( cells: TickCells<'a>, - last_change_tick: u32, - change_tick: u32, + last_run: Tick, + this_run: Tick, ) -> Self { Self { added: cells.added.deref(), changed: cells.changed.deref(), - last_change_tick, - change_tick, + last_run, + this_run, } } } @@ -324,8 +320,8 @@ impl<'a> Ticks<'a> { pub(crate) struct TicksMut<'a> { pub(crate) added: &'a mut Tick, pub(crate) changed: &'a mut Tick, - pub(crate) last_change_tick: u32, - pub(crate) change_tick: u32, + pub(crate) last_run: Tick, + pub(crate) this_run: Tick, } impl<'a> TicksMut<'a> { @@ -334,14 +330,14 @@ impl<'a> TicksMut<'a> { #[inline] pub(crate) unsafe fn from_tick_cells( cells: TickCells<'a>, - last_change_tick: u32, - change_tick: u32, + last_run: Tick, + this_run: Tick, ) -> Self { Self { added: cells.added.deref_mut(), changed: cells.changed.deref_mut(), - last_change_tick, - change_tick, + last_run, + this_run, } } } @@ -351,8 +347,8 @@ impl<'a> From> for Ticks<'a> { Ticks { added: ticks.added, changed: ticks.changed, - last_change_tick: ticks.last_change_tick, - change_tick: ticks.change_tick, + last_run: ticks.last_run, + this_run: ticks.this_run, } } } @@ -537,6 +533,41 @@ pub struct Mut<'a, T: ?Sized> { pub(crate) ticks: TicksMut<'a>, } +impl<'a, T: ?Sized> Mut<'a, T> { + /// Creates a new change-detection enabled smart pointer. + /// In almost all cases you do not need to call this method manually, + /// as instances of `Mut` will be created by engine-internal code. + /// + /// Many use-cases of this method would be better served by [`Mut::map_unchanged`] + /// or [`Mut::reborrow`]. + /// + /// - `value` - The value wrapped by this smart pointer. + /// - `added` - A [`Tick`] that stores the tick when the wrapped value was created. + /// - `last_changed` - A [`Tick`] that stores the last time the wrapped value was changed. + /// This will be updated to the value of `change_tick` if the returned smart pointer + /// is modified. + /// - `last_run` - A [`Tick`], occurring before `this_run`, which is used + /// as a reference to determine whether the wrapped value is newly added or changed. + /// - `this_run` - A [`Tick`] corresponding to the current point in time -- "now". + pub fn new( + value: &'a mut T, + added: &'a mut Tick, + last_changed: &'a mut Tick, + last_run: Tick, + this_run: Tick, + ) -> Self { + Self { + value, + ticks: TicksMut { + added, + changed: last_changed, + last_run, + this_run, + }, + } + } +} + impl<'a, T: ?Sized> From> for Ref<'a, T> { fn from(mut_ref: Mut<'a, T>) -> Self { Self { @@ -608,8 +639,8 @@ impl<'a> MutUntyped<'a> { ticks: TicksMut { added: self.ticks.added, changed: self.ticks.changed, - last_change_tick: self.ticks.last_change_tick, - change_tick: self.ticks.change_tick, + last_run: self.ticks.last_run, + this_run: self.ticks.this_run, }, } } @@ -646,19 +677,19 @@ impl<'a> DetectChanges for MutUntyped<'a> { fn is_added(&self) -> bool { self.ticks .added - .is_newer_than(self.ticks.last_change_tick, self.ticks.change_tick) + .is_newer_than(self.ticks.last_run, self.ticks.this_run) } #[inline] fn is_changed(&self) -> bool { self.ticks .changed - .is_newer_than(self.ticks.last_change_tick, self.ticks.change_tick) + .is_newer_than(self.ticks.last_run, self.ticks.this_run) } #[inline] - fn last_changed(&self) -> u32 { - self.ticks.changed.tick + fn last_changed(&self) -> Tick { + *self.ticks.changed } } @@ -667,12 +698,12 @@ impl<'a> DetectChangesMut for MutUntyped<'a> { #[inline] fn set_changed(&mut self) { - self.ticks.changed.set_changed(self.ticks.change_tick); + *self.ticks.changed = self.ticks.this_run; } #[inline] - fn set_last_changed(&mut self, last_changed: u32) { - self.ticks.changed.set_changed(last_changed); + fn set_last_changed(&mut self, last_changed: Tick) { + *self.ticks.changed = last_changed; } #[inline] @@ -757,7 +788,7 @@ mod tests { } let mut world = World::new(); - world.last_change_tick = u32::MAX; + world.last_change_tick = Tick::new(u32::MAX); *world.change_tick.get_mut() = 0; // component added: 0, changed: 0 @@ -785,8 +816,8 @@ mod tests { let mut query = world.query::>(); for tracker in query.iter(&world) { - let ticks_since_insert = change_tick.wrapping_sub(tracker.ticks.added.tick); - let ticks_since_change = change_tick.wrapping_sub(tracker.ticks.changed.tick); + let ticks_since_insert = change_tick.relative_to(*tracker.ticks.added).get(); + let ticks_since_change = change_tick.relative_to(*tracker.ticks.changed).get(); assert!(ticks_since_insert > MAX_CHANGE_AGE); assert!(ticks_since_change > MAX_CHANGE_AGE); } @@ -795,8 +826,8 @@ mod tests { world.check_change_ticks(); for tracker in query.iter(&world) { - let ticks_since_insert = change_tick.wrapping_sub(tracker.ticks.added.tick); - let ticks_since_change = change_tick.wrapping_sub(tracker.ticks.changed.tick); + let ticks_since_insert = change_tick.relative_to(*tracker.ticks.added).get(); + let ticks_since_change = change_tick.relative_to(*tracker.ticks.changed).get(); assert!(ticks_since_insert == MAX_CHANGE_AGE); assert!(ticks_since_change == MAX_CHANGE_AGE); } @@ -811,8 +842,8 @@ mod tests { let ticks = TicksMut { added: &mut component_ticks.added, changed: &mut component_ticks.changed, - last_change_tick: 3, - change_tick: 4, + last_run: Tick::new(3), + this_run: Tick::new(4), }; let mut res = R {}; let res_mut = ResMut { @@ -821,10 +852,30 @@ mod tests { }; let into_mut: Mut = res_mut.into(); - assert_eq!(1, into_mut.ticks.added.tick); - assert_eq!(2, into_mut.ticks.changed.tick); - assert_eq!(3, into_mut.ticks.last_change_tick); - assert_eq!(4, into_mut.ticks.change_tick); + assert_eq!(1, into_mut.ticks.added.get()); + assert_eq!(2, into_mut.ticks.changed.get()); + assert_eq!(3, into_mut.ticks.last_run.get()); + assert_eq!(4, into_mut.ticks.this_run.get()); + } + + #[test] + fn mut_new() { + let mut component_ticks = ComponentTicks { + added: Tick::new(1), + changed: Tick::new(3), + }; + let mut res = R {}; + + let val = Mut::new( + &mut res, + &mut component_ticks.added, + &mut component_ticks.changed, + Tick::new(2), // last_run + Tick::new(4), // this_run + ); + + assert!(!val.is_added()); + assert!(val.is_changed()); } #[test] @@ -836,8 +887,8 @@ mod tests { let ticks = TicksMut { added: &mut component_ticks.added, changed: &mut component_ticks.changed, - last_change_tick: 3, - change_tick: 4, + last_run: Tick::new(3), + this_run: Tick::new(4), }; let mut res = R {}; let non_send_mut = NonSendMut { @@ -846,10 +897,10 @@ mod tests { }; let into_mut: Mut = non_send_mut.into(); - assert_eq!(1, into_mut.ticks.added.tick); - assert_eq!(2, into_mut.ticks.changed.tick); - assert_eq!(3, into_mut.ticks.last_change_tick); - assert_eq!(4, into_mut.ticks.change_tick); + assert_eq!(1, into_mut.ticks.added.get()); + assert_eq!(2, into_mut.ticks.changed.get()); + assert_eq!(3, into_mut.ticks.last_run.get()); + assert_eq!(4, into_mut.ticks.this_run.get()); } #[test] @@ -857,7 +908,8 @@ mod tests { use super::*; struct Outer(i64); - let (last_change_tick, change_tick) = (2, 3); + let last_run = Tick::new(2); + let this_run = Tick::new(3); let mut component_ticks = ComponentTicks { added: Tick::new(1), changed: Tick::new(2), @@ -865,8 +917,8 @@ mod tests { let ticks = TicksMut { added: &mut component_ticks.added, changed: &mut component_ticks.changed, - last_change_tick, - change_tick, + last_run, + this_run, }; let mut outer = Outer(0); @@ -884,7 +936,7 @@ mod tests { *inner = 64; assert!(inner.is_changed()); // Modifying one field of a component should flag a change for the entire component. - assert!(component_ticks.is_changed(last_change_tick, change_tick)); + assert!(component_ticks.is_changed(last_run, this_run)); } #[test] diff --git a/crates/bevy_ecs/src/component.rs b/crates/bevy_ecs/src/component.rs index ade0eb6b7d..a7c090e0f4 100644 --- a/crates/bevy_ecs/src/component.rs +++ b/crates/bevy_ecs/src/component.rs @@ -586,61 +586,72 @@ impl Components { } } -/// Used to track changes in state between system runs, e.g. components being added or accessed mutably. +/// A value that tracks when a system ran relative to other systems. +/// This is used to power change detection. #[derive(Copy, Clone, Debug)] pub struct Tick { - pub(crate) tick: u32, + tick: u32, } impl Tick { + /// The maximum relative age for a change tick. + /// The value of this is equal to [`crate::change_detection::MAX_CHANGE_AGE`]. + /// + /// Since change detection will not work for any ticks older than this, + /// ticks are periodically scanned to ensure their relative values are below this. + pub const MAX: Self = Self::new(MAX_CHANGE_AGE); + pub const fn new(tick: u32) -> Self { Self { tick } } + /// Gets the value of this change tick. #[inline] - /// Returns `true` if this `Tick` occurred since the system's `last_change_tick`. + pub const fn get(self) -> u32 { + self.tick + } + + /// Sets the value of this change tick. + #[inline] + pub fn set(&mut self, tick: u32) { + self.tick = tick; + } + + #[inline] + /// Returns `true` if this `Tick` occurred since the system's `last_run`. /// - /// `change_tick` is the current tick of the system, used as a reference to help deal with wraparound. - pub fn is_newer_than(&self, last_change_tick: u32, change_tick: u32) -> bool { - // This works even with wraparound because the world tick (`change_tick`) is always "newer" than - // `last_change_tick` and `self.tick`, and we scan periodically to clamp `ComponentTicks` values + /// `this_run` is the current tick of the system, used as a reference to help deal with wraparound. + pub fn is_newer_than(self, last_run: Tick, this_run: Tick) -> bool { + // This works even with wraparound because the world tick (`this_run`) is always "newer" than + // `last_run` and `self.tick`, and we scan periodically to clamp `ComponentTicks` values // so they never get older than `u32::MAX` (the difference would overflow). // // The clamp here ensures determinism (since scans could differ between app runs). - let ticks_since_insert = change_tick.wrapping_sub(self.tick).min(MAX_CHANGE_AGE); - let ticks_since_system = change_tick - .wrapping_sub(last_change_tick) - .min(MAX_CHANGE_AGE); + let ticks_since_insert = this_run.relative_to(self).tick.min(MAX_CHANGE_AGE); + let ticks_since_system = this_run.relative_to(last_run).tick.min(MAX_CHANGE_AGE); ticks_since_system > ticks_since_insert } - pub(crate) fn check_tick(&mut self, change_tick: u32) { - let age = change_tick.wrapping_sub(self.tick); - // This comparison assumes that `age` has not overflowed `u32::MAX` before, which will be true - // so long as this check always runs before that can happen. - if age > MAX_CHANGE_AGE { - self.tick = change_tick.wrapping_sub(MAX_CHANGE_AGE); - } + /// Returns a change tick representing the relationship between `self` and `other`. + pub(crate) fn relative_to(self, other: Self) -> Self { + let tick = self.tick.wrapping_sub(other.tick); + Self { tick } } - /// Manually sets the change tick. + /// Wraps this change tick's value if it exceeds [`Tick::MAX`]. /// - /// This is normally done automatically via the [`DerefMut`](std::ops::DerefMut) implementation - /// on [`Mut`](crate::change_detection::Mut), [`ResMut`](crate::change_detection::ResMut), etc. - /// However, components and resources that make use of interior mutability might require manual updates. - /// - /// # Example - /// ```rust,no_run - /// # use bevy_ecs::{world::World, component::ComponentTicks}; - /// let world: World = unimplemented!(); - /// let component_ticks: ComponentTicks = unimplemented!(); - /// - /// component_ticks.set_changed(world.read_change_tick()); - /// ``` - #[inline] - pub fn set_changed(&mut self, change_tick: u32) { - self.tick = change_tick; + /// Returns `true` if wrapping was performed. Otherwise, returns `false`. + pub(crate) fn check_tick(&mut self, tick: Tick) -> bool { + let age = tick.relative_to(*self); + // This comparison assumes that `age` has not overflowed `u32::MAX` before, which will be true + // so long as this check always runs before that can happen. + if age.get() > Self::MAX.get() { + *self = tick.relative_to(Self::MAX); + true + } else { + false + } } } @@ -673,20 +684,20 @@ pub struct ComponentTicks { impl ComponentTicks { #[inline] /// Returns `true` if the component was added after the system last ran. - pub fn is_added(&self, last_change_tick: u32, change_tick: u32) -> bool { - self.added.is_newer_than(last_change_tick, change_tick) + pub fn is_added(&self, last_run: Tick, this_run: Tick) -> bool { + self.added.is_newer_than(last_run, this_run) } #[inline] /// Returns `true` if the component was added or mutably dereferenced after the system last ran. - pub fn is_changed(&self, last_change_tick: u32, change_tick: u32) -> bool { - self.changed.is_newer_than(last_change_tick, change_tick) + pub fn is_changed(&self, last_run: Tick, this_run: Tick) -> bool { + self.changed.is_newer_than(last_run, this_run) } - pub(crate) fn new(change_tick: u32) -> Self { + pub(crate) fn new(change_tick: Tick) -> Self { Self { - added: Tick::new(change_tick), - changed: Tick::new(change_tick), + added: change_tick, + changed: change_tick, } } @@ -705,8 +716,8 @@ impl ComponentTicks { /// component_ticks.set_changed(world.read_change_tick()); /// ``` #[inline] - pub fn set_changed(&mut self, change_tick: u32) { - self.changed.set_changed(change_tick); + pub fn set_changed(&mut self, change_tick: Tick) { + self.changed = change_tick; } } diff --git a/crates/bevy_ecs/src/entity/mod.rs b/crates/bevy_ecs/src/entity/mod.rs index 1b1afe3151..843e3262a2 100644 --- a/crates/bevy_ecs/src/entity/mod.rs +++ b/crates/bevy_ecs/src/entity/mod.rs @@ -571,8 +571,7 @@ impl Entities { /// Returns the location of an [`Entity`]. /// Note: for pending entities, returns `Some(EntityLocation::INVALID)`. pub fn get(&self, entity: Entity) -> Option { - if (entity.index as usize) < self.meta.len() { - let meta = &self.meta[entity.index as usize]; + if let Some(meta) = self.meta.get(entity.index as usize) { if meta.generation != entity.generation || meta.location.archetype_id == ArchetypeId::INVALID { diff --git a/crates/bevy_ecs/src/event.rs b/crates/bevy_ecs/src/event.rs index 0bf6429175..3cdfc19445 100644 --- a/crates/bevy_ecs/src/event.rs +++ b/crates/bevy_ecs/src/event.rs @@ -939,7 +939,7 @@ mod tests { world.send_event(TestEvent { i: 4 }); let mut schedule = Schedule::new(); - schedule.add_system(|mut events: EventReader| { + schedule.add_systems(|mut events: EventReader| { let mut iter = events.iter(); assert_eq!(iter.next(), Some(&TestEvent { i: 0 })); diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index d8d8e5948a..8107de37e8 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -25,9 +25,6 @@ pub use bevy_ptr as ptr; /// Most commonly used re-exported types. pub mod prelude { - #[doc(hidden)] - #[allow(deprecated)] - pub use crate::query::ChangeTrackers; #[doc(hidden)] #[cfg(feature = "bevy_reflect")] pub use crate::reflect::{ReflectComponent, ReflectResource}; @@ -42,9 +39,8 @@ pub mod prelude { removal_detection::RemovedComponents, schedule::{ apply_state_transition, apply_system_buffers, common_conditions::*, Condition, - IntoSystemConfig, IntoSystemConfigs, IntoSystemSet, IntoSystemSetConfig, - IntoSystemSetConfigs, NextState, OnEnter, OnExit, OnUpdate, Schedule, Schedules, State, - States, SystemSet, + IntoSystemConfigs, IntoSystemSet, IntoSystemSetConfig, IntoSystemSetConfigs, NextState, + OnEnter, OnExit, OnTransition, OnUpdate, Schedule, Schedules, State, States, SystemSet, }, system::{ adapter as system_adapter, @@ -1299,33 +1295,6 @@ mod tests { .unwrap(); } - #[test] - #[allow(deprecated)] - fn trackers_query() { - use crate::prelude::ChangeTrackers; - - let mut world = World::default(); - let e1 = world.spawn((A(0), B(0))).id(); - world.spawn(B(0)); - - let mut trackers_query = world.query::>>(); - let trackers = trackers_query.iter(&world).collect::>(); - let a_trackers = trackers[0].as_ref().unwrap(); - assert!(trackers[1].is_none()); - assert!(a_trackers.is_added()); - assert!(a_trackers.is_changed()); - world.clear_trackers(); - let trackers = trackers_query.iter(&world).collect::>(); - let a_trackers = trackers[0].as_ref().unwrap(); - assert!(!a_trackers.is_added()); - assert!(!a_trackers.is_changed()); - *world.get_mut(e1).unwrap() = A(1); - let trackers = trackers_query.iter(&world).collect::>(); - let a_trackers = trackers[0].as_ref().unwrap(); - assert!(!a_trackers.is_added()); - assert!(a_trackers.is_changed()); - } - #[test] fn exact_size_query() { let mut world = World::default(); diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 7e4c7a3f74..c9a18d1081 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -1,7 +1,7 @@ use crate::{ archetype::{Archetype, ArchetypeComponentId}, change_detection::{Ticks, TicksMut}, - component::{Component, ComponentId, ComponentStorage, ComponentTicks, StorageType, Tick}, + component::{Component, ComponentId, ComponentStorage, StorageType, Tick}, entity::Entity, query::{Access, DebugCheckedUnwrap, FilteredAccess}, storage::{ComponentSparseSet, Table, TableRow}, @@ -37,7 +37,7 @@ use std::{cell::UnsafeCell, marker::PhantomData}; /// Wrapping it into an `Option` will increase the query search space, and it will return `None` if an entity doesn't satisfy the `WorldQuery`. /// - **[`AnyOf`].** /// Equivalent to wrapping each world query inside it into an `Option`. -/// - **[`ChangeTrackers`].** +/// - **[`Ref`].** /// Similar to change detection filters but it is used as a query fetch parameter. /// It exposes methods to check for changes to the wrapped component. /// @@ -92,16 +92,13 @@ use std::{cell::UnsafeCell, marker::PhantomData}; /// /// ## Macro expansion /// -/// Expanding the macro will declare three or six additional structs, depending on whether or not the struct is marked as mutable. +/// Expanding the macro will declare one or three additional structs, depending on whether or not the struct is marked as mutable. /// For a struct named `X`, the additional structs will be: /// /// |Struct name|`mutable` only|Description| /// |:---:|:---:|---| -/// |`XState`|---|Used as the [`State`] type for `X` and `XReadOnly`| /// |`XItem`|---|The type of the query item for `X`| -/// |`XFetch`|---|Used as the [`Fetch`] type for `X`| /// |`XReadOnlyItem`|✓|The type of the query item for `XReadOnly`| -/// |`XReadOnlyFetch`|✓|Used as the [`Fetch`] type for `XReadOnly`| /// |`XReadOnly`|✓|[`ReadOnly`] variant of `X`| /// /// ## Adding mutable references @@ -296,7 +293,6 @@ use std::{cell::UnsafeCell, marker::PhantomData}; /// [`Added`]: crate::query::Added /// [`fetch`]: Self::fetch /// [`Changed`]: crate::query::Changed -/// [`Fetch`]: crate::query::WorldQuery::Fetch /// [`matches_component_set`]: Self::matches_component_set /// [`Or`]: crate::query::Or /// [`Query`]: crate::system::Query @@ -333,8 +329,8 @@ pub unsafe trait WorldQuery { unsafe fn init_fetch<'w>( world: &'w World, state: &Self::State, - last_change_tick: u32, - change_tick: u32, + last_run: Tick, + this_run: Tick, ) -> Self::Fetch<'w>; /// While this function can be called for any query, it is always safe to call if `Self: ReadOnlyWorldQuery` holds. @@ -464,8 +460,8 @@ unsafe impl WorldQuery for Entity { unsafe fn init_fetch<'w>( _world: &'w World, _state: &Self::State, - _last_change_tick: u32, - _change_tick: u32, + _last_run: Tick, + _this_run: Tick, ) -> Self::Fetch<'w> { } @@ -546,8 +542,8 @@ unsafe impl WorldQuery for &T { unsafe fn init_fetch<'w>( world: &'w World, &component_id: &ComponentId, - _last_change_tick: u32, - _change_tick: u32, + _last_run: Tick, + _this_run: Tick, ) -> ReadFetch<'w, T> { ReadFetch { table_components: None, @@ -664,8 +660,8 @@ pub struct RefFetch<'w, T> { // T::Storage = SparseStorage sparse_set: Option<&'w ComponentSparseSet>, - last_change_tick: u32, - change_tick: u32, + last_run: Tick, + this_run: Tick, } /// SAFETY: `Self` is the same as `Self::ReadOnly` @@ -691,8 +687,8 @@ unsafe impl<'__w, T: Component> WorldQuery for Ref<'__w, T> { unsafe fn init_fetch<'w>( world: &'w World, &component_id: &ComponentId, - last_change_tick: u32, - change_tick: u32, + last_run: Tick, + this_run: Tick, ) -> RefFetch<'w, T> { RefFetch { table_data: None, @@ -703,8 +699,8 @@ unsafe impl<'__w, T: Component> WorldQuery for Ref<'__w, T> { .get(component_id) .debug_checked_unwrap() }), - last_change_tick, - change_tick, + last_run, + this_run, } } @@ -712,8 +708,8 @@ unsafe impl<'__w, T: Component> WorldQuery for Ref<'__w, T> { RefFetch { table_data: fetch.table_data, sparse_set: fetch.sparse_set, - last_change_tick: fetch.last_change_tick, - change_tick: fetch.change_tick, + last_run: fetch.last_run, + this_run: fetch.this_run, } } @@ -758,8 +754,8 @@ unsafe impl<'__w, T: Component> WorldQuery for Ref<'__w, T> { ticks: Ticks { added: added_ticks.get(table_row.index()).deref(), changed: changed_ticks.get(table_row.index()).deref(), - change_tick: fetch.change_tick, - last_change_tick: fetch.last_change_tick, + this_run: fetch.this_run, + last_run: fetch.last_run, }, } } @@ -771,7 +767,7 @@ unsafe impl<'__w, T: Component> WorldQuery for Ref<'__w, T> { .debug_checked_unwrap(); Ref { value: component.deref(), - ticks: Ticks::from_tick_cells(ticks, fetch.last_change_tick, fetch.change_tick), + ticks: Ticks::from_tick_cells(ticks, fetch.last_run, fetch.this_run), } } } @@ -825,8 +821,8 @@ pub struct WriteFetch<'w, T> { // T::Storage = SparseStorage sparse_set: Option<&'w ComponentSparseSet>, - last_change_tick: u32, - change_tick: u32, + last_run: Tick, + this_run: Tick, } /// SAFETY: access of `&T` is a subset of `&mut T` @@ -852,8 +848,8 @@ unsafe impl<'__w, T: Component> WorldQuery for &'__w mut T { unsafe fn init_fetch<'w>( world: &'w World, &component_id: &ComponentId, - last_change_tick: u32, - change_tick: u32, + last_run: Tick, + this_run: Tick, ) -> WriteFetch<'w, T> { WriteFetch { table_data: None, @@ -864,8 +860,8 @@ unsafe impl<'__w, T: Component> WorldQuery for &'__w mut T { .get(component_id) .debug_checked_unwrap() }), - last_change_tick, - change_tick, + last_run, + this_run, } } @@ -873,8 +869,8 @@ unsafe impl<'__w, T: Component> WorldQuery for &'__w mut T { WriteFetch { table_data: fetch.table_data, sparse_set: fetch.sparse_set, - last_change_tick: fetch.last_change_tick, - change_tick: fetch.change_tick, + last_run: fetch.last_run, + this_run: fetch.this_run, } } @@ -919,8 +915,8 @@ unsafe impl<'__w, T: Component> WorldQuery for &'__w mut T { ticks: TicksMut { added: added_ticks.get(table_row.index()).deref_mut(), changed: changed_ticks.get(table_row.index()).deref_mut(), - change_tick: fetch.change_tick, - last_change_tick: fetch.last_change_tick, + this_run: fetch.this_run, + last_run: fetch.last_run, }, } } @@ -932,11 +928,7 @@ unsafe impl<'__w, T: Component> WorldQuery for &'__w mut T { .debug_checked_unwrap(); Mut { value: component.assert_unique().deref_mut(), - ticks: TicksMut::from_tick_cells( - ticks, - fetch.last_change_tick, - fetch.change_tick, - ), + ticks: TicksMut::from_tick_cells(ticks, fetch.last_run, fetch.this_run), } } } @@ -1000,11 +992,11 @@ unsafe impl WorldQuery for Option { unsafe fn init_fetch<'w>( world: &'w World, state: &T::State, - last_change_tick: u32, - change_tick: u32, + last_run: Tick, + this_run: Tick, ) -> OptionFetch<'w, T> { OptionFetch { - fetch: T::init_fetch(world, state, last_change_tick, change_tick), + fetch: T::init_fetch(world, state, last_run, this_run), matches: false, } } @@ -1083,246 +1075,6 @@ unsafe impl WorldQuery for Option { /// SAFETY: [`OptionFetch`] is read only because `T` is read only unsafe impl ReadOnlyWorldQuery for Option {} -/// [`WorldQuery`] that tracks changes and additions for component `T`. -/// -/// Wraps a [`Component`] to track whether the component changed for the corresponding entities in -/// a query since the last time the system that includes these queries ran. -/// -/// If you only care about entities that changed or that got added use the -/// [`Changed`](crate::query::Changed) and [`Added`](crate::query::Added) filters instead. -/// -/// # Examples -/// -/// ``` -/// # use bevy_ecs::component::Component; -/// # use bevy_ecs::query::ChangeTrackers; -/// # use bevy_ecs::system::IntoSystem; -/// # use bevy_ecs::system::Query; -/// # -/// # #[derive(Component, Debug)] -/// # struct Name {}; -/// # #[derive(Component)] -/// # struct Transform {}; -/// # -/// fn print_moving_objects_system(query: Query<(&Name, ChangeTrackers)>) { -/// for (name, tracker) in &query { -/// if tracker.is_changed() { -/// println!("Entity moved: {:?}", name); -/// } else { -/// println!("Entity stood still: {:?}", name); -/// } -/// } -/// } -/// # bevy_ecs::system::assert_is_system(print_moving_objects_system); -/// ``` -#[deprecated = "`ChangeTrackers` will be removed in bevy 0.11. Use `bevy_ecs::prelude::Ref` instead."] -pub struct ChangeTrackers { - pub(crate) component_ticks: ComponentTicks, - pub(crate) last_change_tick: u32, - pub(crate) change_tick: u32, - marker: PhantomData, -} - -#[allow(deprecated)] -impl Clone for ChangeTrackers { - fn clone(&self) -> Self { - *self - } -} - -#[allow(deprecated)] -impl Copy for ChangeTrackers {} - -#[allow(deprecated)] -impl std::fmt::Debug for ChangeTrackers { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("ChangeTrackers") - .field("component_ticks", &self.component_ticks) - .field("last_change_tick", &self.last_change_tick) - .field("change_tick", &self.change_tick) - .finish() - } -} - -#[allow(deprecated)] -impl ChangeTrackers { - /// Returns true if this component has been added since the last execution of this system. - pub fn is_added(&self) -> bool { - self.component_ticks - .is_added(self.last_change_tick, self.change_tick) - } - - /// Returns true if this component has been changed since the last execution of this system. - pub fn is_changed(&self) -> bool { - self.component_ticks - .is_changed(self.last_change_tick, self.change_tick) - } -} - -#[doc(hidden)] -pub struct ChangeTrackersFetch<'w, T> { - // T::Storage = TableStorage - table_added: Option>>, - table_changed: Option>>, - // T::Storage = SparseStorage - sparse_set: Option<&'w ComponentSparseSet>, - - marker: PhantomData, - last_change_tick: u32, - change_tick: u32, -} - -#[allow(deprecated)] -// SAFETY: `ROQueryFetch` is the same as `QueryFetch` -unsafe impl WorldQuery for ChangeTrackers { - type Fetch<'w> = ChangeTrackersFetch<'w, T>; - type Item<'w> = ChangeTrackers; - type ReadOnly = Self; - type State = ComponentId; - - fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort> { - item - } - - const IS_DENSE: bool = { - match T::Storage::STORAGE_TYPE { - StorageType::Table => true, - StorageType::SparseSet => false, - } - }; - - const IS_ARCHETYPAL: bool = true; - - unsafe fn init_fetch<'w>( - world: &'w World, - &component_id: &ComponentId, - last_change_tick: u32, - change_tick: u32, - ) -> ChangeTrackersFetch<'w, T> { - ChangeTrackersFetch { - table_added: None, - table_changed: None, - sparse_set: (T::Storage::STORAGE_TYPE == StorageType::SparseSet).then(|| { - world - .storages() - .sparse_sets - .get(component_id) - .debug_checked_unwrap() - }), - marker: PhantomData, - last_change_tick, - change_tick, - } - } - - unsafe fn clone_fetch<'w>(fetch: &Self::Fetch<'w>) -> Self::Fetch<'w> { - ChangeTrackersFetch { - table_added: fetch.table_added, - table_changed: fetch.table_changed, - sparse_set: fetch.sparse_set, - marker: fetch.marker, - last_change_tick: fetch.last_change_tick, - change_tick: fetch.change_tick, - } - } - - #[inline] - unsafe fn set_archetype<'w>( - fetch: &mut ChangeTrackersFetch<'w, T>, - component_id: &ComponentId, - _archetype: &'w Archetype, - table: &'w Table, - ) { - if Self::IS_DENSE { - Self::set_table(fetch, component_id, table); - } - } - - #[inline] - unsafe fn set_table<'w>( - fetch: &mut ChangeTrackersFetch<'w, T>, - &id: &ComponentId, - table: &'w Table, - ) { - let column = table.get_column(id).debug_checked_unwrap(); - fetch.table_added = Some(column.get_added_ticks_slice().into()); - fetch.table_changed = Some(column.get_changed_ticks_slice().into()); - } - - #[inline(always)] - unsafe fn fetch<'w>( - fetch: &mut Self::Fetch<'w>, - entity: Entity, - table_row: TableRow, - ) -> Self::Item<'w> { - match T::Storage::STORAGE_TYPE { - StorageType::Table => ChangeTrackers { - component_ticks: { - ComponentTicks { - added: fetch - .table_added - .debug_checked_unwrap() - .get(table_row.index()) - .read(), - changed: fetch - .table_changed - .debug_checked_unwrap() - .get(table_row.index()) - .read(), - } - }, - marker: PhantomData, - last_change_tick: fetch.last_change_tick, - change_tick: fetch.change_tick, - }, - StorageType::SparseSet => ChangeTrackers { - component_ticks: fetch - .sparse_set - .debug_checked_unwrap() - .get_ticks(entity) - .debug_checked_unwrap(), - marker: PhantomData, - last_change_tick: fetch.last_change_tick, - change_tick: fetch.change_tick, - }, - } - } - - fn update_component_access(&id: &ComponentId, access: &mut FilteredAccess) { - assert!( - !access.access().has_write(id), - "ChangeTrackers<{}> conflicts with a previous access in this query. Shared access cannot coincide with exclusive access.", - std::any::type_name::() - ); - access.add_read(id); - } - - fn update_archetype_component_access( - &id: &ComponentId, - archetype: &Archetype, - access: &mut Access, - ) { - if let Some(archetype_component_id) = archetype.get_archetype_component_id(id) { - access.add_read(archetype_component_id); - } - } - - fn init_state(world: &mut World) -> ComponentId { - world.init_component::() - } - - fn matches_component_set( - &id: &ComponentId, - set_contains_id: &impl Fn(ComponentId) -> bool, - ) -> bool { - set_contains_id(id) - } -} - -#[allow(deprecated)] -/// SAFETY: access is read only -unsafe impl ReadOnlyWorldQuery for ChangeTrackers {} - macro_rules! impl_tuple_fetch { ($(($name: ident, $state: ident)),*) => { #[allow(non_snake_case)] @@ -1342,9 +1094,9 @@ macro_rules! impl_tuple_fetch { } #[allow(clippy::unused_unit)] - unsafe fn init_fetch<'w>(_world: &'w World, state: &Self::State, _last_change_tick: u32, _change_tick: u32) -> Self::Fetch<'w> { + unsafe fn init_fetch<'w>(_world: &'w World, state: &Self::State, _last_run: Tick, _this_run: Tick) -> Self::Fetch<'w> { let ($($name,)*) = state; - ($($name::init_fetch(_world, $name, _last_change_tick, _change_tick),)*) + ($($name::init_fetch(_world, $name, _last_run, _this_run),)*) } unsafe fn clone_fetch<'w>( @@ -1451,9 +1203,9 @@ macro_rules! impl_anytuple_fetch { } #[allow(clippy::unused_unit)] - unsafe fn init_fetch<'w>(_world: &'w World, state: &Self::State, _last_change_tick: u32, _change_tick: u32) -> Self::Fetch<'w> { + unsafe fn init_fetch<'w>(_world: &'w World, state: &Self::State, _last_run: Tick, _this_run: Tick) -> Self::Fetch<'w> { let ($($name,)*) = state; - ($(($name::init_fetch(_world, $name, _last_change_tick, _change_tick), false),)*) + ($(($name::init_fetch(_world, $name, _last_run, _this_run), false),)*) } unsafe fn clone_fetch<'w>( @@ -1589,13 +1341,7 @@ unsafe impl WorldQuery for NopWorldQuery { const IS_ARCHETYPAL: bool = true; #[inline(always)] - unsafe fn init_fetch( - _world: &World, - _state: &Q::State, - _last_change_tick: u32, - _change_tick: u32, - ) { - } + unsafe fn init_fetch(_world: &World, _state: &Q::State, _last_run: Tick, _this_run: Tick) {} unsafe fn clone_fetch<'w>(_fetch: &Self::Fetch<'w>) -> Self::Fetch<'w> {} @@ -1642,3 +1388,65 @@ unsafe impl WorldQuery for NopWorldQuery { /// SAFETY: `NopFetch` never accesses any data unsafe impl ReadOnlyWorldQuery for NopWorldQuery {} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{self as bevy_ecs, system::Query}; + + #[derive(Component)] + pub struct A; + + // Ensures that each field of a `WorldQuery` struct's read-only variant + // has the same visibility as its corresponding mutable field. + #[test] + fn read_only_field_visibility() { + mod private { + use super::*; + + #[derive(WorldQuery)] + #[world_query(mutable)] + pub struct Q { + pub a: &'static mut A, + } + } + + let _ = private::QReadOnly { a: &A }; + + fn my_system(query: Query) { + for q in &query { + let _ = &q.a; + } + } + + crate::system::assert_is_system(my_system); + } + + // Ensures that metadata types generated by the WorldQuery macro + // do not conflict with user-defined types. + // Regression test for https://github.com/bevyengine/bevy/issues/8010. + #[test] + fn world_query_metadata_collision() { + // The metadata types generated would be named `ClientState` and `ClientFetch`, + // but they should rename themselves to avoid conflicts. + #[derive(WorldQuery)] + pub struct Client { + pub state: &'static S, + pub fetch: &'static ClientFetch, + } + + pub trait ClientState: Component {} + + #[derive(Component)] + pub struct ClientFetch; + + #[derive(Component)] + pub struct C; + + impl ClientState for C {} + + fn client_system(_: Query>) {} + + crate::system::assert_is_system(client_system); + } +} diff --git a/crates/bevy_ecs/src/query/filter.rs b/crates/bevy_ecs/src/query/filter.rs index b2dd9023a6..41f2a7941b 100644 --- a/crates/bevy_ecs/src/query/filter.rs +++ b/crates/bevy_ecs/src/query/filter.rs @@ -50,13 +50,7 @@ unsafe impl WorldQuery for With { fn shrink<'wlong: 'wshort, 'wshort>(_: Self::Item<'wlong>) -> Self::Item<'wshort> {} - unsafe fn init_fetch( - _world: &World, - _state: &ComponentId, - _last_change_tick: u32, - _change_tick: u32, - ) { - } + unsafe fn init_fetch(_world: &World, _state: &ComponentId, _last_run: Tick, _this_run: Tick) {} unsafe fn clone_fetch<'w>(_fetch: &Self::Fetch<'w>) -> Self::Fetch<'w> {} @@ -152,13 +146,7 @@ unsafe impl WorldQuery for Without { fn shrink<'wlong: 'wshort, 'wshort>(_: Self::Item<'wlong>) -> Self::Item<'wshort> {} - unsafe fn init_fetch( - _world: &World, - _state: &ComponentId, - _last_change_tick: u32, - _change_tick: u32, - ) { - } + unsafe fn init_fetch(_world: &World, _state: &ComponentId, _last_run: Tick, _this_run: Tick) {} unsafe fn clone_fetch<'w>(_fetch: &Self::Fetch<'w>) -> Self::Fetch<'w> {} @@ -277,10 +265,10 @@ macro_rules! impl_query_filter_tuple { const IS_ARCHETYPAL: bool = true $(&& $filter::IS_ARCHETYPAL)*; - unsafe fn init_fetch<'w>(world: &'w World, state: &Self::State, last_change_tick: u32, change_tick: u32) -> Self::Fetch<'w> { + unsafe fn init_fetch<'w>(world: &'w World, state: &Self::State, last_run: Tick, this_run: Tick) -> Self::Fetch<'w> { let ($($filter,)*) = state; ($(OrFetch { - fetch: $filter::init_fetch(world, $filter, last_change_tick, change_tick), + fetch: $filter::init_fetch(world, $filter, last_run, this_run), matches: false, },)*) } @@ -417,8 +405,8 @@ macro_rules! impl_tick_filter { table_ticks: Option< ThinSlicePtr<'w, UnsafeCell>>, marker: PhantomData, sparse_set: Option<&'w ComponentSparseSet>, - last_change_tick: u32, - change_tick: u32, + last_run: Tick, + this_run: Tick, } // SAFETY: `Self::ReadOnly` is the same as `Self` @@ -432,7 +420,7 @@ macro_rules! impl_tick_filter { item } - unsafe fn init_fetch<'w>(world: &'w World, &id: &ComponentId, last_change_tick: u32, change_tick: u32) -> Self::Fetch<'w> { + unsafe fn init_fetch<'w>(world: &'w World, &id: &ComponentId, last_run: Tick, this_run: Tick) -> Self::Fetch<'w> { Self::Fetch::<'w> { table_ticks: None, sparse_set: (T::Storage::STORAGE_TYPE == StorageType::SparseSet) @@ -443,8 +431,8 @@ macro_rules! impl_tick_filter { .debug_checked_unwrap() }), marker: PhantomData, - last_change_tick, - change_tick, + last_run, + this_run, } } @@ -454,8 +442,8 @@ macro_rules! impl_tick_filter { $fetch_name { table_ticks: fetch.table_ticks, sparse_set: fetch.sparse_set, - last_change_tick: fetch.last_change_tick, - change_tick: fetch.change_tick, + last_run: fetch.last_run, + this_run: fetch.this_run, marker: PhantomData, } } @@ -509,7 +497,7 @@ macro_rules! impl_tick_filter { .debug_checked_unwrap() .get(table_row.index()) .deref() - .is_newer_than(fetch.last_change_tick, fetch.change_tick) + .is_newer_than(fetch.last_run, fetch.this_run) } StorageType::SparseSet => { let sparse_set = &fetch @@ -518,7 +506,7 @@ macro_rules! impl_tick_filter { $get_sparse_set(sparse_set, entity) .debug_checked_unwrap() .deref() - .is_newer_than(fetch.last_change_tick, fetch.change_tick) + .is_newer_than(fetch.last_run, fetch.this_run) } } } @@ -608,7 +596,7 @@ impl_tick_filter!( /// Bevy does not compare components to their previous values. /// /// To retain all results without filtering but still check whether they were changed after the - /// system last ran, use [`ChangeTrackers`](crate::query::ChangeTrackers). + /// system last ran, use [`Ref`](crate::change_detection::Ref). /// /// # Examples /// diff --git a/crates/bevy_ecs/src/query/iter.rs b/crates/bevy_ecs/src/query/iter.rs index d456b04410..893df47aac 100644 --- a/crates/bevy_ecs/src/query/iter.rs +++ b/crates/bevy_ecs/src/query/iter.rs @@ -1,5 +1,6 @@ use crate::{ archetype::{ArchetypeEntity, ArchetypeId, Archetypes}, + component::Tick, entity::{Entities, Entity}, prelude::World, query::{ArchetypeFilter, DebugCheckedUnwrap, QueryState, WorldQuery}, @@ -29,14 +30,14 @@ impl<'w, 's, Q: WorldQuery, F: ReadOnlyWorldQuery> QueryIter<'w, 's, Q, F> { pub(crate) unsafe fn new( world: &'w World, query_state: &'s QueryState, - last_change_tick: u32, - change_tick: u32, + last_run: Tick, + this_run: Tick, ) -> Self { QueryIter { query_state, tables: &world.storages().tables, archetypes: &world.archetypes, - cursor: QueryIterationCursor::init(world, query_state, last_change_tick, change_tick), + cursor: QueryIterationCursor::init(world, query_state, last_run, this_run), } } } @@ -98,21 +99,11 @@ where world: &'w World, query_state: &'s QueryState, entity_list: EntityList, - last_change_tick: u32, - change_tick: u32, + last_run: Tick, + this_run: Tick, ) -> QueryManyIter<'w, 's, Q, F, I> { - let fetch = Q::init_fetch( - world, - &query_state.fetch_state, - last_change_tick, - change_tick, - ); - let filter = F::init_fetch( - world, - &query_state.filter_state, - last_change_tick, - change_tick, - ); + let fetch = Q::init_fetch(world, &query_state.fetch_state, last_run, this_run); + let filter = F::init_fetch(world, &query_state.filter_state, last_run, this_run); QueryManyIter { query_state, entities: &world.entities, @@ -298,8 +289,8 @@ impl<'w, 's, Q: WorldQuery, F: ReadOnlyWorldQuery, const K: usize> pub(crate) unsafe fn new( world: &'w World, query_state: &'s QueryState, - last_change_tick: u32, - change_tick: u32, + last_run: Tick, + this_run: Tick, ) -> Self { // Initialize array with cursors. // There is no FromIterator on arrays, so instead initialize it manually with MaybeUninit @@ -312,16 +303,16 @@ impl<'w, 's, Q: WorldQuery, F: ReadOnlyWorldQuery, const K: usize> ptr.write(QueryIterationCursor::init( world, query_state, - last_change_tick, - change_tick, + last_run, + this_run, )); } for slot in (1..K).map(|offset| ptr.add(offset)) { slot.write(QueryIterationCursor::init_empty( world, query_state, - last_change_tick, - change_tick, + last_run, + this_run, )); } @@ -496,34 +487,24 @@ impl<'w, 's, Q: WorldQuery, F: ReadOnlyWorldQuery> QueryIterationCursor<'w, 's, unsafe fn init_empty( world: &'w World, query_state: &'s QueryState, - last_change_tick: u32, - change_tick: u32, + last_run: Tick, + this_run: Tick, ) -> Self { QueryIterationCursor { table_id_iter: [].iter(), archetype_id_iter: [].iter(), - ..Self::init(world, query_state, last_change_tick, change_tick) + ..Self::init(world, query_state, last_run, this_run) } } unsafe fn init( world: &'w World, query_state: &'s QueryState, - last_change_tick: u32, - change_tick: u32, + last_run: Tick, + this_run: Tick, ) -> Self { - let fetch = Q::init_fetch( - world, - &query_state.fetch_state, - last_change_tick, - change_tick, - ); - let filter = F::init_fetch( - world, - &query_state.filter_state, - last_change_tick, - change_tick, - ); + let fetch = Q::init_fetch(world, &query_state.fetch_state, last_run, this_run); + let filter = F::init_fetch(world, &query_state.filter_state, last_run, this_run); QueryIterationCursor { fetch, filter, diff --git a/crates/bevy_ecs/src/query/mod.rs b/crates/bevy_ecs/src/query/mod.rs index 83fe596766..8b665219b4 100644 --- a/crates/bevy_ecs/src/query/mod.rs +++ b/crates/bevy_ecs/src/query/mod.rs @@ -61,8 +61,9 @@ impl DebugCheckedUnwrap for Option { #[cfg(test)] mod tests { use super::{ReadOnlyWorldQuery, WorldQuery}; - use crate::prelude::{AnyOf, Entity, Or, QueryState, With, Without}; + use crate::prelude::{AnyOf, Changed, Entity, Or, QueryState, With, Without}; use crate::query::{ArchetypeFilter, QueryCombinationIter}; + use crate::schedule::{IntoSystemConfigs, Schedule}; use crate::system::{IntoSystem, Query, System, SystemState}; use crate::{self as bevy_ecs, component::Component, world::World}; use std::any::type_name; @@ -749,4 +750,33 @@ mod tests { let _: [&Foo; 1] = q.many([e]); let _: &Foo = q.single(); } + + // regression test for https://github.com/bevyengine/bevy/pull/8029 + #[test] + fn par_iter_mut_change_detection() { + let mut world = World::new(); + world.spawn((A(1), B(1))); + + fn propagate_system(mut query: Query<(&A, &mut B), Changed>) { + query.par_iter_mut().for_each_mut(|(a, mut b)| { + b.0 = a.0; + }); + } + + fn modify_system(mut query: Query<&mut A>) { + for mut a in &mut query { + a.0 = 2; + } + } + + let mut schedule = Schedule::new(); + schedule.add_systems((propagate_system, modify_system).chain()); + schedule.run(&mut world); + world.clear_trackers(); + schedule.run(&mut world); + world.clear_trackers(); + + let values = world.query::<&B>().iter(&world).collect::>(); + assert_eq!(values, vec![&B(2)]); + } } diff --git a/crates/bevy_ecs/src/query/par_iter.rs b/crates/bevy_ecs/src/query/par_iter.rs index 72744a8efa..4d3393c8f0 100644 --- a/crates/bevy_ecs/src/query/par_iter.rs +++ b/crates/bevy_ecs/src/query/par_iter.rs @@ -1,4 +1,4 @@ -use crate::world::World; +use crate::{component::Tick, world::World}; use bevy_tasks::ComputeTaskPool; use std::ops::Range; @@ -81,6 +81,8 @@ impl BatchingStrategy { pub struct QueryParIter<'w, 's, Q: WorldQuery, F: ReadOnlyWorldQuery> { pub(crate) world: &'w World, pub(crate) state: &'s QueryState, + pub(crate) last_run: Tick, + pub(crate) this_run: Tick, pub(crate) batching_strategy: BatchingStrategy, } @@ -148,12 +150,8 @@ impl<'w, 's, Q: WorldQuery, F: ReadOnlyWorldQuery> QueryParIter<'w, 's, Q, F> { ) { let thread_count = ComputeTaskPool::get().thread_num(); if thread_count <= 1 { - self.state.for_each_unchecked_manual( - self.world, - func, - self.world.last_change_tick(), - self.world.read_change_tick(), - ); + self.state + .for_each_unchecked_manual(self.world, func, self.last_run, self.this_run); } else { // Need a batch size of at least 1. let batch_size = self.get_batch_size(thread_count).max(1); @@ -161,8 +159,8 @@ impl<'w, 's, Q: WorldQuery, F: ReadOnlyWorldQuery> QueryParIter<'w, 's, Q, F> { self.world, batch_size, func, - self.world.last_change_tick(), - self.world.read_change_tick(), + self.last_run, + self.this_run, ); } } diff --git a/crates/bevy_ecs/src/query/state.rs b/crates/bevy_ecs/src/query/state.rs index f4ae1a6837..65774d40a9 100644 --- a/crates/bevy_ecs/src/query/state.rs +++ b/crates/bevy_ecs/src/query/state.rs @@ -1,6 +1,6 @@ use crate::{ archetype::{Archetype, ArchetypeComponentId, ArchetypeGeneration, ArchetypeId}, - component::ComponentId, + component::{ComponentId, Tick}, entity::Entity, prelude::FromWorld, query::{ @@ -130,11 +130,11 @@ impl QueryState { /// Checks if the query is empty for the given [`World`], where the last change and current tick are given. #[inline] - pub fn is_empty(&self, world: &World, last_change_tick: u32, change_tick: u32) -> bool { + pub fn is_empty(&self, world: &World, last_run: Tick, this_run: Tick) -> bool { // SAFETY: NopFetch does not access any members while &self ensures no one has exclusive access unsafe { self.as_nop() - .iter_unchecked_manual(world, last_change_tick, change_tick) + .iter_unchecked_manual(world, last_run, this_run) .next() .is_none() } @@ -390,8 +390,8 @@ impl QueryState { &self, world: &'w World, entity: Entity, - last_change_tick: u32, - change_tick: u32, + last_run: Tick, + this_run: Tick, ) -> Result, QueryEntityError> { let location = world .entities @@ -407,8 +407,8 @@ impl QueryState { .archetypes .get(location.archetype_id) .debug_checked_unwrap(); - let mut fetch = Q::init_fetch(world, &self.fetch_state, last_change_tick, change_tick); - let mut filter = F::init_fetch(world, &self.filter_state, last_change_tick, change_tick); + let mut fetch = Q::init_fetch(world, &self.fetch_state, last_run, this_run); + let mut filter = F::init_fetch(world, &self.filter_state, last_run, this_run); let table = world .storages() @@ -436,20 +436,17 @@ impl QueryState { &self, world: &'w World, entities: [Entity; N], - last_change_tick: u32, - change_tick: u32, + last_run: Tick, + this_run: Tick, ) -> Result<[ROQueryItem<'w, Q>; N], QueryEntityError> { let mut values = [(); N].map(|_| MaybeUninit::uninit()); for (value, entity) in std::iter::zip(&mut values, entities) { // SAFETY: fetch is read-only // and world must be validated - let item = self.as_readonly().get_unchecked_manual( - world, - entity, - last_change_tick, - change_tick, - )?; + let item = self + .as_readonly() + .get_unchecked_manual(world, entity, last_run, this_run)?; *value = MaybeUninit::new(item); } @@ -471,8 +468,8 @@ impl QueryState { &self, world: &'w World, entities: [Entity; N], - last_change_tick: u32, - change_tick: u32, + last_run: Tick, + this_run: Tick, ) -> Result<[Q::Item<'w>; N], QueryEntityError> { // Verify that all entities are unique for i in 0..N { @@ -486,7 +483,7 @@ impl QueryState { let mut values = [(); N].map(|_| MaybeUninit::uninit()); for (value, entity) in std::iter::zip(&mut values, entities) { - let item = self.get_unchecked_manual(world, entity, last_change_tick, change_tick)?; + let item = self.get_unchecked_manual(world, entity, last_run, this_run)?; *value = MaybeUninit::new(item); } @@ -708,10 +705,10 @@ impl QueryState { pub(crate) unsafe fn iter_unchecked_manual<'w, 's>( &'s self, world: &'w World, - last_change_tick: u32, - change_tick: u32, + last_run: Tick, + this_run: Tick, ) -> QueryIter<'w, 's, Q, F> { - QueryIter::new(world, self, last_change_tick, change_tick) + QueryIter::new(world, self, last_run, this_run) } /// Returns an [`Iterator`] for the given [`World`] and list of [`Entity`]'s, where the last change and @@ -729,13 +726,13 @@ impl QueryState { &'s self, entities: EntityList, world: &'w World, - last_change_tick: u32, - change_tick: u32, + last_run: Tick, + this_run: Tick, ) -> QueryManyIter<'w, 's, Q, F, EntityList::IntoIter> where EntityList::Item: Borrow, { - QueryManyIter::new(world, self, entities, last_change_tick, change_tick) + QueryManyIter::new(world, self, entities, last_run, this_run) } /// Returns an [`Iterator`] over all possible combinations of `K` query results for the @@ -752,10 +749,10 @@ impl QueryState { pub(crate) unsafe fn iter_combinations_unchecked_manual<'w, 's, const K: usize>( &'s self, world: &'w World, - last_change_tick: u32, - change_tick: u32, + last_run: Tick, + this_run: Tick, ) -> QueryCombinationIter<'w, 's, Q, F, K> { - QueryCombinationIter::new(world, self, last_change_tick, change_tick) + QueryCombinationIter::new(world, self, last_run, this_run) } /// Runs `func` on each query result for the given [`World`]. This is faster than the equivalent @@ -823,6 +820,8 @@ impl QueryState { QueryParIter { world, state: self, + last_run: world.last_change_tick(), + this_run: world.read_change_tick(), batching_strategy: BatchingStrategy::new(), } } @@ -835,9 +834,12 @@ impl QueryState { #[inline] pub fn par_iter_mut<'w, 's>(&'s mut self, world: &'w mut World) -> QueryParIter<'w, 's, Q, F> { self.update_archetypes(world); + let this_run = world.change_tick(); QueryParIter { world, state: self, + last_run: world.last_change_tick(), + this_run, batching_strategy: BatchingStrategy::new(), } } @@ -856,13 +858,13 @@ impl QueryState { &self, world: &'w World, mut func: FN, - last_change_tick: u32, - change_tick: u32, + last_run: Tick, + this_run: Tick, ) { // NOTE: If you are changing query iteration code, remember to update the following places, where relevant: // QueryIter, QueryIterationCursor, QueryManyIter, QueryCombinationIter, QueryState::for_each_unchecked_manual, QueryState::par_for_each_unchecked_manual - let mut fetch = Q::init_fetch(world, &self.fetch_state, last_change_tick, change_tick); - let mut filter = F::init_fetch(world, &self.filter_state, last_change_tick, change_tick); + let mut fetch = Q::init_fetch(world, &self.fetch_state, last_run, this_run); + let mut filter = F::init_fetch(world, &self.filter_state, last_run, this_run); let tables = &world.storages().tables; if Q::IS_DENSE && F::IS_DENSE { @@ -931,8 +933,8 @@ impl QueryState { world: &'w World, batch_size: usize, func: FN, - last_change_tick: u32, - change_tick: u32, + last_run: Tick, + this_run: Tick, ) { // NOTE: If you are changing query iteration code, remember to update the following places, where relevant: // QueryIter, QueryIterationCursor, QueryManyIter, QueryCombinationIter, QueryState::for_each_unchecked_manual, QueryState::par_for_each_unchecked_manual @@ -950,18 +952,10 @@ impl QueryState { let func = func.clone(); let len = batch_size.min(table.entity_count() - offset); let task = async move { - let mut fetch = Q::init_fetch( - world, - &self.fetch_state, - last_change_tick, - change_tick, - ); - let mut filter = F::init_fetch( - world, - &self.filter_state, - last_change_tick, - change_tick, - ); + let mut fetch = + Q::init_fetch(world, &self.fetch_state, last_run, this_run); + let mut filter = + F::init_fetch(world, &self.filter_state, last_run, this_run); let tables = &world.storages().tables; let table = tables.get(*table_id).debug_checked_unwrap(); let entities = table.entities(); @@ -1002,18 +996,10 @@ impl QueryState { let func = func.clone(); let len = batch_size.min(archetype.len() - offset); let task = async move { - let mut fetch = Q::init_fetch( - world, - &self.fetch_state, - last_change_tick, - change_tick, - ); - let mut filter = F::init_fetch( - world, - &self.filter_state, - last_change_tick, - change_tick, - ); + let mut fetch = + Q::init_fetch(world, &self.fetch_state, last_run, this_run); + let mut filter = + F::init_fetch(world, &self.filter_state, last_run, this_run); let tables = &world.storages().tables; let archetype = world.archetypes.get(*archetype_id).debug_checked_unwrap(); @@ -1161,10 +1147,10 @@ impl QueryState { pub unsafe fn get_single_unchecked_manual<'w>( &self, world: &'w World, - last_change_tick: u32, - change_tick: u32, + last_run: Tick, + this_run: Tick, ) -> Result, QuerySingleError> { - let mut query = self.iter_unchecked_manual(world, last_change_tick, change_tick); + let mut query = self.iter_unchecked_manual(world, last_run, this_run); let first = query.next(); let extra = query.next().is_some(); diff --git a/crates/bevy_ecs/src/reflect.rs b/crates/bevy_ecs/src/reflect.rs index bb4dbb1cf3..927973dea2 100644 --- a/crates/bevy_ecs/src/reflect.rs +++ b/crates/bevy_ecs/src/reflect.rs @@ -44,6 +44,8 @@ pub struct ReflectComponent(ReflectComponentFns); /// world. #[derive(Clone)] pub struct ReflectComponentFns { + /// Function pointer implementing [`ReflectComponent::from_world()`]. + pub from_world: fn(&mut World) -> Box, /// Function pointer implementing [`ReflectComponent::insert()`]. pub insert: fn(&mut EntityMut, &dyn Reflect), /// Function pointer implementing [`ReflectComponent::apply()`]. @@ -79,6 +81,11 @@ impl ReflectComponentFns { } impl ReflectComponent { + /// Constructs default reflected [`Component`] from world using [`from_world()`](FromWorld::from_world). + pub fn from_world(&self, world: &mut World) -> Box { + (self.0.from_world)(world) + } + /// Insert a reflected [`Component`] into the entity like [`insert()`](crate::world::EntityMut::insert). pub fn insert(&self, entity: &mut EntityMut, component: &dyn Reflect) { (self.0.insert)(entity, component); @@ -170,6 +177,7 @@ impl ReflectComponent { impl FromType for ReflectComponent { fn from_type() -> Self { ReflectComponent(ReflectComponentFns { + from_world: |world| Box::new(C::from_world(world)), insert: |entity, reflected_component| { let mut component = entity.world_scope(|world| C::from_world(world)); component.apply(reflected_component); diff --git a/crates/bevy_ecs/src/removal_detection.rs b/crates/bevy_ecs/src/removal_detection.rs index 004f08ca34..139c092bb8 100644 --- a/crates/bevy_ecs/src/removal_detection.rs +++ b/crates/bevy_ecs/src/removal_detection.rs @@ -2,7 +2,7 @@ use crate::{ self as bevy_ecs, - component::{Component, ComponentId, ComponentIdFor}, + component::{Component, ComponentId, ComponentIdFor, Tick}, entity::Entity, event::{EventId, Events, ManualEventIterator, ManualEventIteratorWithId, ManualEventReader}, prelude::Local, @@ -265,7 +265,7 @@ unsafe impl<'a> SystemParam for &'a RemovedComponentEvents { _state: &'s mut Self::State, _system_meta: &SystemMeta, world: &'w World, - _change_tick: u32, + _change_tick: Tick, ) -> Self::Item<'w, 's> { world.removed_components() } diff --git a/crates/bevy_ecs/src/schedule/condition.rs b/crates/bevy_ecs/src/schedule/condition.rs index ba39981b63..0ad7f05917 100644 --- a/crates/bevy_ecs/src/schedule/condition.rs +++ b/crates/bevy_ecs/src/schedule/condition.rs @@ -1,6 +1,11 @@ +use std::any::TypeId; use std::borrow::Cow; +use std::ops::Not; +use crate::component::{self, ComponentId}; +use crate::query::Access; use crate::system::{CombinatorSystem, Combine, IntoSystem, ReadOnlySystem, System}; +use crate::world::World; pub type BoxedCondition = Box>; @@ -26,7 +31,7 @@ pub trait Condition: sealed::Condition { /// # let mut app = Schedule::new(); /// # let mut world = World::new(); /// # fn my_system() {} - /// app.add_system( + /// app.add_systems( /// // The `resource_equals` run condition will panic since we don't initialize `R`, /// // just like if we used `Res` in a system. /// my_system.run_if(resource_equals(R(0))), @@ -43,7 +48,7 @@ pub trait Condition: sealed::Condition { /// # let mut app = Schedule::new(); /// # let mut world = World::new(); /// # fn my_system() {} - /// app.add_system( + /// app.add_systems( /// // `resource_equals` will only get run if the resource `R` exists. /// my_system.run_if(resource_exists::().and_then(resource_equals(R(0)))), /// ); @@ -81,7 +86,7 @@ pub trait Condition: sealed::Condition { /// # let mut world = World::new(); /// # #[derive(Resource)] struct C(bool); /// # fn my_system(mut c: ResMut) { c.0 = true; } - /// app.add_system( + /// app.add_systems( /// // Only run the system if either `A` or `B` exist. /// my_system.run_if(resource_exists::().or_else(resource_exists::())), /// ); @@ -131,18 +136,47 @@ mod sealed { } pub mod common_conditions { - use super::Condition; + use std::borrow::Cow; + + use super::NotSystem; use crate::{ change_detection::DetectChanges, event::{Event, EventReader}, prelude::{Component, Query, With}, schedule::{State, States}, - system::{In, IntoPipeSystem, Res, Resource}, + system::{IntoSystem, Res, Resource, System}, }; /// Generates a [`Condition`](super::Condition)-satisfying closure that returns `true` /// if the first time the condition is run and false every time after - pub fn run_once() -> impl FnMut() -> bool { + /// + /// # Example + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// # #[derive(Resource, Default)] + /// # struct Counter(u8); + /// # let mut app = Schedule::new(); + /// # let mut world = World::new(); + /// # world.init_resource::(); + /// app.add_systems( + /// // `run_once` will only return true the first time it's evaluated + /// my_system.run_if(run_once()), + /// ); + /// + /// fn my_system(mut counter: ResMut) { + /// counter.0 += 1; + /// } + /// + /// // This is the first time the condition will be evaluated so `my_system` will run + /// app.run(&mut world); + /// assert_eq!(world.resource::().0, 1); + /// + /// // This is the seconds time the condition will be evaluated so `my_system` won't run + /// app.run(&mut world); + /// assert_eq!(world.resource::().0, 1); + /// ``` + pub fn run_once() -> impl FnMut() -> bool + Clone { let mut has_run = false; move || { if !has_run { @@ -156,7 +190,33 @@ pub mod common_conditions { /// Generates a [`Condition`](super::Condition)-satisfying closure that returns `true` /// if the resource exists. - pub fn resource_exists() -> impl FnMut(Option>) -> bool + /// + /// # Example + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// # #[derive(Resource, Default)] + /// # struct Counter(u8); + /// # let mut app = Schedule::new(); + /// # let mut world = World::new(); + /// app.add_systems( + /// // `resource_exsists` will only return true if the given resource exsists in the world + /// my_system.run_if(resource_exists::()), + /// ); + /// + /// fn my_system(mut counter: ResMut) { + /// counter.0 += 1; + /// } + /// + /// // `Counter` hasn't been added so `my_system` won't run + /// app.run(&mut world); + /// world.init_resource::(); + /// + /// // `Counter` has now been added so `my_system` can run + /// app.run(&mut world); + /// assert_eq!(world.resource::().0, 1); + /// ``` + pub fn resource_exists() -> impl FnMut(Option>) -> bool + Clone where T: Resource, { @@ -169,6 +229,33 @@ pub mod common_conditions { /// # Panics /// /// The condition will panic if the resource does not exist. + /// + /// # Example + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// # #[derive(Resource, Default, PartialEq)] + /// # struct Counter(u8); + /// # let mut app = Schedule::new(); + /// # let mut world = World::new(); + /// # world.init_resource::(); + /// app.add_systems( + /// // `resource_equals` will only return true if the given resource equals the given value + /// my_system.run_if(resource_equals(Counter(0))), + /// ); + /// + /// fn my_system(mut counter: ResMut) { + /// counter.0 += 1; + /// } + /// + /// // `Counter` is `0` so `my_system` can run + /// app.run(&mut world); + /// assert_eq!(world.resource::().0, 1); + /// + /// // `Counter` is no longer `0` so `my_system` won't run + /// app.run(&mut world); + /// assert_eq!(world.resource::().0, 1); + /// ``` pub fn resource_equals(value: T) -> impl FnMut(Res) -> bool where T: Resource + PartialEq, @@ -180,6 +267,37 @@ pub mod common_conditions { /// if the resource exists and is equal to `value`. /// /// The condition will return `false` if the resource does not exist. + /// + /// # Example + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// # #[derive(Resource, Default, PartialEq)] + /// # struct Counter(u8); + /// # let mut app = Schedule::new(); + /// # let mut world = World::new(); + /// app.add_systems( + /// // `resource_exists_and_equals` will only return true + /// // if the given resource exsists and equals the given value + /// my_system.run_if(resource_exists_and_equals(Counter(0))), + /// ); + /// + /// fn my_system(mut counter: ResMut) { + /// counter.0 += 1; + /// } + /// + /// // `Counter` hasn't been added so `my_system` can't run + /// app.run(&mut world); + /// world.init_resource::(); + /// + /// // `Counter` is `0` so `my_system` can run + /// app.run(&mut world); + /// assert_eq!(world.resource::().0, 1); + /// + /// // `Counter` is no longer `0` so `my_system` won't run + /// app.run(&mut world); + /// assert_eq!(world.resource::().0, 1); + /// ``` pub fn resource_exists_and_equals(value: T) -> impl FnMut(Option>) -> bool where T: Resource + PartialEq, @@ -192,7 +310,36 @@ pub mod common_conditions { /// Generates a [`Condition`](super::Condition)-satisfying closure that returns `true` /// if the resource of the given type has been added since the condition was last checked. - pub fn resource_added() -> impl FnMut(Option>) -> bool + /// + /// # Example + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// # #[derive(Resource, Default)] + /// # struct Counter(u8); + /// # let mut app = Schedule::new(); + /// # let mut world = World::new(); + /// app.add_systems( + /// // `resource_added` will only return true if the + /// // given resource was just added + /// my_system.run_if(resource_added::()), + /// ); + /// + /// fn my_system(mut counter: ResMut) { + /// counter.0 += 1; + /// } + /// + /// world.init_resource::(); + /// + /// // `Counter` was just added so `my_system` will run + /// app.run(&mut world); + /// assert_eq!(world.resource::().0, 1); + /// + /// // `Counter` was not just added so `my_system` will not run + /// app.run(&mut world); + /// assert_eq!(world.resource::().0, 1); + /// ``` + pub fn resource_added() -> impl FnMut(Option>) -> bool + Clone where T: Resource, { @@ -213,7 +360,43 @@ pub mod common_conditions { /// # Panics /// /// The condition will panic if the resource does not exist. - pub fn resource_changed() -> impl FnMut(Res) -> bool + /// + /// # Example + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// # #[derive(Resource, Default)] + /// # struct Counter(u8); + /// # let mut app = Schedule::new(); + /// # let mut world = World::new(); + /// # world.init_resource::(); + /// app.add_systems( + /// // `resource_changed` will only return true if the + /// // given resource was just changed (or added) + /// my_system.run_if( + /// resource_changed::() + /// // By default detecting changes will also trigger if the resource was + /// // just added, this won't work with my example so I will addd a second + /// // condition to make sure the resource wasn't just added + /// .and_then(not(resource_added::())) + /// ), + /// ); + /// + /// fn my_system(mut counter: ResMut) { + /// counter.0 += 1; + /// } + /// + /// // `Counter` hasn't been changed so `my_system` won't run + /// app.run(&mut world); + /// assert_eq!(world.resource::().0, 0); + /// + /// world.resource_mut::().0 = 50; + /// + /// // `Counter` was just changed so `my_system` will run + /// app.run(&mut world); + /// assert_eq!(world.resource::().0, 51); + /// ``` + pub fn resource_changed() -> impl FnMut(Res) -> bool + Clone where T: Resource, { @@ -231,7 +414,46 @@ pub mod common_conditions { /// This run condition does not detect when the resource is removed. /// /// The condition will return `false` if the resource does not exist. - pub fn resource_exists_and_changed() -> impl FnMut(Option>) -> bool + /// + /// # Example + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// # #[derive(Resource, Default)] + /// # struct Counter(u8); + /// # let mut app = Schedule::new(); + /// # let mut world = World::new(); + /// app.add_systems( + /// // `resource_exists_and_changed` will only return true if the + /// // given resource exsists and was just changed (or added) + /// my_system.run_if( + /// resource_exists_and_changed::() + /// // By default detecting changes will also trigger if the resource was + /// // just added, this won't work with my example so I will addd a second + /// // condition to make sure the resource wasn't just added + /// .and_then(not(resource_added::())) + /// ), + /// ); + /// + /// fn my_system(mut counter: ResMut) { + /// counter.0 += 1; + /// } + /// + /// // `Counter` doesn't exist so `my_system` won't run + /// app.run(&mut world); + /// world.init_resource::(); + /// + /// // `Counter` hasn't been changed so `my_system` won't run + /// app.run(&mut world); + /// assert_eq!(world.resource::().0, 0); + /// + /// world.resource_mut::().0 = 50; + /// + /// // `Counter` was just changed so `my_system` will run + /// app.run(&mut world); + /// assert_eq!(world.resource::().0, 51); + /// ``` + pub fn resource_exists_and_changed() -> impl FnMut(Option>) -> bool + Clone where T: Resource, { @@ -253,7 +475,57 @@ pub mod common_conditions { /// has been removed since the run condition was last checked. /// /// The condition will return `false` if the resource does not exist. - pub fn resource_changed_or_removed() -> impl FnMut(Option>) -> bool + /// + /// # Example + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// # #[derive(Resource, Default)] + /// # struct Counter(u8); + /// # let mut app = Schedule::new(); + /// # let mut world = World::new(); + /// # world.init_resource::(); + /// app.add_systems( + /// // `resource_changed_or_removed` will only return true if the + /// // given resource was just changed or removed (or added) + /// my_system.run_if( + /// resource_changed_or_removed::() + /// // By default detecting changes will also trigger if the resource was + /// // just added, this won't work with my example so I will addd a second + /// // condition to make sure the resource wasn't just added + /// .and_then(not(resource_added::())) + /// ), + /// ); + /// + /// #[derive(Resource, Default)] + /// struct MyResource; + /// + /// // If `Counter` exists, increment it, otherwise insert `MyResource` + /// fn my_system(mut commands: Commands, mut counter: Option>) { + /// if let Some(mut counter) = counter { + /// counter.0 += 1; + /// } else { + /// commands.init_resource::(); + /// } + /// } + /// + /// // `Counter` hasn't been changed so `my_system` won't run + /// app.run(&mut world); + /// assert_eq!(world.resource::().0, 0); + /// + /// world.resource_mut::().0 = 50; + /// + /// // `Counter` was just changed so `my_system` will run + /// app.run(&mut world); + /// assert_eq!(world.resource::().0, 51); + /// + /// world.remove_resource::(); + /// + /// // `Counter` was just removed so `my_system` will run + /// app.run(&mut world); + /// assert_eq!(world.contains_resource::(), true); + /// ``` + pub fn resource_changed_or_removed() -> impl FnMut(Option>) -> bool + Clone where T: Resource, { @@ -273,7 +545,42 @@ pub mod common_conditions { /// Generates a [`Condition`](super::Condition)-satisfying closure that returns `true` /// if the resource of the given type has been removed since the condition was last checked. - pub fn resource_removed() -> impl FnMut(Option>) -> bool + /// + /// # Example + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// # #[derive(Resource, Default)] + /// # struct Counter(u8); + /// # let mut app = Schedule::new(); + /// # let mut world = World::new(); + /// # world.init_resource::(); + /// app.add_systems( + /// // `resource_removed` will only return true if the + /// // given resource was just removed + /// my_system.run_if(resource_removed::()), + /// ); + /// + /// #[derive(Resource, Default)] + /// struct MyResource; + /// + /// fn my_system(mut counter: ResMut) { + /// counter.0 += 1; + /// } + /// + /// world.init_resource::(); + /// + /// // `MyResource` hasn't just been removed so `my_system` won't run + /// app.run(&mut world); + /// assert_eq!(world.resource::().0, 0); + /// + /// world.remove_resource::(); + /// + /// // `MyResource` was just removed so `my_system` will run + /// app.run(&mut world); + /// assert_eq!(world.resource::().0, 1); + /// ``` + pub fn resource_removed() -> impl FnMut(Option>) -> bool + Clone where T: Resource, { @@ -293,7 +600,44 @@ pub mod common_conditions { /// Generates a [`Condition`](super::Condition)-satisfying closure that returns `true` /// if the state machine exists. - pub fn state_exists() -> impl FnMut(Option>>) -> bool { + /// + /// # Example + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// # #[derive(Resource, Default)] + /// # struct Counter(u8); + /// # let mut app = Schedule::new(); + /// # let mut world = World::new(); + /// # world.init_resource::(); + /// #[derive(States, Clone, Copy, Default, Eq, PartialEq, Hash, Debug)] + /// enum GameState { + /// #[default] + /// Playing, + /// Paused, + /// } + /// + /// app.add_systems( + /// // `state_exists` will only return true if the + /// // given state exsists + /// my_system.run_if(state_exists::()), + /// ); + /// + /// fn my_system(mut counter: ResMut) { + /// counter.0 += 1; + /// } + /// + /// // `GameState` does not yet exist `my_system` won't run + /// app.run(&mut world); + /// assert_eq!(world.resource::().0, 0); + /// + /// world.init_resource::>(); + /// + /// // `GameState` now exists so `my_system` will run + /// app.run(&mut world); + /// assert_eq!(world.resource::().0, 1); + /// ``` + pub fn state_exists() -> impl FnMut(Option>>) -> bool + Clone { move |current_state: Option>>| current_state.is_some() } @@ -303,7 +647,51 @@ pub mod common_conditions { /// # Panics /// /// The condition will panic if the resource does not exist. - pub fn in_state(state: S) -> impl FnMut(Res>) -> bool { + /// + /// # Example + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// # #[derive(Resource, Default)] + /// # struct Counter(u8); + /// # let mut app = Schedule::new(); + /// # let mut world = World::new(); + /// # world.init_resource::(); + /// #[derive(States, Clone, Copy, Default, Eq, PartialEq, Hash, Debug)] + /// enum GameState { + /// #[default] + /// Playing, + /// Paused, + /// } + /// + /// world.init_resource::>(); + /// + /// app.add_systems(( + /// // `in_state` will only return true if the + /// // given state equals the given value + /// play_system.run_if(in_state(GameState::Playing)), + /// pause_system.run_if(in_state(GameState::Paused)), + /// )); + /// + /// fn play_system(mut counter: ResMut) { + /// counter.0 += 1; + /// } + /// + /// fn pause_system(mut counter: ResMut) { + /// counter.0 -= 1; + /// } + /// + /// // We default to `GameState::Playing` so `play_system` runs + /// app.run(&mut world); + /// assert_eq!(world.resource::().0, 1); + /// + /// *world.resource_mut::>() = State(GameState::Paused); + /// + /// // Now that we are in `GameState::Pause`, `pause_system` will run + /// app.run(&mut world); + /// assert_eq!(world.resource::().0, 0); + /// ``` + pub fn in_state(state: S) -> impl FnMut(Res>) -> bool + Clone { move |current_state: Res>| current_state.0 == state } @@ -311,9 +699,57 @@ pub mod common_conditions { /// if the state machine exists and is currently in `state`. /// /// The condition will return `false` if the state does not exist. + /// + /// # Example + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// # #[derive(Resource, Default)] + /// # struct Counter(u8); + /// # let mut app = Schedule::new(); + /// # let mut world = World::new(); + /// # world.init_resource::(); + /// #[derive(States, Clone, Copy, Default, Eq, PartialEq, Hash, Debug)] + /// enum GameState { + /// #[default] + /// Playing, + /// Paused, + /// } + /// + /// app.add_systems(( + /// // `state_exists_and_equals` will only return true if the + /// // given state exsists and equals the given value + /// play_system.run_if(state_exists_and_equals(GameState::Playing)), + /// pause_system.run_if(state_exists_and_equals(GameState::Paused)), + /// )); + /// + /// fn play_system(mut counter: ResMut) { + /// counter.0 += 1; + /// } + /// + /// fn pause_system(mut counter: ResMut) { + /// counter.0 -= 1; + /// } + /// + /// // `GameState` does not yet exists so neither system will run + /// app.run(&mut world); + /// assert_eq!(world.resource::().0, 0); + /// + /// world.init_resource::>(); + /// + /// // We default to `GameState::Playing` so `play_system` runs + /// app.run(&mut world); + /// assert_eq!(world.resource::().0, 1); + /// + /// *world.resource_mut::>() = State(GameState::Paused); + /// + /// // Now that we are in `GameState::Pause`, `pause_system` will run + /// app.run(&mut world); + /// assert_eq!(world.resource::().0, 0); + /// ``` pub fn state_exists_and_equals( state: S, - ) -> impl FnMut(Option>>) -> bool { + ) -> impl FnMut(Option>>) -> bool + Clone { move |current_state: Option>>| match current_state { Some(current_state) => current_state.0 == state, None => false, @@ -329,13 +765,90 @@ pub mod common_conditions { /// # Panics /// /// The condition will panic if the resource does not exist. - pub fn state_changed() -> impl FnMut(Res>) -> bool { + /// + /// # Example + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// # #[derive(Resource, Default)] + /// # struct Counter(u8); + /// # let mut app = Schedule::new(); + /// # let mut world = World::new(); + /// # world.init_resource::(); + /// #[derive(States, Clone, Copy, Default, Eq, PartialEq, Hash, Debug)] + /// enum GameState { + /// #[default] + /// Playing, + /// Paused, + /// } + /// + /// world.init_resource::>(); + /// + /// app.add_systems( + /// // `state_changed` will only return true if the + /// // given states value has just been updated or + /// // the state has just been added + /// my_system.run_if(state_changed::()), + /// ); + /// + /// fn my_system(mut counter: ResMut) { + /// counter.0 += 1; + /// } + /// + /// // `GameState` has just been added so `my_system` will run + /// app.run(&mut world); + /// assert_eq!(world.resource::().0, 1); + /// + /// // `GameState` has not been updated so `my_system` will not run + /// app.run(&mut world); + /// assert_eq!(world.resource::().0, 1); + /// + /// *world.resource_mut::>() = State(GameState::Paused); + /// + /// // Now that `GameState` has been updated `my_system` will run + /// app.run(&mut world); + /// assert_eq!(world.resource::().0, 2); + /// ``` + pub fn state_changed() -> impl FnMut(Res>) -> bool + Clone { move |current_state: Res>| current_state.is_changed() } /// Generates a [`Condition`](super::Condition)-satisfying closure that returns `true` /// if there are any new events of the given type since it was last called. - pub fn on_event() -> impl FnMut(EventReader) -> bool { + /// + /// # Example + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// # #[derive(Resource, Default)] + /// # struct Counter(u8); + /// # let mut app = Schedule::new(); + /// # let mut world = World::new(); + /// # world.init_resource::(); + /// # world.init_resource::>(); + /// # app.add_systems(Events::::update_system.before(my_system)); + /// + /// app.add_systems( + /// my_system.run_if(on_event::()), + /// ); + /// + /// struct MyEvent; + /// + /// fn my_system(mut counter: ResMut) { + /// counter.0 += 1; + /// } + /// + /// // No new `MyEvent` events have been push so `my_system` won't run + /// app.run(&mut world); + /// assert_eq!(world.resource::().0, 0); + /// + /// world.resource_mut::>().send(MyEvent); + /// + /// // A `MyEvent` event has been push so `my_system` will run + /// app.run(&mut world); + /// assert_eq!(world.resource::().0, 1); + /// ``` + pub fn on_event() -> impl FnMut(EventReader) -> bool + Clone { // The events need to be consumed, so that there are no false positives on subsequent // calls of the run condition. Simply checking `is_empty` would not be enough. // PERF: note that `count` is efficient (not actually looping/iterating), @@ -345,39 +858,169 @@ pub mod common_conditions { /// Generates a [`Condition`](super::Condition)-satisfying closure that returns `true` /// if there are any entities with the given component type. - pub fn any_with_component() -> impl FnMut(Query<(), With>) -> bool { + /// + /// # Example + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// # #[derive(Resource, Default)] + /// # struct Counter(u8); + /// # let mut app = Schedule::new(); + /// # let mut world = World::new(); + /// # world.init_resource::(); + /// app.add_systems( + /// my_system.run_if(any_with_component::()), + /// ); + /// + /// #[derive(Component)] + /// struct MyComponent; + /// + /// fn my_system(mut counter: ResMut) { + /// counter.0 += 1; + /// } + /// + /// // No entities exist yet with a `MyComponent` component so `my_system` won't run + /// app.run(&mut world); + /// assert_eq!(world.resource::().0, 0); + /// + /// world.spawn(MyComponent); + /// + /// // An entities with `MyComponent` now exists so `my_system` will run + /// app.run(&mut world); + /// assert_eq!(world.resource::().0, 1); + /// ``` + pub fn any_with_component() -> impl FnMut(Query<(), With>) -> bool + Clone { move |query: Query<(), With>| !query.is_empty() } /// Generates a [`Condition`](super::Condition) that inverses the result of passed one. /// - /// # Examples + /// # Example /// /// ``` - /// use bevy_ecs::prelude::*; - /// // Building a new schedule/app... - /// let mut sched = Schedule::default(); - /// sched.add_system( - /// // This system will never run. - /// my_system.run_if(not(always_true)) - /// ) - /// // ... - /// # ; + /// # use bevy_ecs::prelude::*; + /// # #[derive(Resource, Default)] + /// # struct Counter(u8); + /// # let mut app = Schedule::new(); /// # let mut world = World::new(); - /// # sched.run(&mut world); + /// # world.init_resource::(); + /// app.add_systems( + /// // `not` will inverse any condition you pass in. + /// // Since the condition we choose always returns true + /// // this system will never run + /// my_system.run_if(not(always)), + /// ); /// - /// // A condition that always returns true. - /// fn always_true() -> bool { - /// true + /// fn my_system(mut counter: ResMut) { + /// counter.0 += 1; /// } - /// # - /// # fn my_system() { unreachable!() } + /// + /// fn always() -> bool { + /// true + /// } + /// + /// app.run(&mut world); + /// assert_eq!(world.resource::().0, 0); /// ``` - pub fn not(condition: impl Condition) -> impl Condition<()> { - condition.pipe(|In(val): In| !val) + pub fn not(condition: T) -> NotSystem + where + TOut: std::ops::Not, + T: IntoSystem<(), TOut, Marker>, + { + let condition = IntoSystem::into_system(condition); + let name = format!("!{}", condition.name()); + NotSystem:: { + condition, + name: Cow::Owned(name), + } } } +/// Invokes [`Not`] with the output of another system. +/// +/// See [`common_conditions::not`] for examples. +#[derive(Clone)] +pub struct NotSystem +where + T::Out: Not, +{ + condition: T, + name: Cow<'static, str>, +} +impl System for NotSystem +where + T::Out: Not, +{ + type In = T::In; + type Out = ::Output; + + fn name(&self) -> Cow<'static, str> { + self.name.clone() + } + + fn type_id(&self) -> TypeId { + TypeId::of::() + } + + fn component_access(&self) -> &Access { + self.condition.component_access() + } + + fn archetype_component_access(&self) -> &Access { + self.condition.archetype_component_access() + } + + fn is_send(&self) -> bool { + self.condition.is_send() + } + + fn is_exclusive(&self) -> bool { + self.condition.is_exclusive() + } + + unsafe fn run_unsafe(&mut self, input: Self::In, world: &World) -> Self::Out { + // SAFETY: The inner condition system asserts its own safety. + !self.condition.run_unsafe(input, world) + } + + fn run(&mut self, input: Self::In, world: &mut World) -> Self::Out { + !self.condition.run(input, world) + } + + fn apply_buffers(&mut self, world: &mut World) { + self.condition.apply_buffers(world); + } + + fn initialize(&mut self, world: &mut World) { + self.condition.initialize(world); + } + + fn update_archetype_component_access(&mut self, world: &World) { + self.condition.update_archetype_component_access(world); + } + + fn check_change_tick(&mut self, change_tick: component::Tick) { + self.condition.check_change_tick(change_tick); + } + + fn get_last_run(&self) -> component::Tick { + self.condition.get_last_run() + } + + fn set_last_run(&mut self, last_run: component::Tick) { + self.condition.set_last_run(last_run); + } +} + +// SAFETY: This trait is only implemented when the inner system is read-only. +// The `Not` condition does not modify access, and thus cannot transform a read-only system into one that is not. +unsafe impl ReadOnlySystem for NotSystem +where + T: ReadOnlySystem, + T::Out: Not, +{ +} + /// Combines the outputs of two systems using the `&&` operator. pub type AndThen = CombinatorSystem; @@ -425,3 +1068,142 @@ where a(input) || b(input) } } + +#[cfg(test)] +mod tests { + use super::{common_conditions::*, Condition}; + use crate as bevy_ecs; + use crate::component::Component; + use crate::schedule::IntoSystemConfigs; + use crate::schedule::{common_conditions::not, State, States}; + use crate::system::Local; + use crate::{change_detection::ResMut, schedule::Schedule, world::World}; + use bevy_ecs_macros::Resource; + + #[derive(Resource, Default)] + struct Counter(usize); + + fn increment_counter(mut counter: ResMut) { + counter.0 += 1; + } + + fn every_other_time(mut has_ran: Local) -> bool { + *has_ran = !*has_ran; + *has_ran + } + + #[test] + fn run_condition() { + let mut world = World::new(); + world.init_resource::(); + let mut schedule = Schedule::new(); + + // Run every other cycle + schedule.add_systems(increment_counter.run_if(every_other_time)); + + schedule.run(&mut world); + schedule.run(&mut world); + assert_eq!(world.resource::().0, 1); + schedule.run(&mut world); + schedule.run(&mut world); + assert_eq!(world.resource::().0, 2); + + // Run every other cycle oppsite to the last one + schedule.add_systems(increment_counter.run_if(not(every_other_time))); + + schedule.run(&mut world); + schedule.run(&mut world); + assert_eq!(world.resource::().0, 4); + schedule.run(&mut world); + schedule.run(&mut world); + assert_eq!(world.resource::().0, 6); + } + + #[test] + fn run_condition_combinators() { + let mut world = World::new(); + world.init_resource::(); + let mut schedule = Schedule::new(); + + // Always run + schedule.add_systems(increment_counter.run_if(every_other_time.or_else(|| true))); + // Run every other cycle + schedule.add_systems(increment_counter.run_if(every_other_time.and_then(|| true))); + + schedule.run(&mut world); + assert_eq!(world.resource::().0, 2); + schedule.run(&mut world); + assert_eq!(world.resource::().0, 3); + } + + #[test] + fn multiple_run_conditions() { + let mut world = World::new(); + world.init_resource::(); + let mut schedule = Schedule::new(); + + // Run every other cycle + schedule.add_systems(increment_counter.run_if(every_other_time).run_if(|| true)); + // Never run + schedule.add_systems(increment_counter.run_if(every_other_time).run_if(|| false)); + + schedule.run(&mut world); + assert_eq!(world.resource::().0, 1); + schedule.run(&mut world); + assert_eq!(world.resource::().0, 1); + } + + #[test] + fn multiple_run_conditions_is_and_operation() { + let mut world = World::new(); + world.init_resource::(); + + let mut schedule = Schedule::new(); + + // This should never run, if multiple run conditions worked + // like an OR condition then it would always run + schedule.add_systems( + increment_counter + .run_if(every_other_time) + .run_if(not(every_other_time)), + ); + + schedule.run(&mut world); + assert_eq!(world.resource::().0, 0); + schedule.run(&mut world); + assert_eq!(world.resource::().0, 0); + } + + #[derive(States, PartialEq, Eq, Debug, Default, Hash, Clone)] + enum TestState { + #[default] + A, + B, + } + + #[derive(Component)] + struct TestComponent; + + fn test_system() {} + + // Ensure distributive_run_if compiles with the common conditions. + #[test] + fn distributive_run_if_compiles() { + Schedule::default().add_systems( + (test_system, test_system) + .distributive_run_if(run_once()) + .distributive_run_if(resource_exists::>()) + .distributive_run_if(resource_added::>()) + .distributive_run_if(resource_changed::>()) + .distributive_run_if(resource_exists_and_changed::>()) + .distributive_run_if(resource_changed_or_removed::>()) + .distributive_run_if(resource_removed::>()) + .distributive_run_if(state_exists::()) + .distributive_run_if(in_state(TestState::A)) + .distributive_run_if(state_changed::()) + .distributive_run_if(on_event::()) + .distributive_run_if(any_with_component::()) + .distributive_run_if(not(run_once())), + ); + } +} diff --git a/crates/bevy_ecs/src/schedule/config.rs b/crates/bevy_ecs/src/schedule/config.rs index 54eb9f863f..50aa7bfe90 100644 --- a/crates/bevy_ecs/src/schedule/config.rs +++ b/crates/bevy_ecs/src/schedule/config.rs @@ -5,57 +5,11 @@ use crate::{ condition::{BoxedCondition, Condition}, graph_utils::{Ambiguity, Dependency, DependencyKind, GraphInfo}, set::{BoxedSystemSet, IntoSystemSet, SystemSet}, + ScheduleLabel, }, system::{BoxedSystem, IntoSystem, System}, }; -use super::{BaseSystemSet, FreeSystemSet}; - -/// A [`SystemSet`] with scheduling metadata. -pub struct SystemSetConfig { - pub(super) set: BoxedSystemSet, - pub(super) graph_info: GraphInfo, - pub(super) conditions: Vec, -} - -impl SystemSetConfig { - fn new(set: BoxedSystemSet) -> Self { - // system type sets are automatically populated - // to avoid unintentionally broad changes, they cannot be configured - assert!( - set.system_type().is_none(), - "configuring system type sets is not allowed" - ); - - Self { - set, - graph_info: GraphInfo::system_set(), - conditions: Vec::new(), - } - } -} - -/// A [`System`] with scheduling metadata. -pub struct SystemConfig { - pub(super) system: BoxedSystem, - pub(super) graph_info: GraphInfo, - pub(super) conditions: Vec, -} - -impl SystemConfig { - fn new(system: BoxedSystem) -> Self { - // include system in its default sets - let sets = system.default_system_sets().into_iter().collect(); - let mut graph_info = GraphInfo::system(); - graph_info.sets = sets; - Self { - system, - graph_info, - conditions: Vec::new(), - } - } -} - fn new_condition(condition: impl Condition) -> BoxedCondition { let condition_system = IntoSystem::into_system(condition); assert!( @@ -79,6 +33,370 @@ fn ambiguous_with(graph_info: &mut GraphInfo, set: BoxedSystemSet) { } } +impl IntoSystemConfigs for F +where + F: IntoSystem<(), (), Marker>, +{ + fn into_configs(self) -> SystemConfigs { + SystemConfigs::new_system(Box::new(IntoSystem::into_system(self))) + } +} + +impl IntoSystemConfigs<()> for BoxedSystem<(), ()> { + fn into_configs(self) -> SystemConfigs { + SystemConfigs::new_system(self) + } +} + +pub struct SystemConfig { + pub(crate) system: BoxedSystem, + pub(crate) graph_info: GraphInfo, + pub(crate) conditions: Vec, +} + +/// A collection of [`SystemConfig`]. +pub enum SystemConfigs { + SystemConfig(SystemConfig), + Configs { + configs: Vec, + /// If `true`, adds `before -> after` ordering constraints between the successive elements. + chained: bool, + }, +} + +impl SystemConfigs { + fn new_system(system: BoxedSystem) -> Self { + // include system in its default sets + let sets = system.default_system_sets().into_iter().collect(); + Self::SystemConfig(SystemConfig { + system, + graph_info: GraphInfo { + sets, + ..Default::default() + }, + conditions: Vec::new(), + }) + } + + fn in_set_inner(&mut self, set: BoxedSystemSet) { + match self { + SystemConfigs::SystemConfig(config) => { + config.graph_info.sets.push(set); + } + SystemConfigs::Configs { configs, .. } => { + for config in configs { + config.in_set_inner(set.dyn_clone()); + } + } + } + } + + fn before_inner(&mut self, set: BoxedSystemSet) { + match self { + SystemConfigs::SystemConfig(config) => { + config + .graph_info + .dependencies + .push(Dependency::new(DependencyKind::Before, set)); + } + SystemConfigs::Configs { configs, .. } => { + for config in configs { + config.before_inner(set.dyn_clone()); + } + } + } + } + + fn after_inner(&mut self, set: BoxedSystemSet) { + match self { + SystemConfigs::SystemConfig(config) => { + config + .graph_info + .dependencies + .push(Dependency::new(DependencyKind::After, set)); + } + SystemConfigs::Configs { configs, .. } => { + for config in configs { + config.after_inner(set.dyn_clone()); + } + } + } + } + + fn distributive_run_if_inner(&mut self, condition: impl Condition + Clone) { + match self { + SystemConfigs::SystemConfig(config) => { + config.conditions.push(new_condition(condition)); + } + SystemConfigs::Configs { configs, .. } => { + for config in configs { + config.distributive_run_if_inner(condition.clone()); + } + } + } + } + + fn ambiguous_with_inner(&mut self, set: BoxedSystemSet) { + match self { + SystemConfigs::SystemConfig(config) => { + ambiguous_with(&mut config.graph_info, set); + } + SystemConfigs::Configs { configs, .. } => { + for config in configs { + config.ambiguous_with_inner(set.dyn_clone()); + } + } + } + } + + fn ambiguous_with_all_inner(&mut self) { + match self { + SystemConfigs::SystemConfig(config) => { + config.graph_info.ambiguous_with = Ambiguity::IgnoreAll; + } + SystemConfigs::Configs { configs, .. } => { + for config in configs { + config.ambiguous_with_all_inner(); + } + } + } + } + + fn run_if_inner(&mut self, condition: BoxedCondition) { + match self { + SystemConfigs::SystemConfig(config) => { + config.conditions.push(condition); + } + SystemConfigs::Configs { .. } => { + todo!("run_if is not implemented for groups of systems yet") + } + } + } +} + +/// Types that can convert into a [`SystemConfigs`]. +pub trait IntoSystemConfigs +where + Self: Sized, +{ + /// Convert into a [`SystemConfigs`]. + #[doc(hidden)] + fn into_configs(self) -> SystemConfigs; + + /// Add these systems to the provided `set`. + #[track_caller] + fn in_set(self, set: impl SystemSet) -> SystemConfigs { + self.into_configs().in_set(set) + } + + /// Run before all systems in `set`. + fn before(self, set: impl IntoSystemSet) -> SystemConfigs { + self.into_configs().before(set) + } + + /// Run after all systems in `set`. + fn after(self, set: impl IntoSystemSet) -> SystemConfigs { + self.into_configs().after(set) + } + + /// Add a run condition to each contained system. + /// + /// Each system will receive its own clone of the [`Condition`] and will only run + /// if the `Condition` is true. + /// + /// Each individual condition will be evaluated at most once (per schedule run), + /// right before the corresponding system prepares to run. + /// + /// This is equivalent to calling [`run_if`](IntoSystemConfigs::run_if) on each individual + /// system, as shown below: + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// # let mut app = Schedule::new(); + /// # fn a() {} + /// # fn b() {} + /// # fn condition() -> bool { true } + /// app.add_systems((a, b).distributive_run_if(condition)); + /// app.add_systems((a.run_if(condition), b.run_if(condition))); + /// ``` + /// + /// # Note + /// + /// Because the conditions are evaluated separately for each system, there is no guarantee + /// that all evaluations in a single schedule run will yield the same result. If another + /// system is run inbetween two evaluations it could cause the result of the condition to change. + /// + /// Use [`run_if`](IntoSystemSetConfig::run_if) on a [`SystemSet`] if you want to make sure + /// that either all or none of the systems are run, or you don't want to evaluate the run + /// condition for each contained system separately. + fn distributive_run_if(self, condition: impl Condition + Clone) -> SystemConfigs { + self.into_configs().distributive_run_if(condition) + } + + /// Run the systems only if the [`Condition`] is `true`. + /// + /// The `Condition` will be evaluated at most once (per schedule run), + /// the first time a system in this set prepares to run. + fn run_if(self, condition: impl Condition) -> SystemConfigs { + self.into_configs().run_if(condition) + } + + /// Suppress warnings and errors that would result from these systems having ambiguities + /// (conflicting access but indeterminate order) with systems in `set`. + fn ambiguous_with(self, set: impl IntoSystemSet) -> SystemConfigs { + self.into_configs().ambiguous_with(set) + } + + /// Suppress warnings and errors that would result from these systems having ambiguities + /// (conflicting access but indeterminate order) with any other system. + fn ambiguous_with_all(self) -> SystemConfigs { + self.into_configs().ambiguous_with_all() + } + + /// Treat this collection as a sequence of systems. + /// + /// Ordering constraints will be applied between the successive elements. + fn chain(self) -> SystemConfigs { + self.into_configs().chain() + } + + /// This used to add the system to `CoreSchedule::Startup`. + /// This was a shorthand for `self.in_schedule(CoreSchedule::Startup)`. + /// + /// # Panics + /// + /// Always panics. Please migrate to the new `App::add_systems` with the `Startup` schedule: + /// Ex: `app.add_system(foo.on_startup())` -> `app.add_systems(Startup, foo)` + #[deprecated( + since = "0.11.0", + note = "`app.add_system(foo.on_startup())` has been deprecated in favor of `app.add_systems(Startup, foo)`. Please migrate to that API." + )] + fn on_startup(self) -> SystemConfigs { + panic!("`app.add_system(foo.on_startup())` has been deprecated in favor of `app.add_systems(Startup, foo)`. Please migrate to that API."); + } + + /// This used to add the system to the provided `schedule`. + /// + /// # Panics + /// + /// Always panics. Please migrate to the new `App::add_systems`: + /// Ex: `app.add_system(foo.in_schedule(SomeSchedule))` -> `app.add_systems(SomeSchedule, foo)` + #[deprecated( + since = "0.11.0", + note = "`app.add_system(foo.in_schedule(SomeSchedule))` has been deprecated in favor of `app.add_systems(SomeSchedule, foo)`. Please migrate to that API." + )] + fn in_schedule(self, _schedule: impl ScheduleLabel) -> SystemConfigs { + panic!("`app.add_system(foo.in_schedule(SomeSchedule))` has been deprecated in favor of `app.add_systems(SomeSchedule, foo)`. Please migrate to that API."); + } +} + +impl IntoSystemConfigs<()> for SystemConfigs { + fn into_configs(self) -> Self { + self + } + + #[track_caller] + fn in_set(mut self, set: impl SystemSet) -> Self { + assert!( + set.system_type().is_none(), + "adding arbitrary systems to a system type set is not allowed" + ); + + self.in_set_inner(set.dyn_clone()); + + self + } + + fn before(mut self, set: impl IntoSystemSet) -> Self { + let set = set.into_system_set(); + self.before_inner(set.dyn_clone()); + self + } + + fn after(mut self, set: impl IntoSystemSet) -> Self { + let set = set.into_system_set(); + self.after_inner(set.dyn_clone()); + self + } + + fn distributive_run_if(mut self, condition: impl Condition + Clone) -> SystemConfigs { + self.distributive_run_if_inner(condition); + self + } + + fn ambiguous_with(mut self, set: impl IntoSystemSet) -> Self { + let set = set.into_system_set(); + self.ambiguous_with_inner(set.dyn_clone()); + self + } + + fn ambiguous_with_all(mut self) -> Self { + self.ambiguous_with_all_inner(); + self + } + + fn run_if(mut self, condition: impl Condition) -> SystemConfigs { + self.run_if_inner(new_condition(condition)); + self + } + + fn chain(mut self) -> Self { + match &mut self { + SystemConfigs::SystemConfig(_) => { /* no op */ } + SystemConfigs::Configs { chained, .. } => { + *chained = true; + } + } + self + } +} + +pub struct SystemConfigTupleMarker; + +macro_rules! impl_system_collection { + ($(($param: ident, $sys: ident)),*) => { + impl<$($param, $sys),*> IntoSystemConfigs<(SystemConfigTupleMarker, $($param,)*)> for ($($sys,)*) + where + $($sys: IntoSystemConfigs<$param>),* + { + #[allow(non_snake_case)] + fn into_configs(self) -> SystemConfigs { + let ($($sys,)*) = self; + SystemConfigs::Configs { + configs: vec![$($sys.into_configs(),)*], + chained: false, + } + } + } + } +} + +all_tuples!(impl_system_collection, 1, 20, P, S); + +/// A [`SystemSet`] with scheduling metadata. +pub struct SystemSetConfig { + pub(super) set: BoxedSystemSet, + pub(super) graph_info: GraphInfo, + pub(super) conditions: Vec, +} + +impl SystemSetConfig { + fn new(set: BoxedSystemSet) -> Self { + // system type sets are automatically populated + // to avoid unintentionally broad changes, they cannot be configured + assert!( + set.system_type().is_none(), + "configuring system type sets is not allowed" + ); + + Self { + set, + graph_info: GraphInfo::default(), + conditions: Vec::new(), + } + } +} + /// Types that can be converted into a [`SystemSetConfig`]. /// /// This has been implemented for all types that implement [`SystemSet`] and boxed trait objects. @@ -88,18 +406,9 @@ pub trait IntoSystemSetConfig: Sized { fn into_config(self) -> SystemSetConfig; /// Add to the provided `set`. #[track_caller] - fn in_set(self, set: impl FreeSystemSet) -> SystemSetConfig { + fn in_set(self, set: impl SystemSet) -> SystemSetConfig { self.into_config().in_set(set) } - /// Add to the provided "base" `set`. For more information on base sets, see [`SystemSet::is_base`]. - #[track_caller] - fn in_base_set(self, set: impl BaseSystemSet) -> SystemSetConfig { - self.into_config().in_base_set(set) - } - /// Add this set to the schedules's default base set. - fn in_default_base_set(self) -> SystemSetConfig { - self.into_config().in_default_base_set() - } /// Run before all systems in `set`. fn before(self, set: impl IntoSystemSet) -> SystemSetConfig { self.into_config().before(set) @@ -125,6 +434,35 @@ pub trait IntoSystemSetConfig: Sized { fn ambiguous_with_all(self) -> SystemSetConfig { self.into_config().ambiguous_with_all() } + + /// This used to configure the set in the `CoreSchedule::Startup` schedule. + /// This was a shorthand for `self.in_schedule(CoreSchedule::Startup)`. + /// + /// # Panics + /// + /// Always panics. Please migrate to the new `App::configure_set` with the `Startup` schedule: + /// Ex: `app.configure_set(MySet.on_startup())` -> `app.configure_set(Startup, MySet)` + #[deprecated( + since = "0.11.0", + note = "`app.configure_set(MySet.on_startup())` has been deprecated in favor of `app.configure_set(Startup, MySet)`. Please migrate to that API." + )] + fn on_startup(self) -> SystemSetConfigs { + panic!("`app.configure_set(MySet.on_startup())` has been deprecated in favor of `app.configure_set(Startup, MySet)`. Please migrate to that API."); + } + + /// This used to configure the set in the provided `schedule`. + /// + /// # Panics + /// + /// Always panics. Please migrate to the new `App::configure_set`: + /// Ex: `app.configure_set(MySet.in_schedule(SomeSchedule))` -> `app.configure_set(SomeSchedule, MySet)` + #[deprecated( + since = "0.11.0", + note = "`app.configure_set(MySet.in_schedule(SomeSchedule))` has been deprecated in favor of `app.configure_set(SomeSchedule, MySet)`. Please migrate to that API." + )] + fn in_schedule(self, _schedule: impl ScheduleLabel) -> SystemSetConfigs { + panic!("`app.configure_set(MySet.in_schedule(SomeSchedule))` has been deprecated in favor of `app.configure_set(SomeSchedule, MySet)`. Please migrate to that API."); + } } impl IntoSystemSetConfig for S { @@ -150,41 +488,10 @@ impl IntoSystemSetConfig for SystemSetConfig { set.system_type().is_none(), "adding arbitrary systems to a system type set is not allowed" ); - assert!( - !set.is_base(), - "Sets cannot be added to 'base' system sets using 'in_set'. Use 'in_base_set' instead." - ); - assert!( - !self.set.is_base(), - "Base system sets cannot be added to other sets." - ); self.graph_info.sets.push(Box::new(set)); self } - #[track_caller] - fn in_base_set(mut self, set: impl SystemSet) -> Self { - assert!( - set.system_type().is_none(), - "System type sets cannot be base sets." - ); - assert!( - set.is_base(), - "Sets cannot be added to normal sets using 'in_base_set'. Use 'in_set' instead." - ); - assert!( - !self.set.is_base(), - "Base system sets cannot be added to other sets." - ); - self.graph_info.set_base_set(Box::new(set)); - self - } - - fn in_default_base_set(mut self) -> SystemSetConfig { - self.graph_info.add_default_base_set = true; - self - } - fn before(mut self, set: impl IntoSystemSet) -> Self { self.graph_info.dependencies.push(Dependency::new( DependencyKind::Before, @@ -217,329 +524,6 @@ impl IntoSystemSetConfig for SystemSetConfig { } } -/// Types that can be converted into a [`SystemConfig`]. -/// -/// This has been implemented for boxed [`System`](crate::system::System) -/// trait objects and all functions that turn into such. -pub trait IntoSystemConfig: Sized -where - Config: IntoSystemConfig<(), Config>, -{ - /// Convert into a [`SystemConfig`]. - #[doc(hidden)] - fn into_config(self) -> Config; - /// Add to `set` membership. - #[track_caller] - fn in_set(self, set: impl FreeSystemSet) -> Config { - self.into_config().in_set(set) - } - /// Add to the provided "base" `set`. For more information on base sets, see [`SystemSet::is_base`]. - #[track_caller] - fn in_base_set(self, set: impl BaseSystemSet) -> Config { - self.into_config().in_base_set(set) - } - /// Don't add this system to the schedules's default set. - fn no_default_base_set(self) -> Config { - self.into_config().no_default_base_set() - } - /// Run before all systems in `set`. - fn before(self, set: impl IntoSystemSet) -> Config { - self.into_config().before(set) - } - /// Run after all systems in `set`. - fn after(self, set: impl IntoSystemSet) -> Config { - self.into_config().after(set) - } - /// Run only if the [`Condition`] is `true`. - /// - /// The `Condition` will be evaluated at most once (per schedule run), - /// when the system prepares to run. - fn run_if(self, condition: impl Condition) -> Config { - self.into_config().run_if(condition) - } - /// Suppress warnings and errors that would result from this system having ambiguities - /// (conflicting access but indeterminate order) with systems in `set`. - fn ambiguous_with(self, set: impl IntoSystemSet) -> Config { - self.into_config().ambiguous_with(set) - } - /// Suppress warnings and errors that would result from this system having ambiguities - /// (conflicting access but indeterminate order) with any other system. - fn ambiguous_with_all(self) -> Config { - self.into_config().ambiguous_with_all() - } -} - -impl IntoSystemConfig for F -where - F: IntoSystem<(), (), Marker>, -{ - fn into_config(self) -> SystemConfig { - SystemConfig::new(Box::new(IntoSystem::into_system(self))) - } -} - -impl IntoSystemConfig<()> for BoxedSystem<(), ()> { - fn into_config(self) -> SystemConfig { - SystemConfig::new(self) - } -} - -impl IntoSystemConfig<()> for SystemConfig { - fn into_config(self) -> Self { - self - } - - #[track_caller] - fn in_set(mut self, set: impl SystemSet) -> Self { - assert!( - set.system_type().is_none(), - "adding arbitrary systems to a system type set is not allowed" - ); - assert!( - !set.is_base(), - "Systems cannot be added to 'base' system sets using 'in_set'. Use 'in_base_set' instead." - ); - self.graph_info.sets.push(Box::new(set)); - self - } - - #[track_caller] - fn in_base_set(mut self, set: impl SystemSet) -> Self { - assert!( - set.system_type().is_none(), - "System type sets cannot be base sets." - ); - assert!( - set.is_base(), - "Systems cannot be added to normal sets using 'in_base_set'. Use 'in_set' instead." - ); - self.graph_info.set_base_set(Box::new(set)); - self - } - - fn no_default_base_set(mut self) -> SystemConfig { - self.graph_info.add_default_base_set = false; - self - } - - fn before(mut self, set: impl IntoSystemSet) -> Self { - self.graph_info.dependencies.push(Dependency::new( - DependencyKind::Before, - Box::new(set.into_system_set()), - )); - self - } - - fn after(mut self, set: impl IntoSystemSet) -> Self { - self.graph_info.dependencies.push(Dependency::new( - DependencyKind::After, - Box::new(set.into_system_set()), - )); - self - } - - fn run_if(mut self, condition: impl Condition) -> Self { - self.conditions.push(new_condition(condition)); - self - } - - fn ambiguous_with(mut self, set: impl IntoSystemSet) -> Self { - ambiguous_with(&mut self.graph_info, Box::new(set.into_system_set())); - self - } - - fn ambiguous_with_all(mut self) -> Self { - self.graph_info.ambiguous_with = Ambiguity::IgnoreAll; - self - } -} - -/// A collection of [`SystemConfig`]. -pub struct SystemConfigs { - pub(super) systems: Vec, - /// If `true`, adds `before -> after` ordering constraints between the successive elements. - pub(super) chained: bool, -} - -/// Types that can convert into a [`SystemConfigs`]. -pub trait IntoSystemConfigs -where - Self: Sized, -{ - /// Convert into a [`SystemConfigs`]. - #[doc(hidden)] - fn into_configs(self) -> SystemConfigs; - - /// Add these systems to the provided `set`. - #[track_caller] - fn in_set(self, set: impl FreeSystemSet) -> SystemConfigs { - self.into_configs().in_set(set) - } - - /// Add these systems to the provided "base" `set`. For more information on base sets, see [`SystemSet::is_base`]. - #[track_caller] - fn in_base_set(self, set: impl BaseSystemSet) -> SystemConfigs { - self.into_configs().in_base_set(set) - } - - /// Run before all systems in `set`. - fn before(self, set: impl IntoSystemSet) -> SystemConfigs { - self.into_configs().before(set) - } - - /// Run after all systems in `set`. - fn after(self, set: impl IntoSystemSet) -> SystemConfigs { - self.into_configs().after(set) - } - - /// Add a run condition to each contained system. - /// - /// Each system will receive its own clone of the [`Condition`] and will only run - /// if the `Condition` is true. - /// - /// Each individual condition will be evaluated at most once (per schedule run), - /// right before the corresponding system prepares to run. - /// - /// This is equivalent to calling [`run_if`](IntoSystemConfig::run_if) on each individual - /// system, as shown below: - /// - /// ``` - /// # use bevy_ecs::prelude::*; - /// # let mut app = Schedule::new(); - /// # fn a() {} - /// # fn b() {} - /// # fn condition() -> bool { true } - /// app.add_systems((a, b).distributive_run_if(condition)); - /// app.add_systems((a.run_if(condition), b.run_if(condition))); - /// ``` - /// - /// # Note - /// - /// Because the conditions are evaluated separately for each system, there is no guarantee - /// that all evaluations in a single schedule run will yield the same result. If another - /// system is run inbetween two evaluations it could cause the result of the condition to change. - /// - /// Use [`run_if`](IntoSystemSetConfig::run_if) on a [`SystemSet`] if you want to make sure - /// that either all or none of the systems are run, or you don't want to evaluate the run - /// condition for each contained system separately. - fn distributive_run_if(self, condition: impl Condition + Clone) -> SystemConfigs { - self.into_configs().distributive_run_if(condition) - } - - /// Suppress warnings and errors that would result from these systems having ambiguities - /// (conflicting access but indeterminate order) with systems in `set`. - fn ambiguous_with(self, set: impl IntoSystemSet) -> SystemConfigs { - self.into_configs().ambiguous_with(set) - } - - /// Suppress warnings and errors that would result from these systems having ambiguities - /// (conflicting access but indeterminate order) with any other system. - fn ambiguous_with_all(self) -> SystemConfigs { - self.into_configs().ambiguous_with_all() - } - - /// Treat this collection as a sequence of systems. - /// - /// Ordering constraints will be applied between the successive elements. - fn chain(self) -> SystemConfigs { - self.into_configs().chain() - } -} - -impl IntoSystemConfigs<()> for SystemConfigs { - fn into_configs(self) -> Self { - self - } - - #[track_caller] - fn in_set(mut self, set: impl SystemSet) -> Self { - assert!( - set.system_type().is_none(), - "adding arbitrary systems to a system type set is not allowed" - ); - assert!( - !set.is_base(), - "Systems cannot be added to 'base' system sets using 'in_set'. Use 'in_base_set' instead." - ); - for config in &mut self.systems { - config.graph_info.sets.push(set.dyn_clone()); - } - - self - } - - #[track_caller] - fn in_base_set(mut self, set: impl SystemSet) -> Self { - assert!( - set.system_type().is_none(), - "System type sets cannot be base sets." - ); - assert!( - set.is_base(), - "Systems cannot be added to normal sets using 'in_base_set'. Use 'in_set' instead." - ); - for config in &mut self.systems { - config.graph_info.set_base_set(set.dyn_clone()); - } - - self - } - - fn before(mut self, set: impl IntoSystemSet) -> Self { - let set = set.into_system_set(); - for config in &mut self.systems { - config - .graph_info - .dependencies - .push(Dependency::new(DependencyKind::Before, set.dyn_clone())); - } - - self - } - - fn after(mut self, set: impl IntoSystemSet) -> Self { - let set = set.into_system_set(); - for config in &mut self.systems { - config - .graph_info - .dependencies - .push(Dependency::new(DependencyKind::After, set.dyn_clone())); - } - - self - } - - fn distributive_run_if(mut self, condition: impl Condition + Clone) -> SystemConfigs { - for config in &mut self.systems { - config.conditions.push(new_condition(condition.clone())); - } - - self - } - - fn ambiguous_with(mut self, set: impl IntoSystemSet) -> Self { - let set = set.into_system_set(); - for config in &mut self.systems { - ambiguous_with(&mut config.graph_info, set.dyn_clone()); - } - - self - } - - fn ambiguous_with_all(mut self) -> Self { - for config in &mut self.systems { - config.graph_info.ambiguous_with = Ambiguity::IgnoreAll; - } - - self - } - - fn chain(mut self) -> Self { - self.chained = true; - self - } -} - /// A collection of [`SystemSetConfig`]. pub struct SystemSetConfigs { pub(super) sets: Vec, @@ -558,16 +542,10 @@ where /// Add these system sets to the provided `set`. #[track_caller] - fn in_set(self, set: impl FreeSystemSet) -> SystemSetConfigs { + fn in_set(self, set: impl SystemSet) -> SystemSetConfigs { self.into_configs().in_set(set) } - /// Add these system sets to the provided "base" `set`. For more information on base sets, see [`SystemSet::is_base`]. - #[track_caller] - fn in_base_set(self, set: impl BaseSystemSet) -> SystemSetConfigs { - self.into_configs().in_base_set(set) - } - /// Run before all systems in `set`. fn before(self, set: impl IntoSystemSet) -> SystemSetConfigs { self.into_configs().before(set) @@ -596,6 +574,35 @@ where fn chain(self) -> SystemSetConfigs { self.into_configs().chain() } + + /// This used to configure the sets in the `CoreSchedule::Startup` schedule. + /// This was a shorthand for `self.in_schedule(CoreSchedule::Startup)`. + /// + /// # Panics + /// + /// Always panics. Please migrate to the new `App::configure_sets` with the `Startup` schedule: + /// Ex: `app.configure_sets((A, B).on_startup())` -> `app.configure_sets(Startup, (A, B))` + #[deprecated( + since = "0.11.0", + note = "`app.configure_sets((A, B).on_startup())` has been deprecated in favor of `app.configure_sets(Startup, (A, B))`. Please migrate to that API." + )] + fn on_startup(self) -> SystemSetConfigs { + panic!("`app.configure_sets((A, B).on_startup())` has been deprecated in favor of `app.configure_sets(Startup, (A, B))`. Please migrate to that API."); + } + + /// This used to configure the sets in the provided `schedule`. + /// + /// # Panics + /// + /// Always panics. Please migrate to the new `App::configure_set`: + /// Ex: `app.configure_sets((A, B).in_schedule(SomeSchedule))` -> `app.configure_sets(SomeSchedule, (A, B))` + #[deprecated( + since = "0.11.0", + note = "`app.configure_sets((A, B).in_schedule(SomeSchedule))` has been deprecated in favor of `app.configure_sets(SomeSchedule, (A, B))`. Please migrate to that API." + )] + fn in_schedule(self, _schedule: impl ScheduleLabel) -> SystemSetConfigs { + panic!("`app.configure_sets((A, B).in_schedule(SomeSchedule))` has been deprecated in favor of `app.configure_sets(SomeSchedule, (A, B))`. Please migrate to that API."); + } } impl IntoSystemSetConfigs for SystemSetConfigs { @@ -609,42 +616,13 @@ impl IntoSystemSetConfigs for SystemSetConfigs { set.system_type().is_none(), "adding arbitrary systems to a system type set is not allowed" ); - assert!( - !set.is_base(), - "Sets cannot be added to 'base' system sets using 'in_set'. Use 'in_base_set' instead." - ); for config in &mut self.sets { - assert!( - !config.set.is_base(), - "Base system sets cannot be added to other sets." - ); config.graph_info.sets.push(set.dyn_clone()); } self } - #[track_caller] - fn in_base_set(mut self, set: impl SystemSet) -> Self { - assert!( - set.system_type().is_none(), - "System type sets cannot be base sets." - ); - assert!( - set.is_base(), - "Sets cannot be added to normal sets using 'in_base_set'. Use 'in_set' instead." - ); - for config in &mut self.sets { - assert!( - !config.set.is_base(), - "Base system sets cannot be added to other sets." - ); - config.graph_info.set_base_set(set.dyn_clone()); - } - - self - } - fn before(mut self, set: impl IntoSystemSet) -> Self { let set = set.into_system_set(); for config in &mut self.sets { @@ -692,24 +670,6 @@ impl IntoSystemSetConfigs for SystemSetConfigs { } } -macro_rules! impl_system_collection { - ($(($param: ident, $sys: ident)),*) => { - impl<$($param, $sys),*> IntoSystemConfigs<($($param,)*)> for ($($sys,)*) - where - $($sys: IntoSystemConfig<$param>),* - { - #[allow(non_snake_case)] - fn into_configs(self) -> SystemConfigs { - let ($($sys,)*) = self; - SystemConfigs { - systems: vec![$($sys.into_config(),)*], - chained: false, - } - } - } - } -} - macro_rules! impl_system_set_collection { ($($set: ident),*) => { impl<$($set: IntoSystemSetConfig),*> IntoSystemSetConfigs for ($($set,)*) @@ -726,5 +686,4 @@ macro_rules! impl_system_set_collection { } } -all_tuples!(impl_system_collection, 0, 15, P, S); all_tuples!(impl_system_set_collection, 0, 15, S); diff --git a/crates/bevy_ecs/src/schedule/graph_utils.rs b/crates/bevy_ecs/src/schedule/graph_utils.rs index f7e28e6bb5..4a0311c8be 100644 --- a/crates/bevy_ecs/src/schedule/graph_utils.rs +++ b/crates/bevy_ecs/src/schedule/graph_utils.rs @@ -67,56 +67,14 @@ pub(crate) enum Ambiguity { IgnoreAll, } -#[derive(Clone)] +#[derive(Clone, Default)] pub(crate) struct GraphInfo { pub(crate) sets: Vec, pub(crate) dependencies: Vec, pub(crate) ambiguous_with: Ambiguity, - pub(crate) add_default_base_set: bool, pub(crate) base_set: Option, } -impl Default for GraphInfo { - fn default() -> Self { - GraphInfo { - sets: Vec::new(), - base_set: None, - dependencies: Vec::new(), - ambiguous_with: Ambiguity::default(), - add_default_base_set: true, - } - } -} - -impl GraphInfo { - pub(crate) fn system() -> GraphInfo { - GraphInfo { - // systems get the default base set automatically - add_default_base_set: true, - ..Default::default() - } - } - - pub(crate) fn system_set() -> GraphInfo { - GraphInfo { - // sets do not get the default base set automatically - add_default_base_set: false, - ..Default::default() - } - } - - #[track_caller] - pub(crate) fn set_base_set(&mut self, set: BoxedSystemSet) { - if let Some(current) = &self.base_set { - panic!( - "Cannot set the base set because base set {current:?} has already been configured." - ); - } else { - self.base_set = Some(set); - } - } -} - /// Converts 2D row-major pair of indices into a 1D array index. pub(crate) fn index(row: usize, col: usize, num_cols: usize) -> usize { debug_assert!(col < num_cols); diff --git a/crates/bevy_ecs/src/schedule/mod.rs b/crates/bevy_ecs/src/schedule/mod.rs index f886d815d1..05aaba9f8b 100644 --- a/crates/bevy_ecs/src/schedule/mod.rs +++ b/crates/bevy_ecs/src/schedule/mod.rs @@ -23,7 +23,7 @@ mod tests { use std::sync::atomic::{AtomicU32, Ordering}; pub use crate as bevy_ecs; - pub use crate::schedule::{IntoSystemConfig, IntoSystemSetConfig, Schedule, SystemSet}; + pub use crate::schedule::{IntoSystemSetConfig, Schedule, SystemSet}; pub use crate::system::{Res, ResMut}; pub use crate::{prelude::World, system::Resource}; @@ -75,7 +75,7 @@ mod tests { world.init_resource::(); - schedule.add_system(make_function_system(0)); + schedule.add_systems(make_function_system(0)); schedule.run(&mut world); assert_eq!(world.resource::().0, vec![0]); @@ -88,7 +88,7 @@ mod tests { world.init_resource::(); - schedule.add_system(make_exclusive_system(0)); + schedule.add_systems(make_exclusive_system(0)); schedule.run(&mut world); assert_eq!(world.resource::().0, vec![0]); @@ -108,7 +108,7 @@ mod tests { for _ in 0..thread_count { let inner = barrier.clone(); - schedule.add_system(move || { + schedule.add_systems(move || { inner.wait(); }); } @@ -127,13 +127,13 @@ mod tests { world.init_resource::(); - schedule.add_system(named_system); - schedule.add_system(make_function_system(1).before(named_system)); - schedule.add_system( + schedule.add_systems(( + named_system, + make_function_system(1).before(named_system), make_function_system(0) .after(named_system) .in_set(TestSet::A), - ); + )); schedule.run(&mut world); assert_eq!(world.resource::().0, vec![1, u32::MAX, 0]); @@ -144,12 +144,12 @@ mod tests { // modify the schedule after it's been initialized and test ordering with sets schedule.configure_set(TestSet::A.after(named_system)); - schedule.add_system( + schedule.add_systems(( make_function_system(3) .before(TestSet::A) .after(named_system), - ); - schedule.add_system(make_function_system(4).after(TestSet::A)); + make_function_system(4).after(TestSet::A), + )); schedule.run(&mut world); assert_eq!( @@ -195,6 +195,50 @@ mod tests { schedule.run(&mut world); assert_eq!(world.resource::().0, vec![0, 1, 2, 3]); } + + #[test] + fn add_systems_correct_order_nested() { + let mut world = World::new(); + let mut schedule = Schedule::new(); + + world.init_resource::(); + + schedule.add_systems( + ( + (make_function_system(0), make_function_system(1)).chain(), + make_function_system(2), + (make_function_system(3), make_function_system(4)).chain(), + ( + make_function_system(5), + (make_function_system(6), make_function_system(7)), + ), + ( + (make_function_system(8), make_function_system(9)).chain(), + make_function_system(10), + ), + ) + .chain(), + ); + + schedule.run(&mut world); + let order = &world.resource::().0; + assert_eq!( + &order[0..5], + &[0, 1, 2, 3, 4], + "first five items should be exactly ordered" + ); + let unordered = &order[5..8]; + assert!( + unordered.contains(&5) && unordered.contains(&6) && unordered.contains(&7), + "unordered must be 5, 6, and 7 in any order" + ); + let partially_ordered = &order[8..11]; + assert!( + partially_ordered == [8, 9, 10] || partially_ordered == [10, 8, 9], + "partially_ordered must be [8, 9, 10] or [10, 8, 9]" + ); + assert!(order.len() == 11, "must have exacty 11 order entries"); + } } mod conditions { @@ -210,7 +254,7 @@ mod tests { world.init_resource::(); world.init_resource::(); - schedule.add_system( + schedule.add_systems( make_function_system(0).run_if(|condition: Res| condition.0), ); @@ -256,7 +300,7 @@ mod tests { world.init_resource::(); world.init_resource::(); - schedule.add_system( + schedule.add_systems( make_exclusive_system(0).run_if(|condition: Res| condition.0), ); @@ -275,10 +319,12 @@ mod tests { world.init_resource::(); - schedule.add_system(counting_system.run_if(|| false).run_if(|| false)); - schedule.add_system(counting_system.run_if(|| true).run_if(|| false)); - schedule.add_system(counting_system.run_if(|| false).run_if(|| true)); - schedule.add_system(counting_system.run_if(|| true).run_if(|| true)); + schedule.add_systems(( + counting_system.run_if(|| false).run_if(|| false), + counting_system.run_if(|| true).run_if(|| false), + counting_system.run_if(|| false).run_if(|| true), + counting_system.run_if(|| true).run_if(|| true), + )); schedule.run(&mut world); assert_eq!(world.resource::().0.load(Ordering::Relaxed), 1); @@ -292,13 +338,13 @@ mod tests { world.init_resource::(); schedule.configure_set(TestSet::A.run_if(|| false).run_if(|| false)); - schedule.add_system(counting_system.in_set(TestSet::A)); + schedule.add_systems(counting_system.in_set(TestSet::A)); schedule.configure_set(TestSet::B.run_if(|| true).run_if(|| false)); - schedule.add_system(counting_system.in_set(TestSet::B)); + schedule.add_systems(counting_system.in_set(TestSet::B)); schedule.configure_set(TestSet::C.run_if(|| false).run_if(|| true)); - schedule.add_system(counting_system.in_set(TestSet::C)); + schedule.add_systems(counting_system.in_set(TestSet::C)); schedule.configure_set(TestSet::D.run_if(|| true).run_if(|| true)); - schedule.add_system(counting_system.in_set(TestSet::D)); + schedule.add_systems(counting_system.in_set(TestSet::D)); schedule.run(&mut world); assert_eq!(world.resource::().0.load(Ordering::Relaxed), 1); @@ -312,13 +358,13 @@ mod tests { world.init_resource::(); schedule.configure_set(TestSet::A.run_if(|| false)); - schedule.add_system(counting_system.in_set(TestSet::A).run_if(|| false)); + schedule.add_systems(counting_system.in_set(TestSet::A).run_if(|| false)); schedule.configure_set(TestSet::B.run_if(|| true)); - schedule.add_system(counting_system.in_set(TestSet::B).run_if(|| false)); + schedule.add_systems(counting_system.in_set(TestSet::B).run_if(|| false)); schedule.configure_set(TestSet::C.run_if(|| false)); - schedule.add_system(counting_system.in_set(TestSet::C).run_if(|| true)); + schedule.add_systems(counting_system.in_set(TestSet::C).run_if(|| true)); schedule.configure_set(TestSet::D.run_if(|| true)); - schedule.add_system(counting_system.in_set(TestSet::D).run_if(|| true)); + schedule.add_systems(counting_system.in_set(TestSet::D).run_if(|| true)); schedule.run(&mut world); assert_eq!(world.resource::().0.load(Ordering::Relaxed), 1); @@ -335,7 +381,7 @@ mod tests { world.init_resource::(); let mut schedule = Schedule::default(); - schedule.add_system( + schedule.add_systems( counting_system .run_if(|res1: Res| res1.is_changed()) .run_if(|res2: Res| res2.is_changed()), @@ -389,7 +435,7 @@ mod tests { .run_if(|res2: Res| res2.is_changed()), ); - schedule.add_system(counting_system.in_set(TestSet::A)); + schedule.add_systems(counting_system.in_set(TestSet::A)); // both resource were just added. schedule.run(&mut world); @@ -436,7 +482,7 @@ mod tests { schedule .configure_set(TestSet::A.run_if(|res1: Res| res1.is_changed())); - schedule.add_system( + schedule.add_systems( counting_system .run_if(|res2: Res| res2.is_changed()) .in_set(TestSet::A), @@ -535,15 +581,14 @@ mod tests { let mut schedule = Schedule::new(); // Schedule `bar` to run after `foo`. - schedule.add_system(foo); - schedule.add_system(bar.after(foo)); + schedule.add_systems((foo, bar.after(foo))); // There's only one `foo`, so it's fine. let result = schedule.initialize(&mut world); assert!(result.is_ok()); // Schedule another `foo`. - schedule.add_system(foo); + schedule.add_systems(foo); // When there are multiple instances of `foo`, dependencies on // `foo` are no longer allowed. Too much ambiguity. @@ -555,11 +600,11 @@ mod tests { // same goes for `ambiguous_with` let mut schedule = Schedule::new(); - schedule.add_system(foo); - schedule.add_system(bar.ambiguous_with(foo)); + schedule.add_systems(foo); + schedule.add_systems(bar.ambiguous_with(foo)); let result = schedule.initialize(&mut world); assert!(result.is_ok()); - schedule.add_system(foo); + schedule.add_systems(foo); let result = schedule.initialize(&mut world); assert!(matches!( result, @@ -625,7 +670,7 @@ mod tests { fn foo() {} // Add `foo` to both `A` and `C`. - schedule.add_system(foo.in_set(TestSet::A).in_set(TestSet::C)); + schedule.add_systems(foo.in_set(TestSet::A).in_set(TestSet::C)); // Order `A -> B -> C`. schedule.configure_sets(( @@ -663,138 +708,4 @@ mod tests { assert!(matches!(result, Err(ScheduleBuildError::Ambiguity))); } } - - mod base_sets { - use super::*; - - #[derive(SystemSet, Hash, Debug, Eq, PartialEq, Clone)] - #[system_set(base)] - enum Base { - A, - B, - } - - #[derive(SystemSet, Hash, Debug, Eq, PartialEq, Clone)] - enum Normal { - X, - Y, - } - - #[test] - #[should_panic] - fn disallow_adding_base_sets_to_sets() { - let mut schedule = Schedule::new(); - schedule.configure_set(Base::A.in_set(Normal::X)); - } - - #[test] - #[should_panic] - fn disallow_adding_base_sets_to_base_sets() { - let mut schedule = Schedule::new(); - schedule.configure_set(Base::A.in_base_set(Base::B)); - } - - #[test] - #[should_panic] - fn disallow_adding_set_to_multiple_base_sets() { - let mut schedule = Schedule::new(); - schedule.configure_set(Normal::X.in_base_set(Base::A).in_base_set(Base::B)); - } - - #[test] - #[should_panic] - fn disallow_adding_sets_to_multiple_base_sets() { - let mut schedule = Schedule::new(); - schedule.configure_sets( - (Normal::X, Normal::Y) - .in_base_set(Base::A) - .in_base_set(Base::B), - ); - } - - #[test] - #[should_panic] - fn disallow_adding_system_to_multiple_base_sets() { - let mut schedule = Schedule::new(); - schedule.add_system(named_system.in_base_set(Base::A).in_base_set(Base::B)); - } - - #[test] - #[should_panic] - fn disallow_adding_systems_to_multiple_base_sets() { - let mut schedule = Schedule::new(); - schedule.add_systems( - (make_function_system(0), make_function_system(1)) - .in_base_set(Base::A) - .in_base_set(Base::B), - ); - } - - #[test] - fn disallow_multiple_base_sets() { - let mut world = World::new(); - - let mut schedule = Schedule::new(); - schedule - .configure_set(Normal::X.in_base_set(Base::A)) - .configure_set(Normal::Y.in_base_set(Base::B)) - .add_system(named_system.in_set(Normal::X).in_set(Normal::Y)); - - let result = schedule.initialize(&mut world); - assert!(matches!( - result, - Err(ScheduleBuildError::SystemInMultipleBaseSets { .. }) - )); - - let mut schedule = Schedule::new(); - schedule - .configure_set(Normal::X.in_base_set(Base::A)) - .configure_set(Normal::Y.in_base_set(Base::B).in_set(Normal::X)); - - let result = schedule.initialize(&mut world); - assert!(matches!( - result, - Err(ScheduleBuildError::SetInMultipleBaseSets { .. }) - )); - } - - #[test] - fn allow_same_base_sets() { - let mut world = World::new(); - - let mut schedule = Schedule::new(); - schedule - .configure_set(Normal::X.in_base_set(Base::A)) - .configure_set(Normal::Y.in_base_set(Base::A)) - .add_system(named_system.in_set(Normal::X).in_set(Normal::Y)); - - let result = schedule.initialize(&mut world); - assert!(matches!(result, Ok(()))); - - let mut schedule = Schedule::new(); - schedule - .configure_set(Normal::X.in_base_set(Base::A)) - .configure_set(Normal::Y.in_base_set(Base::A).in_set(Normal::X)); - - let result = schedule.initialize(&mut world); - assert!(matches!(result, Ok(()))); - } - - #[test] - fn default_base_set_ordering() { - let mut world = World::default(); - let mut schedule = Schedule::default(); - - world.init_resource::(); - - schedule - .set_default_base_set(Base::A) - .configure_set(Base::A.before(Base::B)) - .add_system(make_function_system(0).in_base_set(Base::B)) - .add_system(make_function_system(1)); - schedule.run(&mut world); - - assert_eq!(world.resource::().0, vec![1, 0]); - } - } } diff --git a/crates/bevy_ecs/src/schedule/schedule.rs b/crates/bevy_ecs/src/schedule/schedule.rs index df4b26c6cb..303ac8dd29 100644 --- a/crates/bevy_ecs/src/schedule/schedule.rs +++ b/crates/bevy_ecs/src/schedule/schedule.rs @@ -17,7 +17,7 @@ use fixedbitset::FixedBitSet; use crate::{ self as bevy_ecs, - component::{ComponentId, Components}, + component::{ComponentId, Components, Tick}, schedule::*, system::{BoxedSystem, Resource, System}, world::World, @@ -43,17 +43,11 @@ impl Schedules { /// and the old schedule is returned. Otherwise, `None` is returned. pub fn insert(&mut self, label: impl ScheduleLabel, schedule: Schedule) -> Option { let label = label.dyn_clone(); - if self.inner.contains_key(&label) { - warn!("schedule with label {:?} already exists", label); - } self.inner.insert(label, schedule) } /// Removes the schedule corresponding to the `label` from the map, returning it if it existed. pub fn remove(&mut self, label: &dyn ScheduleLabel) -> Option { - if !self.inner.contains_key(label) { - warn!("schedule with label {:?} not found", label); - } self.inner.remove(label) } @@ -62,9 +56,6 @@ impl Schedules { &mut self, label: &dyn ScheduleLabel, ) -> Option<(Box, Schedule)> { - if !self.inner.contains_key(label) { - warn!("schedule with label {:?} not found", label); - } self.inner.remove_entry(label) } @@ -99,7 +90,7 @@ impl Schedules { /// Iterates the change ticks of all systems in all stored schedules and clamps any older than /// [`MAX_CHANGE_AGE`](crate::change_detection::MAX_CHANGE_AGE). /// This prevents overflow and thus prevents false positives. - pub(crate) fn check_change_ticks(&mut self, change_tick: u32) { + pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) { #[cfg(feature = "trace")] let _all_span = info_span!("check stored schedule ticks").entered(); // label used when trace feature is enabled @@ -134,7 +125,7 @@ fn make_executor(kind: ExecutorKind) -> Box { /// fn main() { /// let mut world = World::new(); /// let mut schedule = Schedule::default(); -/// schedule.add_system(hello_world); +/// schedule.add_systems(hello_world); /// /// schedule.run(&mut world); /// } @@ -183,21 +174,16 @@ impl Schedule { } } - pub fn set_default_base_set(&mut self, default_base_set: impl SystemSet) -> &mut Self { - self.graph - .set_default_base_set(Some(Box::new(default_base_set))); - self - } - /// Add a system to the schedule. - pub fn add_system(&mut self, system: impl IntoSystemConfig) -> &mut Self { - self.graph.add_system(system); + #[deprecated(since = "0.11.0", note = "please use `add_systems` instead")] + pub fn add_system(&mut self, system: impl IntoSystemConfigs) -> &mut Self { + self.graph.add_systems_inner(system.into_configs(), false); self } /// Add a collection of systems to the schedule. pub fn add_systems(&mut self, systems: impl IntoSystemConfigs) -> &mut Self { - self.graph.add_systems(systems); + self.graph.add_systems_inner(systems.into_configs(), false); self } @@ -283,7 +269,7 @@ impl Schedule { /// Iterates the change ticks of all systems in the schedule and clamps any older than /// [`MAX_CHANGE_AGE`](crate::change_detection::MAX_CHANGE_AGE). /// This prevents overflow and thus prevents false positives. - pub(crate) fn check_change_ticks(&mut self, change_tick: u32) { + pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) { for system in &mut self.executable.systems { system.check_change_tick(change_tick); } @@ -346,29 +332,14 @@ impl Dag { } } -/// Describes which base set (i.e. [`SystemSet`] where [`SystemSet::is_base`] returns true) -/// a system belongs to. -/// -/// Note that this is only populated once [`ScheduleGraph::build_schedule`] is called. -#[derive(Copy, Clone, Eq, PartialEq, Debug)] -pub enum BaseSetMembership { - Uncalculated, - None, - Some(NodeId), -} - /// A [`SystemSet`] with metadata, stored in a [`ScheduleGraph`]. struct SystemSetNode { inner: BoxedSystemSet, - base_set_membership: BaseSetMembership, } impl SystemSetNode { pub fn new(set: BoxedSystemSet) -> Self { - Self { - inner: set, - base_set_membership: BaseSetMembership::Uncalculated, - } + Self { inner: set } } pub fn name(&self) -> String { @@ -383,14 +354,12 @@ impl SystemSetNode { /// A [`BoxedSystem`] with metadata, stored in a [`ScheduleGraph`]. struct SystemNode { inner: Option, - base_set_membership: BaseSetMembership, } impl SystemNode { pub fn new(system: BoxedSystem) -> Self { Self { inner: Some(system), - base_set_membership: BaseSetMembership::Uncalculated, } } @@ -401,10 +370,6 @@ impl SystemNode { pub fn get_mut(&mut self) -> Option<&mut BoxedSystem> { self.inner.as_mut() } - - pub fn name(&self) -> String { - format!("{:?}", &self.inner) - } } /// Metadata for a [`Schedule`]. @@ -416,7 +381,6 @@ pub struct ScheduleGraph { system_set_conditions: Vec>>, system_set_ids: HashMap, uninit: Vec<(NodeId, usize)>, - maybe_default_base_set: Vec, hierarchy: Dag, dependency: Dag, dependency_flattened: Dag, @@ -426,7 +390,6 @@ pub struct ScheduleGraph { conflicting_systems: Vec<(NodeId, NodeId, Vec)>, changed: bool, settings: ScheduleBuildSettings, - default_base_set: Option, } impl ScheduleGraph { @@ -437,7 +400,6 @@ impl ScheduleGraph { system_sets: Vec::new(), system_set_conditions: Vec::new(), system_set_ids: HashMap::new(), - maybe_default_base_set: Vec::new(), uninit: Vec::new(), hierarchy: Dag::new(), dependency: Dag::new(), @@ -448,7 +410,6 @@ impl ScheduleGraph { conflicting_systems: Vec::new(), changed: false, settings: default(), - default_base_set: None, } } @@ -491,44 +452,29 @@ impl ScheduleGraph { } /// Returns an iterator over all systems in this schedule. - /// - /// Note that the [`BaseSetMembership`] will only be initialized after [`ScheduleGraph::build_schedule`] is called. pub fn systems( &self, - ) -> impl Iterator< - Item = ( - NodeId, - &dyn System, - BaseSetMembership, - &[BoxedCondition], - ), - > { + ) -> impl Iterator, &[BoxedCondition])> { self.systems .iter() .zip(self.system_conditions.iter()) .enumerate() .filter_map(|(i, (system_node, condition))| { let system = system_node.inner.as_deref()?; - let base_set_membership = system_node.base_set_membership; let condition = condition.as_ref()?.as_slice(); - Some((NodeId::System(i), system, base_set_membership, condition)) + Some((NodeId::System(i), system, condition)) }) } /// Returns an iterator over all system sets in this schedule. - /// - /// Note that the [`BaseSetMembership`] will only be initialized after [`ScheduleGraph::build_schedule`] is called. - pub fn system_sets( - &self, - ) -> impl Iterator { + pub fn system_sets(&self) -> impl Iterator { self.system_set_ids.iter().map(|(_, node_id)| { let set_node = &self.system_sets[node_id.index()]; let set = &*set_node.inner; - let base_set_membership = set_node.base_set_membership; let conditions = self.system_set_conditions[node_id.index()] .as_deref() .unwrap_or(&[]); - (*node_id, set, base_set_membership, conditions) + (*node_id, set, conditions) }) } @@ -556,47 +502,144 @@ impl ScheduleGraph { &self.conflicting_systems } - fn add_systems(&mut self, systems: impl IntoSystemConfigs) { - let SystemConfigs { systems, chained } = systems.into_configs(); - let mut system_iter = systems.into_iter(); - if chained { - let Some(prev) = system_iter.next() else { return }; - let mut prev_id = self.add_system_inner(prev).unwrap(); - for next in system_iter { - let next_id = self.add_system_inner(next).unwrap(); - self.dependency.graph.add_edge(prev_id, next_id, ()); - prev_id = next_id; + /// Adds the systems to the graph. Returns a vector of all node ids contained the nested `SystemConfigs` + /// if `ancestor_chained` is true. Also returns true if "densely chained", meaning that all nested items + /// are linearly chained in the order they are defined + fn add_systems_inner( + &mut self, + configs: SystemConfigs, + ancestor_chained: bool, + ) -> AddSystemsInnerResult { + match configs { + SystemConfigs::SystemConfig(config) => { + let node_id = self.add_system_inner(config).unwrap(); + if ancestor_chained { + AddSystemsInnerResult { + densely_chained: true, + nodes: vec![node_id], + } + } else { + AddSystemsInnerResult { + densely_chained: true, + nodes: Vec::new(), + } + } } - } else { - for system in system_iter { - self.add_system_inner(system).unwrap(); + SystemConfigs::Configs { configs, chained } => { + let mut config_iter = configs.into_iter(); + let mut nodes_in_scope = Vec::new(); + let mut densely_chained = true; + if chained { + let Some(prev) = config_iter.next() else { + return AddSystemsInnerResult { + nodes: Vec::new(), + densely_chained: true + } + }; + let mut previous_result = self.add_systems_inner(prev, true); + densely_chained = previous_result.densely_chained; + for current in config_iter { + let current_result = self.add_systems_inner(current, true); + densely_chained = densely_chained && current_result.densely_chained; + match ( + previous_result.densely_chained, + current_result.densely_chained, + ) { + // Both groups are "densely" chained, so we can simplify the graph by only + // chaining the last in the previous list to the first in the current list + (true, true) => { + let last_in_prev = previous_result.nodes.last().unwrap(); + let first_in_current = current_result.nodes.first().unwrap(); + self.dependency.graph.add_edge( + *last_in_prev, + *first_in_current, + (), + ); + } + // The previous group is "densely" chained, so we can simplify the graph by only + // chaining the last item from the previous list to every item in the current list + (true, false) => { + let last_in_prev = previous_result.nodes.last().unwrap(); + for current_node in ¤t_result.nodes { + self.dependency.graph.add_edge( + *last_in_prev, + *current_node, + (), + ); + } + } + // The current list is currently "densely" chained, so we can simplify the graph by + // only chaining every item in the previous list to the first item in the current list + (false, true) => { + let first_in_current = current_result.nodes.first().unwrap(); + for previous_node in &previous_result.nodes { + self.dependency.graph.add_edge( + *previous_node, + *first_in_current, + (), + ); + } + } + // Neither of the lists are "densely" chained, so we must chain every item in the first + // list to every item in the second list + (false, false) => { + for previous_node in &previous_result.nodes { + for current_node in ¤t_result.nodes { + self.dependency.graph.add_edge( + *previous_node, + *current_node, + (), + ); + } + } + } + } + + if ancestor_chained { + nodes_in_scope.append(&mut previous_result.nodes); + } + + previous_result = current_result; + } + + // ensure the last config's nodes are added + if ancestor_chained { + nodes_in_scope.append(&mut previous_result.nodes); + } + } else { + let more_than_one_entry = config_iter.len() > 1; + for config in config_iter { + let result = self.add_systems_inner(config, ancestor_chained); + densely_chained = densely_chained && result.densely_chained; + if ancestor_chained { + nodes_in_scope.extend(result.nodes); + } + } + + // an "unchained" SystemConfig is only densely chained if it has exactly one densely chained entry + if more_than_one_entry { + densely_chained = false; + } + } + + AddSystemsInnerResult { + nodes: nodes_in_scope, + densely_chained, + } } } } - fn add_system(&mut self, system: impl IntoSystemConfig) { - self.add_system_inner(system).unwrap(); - } - - fn add_system_inner( - &mut self, - system: impl IntoSystemConfig, - ) -> Result { - let SystemConfig { - system, - graph_info, - conditions, - } = system.into_config(); - + fn add_system_inner(&mut self, config: SystemConfig) -> Result { let id = NodeId::System(self.systems.len()); // graph updates are immediate - self.update_graphs(id, graph_info, false)?; + self.update_graphs(id, config.graph_info)?; // system init has to be deferred (need `&mut World`) self.uninit.push((id, 0)); - self.systems.push(SystemNode::new(system)); - self.system_conditions.push(Some(conditions)); + self.systems.push(SystemNode::new(config.system)); + self.system_conditions.push(Some(config.conditions)); Ok(id) } @@ -639,7 +682,7 @@ impl ScheduleGraph { }; // graph updates are immediate - self.update_graphs(id, graph_info, set.is_base())?; + self.update_graphs(id, graph_info)?; // system init has to be deferred (need `&mut World`) let system_set_conditions = @@ -724,7 +767,6 @@ impl ScheduleGraph { &mut self, id: NodeId, graph_info: GraphInfo, - is_base_set: bool, ) -> Result<(), ScheduleBuildError> { self.check_sets(&id, &graph_info)?; self.check_edges(&id, &graph_info)?; @@ -734,8 +776,6 @@ impl ScheduleGraph { sets, dependencies, ambiguous_with, - base_set, - add_default_base_set, .. } = graph_info; @@ -749,30 +789,6 @@ impl ScheduleGraph { self.dependency.graph.add_node(set); } - // If the current node is not a base set, set the base set if it was configured - if !is_base_set { - if let Some(base_set) = base_set { - let set_id = self.system_set_ids[&base_set]; - self.hierarchy.graph.add_edge(set_id, id, ()); - } else if let Some(default_base_set) = &self.default_base_set { - if add_default_base_set { - match id { - NodeId::System(_) => { - // Queue the default base set. We queue systems instead of adding directly to allow - // sets to define base sets, which will override the default inheritance behavior - self.maybe_default_base_set.push(id); - } - NodeId::Set(_) => { - // Sets should be added automatically because developers explicitly called - // in_default_base_set() - let set_id = self.system_set_ids[default_base_set]; - self.hierarchy.graph.add_edge(set_id, id, ()); - } - } - } - } - } - if !self.dependency.graph.contains_node(id) { self.dependency.graph.add_node(id); } @@ -832,149 +848,15 @@ impl ScheduleGraph { } } - /// Calculates the base set for each node and caches the results on the node - fn calculate_base_sets_and_detect_cycles(&mut self) -> Result<(), ScheduleBuildError> { - let set_ids = (0..self.system_sets.len()).map(NodeId::Set); - let system_ids = (0..self.systems.len()).map(NodeId::System); - let mut visited_sets = vec![false; self.system_sets.len()]; - // reset base set membership, as this can change when the schedule updates - for system in &mut self.systems { - system.base_set_membership = BaseSetMembership::Uncalculated; - } - for system_set in &mut self.system_sets { - system_set.base_set_membership = BaseSetMembership::Uncalculated; - } - for node_id in set_ids.chain(system_ids) { - Self::calculate_base_set( - &self.hierarchy, - &mut self.system_sets, - &mut self.systems, - &mut visited_sets, - node_id, - )?; - } - Ok(()) - } - - fn calculate_base_set( - hierarchy: &Dag, - system_sets: &mut [SystemSetNode], - systems: &mut [SystemNode], - visited_sets: &mut [bool], - node_id: NodeId, - ) -> Result, ScheduleBuildError> { - let base_set_membership = match node_id { - // systems only have - NodeId::System(_) => BaseSetMembership::Uncalculated, - NodeId::Set(index) => { - let set_node = &mut system_sets[index]; - if set_node.inner.is_base() { - set_node.base_set_membership = BaseSetMembership::Some(node_id); - } - set_node.base_set_membership - } - }; - let base_set = match base_set_membership { - BaseSetMembership::None => None, - BaseSetMembership::Some(node_id) => Some(node_id), - BaseSetMembership::Uncalculated => { - let mut base_set: Option = None; - if let NodeId::Set(index) = node_id { - if visited_sets[index] { - return Err(ScheduleBuildError::HierarchyCycle); - } - visited_sets[index] = true; - } - for neighbor in hierarchy - .graph - .neighbors_directed(node_id, Direction::Incoming) - { - if let Some(calculated_base_set) = Self::calculate_base_set( - hierarchy, - system_sets, - systems, - visited_sets, - neighbor, - )? { - if let Some(first_set) = base_set { - if first_set != calculated_base_set { - return Err(match node_id { - NodeId::System(index) => { - ScheduleBuildError::SystemInMultipleBaseSets { - system: systems[index].name(), - first_set: system_sets[first_set.index()].name(), - second_set: system_sets[calculated_base_set.index()] - .name(), - } - } - NodeId::Set(index) => { - ScheduleBuildError::SetInMultipleBaseSets { - set: system_sets[index].name(), - first_set: system_sets[first_set.index()].name(), - second_set: system_sets[calculated_base_set.index()] - .name(), - } - } - }); - } - } - base_set = Some(calculated_base_set); - } - } - - match node_id { - NodeId::System(index) => { - systems[index].base_set_membership = if let Some(base_set) = base_set { - BaseSetMembership::Some(base_set) - } else { - BaseSetMembership::None - }; - } - NodeId::Set(index) => { - system_sets[index].base_set_membership = if let Some(base_set) = base_set { - BaseSetMembership::Some(base_set) - } else { - BaseSetMembership::None - }; - } - } - base_set - } - }; - Ok(base_set) - } - /// Build a [`SystemSchedule`] optimized for scheduler access from the [`ScheduleGraph`]. /// /// This method also - /// - calculates [`BaseSetMembership`] /// - checks for dependency or hierarchy cycles /// - checks for system access conflicts and reports ambiguities pub fn build_schedule( &mut self, components: &Components, ) -> Result { - self.calculate_base_sets_and_detect_cycles()?; - - // Add missing base set membership to systems that defaulted to using the - // default base set and weren't added to a set that belongs to a base set. - if let Some(default_base_set) = &self.default_base_set { - let default_set_id = self.system_set_ids[default_base_set]; - for system_id in std::mem::take(&mut self.maybe_default_base_set) { - let system_node = &mut self.systems[system_id.index()]; - if system_node.base_set_membership == BaseSetMembership::None { - self.hierarchy.graph.add_edge(default_set_id, system_id, ()); - system_node.base_set_membership = BaseSetMembership::Some(default_set_id); - } - - debug_assert_ne!( - system_node.base_set_membership, - BaseSetMembership::Uncalculated, - "base set membership should have been calculated" - ); - } - } - // check hierarchy for cycles self.hierarchy.topsort = self .topsort_graph(&self.hierarchy.graph, ReportCycles::Hierarchy) @@ -1138,18 +1020,18 @@ impl ScheduleGraph { ambiguous_with_flattened.add_edge(lhs, rhs, ()); } (NodeId::Set(_), NodeId::System(_)) => { - for &lhs_ in set_systems.get(&lhs).unwrap() { + for &lhs_ in set_systems.get(&lhs).unwrap_or(&Vec::new()) { ambiguous_with_flattened.add_edge(lhs_, rhs, ()); } } (NodeId::System(_), NodeId::Set(_)) => { - for &rhs_ in set_systems.get(&rhs).unwrap() { + for &rhs_ in set_systems.get(&rhs).unwrap_or(&Vec::new()) { ambiguous_with_flattened.add_edge(lhs, rhs_, ()); } } (NodeId::Set(_), NodeId::Set(_)) => { - for &lhs_ in set_systems.get(&lhs).unwrap() { - for &rhs_ in set_systems.get(&rhs).unwrap() { + for &lhs_ in set_systems.get(&lhs).unwrap_or(&Vec::new()) { + for &rhs_ in set_systems.get(&rhs).unwrap_or(&vec![]) { ambiguous_with_flattened.add_edge(lhs_, rhs_, ()); } } @@ -1340,17 +1222,14 @@ impl ScheduleGraph { Ok(()) } +} - fn set_default_base_set(&mut self, set: Option) { - if let Some(set) = set { - self.default_base_set = Some(set.dyn_clone()); - if self.system_set_ids.get(&set).is_none() { - self.add_set(set); - } - } else { - self.default_base_set = None; - } - } +/// Values returned by `ScheduleGraph::add_systems_inner` +struct AddSystemsInnerResult { + /// All nodes contained inside this add_systems_inner call's SystemConfigs hierarchy + nodes: Vec, + /// True if and only if all nodes are "densely chained" + densely_chained: bool, } /// Used to select the appropriate reporting function. diff --git a/crates/bevy_ecs/src/schedule/set.rs b/crates/bevy_ecs/src/schedule/set.rs index 23058a3de9..56deb5c398 100644 --- a/crates/bevy_ecs/src/schedule/set.rs +++ b/crates/bevy_ecs/src/schedule/set.rs @@ -23,34 +23,10 @@ pub trait SystemSet: DynHash + Debug + Send + Sync + 'static { None } - /// Returns `true` if this set is a "base system set". Systems - /// can only belong to one base set at a time. Systems and Sets - /// can only be added to base sets using specialized `in_base_set` - /// APIs. This enables "mutually exclusive" behaviors. It also - /// enables schedules to have a "default base set", which can be used - /// to apply default configuration to systems. - fn is_base(&self) -> bool { - false - } - /// Creates a boxed clone of the label corresponding to this system set. fn dyn_clone(&self) -> Box; } -/// A marker trait for `SystemSet` types where [`is_base`] returns `true`. -/// This should only be implemented for types that satisfy this requirement. -/// It is automatically implemented for base set types by `#[derive(SystemSet)]`. -/// -/// [`is_base`]: SystemSet::is_base -pub trait BaseSystemSet: SystemSet {} - -/// A marker trait for `SystemSet` types where [`is_base`] returns `false`. -/// This should only be implemented for types that satisfy this requirement. -/// It is automatically implemented for non-base set types by `#[derive(SystemSet)]`. -/// -/// [`is_base`]: SystemSet::is_base -pub trait FreeSystemSet: SystemSet {} - impl PartialEq for dyn SystemSet { fn eq(&self, other: &Self) -> bool { self.dyn_eq(other.as_dyn_eq()) diff --git a/crates/bevy_ecs/src/schedule/state.rs b/crates/bevy_ecs/src/schedule/state.rs index 92f7c3864c..326c99d2af 100644 --- a/crates/bevy_ecs/src/schedule/state.rs +++ b/crates/bevy_ecs/src/schedule/state.rs @@ -54,6 +54,18 @@ pub struct OnEnter(pub S); #[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] pub struct OnExit(pub S); +/// The label of a [`Schedule`](super::Schedule) that **only** runs whenever [`State`] +/// exits the `from` state, AND enters the `to` state. +/// +/// Systems added to this schedule are always ran *after* [`OnExit`], and *before* [`OnEnter`]. +#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] +pub struct OnTransition { + /// The state being exited. + pub from: S, + /// The state being entered. + pub to: S, +} + /// A [`SystemSet`] that will run within `CoreSet::Update` when this state is active. /// /// This set, when created via `App::add_state`, is configured with both a base set and a run condition. @@ -88,15 +100,18 @@ impl NextState { } } -/// Run the enter schedule for the current state +/// Run the enter schedule (if it exists) for the current state. pub fn run_enter_schedule(world: &mut World) { - world.run_schedule(OnEnter(world.resource::>().0.clone())); + world + .try_run_schedule(OnEnter(world.resource::>().0.clone())) + .ok(); } /// If a new state is queued in [`NextState`], this system: /// - Takes the new state value from [`NextState`] and updates [`State`]. -/// - Runs the [`OnExit(exited_state)`] schedule. -/// - Runs the [`OnEnter(entered_state)`] schedule. +/// - Runs the [`OnExit(exited_state)`] schedule, if it exists. +/// - Runs the [`OnTransition { from: exited_state, to: entered_state }`](OnTransition), if it exists. +/// - Runs the [`OnEnter(entered_state)`] schedule, if it exists. pub fn apply_state_transition(world: &mut World) { // We want to take the `NextState` resource, // but only mark it as changed if it wasn't empty. @@ -105,7 +120,15 @@ pub fn apply_state_transition(world: &mut World) { next_state_resource.set_changed(); let exited = mem::replace(&mut world.resource_mut::>().0, entered.clone()); - world.run_schedule(OnExit(exited)); - world.run_schedule(OnEnter(entered)); + + // Try to run the schedules if they exist. + world.try_run_schedule(OnExit(exited.clone())).ok(); + world + .try_run_schedule(OnTransition { + from: exited, + to: entered.clone(), + }) + .ok(); + world.try_run_schedule(OnEnter(entered)).ok(); } } diff --git a/crates/bevy_ecs/src/storage/blob_vec.rs b/crates/bevy_ecs/src/storage/blob_vec.rs index 49b370b7b8..1f7f636fb6 100644 --- a/crates/bevy_ecs/src/storage/blob_vec.rs +++ b/crates/bevy_ecs/src/storage/blob_vec.rs @@ -10,7 +10,9 @@ use bevy_utils::OnDrop; /// A flat, type-erased data storage type /// -/// Used to densely store homogeneous ECS data. +/// Used to densely store homogeneous ECS data. A blob is usually just an arbitrary block of contiguous memory without any identity, and +/// could be used to represent any arbitrary data (i.e. string, arrays, etc). This type is an extendable and reallcatable blob, which makes +/// it a blobby Vec, a `BlobVec`. pub(super) struct BlobVec { item_layout: Layout, capacity: usize, @@ -35,6 +37,13 @@ impl std::fmt::Debug for BlobVec { } impl BlobVec { + /// Creates a new [`BlobVec`] with the specified `capacity`. + /// + /// `drop` is an optional function pointer that is meant to be invoked when any element in the [`BlobVec`] + /// should be dropped. For all Rust-based types, this should match 1:1 with the implementation of [`Drop`] + /// if present, and should be `None` if `T: !Drop`. For non-Rust based types, this should match any cleanup + /// processes typically associated with the stored element. + /// /// # Safety /// /// `drop` should be safe to call with an [`OwningPtr`] pointing to any item that's been pushed into this [`BlobVec`]. @@ -42,6 +51,7 @@ impl BlobVec { /// If `drop` is `None`, the items will be leaked. This should generally be set as None based on [`needs_drop`]. /// /// [`needs_drop`]: core::mem::needs_drop + /// [`Drop`]: core::ops::Drop pub unsafe fn new( item_layout: Layout, drop: Option)>, @@ -70,26 +80,36 @@ impl BlobVec { } } + /// Returns the number of elements in the vector. #[inline] pub fn len(&self) -> usize { self.len } + /// Returns `true` if the vector contains no elements. #[inline] pub fn is_empty(&self) -> bool { self.len == 0 } + /// Returns the total number of elements the vector can hold without reallocating. #[inline] pub fn capacity(&self) -> usize { self.capacity } + /// Returns the [`Layout`] of the element type stored in the vector. #[inline] pub fn layout(&self) -> Layout { self.item_layout } + /// Reserves the minimum capacity for at least `additional` more elements to be inserted in the given `BlobVec`. + /// After calling `reserve_exact`, capacity will be greater than or equal to `self.len() + additional`. Does nothing if + /// the capacity is already sufficient. + /// + /// Note that the allocator may give the collection more space than it requests. Therefore, capacity can not be relied upon + /// to be precisely minimal. pub fn reserve_exact(&mut self, additional: usize) { let available_space = self.capacity - self.len; if available_space < additional && self.item_layout.size() > 0 { @@ -134,6 +154,8 @@ impl BlobVec { self.capacity = new_capacity; } + /// Initializes the value at `index` to `value`. This function does not do any bounds checking. + /// /// # Safety /// - index must be in bounds /// - the memory in the [`BlobVec`] starting at index `index`, of a size matching this [`BlobVec`]'s @@ -145,6 +167,8 @@ impl BlobVec { std::ptr::copy_nonoverlapping::(value.as_ptr(), ptr.as_ptr(), self.item_layout.size()); } + /// Replaces the value at `index` with `value`. This function does not do any bounds checking. + /// /// # Safety /// - index must be in-bounds /// - the memory in the [`BlobVec`] starting at index `index`, of a size matching this @@ -201,10 +225,10 @@ impl BlobVec { std::ptr::copy_nonoverlapping::(source, destination.as_ptr(), self.item_layout.size()); } - /// Pushes a value to the [`BlobVec`]. + /// Appends an element to the back of the vector. /// /// # Safety - /// `value` must be valid to add to this [`BlobVec`] + /// The `value` must match the [`layout`](`BlobVec::layout`) of the elements in the [`BlobVec`]. #[inline] pub unsafe fn push(&mut self, value: OwningPtr<'_>) { self.reserve_exact(1); @@ -213,6 +237,8 @@ impl BlobVec { self.initialize_unchecked(index, value); } + /// Forces the length of the vector to `len`. + /// /// # Safety /// `len` must be <= `capacity`. if length is decreased, "out of bounds" items must be dropped. /// Newly added items must be immediately populated with valid values and length must be @@ -255,6 +281,7 @@ impl BlobVec { /// Removes the value at `index` and copies the value stored into `ptr`. /// Does not do any bounds checking on `index`. + /// The removed element is replaced by the last element of the `BlobVec`. /// /// # Safety /// It is the caller's responsibility to ensure that `index` is < `self.len()` @@ -274,6 +301,10 @@ impl BlobVec { self.len -= 1; } + /// Removes the value at `index` and drops it. + /// Does not do any bounds checking on `index`. + /// The removed element is replaced by the last element of the `BlobVec`. + /// /// # Safety /// It is the caller's responsibility to ensure that `index` is < self.len() #[inline] @@ -286,8 +317,10 @@ impl BlobVec { } } + /// Returns a reference to the element at `index`, without doing bounds checking. + /// /// # Safety - /// It is the caller's responsibility to ensure that `index` is < self.len() + /// It is the caller's responsibility to ensure that `index < self.len()`. #[inline] pub unsafe fn get_unchecked(&self, index: usize) -> Ptr<'_> { debug_assert!(index < self.len()); @@ -300,8 +333,10 @@ impl BlobVec { self.get_ptr().byte_add(index * size) } + /// Returns a mutable reference to the element at `index`, without doing bounds checking. + /// /// # Safety - /// It is the caller's responsibility to ensure that `index` is < self.len() + /// It is the caller's responsibility to ensure that `index < self.len()`. #[inline] pub unsafe fn get_unchecked_mut(&mut self, index: usize) -> PtrMut<'_> { debug_assert!(index < self.len()); @@ -337,6 +372,9 @@ impl BlobVec { std::slice::from_raw_parts(self.data.as_ptr() as *const UnsafeCell, self.len) } + /// Clears the vector, removing (and dropping) all values. + /// + /// Note that this method has no effect on the allocated capacity of the vector. pub fn clear(&mut self) { let len = self.len; // We set len to 0 _before_ dropping elements for unwind safety. This ensures we don't diff --git a/crates/bevy_ecs/src/storage/mod.rs b/crates/bevy_ecs/src/storage/mod.rs index b2fab3fdcb..2cabf44303 100644 --- a/crates/bevy_ecs/src/storage/mod.rs +++ b/crates/bevy_ecs/src/storage/mod.rs @@ -1,4 +1,24 @@ //! Storage layouts for ECS data. +//! +//! This module implements the low-level collections that store data in a [`World`]. These all offer minimal and often +//! unsafe APIs, and have been made `pub` primarily for debugging and monitoring purposes. +//! +//! # Fetching Storages +//! Each of the below data stores can be fetched via [`Storages`], which can be fetched from a +//! [`World`] via [`World::storages`]. It exposes a top level container for each class of storage within +//! ECS: +//! +//! - [`Tables`] - columnar contiguous blocks of memory, optimized for fast iteration. +//! - [`SparseSets`] - sparse `HashMap`-like mappings from entities to components, optimized for random +//! lookup and regular insertion/removal of components. +//! - [`Resources`] - singleton storage for the resources in the world +//! +//! # Safety +//! To avoid trivially unsound use of the APIs in this module, it is explicitly impossible to get a mutable +//! reference to [`Storages`] from [`World`], and none of the types publicly expose a mutable interface. +//! +//! [`World`]: crate::world::World +//! [`World::storages`]: crate::world::World::storages mod blob_vec; mod resource; @@ -12,8 +32,12 @@ pub use table::*; /// The raw data stores of a [World](crate::world::World) #[derive(Default)] pub struct Storages { + /// Backing storage for [`SparseSet`] components. pub sparse_sets: SparseSets, + /// Backing storage for [`Table`] components. pub tables: Tables, + /// Backing storage for resources. pub resources: Resources, + /// Backing storage for `!Send` resources. pub non_send_resources: Resources, } diff --git a/crates/bevy_ecs/src/storage/resource.rs b/crates/bevy_ecs/src/storage/resource.rs index f1a0e9fb53..f856bf9bb7 100644 --- a/crates/bevy_ecs/src/storage/resource.rs +++ b/crates/bevy_ecs/src/storage/resource.rs @@ -1,6 +1,6 @@ use crate::archetype::ArchetypeComponentId; use crate::change_detection::{MutUntyped, TicksMut}; -use crate::component::{ComponentId, ComponentTicks, Components, TickCells}; +use crate::component::{ComponentId, ComponentTicks, Components, Tick, TickCells}; use crate::storage::{Column, SparseSet, TableRow}; use bevy_ptr::{OwningPtr, Ptr, UnsafeCellDeref}; use std::{mem::ManuallyDrop, thread::ThreadId}; @@ -42,6 +42,10 @@ impl ResourceData { /// The only row in the underlying column. const ROW: TableRow = TableRow::new(0); + /// Validates the access to `!Send` resources is only done on the thread they were created from. + /// + /// # Panics + /// If `SEND` is false, this will panic if called from a different thread than the one it was inserted from. #[inline] fn validate_access(&self) { if SEND { @@ -70,7 +74,7 @@ impl ResourceData { self.id } - /// Gets a read-only pointer to the underlying resource, if available. + /// Returns a reference to the resource, if it exists. /// /// # Panics /// If `SEND` is false, this will panic if a value is present and is not accessed from the @@ -83,12 +87,14 @@ impl ResourceData { }) } - /// Gets a read-only reference to the change ticks of the underlying resource, if available. + /// Returns a reference to the resource's change ticks, if it exists. #[inline] pub fn get_ticks(&self) -> Option { self.column.get_ticks(Self::ROW) } + /// Returns references to the resource and its change ticks, if it exists. + /// /// # Panics /// If `SEND` is false, this will panic if a value is present and is not accessed from the /// original thread it was inserted in. @@ -100,17 +106,18 @@ impl ResourceData { }) } - pub(crate) fn get_mut( - &mut self, - last_change_tick: u32, - change_tick: u32, - ) -> Option> { + /// Returns a mutable reference to the resource, if it exists. + /// + /// # Panics + /// If `SEND` is false, this will panic if a value is present and is not accessed from the + /// original thread it was inserted in. + pub(crate) fn get_mut(&mut self, last_run: Tick, this_run: Tick) -> Option> { let (ptr, ticks) = self.get_with_ticks()?; Some(MutUntyped { // SAFETY: We have exclusive access to the underlying storage. value: unsafe { ptr.assert_unique() }, // SAFETY: We have exclusive access to the underlying storage. - ticks: unsafe { TicksMut::from_tick_cells(ticks, last_change_tick, change_tick) }, + ticks: unsafe { TicksMut::from_tick_cells(ticks, last_run, this_run) }, }) } @@ -124,7 +131,7 @@ impl ResourceData { /// # Safety /// - `value` must be valid for the underlying type for the resource. #[inline] - pub(crate) unsafe fn insert(&mut self, value: OwningPtr<'_>, change_tick: u32) { + pub(crate) unsafe fn insert(&mut self, value: OwningPtr<'_>, change_tick: Tick) { if self.is_present() { self.validate_access(); self.column.replace(Self::ROW, value, change_tick); @@ -273,7 +280,7 @@ impl Resources { }) } - pub(crate) fn check_change_ticks(&mut self, change_tick: u32) { + pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) { for info in self.resources.values_mut() { info.column.check_change_ticks(change_tick); } diff --git a/crates/bevy_ecs/src/storage/sparse_set.rs b/crates/bevy_ecs/src/storage/sparse_set.rs index 54412b54c2..d15aca7c4a 100644 --- a/crates/bevy_ecs/src/storage/sparse_set.rs +++ b/crates/bevy_ecs/src/storage/sparse_set.rs @@ -41,12 +41,16 @@ impl SparseArray { macro_rules! impl_sparse_array { ($ty:ident) => { impl $ty { + /// Returns `true` if the collection contains a value for the specified `index`. #[inline] pub fn contains(&self, index: I) -> bool { let index = index.sparse_set_index(); self.values.get(index).map(|v| v.is_some()).unwrap_or(false) } + /// Returns a reference to the value at `index`. + /// + /// Returns `None` if `index` does not have a value or if `index` is out of bounds. #[inline] pub fn get(&self, index: I) -> Option<&V> { let index = index.sparse_set_index(); @@ -60,6 +64,9 @@ impl_sparse_array!(SparseArray); impl_sparse_array!(ImmutableSparseArray); impl SparseArray { + /// Inserts `value` at `index` in the array. + /// + /// If `index` is out-of-bounds, this will enlarge the buffer to accommodate it. #[inline] pub fn insert(&mut self, index: I, value: V) { let index = index.sparse_set_index(); @@ -69,6 +76,9 @@ impl SparseArray { self.values[index] = Some(value); } + /// Returns a mutable reference to the value at `index`. + /// + /// Returns `None` if `index` does not have a value or if `index` is out of bounds. #[inline] pub fn get_mut(&mut self, index: I) -> Option<&mut V> { let index = index.sparse_set_index(); @@ -78,16 +88,21 @@ impl SparseArray { .unwrap_or(None) } + /// Removes and returns the value stored at `index`. + /// + /// Returns `None` if `index` did not have a value or if `index` is out of bounds. #[inline] pub fn remove(&mut self, index: I) -> Option { let index = index.sparse_set_index(); self.values.get_mut(index).and_then(|value| value.take()) } + /// Removes all of the values stored within. pub fn clear(&mut self) { self.values.clear(); } + /// Converts the [`SparseArray`] into an immutable variant. pub(crate) fn into_immutable(self) -> ImmutableSparseArray { ImmutableSparseArray { values: self.values.into_boxed_slice(), @@ -113,6 +128,8 @@ pub struct ComponentSparseSet { } impl ComponentSparseSet { + /// Creates a new [`ComponentSparseSet`] with a given component type layout and + /// initial `capacity`. pub(crate) fn new(component_info: &ComponentInfo, capacity: usize) -> Self { Self { dense: Column::with_capacity(component_info, capacity), @@ -121,17 +138,20 @@ impl ComponentSparseSet { } } + /// Removes all of the values stored within. pub(crate) fn clear(&mut self) { self.dense.clear(); self.entities.clear(); self.sparse.clear(); } + /// Returns the number of component values in the sparse set. #[inline] pub fn len(&self) -> usize { self.dense.len() } + /// Returns `true` if the sparse set contains no component values. #[inline] pub fn is_empty(&self) -> bool { self.dense.len() == 0 @@ -143,7 +163,12 @@ impl ComponentSparseSet { /// # Safety /// The `value` pointer must point to a valid address that matches the [`Layout`](std::alloc::Layout) /// inside the [`ComponentInfo`] given when constructing this sparse set. - pub(crate) unsafe fn insert(&mut self, entity: Entity, value: OwningPtr<'_>, change_tick: u32) { + pub(crate) unsafe fn insert( + &mut self, + entity: Entity, + value: OwningPtr<'_>, + change_tick: Tick, + ) { if let Some(&dense_index) = self.sparse.get(entity.index()) { #[cfg(debug_assertions)] assert_eq!(entity, self.entities[dense_index as usize]); @@ -162,6 +187,7 @@ impl ComponentSparseSet { } } + /// Returns `true` if the sparse set has a component value for the provided `entity`. #[inline] pub fn contains(&self, entity: Entity) -> bool { #[cfg(debug_assertions)] @@ -178,6 +204,9 @@ impl ComponentSparseSet { self.sparse.contains(entity.index()) } + /// Returns a reference to the entity's component value. + /// + /// Returns `None` if `entity` does not have a component in the sparse set. #[inline] pub fn get(&self, entity: Entity) -> Option> { self.sparse.get(entity.index()).map(|dense_index| { @@ -189,6 +218,9 @@ impl ComponentSparseSet { }) } + /// Returns references to the entity's component value and its added and changed ticks. + /// + /// Returns `None` if `entity` does not have a component in the sparse set. #[inline] pub fn get_with_ticks(&self, entity: Entity) -> Option<(Ptr<'_>, TickCells<'_>)> { let dense_index = TableRow::new(*self.sparse.get(entity.index())? as usize); @@ -206,6 +238,9 @@ impl ComponentSparseSet { } } + /// Returns a reference to the "added" tick of the entity's component value. + /// + /// Returns `None` if `entity` does not have a component in the sparse set. #[inline] pub fn get_added_ticks(&self, entity: Entity) -> Option<&UnsafeCell> { let dense_index = *self.sparse.get(entity.index())? as usize; @@ -220,6 +255,9 @@ impl ComponentSparseSet { } } + /// Returns a reference to the "changed" tick of the entity's component value. + /// + /// Returns `None` if `entity` does not have a component in the sparse set. #[inline] pub fn get_changed_ticks(&self, entity: Entity) -> Option<&UnsafeCell> { let dense_index = *self.sparse.get(entity.index())? as usize; @@ -234,6 +272,9 @@ impl ComponentSparseSet { } } + /// Returns a reference to the "added" and "changed" ticks of the entity's component value. + /// + /// Returns `None` if `entity` does not have a component in the sparse set. #[inline] pub fn get_ticks(&self, entity: Entity) -> Option { let dense_index = *self.sparse.get(entity.index())? as usize; @@ -270,6 +311,9 @@ impl ComponentSparseSet { }) } + /// Removes (and drops) the entity's component value from the sparse set. + /// + /// Returns `true` if `entity` had a component value in the sparse set. pub(crate) fn remove(&mut self, entity: Entity) -> bool { if let Some(dense_index) = self.sparse.remove(entity.index()) { let dense_index = dense_index as usize; @@ -293,7 +337,7 @@ impl ComponentSparseSet { } } - pub(crate) fn check_change_ticks(&mut self, change_tick: u32) { + pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) { self.dense.check_change_ticks(change_tick); } } @@ -320,16 +364,21 @@ pub(crate) struct ImmutableSparseSet { macro_rules! impl_sparse_set { ($ty:ident) => { impl $ty { + /// Returns the number of elements in the sparse set. #[inline] pub fn len(&self) -> usize { self.dense.len() } + /// Returns `true` if the sparse set contains a value for `index`. #[inline] pub fn contains(&self, index: I) -> bool { self.sparse.contains(index) } + /// Returns a reference to the value for `index`. + /// + /// Returns `None` if `index` does not have a value in the sparse set. pub fn get(&self, index: I) -> Option<&V> { self.sparse.get(index).map(|dense_index| { // SAFETY: if the sparse index points to something in the dense vec, it exists @@ -337,6 +386,9 @@ macro_rules! impl_sparse_set { }) } + /// Returns a mutable reference to the value for `index`. + /// + /// Returns `None` if `index` does not have a value in the sparse set. pub fn get_mut(&mut self, index: I) -> Option<&mut V> { let dense = &mut self.dense; self.sparse.get(index).map(move |dense_index| { @@ -345,22 +397,27 @@ macro_rules! impl_sparse_set { }) } + /// Returns an iterator visiting all keys (indices) in arbitrary order. pub fn indices(&self) -> impl Iterator + '_ { self.indices.iter().cloned() } + /// Returns an iterator visiting all values in arbitrary order. pub fn values(&self) -> impl Iterator { self.dense.iter() } + /// Returns an iterator visiting all values mutably in arbitrary order. pub fn values_mut(&mut self) -> impl Iterator { self.dense.iter_mut() } + /// Returns an iterator visiting all key-value pairs in arbitrary order, with references to the values. pub fn iter(&self) -> impl Iterator { self.indices.iter().zip(self.dense.iter()) } + /// Returns an iterator visiting all key-value pairs in arbitrary order, with mutable references to the values. pub fn iter_mut(&mut self) -> impl Iterator { self.indices.iter().zip(self.dense.iter_mut()) } @@ -378,6 +435,7 @@ impl Default for SparseSet { } impl SparseSet { + /// Creates a new [`SparseSet`]. pub const fn new() -> Self { Self { dense: Vec::new(), @@ -388,6 +446,7 @@ impl SparseSet { } impl SparseSet { + /// Creates a new [`SparseSet`] with a specified initial capacity. pub fn with_capacity(capacity: usize) -> Self { Self { dense: Vec::with_capacity(capacity), @@ -396,11 +455,15 @@ impl SparseSet { } } + /// Returns the total number of elements the [`SparseSet`] can hold without needing to reallocate. #[inline] pub fn capacity(&self) -> usize { self.dense.capacity() } + /// Inserts `value` at `index`. + /// + /// If a value was already present at `index`, it will be overwritten. pub fn insert(&mut self, index: I, value: V) { if let Some(dense_index) = self.sparse.get(index.clone()).cloned() { // SAFETY: dense indices stored in self.sparse always exist @@ -414,6 +477,8 @@ impl SparseSet { } } + /// Returns a reference to the value for `index`, inserting one computed from `func` + /// if not already present. pub fn get_or_insert_with(&mut self, index: I, func: impl FnOnce() -> V) -> &mut V { if let Some(dense_index) = self.sparse.get(index.clone()).cloned() { // SAFETY: dense indices stored in self.sparse always exist @@ -429,11 +494,15 @@ impl SparseSet { } } + /// Returns `true` if the sparse set contains no elements. #[inline] pub fn is_empty(&self) -> bool { self.dense.len() == 0 } + /// Removes and returns the value for `index`. + /// + /// Returns `None` if `index` does not have a value in the sparse set. pub fn remove(&mut self, index: I) -> Option { self.sparse.remove(index).map(|dense_index| { let is_last = dense_index == self.dense.len() - 1; @@ -447,12 +516,14 @@ impl SparseSet { }) } + /// Clears all of the elements from the sparse set. pub fn clear(&mut self) { self.dense.clear(); self.indices.clear(); self.sparse.clear(); } + /// Converts the sparse set into its immutable variant. pub(crate) fn into_immutable(self) -> ImmutableSparseSet { ImmutableSparseSet { dense: self.dense.into_boxed_slice(), @@ -548,7 +619,7 @@ impl SparseSets { } } - pub(crate) fn check_change_ticks(&mut self, change_tick: u32) { + pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) { for set in self.sets.values_mut() { set.check_change_ticks(change_tick); } diff --git a/crates/bevy_ecs/src/storage/table.rs b/crates/bevy_ecs/src/storage/table.rs index a8077be288..bae50e86c2 100644 --- a/crates/bevy_ecs/src/storage/table.rs +++ b/crates/bevy_ecs/src/storage/table.rs @@ -33,16 +33,24 @@ pub struct TableId(u32); impl TableId { pub(crate) const INVALID: TableId = TableId(u32::MAX); + /// Creates a new [`TableId`]. + /// + /// `index` *must* be retrieved from calling [`TableId::index`] on a `TableId` you got + /// from a table of a given [`World`] or the created ID may be invalid. + /// + /// [`World`]: crate::world::World #[inline] pub fn new(index: usize) -> Self { TableId(index as u32) } + /// Gets the underlying table index from the ID. #[inline] pub fn index(self) -> usize { self.0 as usize } + /// The [`TableId`] of the [`Table`] without any components. #[inline] pub const fn empty() -> TableId { TableId(0) @@ -71,7 +79,7 @@ impl TableId { pub struct TableRow(u32); impl TableRow { - pub const INVALID: TableRow = TableRow(u32::MAX); + pub(crate) const INVALID: TableRow = TableRow(u32::MAX); /// Creates a `TableRow`. #[inline] @@ -86,6 +94,19 @@ impl TableRow { } } +/// A type-erased contiguous container for data of a homogenous type. +/// +/// Conceptually, a [`Column`] is very similar to a type-erased `Vec`. +/// It also stores the change detection ticks for its components, kept in two separate +/// contiguous buffers internally. An element shares its data across these buffers by using the +/// same index (i.e. the entity at row 3 has it's data at index 3 and its change detection ticks at +/// index 3). A slice to these contiguous blocks of memory can be fetched +/// via [`Column::get_data_slice`], [`Column::get_added_ticks_slice`], and +/// [`Column::get_changed_ticks_slice`]. +/// +/// Like many other low-level storage types, [`Column`] has a limited and highly unsafe +/// interface. It's highly advised to use higher level types and their safe abstractions +/// instead of working directly with [`Column`]. #[derive(Debug)] pub struct Column { data: BlobVec, @@ -94,6 +115,7 @@ pub struct Column { } impl Column { + /// Constructs a new [`Column`], configured with a component's layout and an initial `capacity`. #[inline] pub(crate) fn with_capacity(component_info: &ComponentInfo, capacity: usize) -> Self { Column { @@ -104,6 +126,7 @@ impl Column { } } + /// Fetches the [`Layout`] for the underlying type. #[inline] pub fn item_layout(&self) -> Layout { self.data.layout() @@ -129,13 +152,10 @@ impl Column { /// # Safety /// Assumes data has already been allocated for the given row. #[inline] - pub(crate) unsafe fn replace(&mut self, row: TableRow, data: OwningPtr<'_>, change_tick: u32) { + pub(crate) unsafe fn replace(&mut self, row: TableRow, data: OwningPtr<'_>, change_tick: Tick) { debug_assert!(row.index() < self.len()); self.data.replace_unchecked(row.index(), data); - self.changed_ticks - .get_unchecked_mut(row.index()) - .get_mut() - .set_changed(change_tick); + *self.changed_ticks.get_unchecked_mut(row.index()).get_mut() = change_tick; } /// Writes component data to the column at given row. @@ -150,18 +170,29 @@ impl Column { self.data.replace_unchecked(row.index(), data); } + /// Gets the current number of elements stored in the column. #[inline] pub fn len(&self) -> usize { self.data.len() } + /// Checks if the column is empty. Returns `true` if there are no elements, `false` otherwise. #[inline] pub fn is_empty(&self) -> bool { self.data.is_empty() } + /// Removes an element from the [`Column`]. + /// + /// - The value will be dropped if it implements [`Drop`]. + /// - This does not preserve ordering, but is O(1). + /// - This does not do any bounds checking. + /// - The element is replaced with the last element in the [`Column`]. + /// /// # Safety - /// index must be in-bounds + /// `row` must be within the range `[0, self.len())`. + /// + /// [`Drop`]: std::ops::Drop #[inline] pub(crate) unsafe fn swap_remove_unchecked(&mut self, row: TableRow) { self.data.swap_remove_and_drop_unchecked(row.index()); @@ -169,6 +200,16 @@ impl Column { self.changed_ticks.swap_remove(row.index()); } + /// Removes an element from the [`Column`] and returns it and its change detection ticks. + /// This does not preserve ordering, but is O(1). + /// + /// The element is replaced with the last element in the [`Column`]. + /// + /// It is the caller's responsibility to ensure that the removed value is dropped or used. + /// Failure to do so may result in resources not being released (i.e. files handles not being + /// released, memory leaks, etc.) + /// + /// Returns `None` if `row` is out of bounds. #[inline] #[must_use = "The returned pointer should be used to drop the removed component"] pub(crate) fn swap_remove_and_forget( @@ -184,8 +225,18 @@ impl Column { }) } + /// Removes an element from the [`Column`] and returns it and its change detection ticks. + /// This does not preserve ordering, but is O(1). Unlike [`Column::swap_remove_and_forget`] + /// this does not do any bounds checking. + /// + /// The element is replaced with the last element in the [`Column`]. + /// + /// It's the caller's responsibility to ensure that the removed value is dropped or used. + /// Failure to do so may result in resources not being released (i.e. files handles not being + /// released, memory leaks, etc.) + /// /// # Safety - /// index must be in-bounds + /// `row` must be within the range `[0, self.len())`. #[inline] #[must_use = "The returned pointer should be used to dropped the removed component"] pub(crate) unsafe fn swap_remove_and_forget_unchecked( @@ -225,8 +276,10 @@ impl Column { other.changed_ticks.swap_remove(src_row.index()); } - // # Safety - // - ptr must point to valid data of this column's component type + /// Pushes a new value onto the end of the [`Column`]. + /// + /// # Safety + /// `ptr` must point to valid data of this column's component type pub(crate) unsafe fn push(&mut self, ptr: OwningPtr<'_>, ticks: ComponentTicks) { self.data.push(ptr); self.added_ticks.push(UnsafeCell::new(ticks.added)); @@ -240,27 +293,57 @@ impl Column { self.changed_ticks.reserve_exact(additional); } + /// Fetches the data pointer to the first element of the [`Column`]. + /// + /// The pointer is type erased, so using this function to fetch anything + /// other than the first element will require computing the offset using + /// [`Column::item_layout`]. #[inline] pub fn get_data_ptr(&self) -> Ptr<'_> { self.data.get_ptr() } + /// Fetches the slice to the [`Column`]'s data cast to a given type. + /// + /// Note: The values stored within are [`UnsafeCell`]. + /// Users of this API must ensure that accesses to each individual element + /// adhere to the safety invariants of [`UnsafeCell`]. + /// /// # Safety /// The type `T` must be the type of the items in this column. + /// + /// [`UnsafeCell`]: std::cell::UnsafeCell pub unsafe fn get_data_slice(&self) -> &[UnsafeCell] { self.data.get_slice() } + /// Fetches the slice to the [`Column`]'s "added" change detection ticks. + /// + /// Note: The values stored within are [`UnsafeCell`]. + /// Users of this API must ensure that accesses to each individual element + /// adhere to the safety invariants of [`UnsafeCell`]. + /// + /// [`UnsafeCell`]: std::cell::UnsafeCell #[inline] pub fn get_added_ticks_slice(&self) -> &[UnsafeCell] { &self.added_ticks } + /// Fetches the slice to the [`Column`]'s "changed" change detection ticks. + /// + /// Note: The values stored within are [`UnsafeCell`]. + /// Users of this API must ensure that accesses to each individual element + /// adhere to the safety invariants of [`UnsafeCell`]. + /// + /// [`UnsafeCell`]: std::cell::UnsafeCell #[inline] pub fn get_changed_ticks_slice(&self) -> &[UnsafeCell] { &self.changed_ticks } + /// Fetches a reference to the data and change detection ticks at `row`. + /// + /// Returns `None` if `row` is out of bounds. #[inline] pub fn get(&self, row: TableRow) -> Option<(Ptr<'_>, TickCells<'_>)> { (row.index() < self.data.len()) @@ -277,6 +360,9 @@ impl Column { }) } + /// Fetches a read-only reference to the data at `row`. + /// + /// Returns `None` if `row` is out of bounds. #[inline] pub fn get_data(&self, row: TableRow) -> Option> { // SAFETY: The row is length checked before fetching the pointer. This is being @@ -284,15 +370,21 @@ impl Column { (row.index() < self.data.len()).then(|| unsafe { self.data.get_unchecked(row.index()) }) } + /// Fetches a read-only reference to the data at `row`. Unlike [`Column::get`] this does not + /// do any bounds checking. + /// /// # Safety - /// - index must be in-bounds - /// - no other reference to the data of the same row can exist at the same time + /// - `row` must be within the range `[0, self.len())`. + /// - no other mutable reference to the data of the same row can exist at the same time #[inline] pub unsafe fn get_data_unchecked(&self, row: TableRow) -> Ptr<'_> { debug_assert!(row.index() < self.data.len()); self.data.get_unchecked(row.index()) } + /// Fetches a mutable reference to the data at `row`. + /// + /// Returns `None` if `row` is out of bounds. #[inline] pub fn get_data_mut(&mut self, row: TableRow) -> Option> { // SAFETY: The row is length checked before fetching the pointer. This is being @@ -300,6 +392,9 @@ impl Column { (row.index() < self.data.len()).then(|| unsafe { self.data.get_unchecked_mut(row.index()) }) } + /// Fetches a mutable reference to the data at `row`. Unlike [`Column::get_data_mut`] this does not + /// do any bounds checking. + /// /// # Safety /// - index must be in-bounds /// - no other reference to the data of the same row can exist at the same time @@ -309,16 +404,37 @@ impl Column { self.data.get_unchecked_mut(row.index()) } + /// Fetches the "added" change detection ticks for the value at `row`. + /// + /// Returns `None` if `row` is out of bounds. + /// + /// Note: The values stored within are [`UnsafeCell`]. + /// Users of this API must ensure that accesses to each individual element + /// adhere to the safety invariants of [`UnsafeCell`]. + /// + /// [`UnsafeCell`]: std::cell::UnsafeCell #[inline] pub fn get_added_ticks(&self, row: TableRow) -> Option<&UnsafeCell> { self.added_ticks.get(row.index()) } + /// Fetches the "changed" change detection ticks for the value at `row`. + /// + /// Returns `None` if `row` is out of bounds. + /// + /// Note: The values stored within are [`UnsafeCell`]. + /// Users of this API must ensure that accesses to each individual element + /// adhere to the safety invariants of [`UnsafeCell`]. + /// + /// [`UnsafeCell`]: std::cell::UnsafeCell #[inline] pub fn get_changed_ticks(&self, row: TableRow) -> Option<&UnsafeCell> { self.changed_ticks.get(row.index()) } + /// Fetches the change detection ticks for the value at `row`. + /// + /// Returns `None` if `row` is out of bounds. #[inline] pub fn get_ticks(&self, row: TableRow) -> Option { if row.index() < self.data.len() { @@ -329,24 +445,33 @@ impl Column { } } + /// Fetches the "added" change detection ticks for the value at `row`. Unlike [`Column::get_added_ticks`] + /// this function does not do any bounds checking. + /// /// # Safety - /// index must be in-bounds + /// `row` must be within the range `[0, self.len())`. #[inline] pub unsafe fn get_added_ticks_unchecked(&self, row: TableRow) -> &UnsafeCell { debug_assert!(row.index() < self.added_ticks.len()); self.added_ticks.get_unchecked(row.index()) } + /// Fetches the "changed" change detection ticks for the value at `row`. Unlike [`Column::get_changed_ticks`] + /// this function does not do any bounds checking. + /// /// # Safety - /// index must be in-bounds + /// `row` must be within the range `[0, self.len())`. #[inline] pub unsafe fn get_changed_ticks_unchecked(&self, row: TableRow) -> &UnsafeCell { debug_assert!(row.index() < self.changed_ticks.len()); self.changed_ticks.get_unchecked(row.index()) } + /// Fetches the change detection ticks for the value at `row`. Unlike [`Column::get_ticks`] + /// this function does not do any bounds checking. + /// /// # Safety - /// index must be in-bounds + /// `row` must be within the range `[0, self.len())`. #[inline] pub unsafe fn get_ticks_unchecked(&self, row: TableRow) -> ComponentTicks { debug_assert!(row.index() < self.added_ticks.len()); @@ -357,6 +482,9 @@ impl Column { } } + /// Clears the column, removing all values. + /// + /// Note that this function has no effect on the allocated capacity of the [`Column`]> pub fn clear(&mut self) { self.data.clear(); self.added_ticks.clear(); @@ -364,7 +492,7 @@ impl Column { } #[inline] - pub(crate) fn check_change_ticks(&mut self, change_tick: u32) { + pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) { for component_ticks in &mut self.added_ticks { component_ticks.get_mut().check_tick(change_tick); } @@ -417,10 +545,10 @@ impl TableBuilder { /// in a [`World`]. /// /// Conceptually, a `Table` can be thought of as an `HashMap`, where -/// each `Column` is a type-erased `Vec`. Each row corresponds to a single entity +/// each [`Column`] is a type-erased `Vec`. Each row corresponds to a single entity /// (i.e. index 3 in Column A and index 3 in Column B point to different components on the same /// entity). Fetching components from a table involves fetching the associated column for a -/// component type (via it's [`ComponentId`]), then fetching the entity's row within that column. +/// component type (via its [`ComponentId`]), then fetching the entity's row within that column. /// /// [structure-of-arrays]: https://en.wikipedia.org/wiki/AoS_and_SoA#Structure_of_arrays /// [`Component`]: crate::component::Component @@ -431,6 +559,7 @@ pub struct Table { } impl Table { + /// Fetches a read-only slice of the entities stored within the [`Table`]. #[inline] pub fn entities(&self) -> &[Entity] { &self.entities @@ -457,7 +586,8 @@ impl Table { /// Moves the `row` column values to `new_table`, for the columns shared between both tables. /// Returns the index of the new row in `new_table` and the entity in this table swapped in /// to replace it (if an entity was swapped in). missing columns will be "forgotten". It is - /// the caller's responsibility to drop them + /// the caller's responsibility to drop them. Failure to do so may result in resources not + /// being released (i.e. files handles not being released, memory leaks, etc.) /// /// # Safety /// Row must be in-bounds @@ -548,21 +678,39 @@ impl Table { } } + /// Fetches a read-only reference to the [`Column`] for a given [`Component`] within the + /// table. + /// + /// Returns `None` if the corresponding component does not belong to the table. + /// + /// [`Component`]: crate::component::Component #[inline] pub fn get_column(&self, component_id: ComponentId) -> Option<&Column> { self.columns.get(component_id) } + /// Fetches a mutable reference to the [`Column`] for a given [`Component`] within the + /// table. + /// + /// Returns `None` if the corresponding component does not belong to the table. + /// + /// [`Component`]: crate::component::Component #[inline] pub(crate) fn get_column_mut(&mut self, component_id: ComponentId) -> Option<&mut Column> { self.columns.get_mut(component_id) } + /// Checks if the table contains a [`Column`] for a given [`Component`]. + /// + /// Returns `true` if the column is present, `false` otherwise. + /// + /// [`Component`]: crate::component::Component #[inline] pub fn has_column(&self, component_id: ComponentId) -> bool { self.columns.contains(component_id) } + /// Reserves `additional` elements worth of capacity within the table. pub(crate) fn reserve(&mut self, additional: usize) { if self.entities.capacity() - self.entities.len() < additional { self.entities.reserve(additional); @@ -592,36 +740,45 @@ impl Table { TableRow::new(index) } + /// Gets the number of entities currently being stored in the table. #[inline] pub fn entity_count(&self) -> usize { self.entities.len() } + /// Gets the number of components being stored in the table. #[inline] pub fn component_count(&self) -> usize { self.columns.len() } + /// Gets the maximum number of entities the table can currently store + /// without reallocating the underlying memory. #[inline] pub fn entity_capacity(&self) -> usize { self.entities.capacity() } + /// Checks if the [`Table`] is empty or not. + /// + /// Returns `true` if the table contains no entities, `false` otherwise. #[inline] pub fn is_empty(&self) -> bool { self.entities.is_empty() } - pub(crate) fn check_change_ticks(&mut self, change_tick: u32) { + pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) { for column in self.columns.values_mut() { column.check_change_ticks(change_tick); } } + /// Iterates over the [`Column`]s of the [`Table`]. pub fn iter(&self) -> impl Iterator { self.columns.values() } + /// Clears all of the stored components in the [`Table`]. pub(crate) fn clear(&mut self) { self.entities.clear(); for column in self.columns.values_mut() { @@ -666,11 +823,19 @@ impl Tables { self.tables.is_empty() } + /// Fetches a [`Table`] by its [`TableId`]. + /// + /// Returns `None` if `id` is invalid. #[inline] pub fn get(&self, id: TableId) -> Option<&Table> { self.tables.get(id.index()) } + /// Fetches mutable references to two different [`Table`]s. + /// + /// # Panics + /// + /// Panics if `a` and `b` are equal. #[inline] pub(crate) fn get_2_mut(&mut self, a: TableId, b: TableId) -> (&mut Table, &mut Table) { if a.index() > b.index() { @@ -682,6 +847,9 @@ impl Tables { } } + /// Attempts to fetch a table based on the provided components, + /// creating and returning a new [`Table`] if one did not already exist. + /// /// # Safety /// `component_ids` must contain components that exist in `components` pub(crate) unsafe fn get_id_or_insert( @@ -706,17 +874,19 @@ impl Tables { *value } + /// Iterates through all of the tables stored within in [`TableId`] order. pub fn iter(&self) -> std::slice::Iter<'_, Table> { self.tables.iter() } + /// Clears all data from all [`Table`]s stored within. pub(crate) fn clear(&mut self) { for table in &mut self.tables { table.clear(); } } - pub(crate) fn check_change_ticks(&mut self, change_tick: u32) { + pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) { for table in &mut self.tables { table.check_change_ticks(change_tick); } diff --git a/crates/bevy_ecs/src/system/combinator.rs b/crates/bevy_ecs/src/system/combinator.rs index 51304831d2..671a29431c 100644 --- a/crates/bevy_ecs/src/system/combinator.rs +++ b/crates/bevy_ecs/src/system/combinator.rs @@ -3,7 +3,10 @@ use std::{borrow::Cow, cell::UnsafeCell, marker::PhantomData}; use bevy_ptr::UnsafeCellDeref; use crate::{ - archetype::ArchetypeComponentId, component::ComponentId, prelude::World, query::Access, + archetype::ArchetypeComponentId, + component::{ComponentId, Tick}, + prelude::World, + query::Access, }; use super::{ReadOnlySystem, System}; @@ -47,7 +50,7 @@ use super::{ReadOnlySystem, System}; /// # world.init_resource::(); /// # /// # let mut app = Schedule::new(); -/// app.add_system(my_system.run_if(Xor::new( +/// app.add_systems(my_system.run_if(Xor::new( /// IntoSystem::into_system(resource_equals(A(1))), /// IntoSystem::into_system(resource_equals(B(1))), /// // The name of the combined system. @@ -203,18 +206,18 @@ where .extend(self.b.archetype_component_access()); } - fn check_change_tick(&mut self, change_tick: u32) { + fn check_change_tick(&mut self, change_tick: Tick) { self.a.check_change_tick(change_tick); self.b.check_change_tick(change_tick); } - fn get_last_change_tick(&self) -> u32 { - self.a.get_last_change_tick() + fn get_last_run(&self) -> Tick { + self.a.get_last_run() } - fn set_last_change_tick(&mut self, last_change_tick: u32) { - self.a.set_last_change_tick(last_change_tick); - self.b.set_last_change_tick(last_change_tick); + fn set_last_run(&mut self, last_run: Tick) { + self.a.set_last_run(last_run); + self.b.set_last_run(last_run); } fn default_system_sets(&self) -> Vec> { diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index 73df904177..fb0f1b388f 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -582,9 +582,9 @@ impl<'w, 's> Commands<'w, 's> { /// # world.init_resource::(); /// # /// # let mut setup_schedule = Schedule::new(); -/// # setup_schedule.add_system(setup); +/// # setup_schedule.add_systems(setup); /// # let mut assert_schedule = Schedule::new(); -/// # assert_schedule.add_system(assert_names); +/// # assert_schedule.add_systems(assert_names); /// # /// # setup_schedule.run(&mut world); /// # assert_schedule.run(&mut world); diff --git a/crates/bevy_ecs/src/system/exclusive_function_system.rs b/crates/bevy_ecs/src/system/exclusive_function_system.rs index 1fde9619e2..d82c6ff153 100644 --- a/crates/bevy_ecs/src/system/exclusive_function_system.rs +++ b/crates/bevy_ecs/src/system/exclusive_function_system.rs @@ -1,7 +1,6 @@ use crate::{ archetype::ArchetypeComponentId, - change_detection::MAX_CHANGE_AGE, - component::ComponentId, + component::{ComponentId, Tick}, query::Access, system::{ check_system_change_tick, ExclusiveSystemParam, ExclusiveSystemParamItem, In, IntoSystem, @@ -95,7 +94,7 @@ where fn run(&mut self, input: Self::In, world: &mut World) -> Self::Out { let saved_last_tick = world.last_change_tick; - world.last_change_tick = self.system_meta.last_change_tick; + world.last_change_tick = self.system_meta.last_run; let params = F::Param::get_param( self.param_state.as_mut().expect(PARAM_MESSAGE), @@ -104,7 +103,7 @@ where let out = self.func.run(world, input, params); let change_tick = world.change_tick.get_mut(); - self.system_meta.last_change_tick = *change_tick; + self.system_meta.last_run.set(*change_tick); *change_tick = change_tick.wrapping_add(1); world.last_change_tick = saved_last_tick; @@ -116,12 +115,12 @@ where true } - fn get_last_change_tick(&self) -> u32 { - self.system_meta.last_change_tick + fn get_last_run(&self) -> Tick { + self.system_meta.last_run } - fn set_last_change_tick(&mut self, last_change_tick: u32) { - self.system_meta.last_change_tick = last_change_tick; + fn set_last_run(&mut self, last_run: Tick) { + self.system_meta.last_run = last_run; } #[inline] @@ -134,16 +133,16 @@ where #[inline] fn initialize(&mut self, world: &mut World) { self.world_id = Some(world.id()); - self.system_meta.last_change_tick = world.change_tick().wrapping_sub(MAX_CHANGE_AGE); + self.system_meta.last_run = world.change_tick().relative_to(Tick::MAX); self.param_state = Some(F::Param::init(world, &mut self.system_meta)); } fn update_archetype_component_access(&mut self, _world: &World) {} #[inline] - fn check_change_tick(&mut self, change_tick: u32) { + fn check_change_tick(&mut self, change_tick: Tick) { check_system_change_tick( - &mut self.system_meta.last_change_tick, + &mut self.system_meta.last_run, change_tick, self.system_meta.name.as_ref(), ); diff --git a/crates/bevy_ecs/src/system/function_system.rs b/crates/bevy_ecs/src/system/function_system.rs index fe6701df60..d8bf01392f 100644 --- a/crates/bevy_ecs/src/system/function_system.rs +++ b/crates/bevy_ecs/src/system/function_system.rs @@ -1,7 +1,6 @@ use crate::{ archetype::{ArchetypeComponentId, ArchetypeGeneration, ArchetypeId}, - change_detection::MAX_CHANGE_AGE, - component::ComponentId, + component::{ComponentId, Tick}, prelude::FromWorld, query::{Access, FilteredAccessSet}, system::{check_system_change_tick, ReadOnlySystemParam, System, SystemParam, SystemParamItem}, @@ -22,7 +21,7 @@ pub struct SystemMeta { // NOTE: this must be kept private. making a SystemMeta non-send is irreversible to prevent // SystemParams from overriding each other is_send: bool, - pub(crate) last_change_tick: u32, + pub(crate) last_run: Tick, } impl SystemMeta { @@ -32,7 +31,7 @@ impl SystemMeta { archetype_component_access: Access::default(), component_access_set: FilteredAccessSet::default(), is_send: true, - last_change_tick: 0, + last_run: Tick::new(0), } } @@ -151,7 +150,7 @@ pub struct SystemState { impl SystemState { pub fn new(world: &mut World) -> Self { let mut meta = SystemMeta::new::(); - meta.last_change_tick = world.change_tick().wrapping_sub(MAX_CHANGE_AGE); + meta.last_run = world.change_tick().relative_to(Tick::MAX); let param_state = Param::init_state(world, &mut meta); Self { meta, @@ -288,10 +287,10 @@ impl SystemState { unsafe fn fetch<'w, 's>( &'s mut self, world: &'w World, - change_tick: u32, + change_tick: Tick, ) -> SystemParamItem<'w, 's, Param> { let param = Param::get_param(&mut self.param_state, &self.meta, world, change_tick); - self.meta.last_change_tick = change_tick; + self.meta.last_run = change_tick; param } } @@ -368,6 +367,9 @@ pub struct In(pub In); /// becomes the functions [`In`] tagged parameter or `()` if no such parameter exists. /// /// [`FunctionSystem`] must be `.initialized` before they can be run. +/// +/// The [`Clone`] implementation for [`FunctionSystem`] returns a new instance which +/// is NOT initialized. The cloned system must also be `.initialized` before it can be run. pub struct FunctionSystem where F: SystemParamFunction, @@ -381,6 +383,23 @@ where marker: PhantomData Marker>, } +// De-initializes the cloned system. +impl Clone for FunctionSystem +where + F: SystemParamFunction + Clone, +{ + fn clone(&self) -> Self { + Self { + func: self.func.clone(), + param_state: None, + system_meta: SystemMeta::new::(), + world_id: None, + archetype_generation: ArchetypeGeneration::initial(), + marker: PhantomData, + } + } +} + pub struct IsFunctionSystem; impl IntoSystem for F @@ -464,16 +483,16 @@ where change_tick, ); let out = self.func.run(input, params); - self.system_meta.last_change_tick = change_tick; + self.system_meta.last_run = change_tick; out } - fn get_last_change_tick(&self) -> u32 { - self.system_meta.last_change_tick + fn get_last_run(&self) -> Tick { + self.system_meta.last_run } - fn set_last_change_tick(&mut self, last_change_tick: u32) { - self.system_meta.last_change_tick = last_change_tick; + fn set_last_run(&mut self, last_run: Tick) { + self.system_meta.last_run = last_run; } #[inline] @@ -485,7 +504,7 @@ where #[inline] fn initialize(&mut self, world: &mut World) { self.world_id = Some(world.id()); - self.system_meta.last_change_tick = world.change_tick().wrapping_sub(MAX_CHANGE_AGE); + self.system_meta.last_run = world.change_tick().relative_to(Tick::MAX); self.param_state = Some(F::Param::init_state(world, &mut self.system_meta)); } @@ -507,9 +526,9 @@ where } #[inline] - fn check_change_tick(&mut self, change_tick: u32) { + fn check_change_tick(&mut self, change_tick: Tick) { check_system_change_tick( - &mut self.system_meta.last_change_tick, + &mut self.system_meta.last_run, change_tick, self.system_meta.name.as_ref(), ); diff --git a/crates/bevy_ecs/src/system/mod.rs b/crates/bevy_ecs/src/system/mod.rs index 0e88adf24e..1ce4bfc17f 100644 --- a/crates/bevy_ecs/src/system/mod.rs +++ b/crates/bevy_ecs/src/system/mod.rs @@ -55,14 +55,18 @@ //! //! ``` //! # use bevy_ecs::prelude::*; -//! # let mut app = Schedule::new(); -//! // Prints "Hello, World!" each frame. -//! app -//! .add_system(print_first.before(print_mid)) -//! .add_system(print_mid) -//! .add_system(print_last.after(print_mid)); +//! # let mut schedule = Schedule::new(); //! # let mut world = World::new(); -//! # app.run(&mut world); +//! // Configure these systems to run in order using `chain()`. +//! schedule.add_systems((print_first, print_last).chain()); +//! // Prints "HelloWorld!" +//! schedule.run(&mut world); +//! +//! // Configure this system to run in between the other two systems +//! // using explicit dependencies. +//! schedule.add_systems(print_mid.after(print_first).before(print_last)); +//! // Prints "Hello, World!" +//! schedule.run(&mut world); //! //! fn print_first() { //! print!("Hello"); @@ -119,17 +123,40 @@ pub use system::*; pub use system_param::*; pub use system_piping::*; +use crate::world::World; + /// Ensure that a given function is a [system](System). /// /// This should be used when writing doc examples, /// to confirm that systems used in an example are /// valid systems. -pub fn assert_is_system>(sys: S) { - if false { - // Check it can be converted into a system - // TODO: This should ensure that the system has no conflicting system params - IntoSystem::into_system(sys); - } +/// +/// # Examples +/// +/// The following example will panic when run since the +/// system's parameters mutably access the same component +/// multiple times. +/// +/// ```should_panic +/// # use bevy_ecs::{prelude::*, system::assert_is_system}; +/// # +/// # #[derive(Component)] +/// # struct Transform; +/// # +/// fn my_system(query1: Query<&mut Transform>, query2: Query<&mut Transform>) { +/// // ... +/// } +/// +/// assert_is_system(my_system); +/// ``` +pub fn assert_is_system( + system: impl IntoSystem, +) { + let mut system = IntoSystem::into_system(system); + + // Initialize the system, which will panic if the system has access conflicts. + let mut world = World::new(); + system.initialize(&mut world); } /// Ensure that a given function is a [read-only system](ReadOnlySystem). @@ -137,15 +164,30 @@ pub fn assert_is_system>(sys: S) /// This should be used when writing doc examples, /// to confirm that systems used in an example are /// valid systems. -pub fn assert_is_read_only_system>(sys: S) +/// +/// # Examples +/// +/// The following example will fail to compile +/// since the system accesses a component mutably. +/// +/// ```compile_fail +/// # use bevy_ecs::{prelude::*, system::assert_is_read_only_system}; +/// # +/// # #[derive(Component)] +/// # struct Transform; +/// # +/// fn my_system(query: Query<&mut Transform>) { +/// // ... +/// } +/// +/// assert_is_read_only_system(my_system); +/// ``` +pub fn assert_is_read_only_system(system: S) where + S: IntoSystem, S::System: ReadOnlySystem, { - if false { - // Check it can be converted into a system - // TODO: This should ensure that the system has no conflicting system params - IntoSystem::into_system(sys); - } + assert_is_system(system); } #[cfg(test)] @@ -157,12 +199,12 @@ mod tests { archetype::{ArchetypeComponentId, Archetypes}, bundle::Bundles, change_detection::DetectChanges, - component::{Component, Components}, + component::{Component, Components, Tick}, entity::{Entities, Entity}, prelude::AnyOf, query::{Added, Changed, Or, With, Without}, removal_detection::RemovedComponents, - schedule::{apply_system_buffers, IntoSystemConfig, Schedule}, + schedule::{apply_system_buffers, IntoSystemConfigs, Schedule}, system::{ Commands, IntoSystem, Local, NonSend, NonSendMut, ParamSet, Query, QueryComponentError, Res, ResMut, Resource, System, SystemState, @@ -210,7 +252,7 @@ mod tests { fn run_system>(world: &mut World, system: S) { let mut schedule = Schedule::default(); - schedule.add_system(system); + schedule.add_systems(system); schedule.run(world); } @@ -272,6 +314,60 @@ mod tests { assert_eq!(*world.resource::(), SystemRan::Yes); } + #[test] + fn get_many_is_ordered() { + use crate::system::Resource; + const ENTITIES_COUNT: usize = 1000; + + #[derive(Resource)] + struct EntitiesArray(Vec); + + fn query_system( + mut ran: ResMut, + entities_array: Res, + q: Query<&W>, + ) { + let entities_array: [Entity; ENTITIES_COUNT] = + entities_array.0.clone().try_into().unwrap(); + + for (i, w) in (0..ENTITIES_COUNT).zip(q.get_many(entities_array).unwrap()) { + assert_eq!(i, w.0); + } + + *ran = SystemRan::Yes; + } + + fn query_system_mut( + mut ran: ResMut, + entities_array: Res, + mut q: Query<&mut W>, + ) { + let entities_array: [Entity; ENTITIES_COUNT] = + entities_array.0.clone().try_into().unwrap(); + + #[allow(unused_mut)] + for (i, mut w) in (0..ENTITIES_COUNT).zip(q.get_many_mut(entities_array).unwrap()) { + assert_eq!(i, w.0); + } + + *ran = SystemRan::Yes; + } + + let mut world = World::default(); + world.insert_resource(SystemRan::No); + let entity_ids = (0..ENTITIES_COUNT) + .map(|i| world.spawn(W(i)).id()) + .collect(); + world.insert_resource(EntitiesArray(entity_ids)); + + run_system(&mut world, query_system); + assert_eq!(*world.resource::(), SystemRan::Yes); + + world.insert_resource(SystemRan::No); + run_system(&mut world, query_system_mut); + assert_eq!(*world.resource::(), SystemRan::Yes); + } + #[test] fn or_param_set_system() { // Regression test for issue #762 @@ -334,9 +430,7 @@ mod tests { let mut schedule = Schedule::default(); - schedule.add_system(incr_e_on_flip); - schedule.add_system(apply_system_buffers.after(incr_e_on_flip)); - schedule.add_system(World::clear_trackers.after(apply_system_buffers)); + schedule.add_systems((incr_e_on_flip, apply_system_buffers, World::clear_trackers).chain()); schedule.run(&mut world); assert_eq!(world.resource::().0, 1); @@ -1227,7 +1321,7 @@ mod tests { let world2 = World::new(); let qstate = world1.query::<()>(); // SAFETY: doesnt access anything - let query = unsafe { Query::new(&world2, &qstate, 0, 0, false) }; + let query = unsafe { Query::new(&world2, &qstate, Tick::new(0), Tick::new(0), false) }; query.iter(); } diff --git a/crates/bevy_ecs/src/system/query.rs b/crates/bevy_ecs/src/system/query.rs index 0c093b2587..4316389ade 100644 --- a/crates/bevy_ecs/src/system/query.rs +++ b/crates/bevy_ecs/src/system/query.rs @@ -1,5 +1,5 @@ use crate::{ - component::Component, + component::{Component, Tick}, entity::Entity, query::{ BatchingStrategy, QueryCombinationIter, QueryEntityError, QueryIter, QueryManyIter, @@ -276,8 +276,8 @@ use std::{any::TypeId, borrow::Borrow, fmt::Debug}; pub struct Query<'world, 'state, Q: WorldQuery, F: ReadOnlyWorldQuery = ()> { world: &'world World, state: &'state QueryState, - last_change_tick: u32, - change_tick: u32, + last_run: Tick, + this_run: Tick, // SAFETY: This is used to ensure that `get_component_mut::` properly fails when a Query writes C // and gets converted to a read-only query using `to_readonly`. Without checking this, `get_component_mut` relies on // QueryState's archetype_component_access, which will continue allowing write access to C after being cast to @@ -288,7 +288,7 @@ pub struct Query<'world, 'state, Q: WorldQuery, F: ReadOnlyWorldQuery = ()> { impl<'w, 's, Q: WorldQuery, F: ReadOnlyWorldQuery> std::fmt::Debug for Query<'w, 's, Q, F> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "Query {{ matched entities: {}, world: {:?}, state: {:?}, last_change_tick: {}, change_tick: {} }}", self.iter().count(), self.world, self.state, self.last_change_tick, self.change_tick) + write!(f, "Query {{ matched entities: {}, world: {:?}, state: {:?}, last_run: {:?}, this_run: {:?} }}", self.iter().count(), self.world, self.state, self.last_run, self.this_run) } } @@ -307,8 +307,8 @@ impl<'w, 's, Q: WorldQuery, F: ReadOnlyWorldQuery> Query<'w, 's, Q, F> { pub(crate) unsafe fn new( world: &'w World, state: &'s QueryState, - last_change_tick: u32, - change_tick: u32, + last_run: Tick, + this_run: Tick, force_read_only_component_access: bool, ) -> Self { state.validate_world(world); @@ -317,8 +317,8 @@ impl<'w, 's, Q: WorldQuery, F: ReadOnlyWorldQuery> Query<'w, 's, Q, F> { force_read_only_component_access, world, state, - last_change_tick, - change_tick, + last_run, + this_run, } } @@ -334,8 +334,8 @@ impl<'w, 's, Q: WorldQuery, F: ReadOnlyWorldQuery> Query<'w, 's, Q, F> { Query::new( self.world, new_state, - self.last_change_tick, - self.change_tick, + self.last_run, + self.this_run, // SAFETY: this must be set to true or `get_component_mut` will be unsound. See the comments // on this field for more details true, @@ -372,11 +372,9 @@ impl<'w, 's, Q: WorldQuery, F: ReadOnlyWorldQuery> Query<'w, 's, Q, F> { // SAFETY: system runs without conflicts with other systems. // same-system queries have runtime borrow checks when they conflict unsafe { - self.state.as_readonly().iter_unchecked_manual( - self.world, - self.last_change_tick, - self.change_tick, - ) + self.state + .as_readonly() + .iter_unchecked_manual(self.world, self.last_run, self.this_run) } } @@ -410,7 +408,7 @@ impl<'w, 's, Q: WorldQuery, F: ReadOnlyWorldQuery> Query<'w, 's, Q, F> { // same-system queries have runtime borrow checks when they conflict unsafe { self.state - .iter_unchecked_manual(self.world, self.last_change_tick, self.change_tick) + .iter_unchecked_manual(self.world, self.last_run, self.this_run) } } @@ -442,8 +440,8 @@ impl<'w, 's, Q: WorldQuery, F: ReadOnlyWorldQuery> Query<'w, 's, Q, F> { unsafe { self.state.as_readonly().iter_combinations_unchecked_manual( self.world, - self.last_change_tick, - self.change_tick, + self.last_run, + self.this_run, ) } } @@ -474,11 +472,8 @@ impl<'w, 's, Q: WorldQuery, F: ReadOnlyWorldQuery> Query<'w, 's, Q, F> { // SAFETY: system runs without conflicts with other systems. // same-system queries have runtime borrow checks when they conflict unsafe { - self.state.iter_combinations_unchecked_manual( - self.world, - self.last_change_tick, - self.change_tick, - ) + self.state + .iter_combinations_unchecked_manual(self.world, self.last_run, self.this_run) } } @@ -532,8 +527,8 @@ impl<'w, 's, Q: WorldQuery, F: ReadOnlyWorldQuery> Query<'w, 's, Q, F> { self.state.as_readonly().iter_many_unchecked_manual( entities, self.world, - self.last_change_tick, - self.change_tick, + self.last_run, + self.this_run, ) } } @@ -585,8 +580,8 @@ impl<'w, 's, Q: WorldQuery, F: ReadOnlyWorldQuery> Query<'w, 's, Q, F> { self.state.iter_many_unchecked_manual( entities, self.world, - self.last_change_tick, - self.change_tick, + self.last_run, + self.this_run, ) } } @@ -606,7 +601,7 @@ impl<'w, 's, Q: WorldQuery, F: ReadOnlyWorldQuery> Query<'w, 's, Q, F> { // SEMI-SAFETY: system runs without conflicts with other systems. // same-system queries have runtime borrow checks when they conflict self.state - .iter_unchecked_manual(self.world, self.last_change_tick, self.change_tick) + .iter_unchecked_manual(self.world, self.last_run, self.this_run) } /// Iterates over all possible combinations of `K` query items without repetition. @@ -625,11 +620,8 @@ impl<'w, 's, Q: WorldQuery, F: ReadOnlyWorldQuery> Query<'w, 's, Q, F> { ) -> QueryCombinationIter<'_, 's, Q, F, K> { // SEMI-SAFETY: system runs without conflicts with other systems. // same-system queries have runtime borrow checks when they conflict - self.state.iter_combinations_unchecked_manual( - self.world, - self.last_change_tick, - self.change_tick, - ) + self.state + .iter_combinations_unchecked_manual(self.world, self.last_run, self.this_run) } /// Returns an [`Iterator`] over the query items generated from an [`Entity`] list. @@ -650,12 +642,8 @@ impl<'w, 's, Q: WorldQuery, F: ReadOnlyWorldQuery> Query<'w, 's, Q, F> { where EntityList::Item: Borrow, { - self.state.iter_many_unchecked_manual( - entities, - self.world, - self.last_change_tick, - self.change_tick, - ) + self.state + .iter_many_unchecked_manual(entities, self.world, self.last_run, self.this_run) } /// Runs `f` on each read-only query item. @@ -690,8 +678,8 @@ impl<'w, 's, Q: WorldQuery, F: ReadOnlyWorldQuery> Query<'w, 's, Q, F> { self.state.as_readonly().for_each_unchecked_manual( self.world, f, - self.last_change_tick, - self.change_tick, + self.last_run, + self.this_run, ); }; } @@ -725,12 +713,8 @@ impl<'w, 's, Q: WorldQuery, F: ReadOnlyWorldQuery> Query<'w, 's, Q, F> { // SAFETY: system runs without conflicts with other systems. same-system queries have runtime // borrow checks when they conflict unsafe { - self.state.for_each_unchecked_manual( - self.world, - f, - self.last_change_tick, - self.change_tick, - ); + self.state + .for_each_unchecked_manual(self.world, f, self.last_run, self.this_run); }; } @@ -744,6 +728,8 @@ impl<'w, 's, Q: WorldQuery, F: ReadOnlyWorldQuery> Query<'w, 's, Q, F> { QueryParIter { world: self.world, state: self.state.as_readonly(), + last_run: self.last_run, + this_run: self.this_run, batching_strategy: BatchingStrategy::new(), } } @@ -758,6 +744,8 @@ impl<'w, 's, Q: WorldQuery, F: ReadOnlyWorldQuery> Query<'w, 's, Q, F> { QueryParIter { world: self.world, state: self.state, + last_run: self.last_run, + this_run: self.this_run, batching_strategy: BatchingStrategy::new(), } } @@ -801,14 +789,15 @@ impl<'w, 's, Q: WorldQuery, F: ReadOnlyWorldQuery> Query<'w, 's, Q, F> { self.state.as_readonly().get_unchecked_manual( self.world, entity, - self.last_change_tick, - self.change_tick, + self.last_run, + self.this_run, ) } } /// Returns the read-only query items for the given array of [`Entity`]. /// + /// The returned query items are in the same order as the input. /// In case of a nonexisting entity or mismatched component, a [`QueryEntityError`] is returned instead. /// The elements of the array do not need to be unique, unlike `get_many_mut`. /// @@ -823,12 +812,8 @@ impl<'w, 's, Q: WorldQuery, F: ReadOnlyWorldQuery> Query<'w, 's, Q, F> { ) -> Result<[ROQueryItem<'_, Q>; N], QueryEntityError> { // SAFETY: it is the scheduler's responsibility to ensure that `Query` is never handed out on the wrong `World`. unsafe { - self.state.get_many_read_only_manual( - self.world, - entities, - self.last_change_tick, - self.change_tick, - ) + self.state + .get_many_read_only_manual(self.world, entities, self.last_run, self.this_run) } } @@ -910,17 +895,14 @@ impl<'w, 's, Q: WorldQuery, F: ReadOnlyWorldQuery> Query<'w, 's, Q, F> { // SAFETY: system runs without conflicts with other systems. // same-system queries have runtime borrow checks when they conflict unsafe { - self.state.get_unchecked_manual( - self.world, - entity, - self.last_change_tick, - self.change_tick, - ) + self.state + .get_unchecked_manual(self.world, entity, self.last_run, self.this_run) } } /// Returns the query items for the given array of [`Entity`]. /// + /// The returned query items are in the same order as the input. /// In case of a nonexisting entity, duplicate entities or mismatched component, a [`QueryEntityError`] is returned instead. /// /// # See also @@ -934,12 +916,8 @@ impl<'w, 's, Q: WorldQuery, F: ReadOnlyWorldQuery> Query<'w, 's, Q, F> { ) -> Result<[Q::Item<'_>; N], QueryEntityError> { // SAFETY: scheduler ensures safe Query world access unsafe { - self.state.get_many_unchecked_manual( - self.world, - entities, - self.last_change_tick, - self.change_tick, - ) + self.state + .get_many_unchecked_manual(self.world, entities, self.last_run, self.this_run) } } @@ -1013,7 +991,7 @@ impl<'w, 's, Q: WorldQuery, F: ReadOnlyWorldQuery> Query<'w, 's, Q, F> { // SEMI-SAFETY: system runs without conflicts with other systems. // same-system queries have runtime borrow checks when they conflict self.state - .get_unchecked_manual(self.world, entity, self.last_change_tick, self.change_tick) + .get_unchecked_manual(self.world, entity, self.last_run, self.this_run) } /// Returns a shared reference to the component `T` of the given [`Entity`]. @@ -1151,7 +1129,7 @@ impl<'w, 's, Q: WorldQuery, F: ReadOnlyWorldQuery> Query<'w, 's, Q, F> { .has_write(archetype_component) { entity_ref - .get_mut_using_ticks::(self.last_change_tick, self.change_tick) + .get_mut_using_ticks::(self.last_run, self.this_run) .ok_or(QueryComponentError::MissingComponent) } else { Err(QueryComponentError::MissingWriteAccess) @@ -1227,8 +1205,8 @@ impl<'w, 's, Q: WorldQuery, F: ReadOnlyWorldQuery> Query<'w, 's, Q, F> { unsafe { self.state.as_readonly().get_single_unchecked_manual( self.world, - self.last_change_tick, - self.change_tick, + self.last_run, + self.this_run, ) } } @@ -1296,11 +1274,8 @@ impl<'w, 's, Q: WorldQuery, F: ReadOnlyWorldQuery> Query<'w, 's, Q, F> { // the query ensures mutable access to the components it accesses, and the query // is uniquely borrowed unsafe { - self.state.get_single_unchecked_manual( - self.world, - self.last_change_tick, - self.change_tick, - ) + self.state + .get_single_unchecked_manual(self.world, self.last_run, self.this_run) } } @@ -1327,7 +1302,7 @@ impl<'w, 's, Q: WorldQuery, F: ReadOnlyWorldQuery> Query<'w, 's, Q, F> { #[inline] pub fn is_empty(&self) -> bool { self.state - .is_empty(self.world, self.last_change_tick, self.change_tick) + .is_empty(self.world, self.last_run, self.this_run) } /// Returns `true` if the given [`Entity`] matches the query. @@ -1358,7 +1333,7 @@ impl<'w, 's, Q: WorldQuery, F: ReadOnlyWorldQuery> Query<'w, 's, Q, F> { unsafe { self.state .as_nop() - .get_unchecked_manual(self.world, entity, self.last_change_tick, self.change_tick) + .get_unchecked_manual(self.world, entity, self.last_run, self.this_run) .is_ok() } } @@ -1459,8 +1434,8 @@ impl<'w, 's, Q: ReadOnlyWorldQuery, F: ReadOnlyWorldQuery> Query<'w, 's, Q, F> { self.state.as_readonly().get_unchecked_manual( self.world, entity, - self.last_change_tick, - self.change_tick, + self.last_run, + self.this_run, ) } } @@ -1493,11 +1468,9 @@ impl<'w, 's, Q: ReadOnlyWorldQuery, F: ReadOnlyWorldQuery> Query<'w, 's, Q, F> { // SAFETY: system runs without conflicts with other systems. // same-system queries have runtime borrow checks when they conflict unsafe { - self.state.as_readonly().iter_unchecked_manual( - self.world, - self.last_change_tick, - self.change_tick, - ) + self.state + .as_readonly() + .iter_unchecked_manual(self.world, self.last_run, self.this_run) } } } diff --git a/crates/bevy_ecs/src/system/system.rs b/crates/bevy_ecs/src/system/system.rs index 78980e9063..b8c11b6262 100644 --- a/crates/bevy_ecs/src/system/system.rs +++ b/crates/bevy_ecs/src/system/system.rs @@ -1,10 +1,8 @@ use bevy_utils::tracing::warn; use core::fmt::Debug; -use crate::{ - archetype::ArchetypeComponentId, change_detection::MAX_CHANGE_AGE, component::ComponentId, - query::Access, world::World, -}; +use crate::component::Tick; +use crate::{archetype::ArchetypeComponentId, component::ComponentId, query::Access, world::World}; use std::any::TypeId; use std::borrow::Cow; @@ -19,7 +17,7 @@ use std::borrow::Cow; /// /// Systems are executed in parallel, in opportunistic order; data access is managed automatically. /// It's possible to specify explicit execution order between specific systems, -/// see [`IntoSystemConfig`](crate::schedule::IntoSystemConfig). +/// see [`IntoSystemConfigs`](crate::schedule::IntoSystemConfigs). pub trait System: Send + Sync + 'static { /// The system's input. See [`In`](crate::system::In) for /// [`FunctionSystem`](crate::system::FunctionSystem)s. @@ -63,19 +61,20 @@ pub trait System: Send + Sync + 'static { fn initialize(&mut self, _world: &mut World); /// Update the system's archetype component [`Access`]. fn update_archetype_component_access(&mut self, world: &World); - fn check_change_tick(&mut self, change_tick: u32); + fn check_change_tick(&mut self, change_tick: Tick); /// Returns the system's default [system sets](crate::schedule::SystemSet). fn default_system_sets(&self) -> Vec> { Vec::new() } - /// Gets the system's last change tick - fn get_last_change_tick(&self) -> u32; - /// Sets the system's last change tick + /// Gets the tick indicating the last time this system ran. + fn get_last_run(&self) -> Tick; + /// Overwrites the tick indicating the last time this system ran. + /// /// # Warning /// This is a complex and error-prone operation, that can have unexpected consequences on any system relying on this code. /// However, it can be an essential escape hatch when, for example, /// you are trying to synchronize representations using change detection and need to avoid infinite recursion. - fn set_last_change_tick(&mut self, last_change_tick: u32); + fn set_last_run(&mut self, last_run: Tick); } /// [`System`] types that do not modify the [`World`] when run. @@ -91,23 +90,14 @@ pub unsafe trait ReadOnlySystem: System {} /// A convenience type alias for a boxed [`System`] trait object. pub type BoxedSystem = Box>; -pub(crate) fn check_system_change_tick( - last_change_tick: &mut u32, - change_tick: u32, - system_name: &str, -) { - let age = change_tick.wrapping_sub(*last_change_tick); - // This comparison assumes that `age` has not overflowed `u32::MAX` before, which will be true - // so long as this check always runs before that can happen. - if age > MAX_CHANGE_AGE { +pub(crate) fn check_system_change_tick(last_run: &mut Tick, this_run: Tick, system_name: &str) { + if last_run.check_tick(this_run) { + let age = this_run.relative_to(*last_run).get(); warn!( - "System '{}' has not run for {} ticks. \ + "System '{system_name}' has not run for {age} ticks. \ Changes older than {} ticks will not be detected.", - system_name, - age, - MAX_CHANGE_AGE - 1, + Tick::MAX.get() - 1, ); - *last_change_tick = change_tick.wrapping_sub(MAX_CHANGE_AGE); } } diff --git a/crates/bevy_ecs/src/system/system_param.rs b/crates/bevy_ecs/src/system/system_param.rs index 437a726f44..d0668025c7 100644 --- a/crates/bevy_ecs/src/system/system_param.rs +++ b/crates/bevy_ecs/src/system/system_param.rs @@ -3,7 +3,7 @@ use crate::{ archetype::{Archetype, Archetypes}, bundle::Bundles, change_detection::{Ticks, TicksMut}, - component::{ComponentId, ComponentTicks, Components}, + component::{ComponentId, ComponentTicks, Components, Tick}, entity::Entities, query::{ Access, FilteredAccess, FilteredAccessSet, QueryState, ReadOnlyWorldQuery, WorldQuery, @@ -89,38 +89,6 @@ use std::{ /// This will most commonly occur when working with `SystemParam`s generically, as the requirement /// has not been proven to the compiler. /// -/// # `!Sync` Resources -/// A `!Sync` type cannot implement `Resource`. However, it is possible to wrap a `Send` but not `Sync` -/// type in [`SyncCell`] or the currently unstable [`Exclusive`] to make it `Sync`. This forces only -/// having mutable access (`&mut T` only, never `&T`), but makes it safe to reference across multiple -/// threads. -/// -/// This will fail to compile since `RefCell` is `!Sync`. -/// ```compile_fail -/// # use std::cell::RefCell; -/// # use bevy_ecs::system::Resource; -/// -/// #[derive(Resource)] -/// struct NotSync { -/// counter: RefCell, -/// } -/// ``` -/// -/// This will compile since the `RefCell` is wrapped with `SyncCell`. -/// ``` -/// # use std::cell::RefCell; -/// # use bevy_ecs::system::Resource; -/// use bevy_utils::synccell::SyncCell; -/// -/// #[derive(Resource)] -/// struct ActuallySync { -/// counter: SyncCell>, -/// } -/// ``` -/// -/// [`SyncCell`]: bevy_utils::synccell::SyncCell -/// [`Exclusive`]: https://doc.rust-lang.org/nightly/std/sync/struct.Exclusive.html -/// /// # Safety /// /// The implementor must ensure the following is true. @@ -169,7 +137,7 @@ pub unsafe trait SystemParam: Sized { state: &'state mut Self::State, system_meta: &SystemMeta, world: &'world World, - change_tick: u32, + change_tick: Tick, ) -> Self::Item<'world, 'state>; } @@ -227,15 +195,9 @@ unsafe impl SystemPara state: &'s mut Self::State, system_meta: &SystemMeta, world: &'w World, - change_tick: u32, + change_tick: Tick, ) -> Self::Item<'w, 's> { - Query::new( - world, - state, - system_meta.last_change_tick, - change_tick, - false, - ) + Query::new(world, state, system_meta.last_run, change_tick, false) } } @@ -371,7 +333,7 @@ pub struct ParamSet<'w, 's, T: SystemParam> { param_states: &'s mut T::State, world: &'w World, system_meta: SystemMeta, - change_tick: u32, + change_tick: Tick, } impl_param_set!(); @@ -402,10 +364,41 @@ impl_param_set!(); /// resource.value = 0; /// assert_eq!(resource.value, 0); /// } -/// # schedule.add_system(read_resource_system); -/// # schedule.add_system(write_resource_system.after(read_resource_system)); +/// # schedule.add_systems((read_resource_system, write_resource_system).chain()); /// # schedule.run(&mut world); /// ``` +/// +/// # `!Sync` Resources +/// A `!Sync` type cannot implement `Resource`. However, it is possible to wrap a `Send` but not `Sync` +/// type in [`SyncCell`] or the currently unstable [`Exclusive`] to make it `Sync`. This forces only +/// having mutable access (`&mut T` only, never `&T`), but makes it safe to reference across multiple +/// threads. +/// +/// This will fail to compile since `RefCell` is `!Sync`. +/// ```compile_fail +/// # use std::cell::RefCell; +/// # use bevy_ecs::system::Resource; +/// +/// #[derive(Resource)] +/// struct NotSync { +/// counter: RefCell, +/// } +/// ``` +/// +/// This will compile since the `RefCell` is wrapped with `SyncCell`. +/// ``` +/// # use std::cell::RefCell; +/// # use bevy_ecs::system::Resource; +/// use bevy_utils::synccell::SyncCell; +/// +/// #[derive(Resource)] +/// struct ActuallySync { +/// counter: SyncCell>, +/// } +/// ``` +/// +/// [`SyncCell`]: bevy_utils::synccell::SyncCell +/// [`Exclusive`]: https://doc.rust-lang.org/nightly/std/sync/struct.Exclusive.html pub trait Resource: Send + Sync + 'static {} // SAFETY: Res only reads a single World resource @@ -445,7 +438,7 @@ unsafe impl<'a, T: Resource> SystemParam for Res<'a, T> { &mut component_id: &'s mut Self::State, system_meta: &SystemMeta, world: &'w World, - change_tick: u32, + change_tick: Tick, ) -> Self::Item<'w, 's> { let (ptr, ticks) = world .as_unsafe_world_cell_migration_internal() @@ -462,8 +455,8 @@ unsafe impl<'a, T: Resource> SystemParam for Res<'a, T> { ticks: Ticks { added: ticks.added.deref(), changed: ticks.changed.deref(), - last_change_tick: system_meta.last_change_tick, - change_tick, + last_run: system_meta.last_run, + this_run: change_tick, }, } } @@ -486,7 +479,7 @@ unsafe impl<'a, T: Resource> SystemParam for Option> { &mut component_id: &'s mut Self::State, system_meta: &SystemMeta, world: &'w World, - change_tick: u32, + change_tick: Tick, ) -> Self::Item<'w, 's> { world .as_unsafe_world_cell_migration_internal() @@ -496,8 +489,8 @@ unsafe impl<'a, T: Resource> SystemParam for Option> { ticks: Ticks { added: ticks.added.deref(), changed: ticks.changed.deref(), - last_change_tick: system_meta.last_change_tick, - change_tick, + last_run: system_meta.last_run, + this_run: change_tick, }, }) } @@ -540,7 +533,7 @@ unsafe impl<'a, T: Resource> SystemParam for ResMut<'a, T> { &mut component_id: &'s mut Self::State, system_meta: &SystemMeta, world: &'w World, - change_tick: u32, + change_tick: Tick, ) -> Self::Item<'w, 's> { let value = world .as_unsafe_world_cell_migration_internal() @@ -557,8 +550,8 @@ unsafe impl<'a, T: Resource> SystemParam for ResMut<'a, T> { ticks: TicksMut { added: value.ticks.added, changed: value.ticks.changed, - last_change_tick: system_meta.last_change_tick, - change_tick, + last_run: system_meta.last_run, + this_run: change_tick, }, } } @@ -578,7 +571,7 @@ unsafe impl<'a, T: Resource> SystemParam for Option> { &mut component_id: &'s mut Self::State, system_meta: &SystemMeta, world: &'w World, - change_tick: u32, + change_tick: Tick, ) -> Self::Item<'w, 's> { world .as_unsafe_world_cell_migration_internal() @@ -588,8 +581,8 @@ unsafe impl<'a, T: Resource> SystemParam for Option> { ticks: TicksMut { added: value.ticks.added, changed: value.ticks.changed, - last_change_tick: system_meta.last_change_tick, - change_tick, + last_run: system_meta.last_run, + this_run: change_tick, }, }) } @@ -631,7 +624,7 @@ unsafe impl SystemParam for &'_ World { _state: &'s mut Self::State, _system_meta: &SystemMeta, world: &'w World, - _change_tick: u32, + _change_tick: Tick, ) -> Self::Item<'w, 's> { world } @@ -681,7 +674,7 @@ unsafe impl SystemParam for &'_ World { /// move |mut val| val.0 = value.0 /// } /// -/// // .add_system(reset_to_system(my_config)) +/// // .add_systems(reset_to_system(my_config)) /// # assert_is_system(reset_to_system(Config(10))); /// ``` pub struct Local<'s, T: FromWorld + Send + 'static>(pub(crate) &'s mut T); @@ -752,7 +745,7 @@ unsafe impl<'a, T: FromWorld + Send + 'static> SystemParam for Local<'a, T> { state: &'s mut Self::State, _system_meta: &SystemMeta, _world: &'w World, - _change_tick: u32, + _change_tick: Tick, ) -> Self::Item<'w, 's> { Local(state.get()) } @@ -867,10 +860,8 @@ pub trait SystemBuffer: FromWorld + Send + 'static { /// }); /// /// let mut schedule = Schedule::new(); -/// schedule -/// // These two systems have no conflicts and will run in parallel. -/// .add_system(alert_criminal) -/// .add_system(alert_monster); +/// // These two systems have no conflicts and will run in parallel. +/// schedule.add_systems((alert_criminal, alert_monster)); /// /// // There are no criminals or monsters, so the alarm is not sounded. /// schedule.run(&mut world); @@ -927,7 +918,7 @@ unsafe impl SystemParam for Deferred<'_, T> { state: &'s mut Self::State, _system_meta: &SystemMeta, _world: &'w World, - _change_tick: u32, + _change_tick: Tick, ) -> Self::Item<'w, 's> { Deferred(state.get()) } @@ -948,8 +939,8 @@ unsafe impl SystemParam for Deferred<'_, T> { pub struct NonSend<'w, T: 'static> { pub(crate) value: &'w T, ticks: ComponentTicks, - last_change_tick: u32, - change_tick: u32, + last_run: Tick, + this_run: Tick, } // SAFETY: Only reads a single World non-send resource @@ -967,13 +958,12 @@ where impl<'w, T: 'static> NonSend<'w, T> { /// Returns `true` if the resource was added after the system last ran. pub fn is_added(&self) -> bool { - self.ticks.is_added(self.last_change_tick, self.change_tick) + self.ticks.is_added(self.last_run, self.this_run) } /// Returns `true` if the resource was added or mutably dereferenced after the system last ran. pub fn is_changed(&self) -> bool { - self.ticks - .is_changed(self.last_change_tick, self.change_tick) + self.ticks.is_changed(self.last_run, self.this_run) } } @@ -992,8 +982,8 @@ impl<'a, T> From> for NonSend<'a, T> { added: nsm.ticks.added.to_owned(), changed: nsm.ticks.changed.to_owned(), }, - change_tick: nsm.ticks.change_tick, - last_change_tick: nsm.ticks.last_change_tick, + this_run: nsm.ticks.this_run, + last_run: nsm.ticks.last_run, } } } @@ -1034,7 +1024,7 @@ unsafe impl<'a, T: 'static> SystemParam for NonSend<'a, T> { &mut component_id: &'s mut Self::State, system_meta: &SystemMeta, world: &'w World, - change_tick: u32, + change_tick: Tick, ) -> Self::Item<'w, 's> { let (ptr, ticks) = world .as_unsafe_world_cell_migration_internal() @@ -1050,8 +1040,8 @@ unsafe impl<'a, T: 'static> SystemParam for NonSend<'a, T> { NonSend { value: ptr.deref(), ticks: ticks.read(), - last_change_tick: system_meta.last_change_tick, - change_tick, + last_run: system_meta.last_run, + this_run: change_tick, } } } @@ -1073,7 +1063,7 @@ unsafe impl SystemParam for Option> { &mut component_id: &'s mut Self::State, system_meta: &SystemMeta, world: &'w World, - change_tick: u32, + change_tick: Tick, ) -> Self::Item<'w, 's> { world .as_unsafe_world_cell_migration_internal() @@ -1081,8 +1071,8 @@ unsafe impl SystemParam for Option> { .map(|(ptr, ticks)| NonSend { value: ptr.deref(), ticks: ticks.read(), - last_change_tick: system_meta.last_change_tick, - change_tick, + last_run: system_meta.last_run, + this_run: change_tick, }) } } @@ -1126,7 +1116,7 @@ unsafe impl<'a, T: 'static> SystemParam for NonSendMut<'a, T> { &mut component_id: &'s mut Self::State, system_meta: &SystemMeta, world: &'w World, - change_tick: u32, + change_tick: Tick, ) -> Self::Item<'w, 's> { let (ptr, ticks) = world .as_unsafe_world_cell_migration_internal() @@ -1140,7 +1130,7 @@ unsafe impl<'a, T: 'static> SystemParam for NonSendMut<'a, T> { }); NonSendMut { value: ptr.assert_unique().deref_mut(), - ticks: TicksMut::from_tick_cells(ticks, system_meta.last_change_tick, change_tick), + ticks: TicksMut::from_tick_cells(ticks, system_meta.last_run, change_tick), } } } @@ -1159,14 +1149,14 @@ unsafe impl<'a, T: 'static> SystemParam for Option> { &mut component_id: &'s mut Self::State, system_meta: &SystemMeta, world: &'w World, - change_tick: u32, + change_tick: Tick, ) -> Self::Item<'w, 's> { world .as_unsafe_world_cell_migration_internal() .get_non_send_with_ticks(component_id) .map(|(ptr, ticks)| NonSendMut { value: ptr.assert_unique().deref_mut(), - ticks: TicksMut::from_tick_cells(ticks, system_meta.last_change_tick, change_tick), + ticks: TicksMut::from_tick_cells(ticks, system_meta.last_run, change_tick), }) } } @@ -1186,7 +1176,7 @@ unsafe impl<'a> SystemParam for &'a Archetypes { _state: &'s mut Self::State, _system_meta: &SystemMeta, world: &'w World, - _change_tick: u32, + _change_tick: Tick, ) -> Self::Item<'w, 's> { world.archetypes() } @@ -1207,7 +1197,7 @@ unsafe impl<'a> SystemParam for &'a Components { _state: &'s mut Self::State, _system_meta: &SystemMeta, world: &'w World, - _change_tick: u32, + _change_tick: Tick, ) -> Self::Item<'w, 's> { world.components() } @@ -1228,7 +1218,7 @@ unsafe impl<'a> SystemParam for &'a Entities { _state: &'s mut Self::State, _system_meta: &SystemMeta, world: &'w World, - _change_tick: u32, + _change_tick: Tick, ) -> Self::Item<'w, 's> { world.entities() } @@ -1249,7 +1239,7 @@ unsafe impl<'a> SystemParam for &'a Bundles { _state: &'s mut Self::State, _system_meta: &SystemMeta, world: &'w World, - _change_tick: u32, + _change_tick: Tick, ) -> Self::Item<'w, 's> { world.bundles() } @@ -1258,29 +1248,29 @@ unsafe impl<'a> SystemParam for &'a Bundles { /// A [`SystemParam`] that reads the previous and current change ticks of the system. /// /// A system's change ticks are updated each time it runs: -/// - `last_change_tick` copies the previous value of `change_tick` -/// - `change_tick` copies the current value of [`World::read_change_tick`] +/// - `last_run` copies the previous value of `change_tick` +/// - `this_run` copies the current value of [`World::read_change_tick`] /// -/// Component change ticks that are more recent than `last_change_tick` will be detected by the system. +/// Component change ticks that are more recent than `last_run` will be detected by the system. /// Those can be read by calling [`last_changed`](crate::change_detection::DetectChanges::last_changed) /// on a [`Mut`](crate::change_detection::Mut) or [`ResMut`](crate::change_detection::ResMut). #[derive(Debug)] pub struct SystemChangeTick { - last_change_tick: u32, - change_tick: u32, + last_run: Tick, + this_run: Tick, } impl SystemChangeTick { /// Returns the current [`World`] change tick seen by the system. #[inline] - pub fn change_tick(&self) -> u32 { - self.change_tick + pub fn this_run(&self) -> Tick { + self.this_run } /// Returns the [`World`] change tick seen by the system the previous time it ran. #[inline] - pub fn last_change_tick(&self) -> u32 { - self.last_change_tick + pub fn last_run(&self) -> Tick { + self.last_run } } @@ -1298,11 +1288,11 @@ unsafe impl SystemParam for SystemChangeTick { _state: &'s mut Self::State, system_meta: &SystemMeta, _world: &'w World, - change_tick: u32, + change_tick: Tick, ) -> Self::Item<'w, 's> { SystemChangeTick { - last_change_tick: system_meta.last_change_tick, - change_tick, + last_run: system_meta.last_run, + this_run: change_tick, } } } @@ -1368,7 +1358,7 @@ unsafe impl SystemParam for SystemName<'_> { name: &'s mut Self::State, _system_meta: &SystemMeta, _world: &'w World, - _change_tick: u32, + _change_tick: Tick, ) -> Self::Item<'w, 's> { SystemName { name } } @@ -1410,7 +1400,7 @@ macro_rules! impl_system_param_tuple { state: &'s mut Self::State, _system_meta: &SystemMeta, _world: &'w World, - _change_tick: u32, + _change_tick: Tick, ) -> Self::Item<'w, 's> { let ($($param,)*) = state; @@ -1534,7 +1524,7 @@ unsafe impl SystemParam for StaticSystemParam<'_, '_, state: &'state mut Self::State, system_meta: &SystemMeta, world: &'world World, - change_tick: u32, + change_tick: Tick, ) -> Self::Item<'world, 'state> { // SAFETY: Defer to the safety of P::SystemParam StaticSystemParam(P::get_param(state, system_meta, world, change_tick)) @@ -1635,7 +1625,7 @@ mod tests { #[derive(SystemParam)] pub struct EncapsulatedParam<'w>(Res<'w, PrivateResource>); - // regression test for https://github.com/bevyengine/bevy/issues/7103. + // Regression test for https://github.com/bevyengine/bevy/issues/7103. #[derive(SystemParam)] pub struct WhereParam<'w, 's, Q> where @@ -1643,4 +1633,13 @@ mod tests { { _q: Query<'w, 's, Q, ()>, } + + // Regression test for https://github.com/bevyengine/bevy/issues/1727. + #[derive(SystemParam)] + pub struct Collide<'w> { + _x: Res<'w, FetchState>, + } + + #[derive(Resource)] + pub struct FetchState; } diff --git a/crates/bevy_ecs/src/system/system_piping.rs b/crates/bevy_ecs/src/system/system_piping.rs index b1551e64f3..dc1f3dc55a 100644 --- a/crates/bevy_ecs/src/system/system_piping.rs +++ b/crates/bevy_ecs/src/system/system_piping.rs @@ -138,7 +138,7 @@ pub mod adapter { /// /// // Building a new schedule/app... /// let mut sched = Schedule::default(); - /// sched.add_system( + /// sched.add_systems( /// // Panic if the load system returns an error. /// load_save_system.pipe(system_adapter::unwrap) /// ) @@ -169,7 +169,7 @@ pub mod adapter { /// /// // Building a new schedule/app... /// let mut sched = Schedule::default(); - /// sched.add_system( + /// sched.add_systems( /// // Prints system information. /// data_pipe_system.pipe(system_adapter::info) /// ) @@ -196,7 +196,7 @@ pub mod adapter { /// /// // Building a new schedule/app... /// let mut sched = Schedule::default(); - /// sched.add_system( + /// sched.add_systems( /// // Prints debug data from system. /// parse_message_system.pipe(system_adapter::dbg) /// ) @@ -223,7 +223,7 @@ pub mod adapter { /// /// // Building a new schedule/app... /// # let mut sched = Schedule::default(); - /// sched.add_system( + /// sched.add_systems( /// // Prints system warning if system returns an error. /// warning_pipe_system.pipe(system_adapter::warn) /// ) @@ -251,7 +251,7 @@ pub mod adapter { /// use bevy_ecs::prelude::*; /// // Building a new schedule/app... /// let mut sched = Schedule::default(); - /// sched.add_system( + /// sched.add_systems( /// // Prints system error if system fails. /// parse_error_message_system.pipe(system_adapter::error) /// ) @@ -287,7 +287,7 @@ pub mod adapter { /// /// // Building a new schedule/app... /// # let mut sched = Schedule::default(); sched - /// .add_system( + /// .add_systems( /// // If the system fails, just move on and try again next frame. /// fallible_system.pipe(system_adapter::ignore) /// ) diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index 1511b7fe88..39e5b8e40f 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -1,6 +1,6 @@ use crate::{ archetype::{Archetype, ArchetypeId, Archetypes}, - bundle::{Bundle, BundleInfo}, + bundle::{Bundle, BundleInfo, BundleInserter, DynamicBundle}, change_detection::MutUntyped, component::{Component, ComponentId, ComponentTicks, Components, StorageType}, entity::{Entities, Entity, EntityLocation}, @@ -279,6 +279,90 @@ impl<'w> EntityMut<'w> { self } + /// Inserts a dynamic [`Component`] into the entity. + /// + /// This will overwrite any previous value(s) of the same component type. + /// + /// You should prefer to use the typed API [`EntityMut::insert`] where possible. + /// + /// # Safety + /// + /// - [`ComponentId`] must be from the same world as [`EntityMut`] + /// - [`OwningPtr`] must be a valid reference to the type represented by [`ComponentId`] + pub unsafe fn insert_by_id( + &mut self, + component_id: ComponentId, + component: OwningPtr<'_>, + ) -> &mut Self { + let change_tick = self.world.change_tick(); + + let bundles = &mut self.world.bundles; + let components = &mut self.world.components; + + let (bundle_info, storage_type) = bundles.init_component_info(components, component_id); + let bundle_inserter = bundle_info.get_bundle_inserter( + &mut self.world.entities, + &mut self.world.archetypes, + &mut self.world.components, + &mut self.world.storages, + self.location.archetype_id, + change_tick, + ); + + self.location = insert_dynamic_bundle( + bundle_inserter, + self.entity, + self.location, + Some(component).into_iter(), + Some(storage_type).into_iter(), + ); + + self + } + + /// Inserts a dynamic [`Bundle`] into the entity. + /// + /// This will overwrite any previous value(s) of the same component type. + /// + /// You should prefer to use the typed API [`EntityMut::insert`] where possible. + /// If your [`Bundle`] only has one component, use the cached API [`EntityMut::insert_by_id`]. + /// + /// If possible, pass a sorted slice of `ComponentId` to maximize caching potential. + /// + /// # Safety + /// - Each [`ComponentId`] must be from the same world as [`EntityMut`] + /// - Each [`OwningPtr`] must be a valid reference to the type represented by [`ComponentId`] + pub unsafe fn insert_by_ids<'a, I: Iterator>>( + &mut self, + component_ids: &[ComponentId], + iter_components: I, + ) -> &mut Self { + let change_tick = self.world.change_tick(); + + let bundles = &mut self.world.bundles; + let components = &mut self.world.components; + + let (bundle_info, storage_types) = bundles.init_dynamic_info(components, component_ids); + let bundle_inserter = bundle_info.get_bundle_inserter( + &mut self.world.entities, + &mut self.world.archetypes, + &mut self.world.components, + &mut self.world.storages, + self.location.archetype_id, + change_tick, + ); + + self.location = insert_dynamic_bundle( + bundle_inserter, + self.entity, + self.location, + iter_components, + storage_types.iter().cloned(), + ); + + self + } + /// Removes all components in the [`Bundle`] from the entity and returns their previous values. /// /// **Note:** If the entity does not have every component in the bundle, this method will not @@ -311,7 +395,7 @@ impl<'w> EntityMut<'w> { return None; } - let mut bundle_components = bundle_info.component_ids.iter().cloned(); + let mut bundle_components = bundle_info.components().iter().cloned(); let entity = self.entity; // SAFETY: bundle components are iterated in order, which guarantees that the component type // matches @@ -463,7 +547,7 @@ impl<'w> EntityMut<'w> { let old_archetype = &mut archetypes[old_location.archetype_id]; let entity = self.entity; - for component_id in bundle_info.component_ids.iter().cloned() { + for component_id in bundle_info.components().iter().cloned() { if old_archetype.contains(component_id) { removed_components.send(component_id, entity); @@ -603,7 +687,7 @@ impl<'w> EntityMut<'w> { /// // Mutate the world while we have access to it. /// let mut r = world.resource_mut::(); /// r.0 += 1; - /// + /// /// // Return a value from the world before giving it back to the `EntityMut`. /// *r /// }); @@ -672,6 +756,44 @@ impl<'w> EntityMut<'w> { } } +/// Inserts a dynamic [`Bundle`] into the entity. +/// +/// # Safety +/// +/// - [`OwningPtr`] and [`StorageType`] iterators must correspond to the +/// [`BundleInfo`] used to construct [`BundleInserter`] +/// - [`Entity`] must correspond to [`EntityLocation`] +unsafe fn insert_dynamic_bundle< + 'a, + I: Iterator>, + S: Iterator, +>( + mut bundle_inserter: BundleInserter<'_, '_>, + entity: Entity, + location: EntityLocation, + components: I, + storage_types: S, +) -> EntityLocation { + struct DynamicInsertBundle<'a, I: Iterator)>> { + components: I, + } + + impl<'a, I: Iterator)>> DynamicBundle + for DynamicInsertBundle<'a, I> + { + fn get_components(self, func: &mut impl FnMut(StorageType, OwningPtr<'_>)) { + self.components.for_each(|(t, ptr)| func(t, ptr)); + } + } + + let bundle = DynamicInsertBundle { + components: storage_types.zip(components), + }; + + // SAFETY: location matches current entity. + unsafe { bundle_inserter.insert(entity, location, bundle) } +} + /// Removes a bundle from the given archetype and returns the resulting archetype (or None if the /// removal was invalid). in the event that adding the given bundle does not result in an Archetype /// change. Results are cached in the Archetype Graph to avoid redundant work. @@ -694,9 +816,11 @@ unsafe fn remove_bundle_from_archetype( let remove_bundle_result = { let current_archetype = &mut archetypes[archetype_id]; if intersection { - current_archetype.edges().get_remove_bundle(bundle_info.id) + current_archetype + .edges() + .get_remove_bundle(bundle_info.id()) } else { - current_archetype.edges().get_take_bundle(bundle_info.id) + current_archetype.edges().get_take_bundle(bundle_info.id()) } }; let result = if let Some(result) = remove_bundle_result { @@ -710,7 +834,7 @@ unsafe fn remove_bundle_from_archetype( let current_archetype = &mut archetypes[archetype_id]; let mut removed_table_components = Vec::new(); let mut removed_sparse_set_components = Vec::new(); - for component_id in bundle_info.component_ids.iter().cloned() { + for component_id in bundle_info.components().iter().cloned() { if current_archetype.contains(component_id) { // SAFETY: bundle components were already initialized by bundles.get_info let component_info = components.get_info_unchecked(component_id); @@ -724,7 +848,7 @@ unsafe fn remove_bundle_from_archetype( // graph current_archetype .edges_mut() - .insert_take_bundle(bundle_info.id, None); + .insert_take_bundle(bundle_info.id(), None); return None; } } @@ -763,11 +887,11 @@ unsafe fn remove_bundle_from_archetype( if intersection { current_archetype .edges_mut() - .insert_remove_bundle(bundle_info.id, result); + .insert_remove_bundle(bundle_info.id(), result); } else { current_archetype .edges_mut() - .insert_take_bundle(bundle_info.id, result); + .insert_take_bundle(bundle_info.id(), result); } result } @@ -833,6 +957,7 @@ pub(crate) unsafe fn take_component<'a>( #[cfg(test)] mod tests { + use bevy_ptr::OwningPtr; use std::panic::AssertUnwindSafe; use crate as bevy_ecs; @@ -860,9 +985,13 @@ mod tests { assert_eq!(a, vec![1]); } - #[derive(Component)] + #[derive(Component, Clone, Copy, Debug, PartialEq)] struct TestComponent(u32); + #[derive(Component, Clone, Copy, Debug, PartialEq)] + #[component(storage = "SparseSet")] + struct TestComponent2(u32); + #[test] fn entity_ref_get_by_id() { let mut world = World::new(); @@ -1105,4 +1234,72 @@ mod tests { assert_eq!(world.entity(e2).get::().unwrap(), &Dense(1)); } + + #[test] + fn entity_mut_insert_by_id() { + let mut world = World::new(); + let test_component_id = world.init_component::(); + + let mut entity = world.spawn_empty(); + OwningPtr::make(TestComponent(42), |ptr| { + // SAFETY: `ptr` matches the component id + unsafe { entity.insert_by_id(test_component_id, ptr) }; + }); + + let components: Vec<_> = world.query::<&TestComponent>().iter(&world).collect(); + + assert_eq!(components, vec![&TestComponent(42)]); + + // Compare with `insert_bundle_by_id` + + let mut entity = world.spawn_empty(); + OwningPtr::make(TestComponent(84), |ptr| { + // SAFETY: `ptr` matches the component id + unsafe { entity.insert_by_ids(&[test_component_id], vec![ptr].into_iter()) }; + }); + + let components: Vec<_> = world.query::<&TestComponent>().iter(&world).collect(); + + assert_eq!(components, vec![&TestComponent(42), &TestComponent(84)]); + } + + #[test] + fn entity_mut_insert_bundle_by_id() { + let mut world = World::new(); + let test_component_id = world.init_component::(); + let test_component_2_id = world.init_component::(); + + let component_ids = [test_component_id, test_component_2_id]; + let test_component_value = TestComponent(42); + let test_component_2_value = TestComponent2(84); + + let mut entity = world.spawn_empty(); + OwningPtr::make(test_component_value, |ptr1| { + OwningPtr::make(test_component_2_value, |ptr2| { + // SAFETY: `ptr1` and `ptr2` match the component ids + unsafe { entity.insert_by_ids(&component_ids, vec![ptr1, ptr2].into_iter()) }; + }); + }); + + let dynamic_components: Vec<_> = world + .query::<(&TestComponent, &TestComponent2)>() + .iter(&world) + .collect(); + + assert_eq!( + dynamic_components, + vec![(&TestComponent(42), &TestComponent2(84))] + ); + + // Compare with `World` generated using static type equivalents + let mut static_world = World::new(); + + static_world.spawn((test_component_value, test_component_2_value)); + let static_components: Vec<_> = static_world + .query::<(&TestComponent, &TestComponent2)>() + .iter(&static_world) + .collect(); + + assert_eq!(dynamic_components, static_components); + } } diff --git a/crates/bevy_ecs/src/world/error.rs b/crates/bevy_ecs/src/world/error.rs new file mode 100644 index 0000000000..a6197ac44c --- /dev/null +++ b/crates/bevy_ecs/src/world/error.rs @@ -0,0 +1,10 @@ +use thiserror::Error; + +use crate::schedule::BoxedScheduleLabel; + +/// The error type returned by [`World::try_run_schedule`] if the provided schedule does not exist. +/// +/// [`World::try_run_schedule`]: crate::world::World::try_run_schedule +#[derive(Error, Debug)] +#[error("The schedule with the label {0:?} was not found.")] +pub struct TryRunScheduleError(pub BoxedScheduleLabel); diff --git a/crates/bevy_ecs/src/world/identifier.rs b/crates/bevy_ecs/src/world/identifier.rs index e6a78be5ca..55e2b6a5fa 100644 --- a/crates/bevy_ecs/src/world/identifier.rs +++ b/crates/bevy_ecs/src/world/identifier.rs @@ -1,4 +1,5 @@ use crate::{ + component::Tick, storage::SparseSetIndex, system::{ReadOnlySystemParam, SystemParam}, world::{FromWorld, World}, @@ -56,7 +57,7 @@ unsafe impl SystemParam for WorldId { _: &'state mut Self::State, _: &crate::system::SystemMeta, world: &'world super::World, - _: u32, + _: Tick, ) -> Self::Item<'world, 'state> { world.id } diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 0f78884e16..288650a1b9 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -1,4 +1,5 @@ mod entity_ref; +pub mod error; mod spawn_batch; pub mod unsafe_world_cell; mod world_cell; @@ -12,7 +13,7 @@ use crate::{ archetype::{ArchetypeComponentId, ArchetypeId, ArchetypeRow, Archetypes}, bundle::{Bundle, BundleInserter, BundleSpawner, Bundles}, change_detection::{MutUntyped, TicksMut}, - component::{Component, ComponentDescriptor, ComponentId, ComponentInfo, Components}, + component::{Component, ComponentDescriptor, ComponentId, ComponentInfo, Components, Tick}, entity::{AllocAtWithoutReplacement, Entities, Entity, EntityLocation}, event::{Event, Events}, query::{DebugCheckedUnwrap, QueryState, ReadOnlyWorldQuery, WorldQuery}, @@ -20,6 +21,7 @@ use crate::{ schedule::{Schedule, ScheduleLabel, Schedules}, storage::{ResourceData, Storages}, system::Resource, + world::error::TryRunScheduleError, }; use bevy_ptr::{OwningPtr, Ptr}; use bevy_utils::tracing::warn; @@ -64,8 +66,8 @@ pub struct World { /// Access cache used by [WorldCell]. Is only accessed in the `Drop` impl of `WorldCell`. pub(crate) archetype_component_access: ArchetypeComponentAccess, pub(crate) change_tick: AtomicU32, - pub(crate) last_change_tick: u32, - pub(crate) last_check_tick: u32, + pub(crate) last_change_tick: Tick, + pub(crate) last_check_tick: Tick, } impl Default for World { @@ -82,8 +84,8 @@ impl Default for World { // 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: 0, - last_check_tick: 0, + last_change_tick: Tick::new(0), + last_check_tick: Tick::new(0), } } } @@ -493,6 +495,7 @@ impl World { /// ``` pub fn spawn(&mut self, bundle: B) -> EntityMut { self.flush(); + let change_tick = self.change_tick(); let entity = self.entities.alloc(); let entity_location = { let bundle_info = self @@ -503,7 +506,7 @@ impl World { &mut self.archetypes, &mut self.components, &mut self.storages, - *self.change_tick.get_mut(), + change_tick, ); // SAFETY: bundle's type matches `bundle_info`, entity is allocated but non-existent @@ -1174,8 +1177,7 @@ impl World { { self.flush(); - let iter = iter.into_iter(); - let change_tick = *self.change_tick.get_mut(); + let change_tick = self.change_tick(); let bundle_info = self .bundles @@ -1306,8 +1308,8 @@ impl World { ticks: TicksMut { added: &mut ticks.added, changed: &mut ticks.changed, - last_change_tick, - change_tick, + last_run: last_change_tick, + this_run: change_tick, }, }; let result = f(self, value_mut); @@ -1467,9 +1469,11 @@ impl World { } } + /// Increments the world's current change tick, and returns the old value. #[inline] - pub fn increment_change_tick(&self) -> u32 { - self.change_tick.fetch_add(1, Ordering::AcqRel) + pub fn increment_change_tick(&self) -> Tick { + let prev_tick = self.change_tick.fetch_add(1, Ordering::AcqRel); + Tick::new(prev_tick) } /// Reads the current change tick of this world. @@ -1477,8 +1481,9 @@ impl World { /// If you have exclusive (`&mut`) access to the world, consider using [`change_tick()`](Self::change_tick), /// which is more efficient since it does not require atomic synchronization. #[inline] - pub fn read_change_tick(&self) -> u32 { - self.change_tick.load(Ordering::Acquire) + pub fn read_change_tick(&self) -> Tick { + let tick = self.change_tick.load(Ordering::Acquire); + Tick::new(tick) } /// Reads the current change tick of this world. @@ -1486,12 +1491,13 @@ impl World { /// This does the same thing as [`read_change_tick()`](Self::read_change_tick), only this method /// is more efficient since it does not require atomic synchronization. #[inline] - pub fn change_tick(&mut self) -> u32 { - *self.change_tick.get_mut() + pub fn change_tick(&mut self) -> Tick { + let tick = *self.change_tick.get_mut(); + Tick::new(tick) } #[inline] - pub fn last_change_tick(&self) -> u32 { + pub fn last_change_tick(&self) -> Tick { self.last_change_tick } @@ -1503,7 +1509,7 @@ impl World { // TODO: benchmark and optimize pub fn check_change_ticks(&mut self) { let change_tick = self.change_tick(); - if change_tick.wrapping_sub(self.last_check_tick) < CHECK_TICK_THRESHOLD { + if change_tick.relative_to(self.last_check_tick).get() < CHECK_TICK_THRESHOLD { return; } @@ -1710,6 +1716,47 @@ impl World { schedules.insert(label, schedule); } + /// Attempts to run the [`Schedule`] associated with the `label` a single time, + /// and returns a [`TryRunScheduleError`] if the schedule does not exist. + /// + /// The [`Schedule`] is fetched from the [`Schedules`] resource of the world by its label, + /// and system state is cached. + /// + /// For simple testing use cases, call [`Schedule::run(&mut world)`](Schedule::run) instead. + pub fn try_run_schedule( + &mut self, + label: impl ScheduleLabel, + ) -> Result<(), TryRunScheduleError> { + self.try_run_schedule_ref(&label) + } + + /// Attempts to run the [`Schedule`] associated with the `label` a single time, + /// and returns a [`TryRunScheduleError`] if the schedule does not exist. + /// + /// Unlike the `try_run_schedule` method, this method takes the label by reference, which can save a clone. + /// + /// The [`Schedule`] is fetched from the [`Schedules`] resource of the world by its label, + /// and system state is cached. + /// + /// For simple testing use cases, call [`Schedule::run(&mut world)`](Schedule::run) instead. + pub fn try_run_schedule_ref( + &mut self, + label: &dyn ScheduleLabel, + ) -> Result<(), TryRunScheduleError> { + let Some((extracted_label, mut schedule)) = self.resource_mut::().remove_entry(label) else { + return Err(TryRunScheduleError(label.dyn_clone())); + }; + + // TODO: move this span to Schedule::run + #[cfg(feature = "trace")] + let _span = bevy_utils::tracing::info_span!("schedule", name = ?extracted_label).entered(); + schedule.run(self); + self.resource_mut::() + .insert(extracted_label, schedule); + + Ok(()) + } + /// Runs the [`Schedule`] associated with the `label` a single time. /// /// The [`Schedule`] is fetched from the [`Schedules`] resource of the world by its label, @@ -1737,17 +1784,8 @@ impl World { /// /// Panics if the requested schedule does not exist, or the [`Schedules`] resource was not added. pub fn run_schedule_ref(&mut self, label: &dyn ScheduleLabel) { - let (extracted_label, mut schedule) = self - .resource_mut::() - .remove_entry(label) - .unwrap_or_else(|| panic!("The schedule with the label {label:?} was not found.")); - - // TODO: move this span to Schedule::run - #[cfg(feature = "trace")] - let _span = bevy_utils::tracing::info_span!("schedule", name = ?extracted_label).entered(); - schedule.run(self); - self.resource_mut::() - .insert(extracted_label, schedule); + self.try_run_schedule_ref(label) + .unwrap_or_else(|e| panic!("{}", e)); } } diff --git a/crates/bevy_ecs/src/world/spawn_batch.rs b/crates/bevy_ecs/src/world/spawn_batch.rs index f5e1bd2792..965ff97540 100644 --- a/crates/bevy_ecs/src/world/spawn_batch.rs +++ b/crates/bevy_ecs/src/world/spawn_batch.rs @@ -25,6 +25,8 @@ where // necessary world.flush(); + let change_tick = world.change_tick(); + let (lower, upper) = iter.size_hint(); let length = upper.unwrap_or(lower); @@ -37,7 +39,7 @@ where &mut world.archetypes, &mut world.components, &mut world.storages, - *world.change_tick.get_mut(), + change_tick, ); spawner.reserve_storage(length); diff --git a/crates/bevy_ecs/src/world/unsafe_world_cell.rs b/crates/bevy_ecs/src/world/unsafe_world_cell.rs index 364cef1bc3..99231fe3f3 100644 --- a/crates/bevy_ecs/src/world/unsafe_world_cell.rs +++ b/crates/bevy_ecs/src/world/unsafe_world_cell.rs @@ -6,7 +6,7 @@ use crate::{ bundle::Bundles, change_detection::{MutUntyped, TicksMut}, component::{ - ComponentId, ComponentStorage, ComponentTicks, Components, StorageType, TickCells, + ComponentId, ComponentStorage, ComponentTicks, Components, StorageType, Tick, TickCells, }, entity::{Entities, Entity, EntityLocation}, prelude::Component, @@ -185,16 +185,17 @@ impl<'w> UnsafeWorldCell<'w> { /// Reads the current change tick of this world. #[inline] - pub fn read_change_tick(self) -> u32 { + pub fn read_change_tick(self) -> Tick { // SAFETY: // - we only access world metadata - unsafe { self.world_metadata() } + let tick = unsafe { self.world_metadata() } .change_tick - .load(Ordering::Acquire) + .load(Ordering::Acquire); + Tick::new(tick) } #[inline] - pub fn last_change_tick(self) -> u32 { + pub fn last_change_tick(self) -> Tick { // SAFETY: // - we only access world metadata unsafe { self.world_metadata() }.last_change_tick @@ -655,8 +656,8 @@ impl<'w> UnsafeEntityCell<'w> { #[inline] pub(crate) unsafe fn get_mut_using_ticks( &self, - last_change_tick: u32, - change_tick: u32, + last_change_tick: Tick, + change_tick: Tick, ) -> Option> { let component_id = self.world.components().get_id(TypeId::of::())?; diff --git a/crates/bevy_ecs_compile_fail_tests/tests/ui/world_query_derive.stderr b/crates/bevy_ecs_compile_fail_tests/tests/ui/world_query_derive.stderr index b762ac275f..158be3f9d7 100644 --- a/crates/bevy_ecs_compile_fail_tests/tests/ui/world_query_derive.stderr +++ b/crates/bevy_ecs_compile_fail_tests/tests/ui/world_query_derive.stderr @@ -18,7 +18,7 @@ note: required by a bound in `_::assert_readonly` --> tests/ui/world_query_derive.rs:7:10 | 7 | #[derive(WorldQuery)] - | ^^^^^^^^^^ required by this bound in `_::assert_readonly` + | ^^^^^^^^^^ required by this bound in `assert_readonly` = note: this error originates in the derive macro `WorldQuery` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0277]: the trait bound `MutableMarked: ReadOnlyWorldQuery` is not satisfied @@ -41,5 +41,5 @@ note: required by a bound in `_::assert_readonly` --> tests/ui/world_query_derive.rs:18:10 | 18 | #[derive(WorldQuery)] - | ^^^^^^^^^^ required by this bound in `_::assert_readonly` + | ^^^^^^^^^^ required by this bound in `assert_readonly` = note: this error originates in the derive macro `WorldQuery` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/crates/bevy_gilrs/src/lib.rs b/crates/bevy_gilrs/src/lib.rs index f011710100..abbf34307c 100644 --- a/crates/bevy_gilrs/src/lib.rs +++ b/crates/bevy_gilrs/src/lib.rs @@ -1,7 +1,7 @@ mod converter; mod gilrs_system; -use bevy_app::{App, CoreSet, Plugin, StartupSet}; +use bevy_app::{App, Plugin, PreStartup, PreUpdate}; use bevy_ecs::prelude::*; use bevy_input::InputSystem; use bevy_utils::tracing::error; @@ -20,14 +20,8 @@ impl Plugin for GilrsPlugin { { Ok(gilrs) => { app.insert_non_send_resource(gilrs) - .add_startup_system( - gilrs_event_startup_system.in_base_set(StartupSet::PreStartup), - ) - .add_system( - gilrs_event_system - .before(InputSystem) - .in_base_set(CoreSet::PreUpdate), - ); + .add_systems(PreStartup, gilrs_event_startup_system) + .add_systems(PreUpdate, gilrs_event_system.before(InputSystem)); } Err(err) => error!("Failed to start Gilrs. {}", err), } diff --git a/crates/bevy_gizmos/Cargo.toml b/crates/bevy_gizmos/Cargo.toml new file mode 100644 index 0000000000..cee285784c --- /dev/null +++ b/crates/bevy_gizmos/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "bevy_gizmos" +version = "0.11.0-dev" +edition = "2021" +description = "Provides gizmos for Bevy Engine" +homepage = "https://bevyengine.org" +repository = "https://github.com/bevyengine/bevy" +license = "MIT OR Apache-2.0" +keywords = ["bevy"] + +[dependencies] +# Bevy +bevy_pbr = { path = "../bevy_pbr", version = "0.11.0-dev", optional = true } +bevy_sprite = { path = "../bevy_sprite", version = "0.11.0-dev", optional = true } +bevy_app = { path = "../bevy_app", version = "0.11.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.11.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.11.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.11.0-dev" } +bevy_render = { path = "../bevy_render", version = "0.11.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.11.0-dev" } +bevy_core = { path = "../bevy_core", version = "0.11.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.11.0-dev" } +bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.11.0-dev" } diff --git a/crates/bevy_gizmos/src/gizmos.rs b/crates/bevy_gizmos/src/gizmos.rs new file mode 100644 index 0000000000..fa37416db8 --- /dev/null +++ b/crates/bevy_gizmos/src/gizmos.rs @@ -0,0 +1,349 @@ +use std::{f32::consts::TAU, iter}; + +use bevy_ecs::{ + system::{Deferred, Resource, SystemBuffer, SystemMeta}, + world::World, +}; +use bevy_math::{Mat2, Quat, Vec2, Vec3}; +use bevy_render::prelude::Color; + +type PositionItem = [f32; 3]; +type ColorItem = [f32; 4]; + +const DEFAULT_CIRCLE_SEGMENTS: usize = 32; + +#[derive(Resource, Default)] +pub(crate) struct GizmoStorage { + pub list_positions: Vec, + pub list_colors: Vec, + pub strip_positions: Vec, + pub strip_colors: Vec, +} + +pub type Gizmos<'s> = Deferred<'s, GizmoBuffer>; + +#[derive(Default)] +pub struct GizmoBuffer { + list_positions: Vec, + list_colors: Vec, + strip_positions: Vec, + strip_colors: Vec, +} + +impl SystemBuffer for GizmoBuffer { + fn apply(&mut self, _system_meta: &SystemMeta, world: &mut World) { + let mut storage = world.resource_mut::(); + storage.list_positions.append(&mut self.list_positions); + storage.list_colors.append(&mut self.list_colors); + storage.strip_positions.append(&mut self.strip_positions); + storage.strip_colors.append(&mut self.strip_colors); + } +} + +impl GizmoBuffer { + #[inline] + pub fn line(&mut self, start: Vec3, end: Vec3, color: Color) { + self.extend_list_positions([start, end]); + self.add_list_color(color, 2); + } + + /// Draw a line from `start` to `end`. + #[inline] + pub fn line_gradient(&mut self, start: Vec3, end: Vec3, start_color: Color, end_color: Color) { + self.extend_list_positions([start, end]); + self.extend_list_colors([start_color, end_color]); + } + + /// Draw a line from `start` to `start + vector`. + #[inline] + pub fn ray(&mut self, start: Vec3, vector: Vec3, color: Color) { + self.line(start, start + vector, color); + } + + /// Draw a line from `start` to `start + vector`. + #[inline] + pub fn ray_gradient( + &mut self, + start: Vec3, + vector: Vec3, + start_color: Color, + end_color: Color, + ) { + self.line_gradient(start, start + vector, start_color, end_color); + } + + #[inline] + pub fn linestrip(&mut self, positions: impl IntoIterator, color: Color) { + self.extend_strip_positions(positions.into_iter()); + self.strip_colors + .resize(self.strip_positions.len() - 1, color.as_linear_rgba_f32()); + self.strip_colors.push([f32::NAN; 4]); + } + + #[inline] + pub fn linestrip_gradient(&mut self, points: impl IntoIterator) { + let points = points.into_iter(); + + let (min, _) = points.size_hint(); + self.strip_positions.reserve(min); + self.strip_colors.reserve(min); + + for (position, color) in points { + self.strip_positions.push(position.to_array()); + self.strip_colors.push(color.as_linear_rgba_f32()); + } + + self.strip_positions.push([f32::NAN; 3]); + self.strip_colors.push([f32::NAN; 4]); + } + + /// Draw a circle at `position` with the flat side facing `normal`. + #[inline] + pub fn circle( + &mut self, + position: Vec3, + normal: Vec3, + radius: f32, + color: Color, + ) -> CircleBuilder { + CircleBuilder { + buffer: self, + position, + normal, + radius, + color, + segments: DEFAULT_CIRCLE_SEGMENTS, + } + } + + /// Draw a sphere. + #[inline] + pub fn sphere( + &mut self, + position: Vec3, + rotation: Quat, + radius: f32, + color: Color, + ) -> SphereBuilder { + SphereBuilder { + buffer: self, + position, + rotation, + radius, + color, + circle_segments: DEFAULT_CIRCLE_SEGMENTS, + } + } + + /// Draw a rectangle. + #[inline] + pub fn rect(&mut self, position: Vec3, rotation: Quat, size: Vec2, color: Color) { + let [tl, tr, br, bl] = rect_inner(size).map(|vec2| position + rotation * vec2.extend(0.)); + self.linestrip([tl, tr, br, bl, tl], color); + } + + /// Draw a box. + #[inline] + pub fn cuboid(&mut self, position: Vec3, rotation: Quat, size: Vec3, color: Color) { + let rect = rect_inner(size.truncate()); + // Front + let [tlf, trf, brf, blf] = rect.map(|vec2| position + rotation * vec2.extend(size.z / 2.)); + // Back + let [tlb, trb, brb, blb] = rect.map(|vec2| position + rotation * vec2.extend(-size.z / 2.)); + + let positions = [ + tlf, trf, trf, brf, brf, blf, blf, tlf, // Front + tlb, trb, trb, brb, brb, blb, blb, tlb, // Back + tlf, tlb, trf, trb, brf, brb, blf, blb, // Front to back + ]; + self.extend_list_positions(positions); + self.add_list_color(color, 24); + } + + /// Draw a line from `start` to `end`. + #[inline] + pub fn line_2d(&mut self, start: Vec2, end: Vec2, color: Color) { + self.line(start.extend(0.), end.extend(0.), color); + } + + /// Draw a line from `start` to `end`. + #[inline] + pub fn line_gradient_2d( + &mut self, + start: Vec2, + end: Vec2, + start_color: Color, + end_color: Color, + ) { + self.line_gradient(start.extend(0.), end.extend(0.), start_color, end_color); + } + + #[inline] + pub fn linestrip_2d(&mut self, positions: impl IntoIterator, color: Color) { + self.linestrip(positions.into_iter().map(|vec2| vec2.extend(0.)), color); + } + + #[inline] + pub fn linestrip_gradient_2d(&mut self, positions: impl IntoIterator) { + self.linestrip_gradient( + positions + .into_iter() + .map(|(vec2, color)| (vec2.extend(0.), color)), + ); + } + + /// Draw a line from `start` to `start + vector`. + #[inline] + pub fn ray_2d(&mut self, start: Vec2, vector: Vec2, color: Color) { + self.line_2d(start, start + vector, color); + } + + /// Draw a line from `start` to `start + vector`. + #[inline] + pub fn ray_gradient_2d( + &mut self, + start: Vec2, + vector: Vec2, + start_color: Color, + end_color: Color, + ) { + self.line_gradient_2d(start, start + vector, start_color, end_color); + } + + // Draw a circle. + #[inline] + pub fn circle_2d(&mut self, position: Vec2, radius: f32, color: Color) -> Circle2dBuilder { + Circle2dBuilder { + buffer: self, + position, + radius, + color, + segments: DEFAULT_CIRCLE_SEGMENTS, + } + } + + /// Draw a rectangle. + #[inline] + pub fn rect_2d(&mut self, position: Vec2, rotation: f32, size: Vec2, color: Color) { + let rotation = Mat2::from_angle(rotation); + let [tl, tr, br, bl] = rect_inner(size).map(|vec2| position + rotation * vec2); + self.linestrip_2d([tl, tr, br, bl, tl], color); + } + + #[inline] + fn extend_list_positions(&mut self, positions: impl IntoIterator) { + self.list_positions + .extend(positions.into_iter().map(|vec3| vec3.to_array())); + } + + #[inline] + fn extend_list_colors(&mut self, colors: impl IntoIterator) { + self.list_colors + .extend(colors.into_iter().map(|color| color.as_linear_rgba_f32())); + } + + #[inline] + fn add_list_color(&mut self, color: Color, count: usize) { + self.list_colors + .extend(iter::repeat(color.as_linear_rgba_f32()).take(count)); + } + + #[inline] + fn extend_strip_positions(&mut self, positions: impl IntoIterator) { + self.strip_positions.extend( + positions + .into_iter() + .map(|vec3| vec3.to_array()) + .chain(iter::once([f32::NAN; 3])), + ); + } +} + +pub struct CircleBuilder<'a> { + buffer: &'a mut GizmoBuffer, + position: Vec3, + normal: Vec3, + radius: f32, + color: Color, + segments: usize, +} + +impl<'a> CircleBuilder<'a> { + pub fn segments(mut self, segments: usize) -> Self { + self.segments = segments; + self + } +} + +impl<'a> Drop for CircleBuilder<'a> { + fn drop(&mut self) { + let rotation = Quat::from_rotation_arc(Vec3::Z, self.normal); + let positions = circle_inner(self.radius, self.segments) + .map(|vec2| (self.position + rotation * vec2.extend(0.))); + self.buffer.linestrip(positions, self.color); + } +} + +pub struct SphereBuilder<'a> { + buffer: &'a mut GizmoBuffer, + position: Vec3, + rotation: Quat, + radius: f32, + color: Color, + circle_segments: usize, +} + +impl SphereBuilder<'_> { + pub fn circle_segments(mut self, segments: usize) -> Self { + self.circle_segments = segments; + self + } +} + +impl Drop for SphereBuilder<'_> { + fn drop(&mut self) { + for axis in Vec3::AXES { + self.buffer + .circle(self.position, self.rotation * axis, self.radius, self.color) + .segments(self.circle_segments); + } + } +} + +pub struct Circle2dBuilder<'a> { + buffer: &'a mut GizmoBuffer, + position: Vec2, + radius: f32, + color: Color, + segments: usize, +} + +impl Circle2dBuilder<'_> { + pub fn segments(mut self, segments: usize) -> Self { + self.segments = segments; + self + } +} + +impl Drop for Circle2dBuilder<'_> { + fn drop(&mut self) { + let positions = circle_inner(self.radius, self.segments).map(|vec2| (vec2 + self.position)); + self.buffer.linestrip_2d(positions, self.color); + } +} + +fn circle_inner(radius: f32, segments: usize) -> impl Iterator { + (0..segments + 1).map(move |i| { + let angle = i as f32 * TAU / segments as f32; + Vec2::from(angle.sin_cos()) * radius + }) +} + +fn rect_inner(size: Vec2) -> [Vec2; 4] { + let half_size = size / 2.; + let tl = Vec2::new(-half_size.x, half_size.y); + let tr = Vec2::new(half_size.x, half_size.y); + let bl = Vec2::new(-half_size.x, -half_size.y); + let br = Vec2::new(half_size.x, -half_size.y); + [tl, tr, br, bl] +} diff --git a/crates/bevy_gizmos/src/lib.rs b/crates/bevy_gizmos/src/lib.rs new file mode 100644 index 0000000000..9d609c05a6 --- /dev/null +++ b/crates/bevy_gizmos/src/lib.rs @@ -0,0 +1,187 @@ +use std::mem; + +use bevy_app::{Last, Plugin}; +use bevy_asset::{load_internal_asset, Assets, Handle, HandleUntyped}; +use bevy_ecs::{ + prelude::{Component, DetectChanges}, + schedule::IntoSystemConfigs, + system::{Commands, Res, ResMut, Resource}, + world::{FromWorld, World}, +}; +use bevy_math::Mat4; +use bevy_reflect::TypeUuid; +use bevy_render::{ + mesh::Mesh, + render_phase::AddRenderCommand, + render_resource::{PrimitiveTopology, Shader, SpecializedMeshPipelines}, + Extract, ExtractSchedule, Render, RenderApp, RenderSet, +}; + +#[cfg(feature = "bevy_pbr")] +use bevy_pbr::MeshUniform; +#[cfg(feature = "bevy_sprite")] +use bevy_sprite::{Mesh2dHandle, Mesh2dUniform}; + +pub mod gizmos; + +#[cfg(feature = "bevy_sprite")] +mod pipeline_2d; +#[cfg(feature = "bevy_pbr")] +mod pipeline_3d; + +use crate::gizmos::GizmoStorage; + +/// The `bevy_gizmos` prelude. +pub mod prelude { + #[doc(hidden)] + pub use crate::{gizmos::Gizmos, GizmoConfig}; +} + +const LINE_SHADER_HANDLE: HandleUntyped = + HandleUntyped::weak_from_u64(Shader::TYPE_UUID, 7414812689238026784); + +pub struct GizmoPlugin; + +impl Plugin for GizmoPlugin { + fn build(&self, app: &mut bevy_app::App) { + load_internal_asset!(app, LINE_SHADER_HANDLE, "lines.wgsl", Shader::from_wgsl); + + app.init_resource::() + .init_resource::() + .init_resource::() + .add_systems(Last, update_gizmo_meshes); + + let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { return; }; + + render_app.add_systems(ExtractSchedule, extract_gizmo_data); + + #[cfg(feature = "bevy_sprite")] + { + use bevy_core_pipeline::core_2d::Transparent2d; + use pipeline_2d::*; + + render_app + .add_render_command::() + .init_resource::() + .init_resource::>() + .add_systems(Render, queue_gizmos_2d.in_set(RenderSet::Queue)); + } + + #[cfg(feature = "bevy_pbr")] + { + use bevy_core_pipeline::core_3d::Opaque3d; + use pipeline_3d::*; + + render_app + .add_render_command::() + .init_resource::() + .init_resource::>() + .add_systems(Render, queue_gizmos_3d.in_set(RenderSet::Queue)); + } + } +} + +#[derive(Resource, Clone, Copy)] +pub struct GizmoConfig { + /// Set to `false` to stop drawing gizmos. + /// + /// Defaults to `true`. + pub enabled: bool, + /// Draw gizmos on top of everything else, ignoring depth. + /// + /// This setting only affects 3D. In 2D, gizmos are always drawn on top. + /// + /// Defaults to `false`. + pub on_top: bool, +} + +impl Default for GizmoConfig { + fn default() -> Self { + Self { + enabled: true, + on_top: false, + } + } +} + +#[derive(Resource)] +struct MeshHandles { + list: Handle, + strip: Handle, +} + +impl FromWorld for MeshHandles { + fn from_world(world: &mut World) -> Self { + let mut meshes = world.resource_mut::>(); + + MeshHandles { + list: meshes.add(Mesh::new(PrimitiveTopology::LineList)), + strip: meshes.add(Mesh::new(PrimitiveTopology::LineStrip)), + } + } +} + +#[derive(Component)] +struct GizmoMesh; + +fn update_gizmo_meshes( + mut meshes: ResMut>, + handles: Res, + mut storage: ResMut, +) { + let list_mesh = meshes.get_mut(&handles.list).unwrap(); + + let positions = mem::take(&mut storage.list_positions); + list_mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions); + + let colors = mem::take(&mut storage.list_colors); + list_mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, colors); + + let strip_mesh = meshes.get_mut(&handles.strip).unwrap(); + + let positions = mem::take(&mut storage.strip_positions); + strip_mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions); + + let colors = mem::take(&mut storage.strip_colors); + strip_mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, colors); +} + +fn extract_gizmo_data( + mut commands: Commands, + handles: Extract>, + config: Extract>, +) { + if config.is_changed() { + commands.insert_resource(**config); + } + + if !config.enabled { + return; + } + + let transform = Mat4::IDENTITY; + let inverse_transpose_model = transform.inverse().transpose(); + commands.spawn_batch([&handles.list, &handles.strip].map(|handle| { + ( + GizmoMesh, + #[cfg(feature = "bevy_pbr")] + ( + handle.clone(), + MeshUniform { + flags: 0, + transform, + inverse_transpose_model, + }, + ), + #[cfg(feature = "bevy_sprite")] + ( + Mesh2dHandle(handle.clone()), + Mesh2dUniform { + flags: 0, + transform, + inverse_transpose_model, + }, + ), + ) + })); +} diff --git a/crates/bevy_gizmos/src/lines.wgsl b/crates/bevy_gizmos/src/lines.wgsl new file mode 100644 index 0000000000..9fa8244f17 --- /dev/null +++ b/crates/bevy_gizmos/src/lines.wgsl @@ -0,0 +1,44 @@ +#ifdef GIZMO_LINES_3D + #import bevy_pbr::mesh_view_bindings +#else + #import bevy_sprite::mesh2d_view_bindings +#endif + +struct VertexInput { + @location(0) pos: vec3, + @location(1) color: vec4, +} + +struct VertexOutput { + @builtin(position) pos: vec4, + @location(0) color: vec4, +} + +struct FragmentOutput { + @builtin(frag_depth) depth: f32, + @location(0) color: vec4, +} + +@vertex +fn vertex(in: VertexInput) -> VertexOutput { + var out: VertexOutput; + + out.pos = view.view_proj * vec4(in.pos, 1.0); + out.color = in.color; + + return out; +} + +@fragment +fn fragment(in: VertexOutput) -> FragmentOutput { + var out: FragmentOutput; + +#ifdef DEPTH_TEST + out.depth = in.pos.z; +#else + out.depth = 1.0; +#endif + + out.color = in.color; + return out; +} diff --git a/crates/bevy_gizmos/src/pipeline_2d.rs b/crates/bevy_gizmos/src/pipeline_2d.rs new file mode 100644 index 0000000000..f3ab5be25c --- /dev/null +++ b/crates/bevy_gizmos/src/pipeline_2d.rs @@ -0,0 +1,128 @@ +use bevy_asset::Handle; +use bevy_core_pipeline::core_2d::Transparent2d; +use bevy_ecs::{ + prelude::Entity, + query::With, + system::{Query, Res, ResMut, Resource}, + world::{FromWorld, World}, +}; +use bevy_render::{ + mesh::{Mesh, MeshVertexBufferLayout}, + render_asset::RenderAssets, + render_phase::{DrawFunctions, RenderPhase, SetItemPipeline}, + render_resource::*, + texture::BevyDefault, + view::{ExtractedView, Msaa, ViewTarget}, +}; +use bevy_sprite::*; +use bevy_utils::FloatOrd; + +use crate::{GizmoMesh, LINE_SHADER_HANDLE}; + +#[derive(Resource)] +pub(crate) struct GizmoLinePipeline { + mesh_pipeline: Mesh2dPipeline, + shader: Handle, +} + +impl FromWorld for GizmoLinePipeline { + fn from_world(render_world: &mut World) -> Self { + GizmoLinePipeline { + mesh_pipeline: render_world.resource::().clone(), + shader: LINE_SHADER_HANDLE.typed(), + } + } +} + +impl SpecializedMeshPipeline for GizmoLinePipeline { + type Key = Mesh2dPipelineKey; + + fn specialize( + &self, + key: Self::Key, + layout: &MeshVertexBufferLayout, + ) -> Result { + let vertex_buffer_layout = layout.get_layout(&[ + Mesh::ATTRIBUTE_POSITION.at_shader_location(0), + Mesh::ATTRIBUTE_COLOR.at_shader_location(1), + ])?; + + let format = if key.contains(Mesh2dPipelineKey::HDR) { + ViewTarget::TEXTURE_FORMAT_HDR + } else { + TextureFormat::bevy_default() + }; + + Ok(RenderPipelineDescriptor { + vertex: VertexState { + shader: self.shader.clone_weak(), + entry_point: "vertex".into(), + shader_defs: vec![], + buffers: vec![vertex_buffer_layout], + }, + fragment: Some(FragmentState { + shader: self.shader.clone_weak(), + shader_defs: vec![], + entry_point: "fragment".into(), + targets: vec![Some(ColorTargetState { + format, + blend: Some(BlendState::ALPHA_BLENDING), + write_mask: ColorWrites::ALL, + })], + }), + layout: vec![self.mesh_pipeline.view_layout.clone()], + primitive: PrimitiveState { + topology: key.primitive_topology(), + ..Default::default() + }, + depth_stencil: None, + multisample: MultisampleState { + count: key.msaa_samples(), + mask: !0, + alpha_to_coverage_enabled: false, + }, + push_constant_ranges: vec![], + label: Some("gizmo_2d_pipeline".into()), + }) + } +} + +pub(crate) type DrawGizmoLines = ( + SetItemPipeline, + SetMesh2dViewBindGroup<0>, + SetMesh2dBindGroup<1>, + DrawMesh2d, +); + +#[allow(clippy::too_many_arguments)] +pub(crate) fn queue_gizmos_2d( + draw_functions: Res>, + pipeline: Res, + pipeline_cache: Res, + mut specialized_pipelines: ResMut>, + gpu_meshes: Res>, + msaa: Res, + mesh_handles: Query<(Entity, &Mesh2dHandle), With>, + mut views: Query<(&ExtractedView, &mut RenderPhase)>, +) { + let draw_function = draw_functions.read().get_id::().unwrap(); + let key = Mesh2dPipelineKey::from_msaa_samples(msaa.samples()); + for (view, mut phase) in &mut views { + let key = key | Mesh2dPipelineKey::from_hdr(view.hdr); + for (entity, mesh_handle) in &mesh_handles { + let Some(mesh) = gpu_meshes.get(&mesh_handle.0) else { continue; }; + + let key = key | Mesh2dPipelineKey::from_primitive_topology(mesh.primitive_topology); + let pipeline = specialized_pipelines + .specialize(&pipeline_cache, &pipeline, key, &mesh.layout) + .unwrap(); + phase.add(Transparent2d { + entity, + draw_function, + pipeline, + sort_key: FloatOrd(f32::MAX), + batch_range: None, + }); + } + } +} diff --git a/crates/bevy_gizmos/src/pipeline_3d.rs b/crates/bevy_gizmos/src/pipeline_3d.rs new file mode 100644 index 0000000000..6064a60a56 --- /dev/null +++ b/crates/bevy_gizmos/src/pipeline_3d.rs @@ -0,0 +1,164 @@ +use bevy_asset::Handle; +use bevy_core_pipeline::core_3d::Opaque3d; +use bevy_ecs::{ + entity::Entity, + query::With, + system::{Query, Res, ResMut, Resource}, + world::{FromWorld, World}, +}; +use bevy_pbr::*; +use bevy_render::{ + mesh::Mesh, + render_resource::Shader, + view::{ExtractedView, ViewTarget}, +}; +use bevy_render::{ + mesh::MeshVertexBufferLayout, + render_asset::RenderAssets, + render_phase::{DrawFunctions, RenderPhase, SetItemPipeline}, + render_resource::*, + texture::BevyDefault, + view::Msaa, +}; + +use crate::{GizmoConfig, GizmoMesh, LINE_SHADER_HANDLE}; + +#[derive(Resource)] +pub(crate) struct GizmoPipeline { + mesh_pipeline: MeshPipeline, + shader: Handle, +} + +impl FromWorld for GizmoPipeline { + fn from_world(render_world: &mut World) -> Self { + GizmoPipeline { + mesh_pipeline: render_world.resource::().clone(), + shader: LINE_SHADER_HANDLE.typed(), + } + } +} + +impl SpecializedMeshPipeline for GizmoPipeline { + type Key = (bool, MeshPipelineKey); + + fn specialize( + &self, + (depth_test, key): Self::Key, + layout: &MeshVertexBufferLayout, + ) -> Result { + let mut shader_defs = Vec::new(); + shader_defs.push("GIZMO_LINES_3D".into()); + shader_defs.push(ShaderDefVal::Int( + "MAX_DIRECTIONAL_LIGHTS".to_string(), + MAX_DIRECTIONAL_LIGHTS as i32, + )); + shader_defs.push(ShaderDefVal::Int( + "MAX_CASCADES_PER_LIGHT".to_string(), + MAX_CASCADES_PER_LIGHT as i32, + )); + if depth_test { + shader_defs.push("DEPTH_TEST".into()); + } + + let vertex_buffer_layout = layout.get_layout(&[ + Mesh::ATTRIBUTE_POSITION.at_shader_location(0), + Mesh::ATTRIBUTE_COLOR.at_shader_location(1), + ])?; + let bind_group_layout = match key.msaa_samples() { + 1 => vec![self.mesh_pipeline.view_layout.clone()], + _ => { + shader_defs.push("MULTISAMPLED".into()); + vec![self.mesh_pipeline.view_layout_multisampled.clone()] + } + }; + + let format = if key.contains(MeshPipelineKey::HDR) { + ViewTarget::TEXTURE_FORMAT_HDR + } else { + TextureFormat::bevy_default() + }; + + Ok(RenderPipelineDescriptor { + vertex: VertexState { + shader: self.shader.clone_weak(), + entry_point: "vertex".into(), + shader_defs: shader_defs.clone(), + buffers: vec![vertex_buffer_layout], + }, + fragment: Some(FragmentState { + shader: self.shader.clone_weak(), + shader_defs, + entry_point: "fragment".into(), + targets: vec![Some(ColorTargetState { + format, + blend: None, + write_mask: ColorWrites::ALL, + })], + }), + layout: bind_group_layout, + primitive: PrimitiveState { + topology: key.primitive_topology(), + ..Default::default() + }, + depth_stencil: Some(DepthStencilState { + format: TextureFormat::Depth32Float, + depth_write_enabled: true, + depth_compare: CompareFunction::Greater, + stencil: Default::default(), + bias: Default::default(), + }), + multisample: MultisampleState { + count: key.msaa_samples(), + mask: !0, + alpha_to_coverage_enabled: false, + }, + push_constant_ranges: vec![], + label: Some("gizmo_3d_pipeline".into()), + }) + } +} + +pub(crate) type DrawGizmoLines = ( + SetItemPipeline, + SetMeshViewBindGroup<0>, + SetMeshBindGroup<1>, + DrawMesh, +); + +#[allow(clippy::too_many_arguments)] +pub(crate) fn queue_gizmos_3d( + draw_functions: Res>, + pipeline: Res, + mut pipelines: ResMut>, + pipeline_cache: Res, + render_meshes: Res>, + msaa: Res, + mesh_handles: Query<(Entity, &Handle), With>, + config: Res, + mut views: Query<(&ExtractedView, &mut RenderPhase)>, +) { + let draw_function = draw_functions.read().get_id::().unwrap(); + let key = MeshPipelineKey::from_msaa_samples(msaa.samples()); + for (view, mut phase) in &mut views { + let key = key | MeshPipelineKey::from_hdr(view.hdr); + for (entity, mesh_handle) in &mesh_handles { + if let Some(mesh) = render_meshes.get(mesh_handle) { + let key = key | MeshPipelineKey::from_primitive_topology(mesh.primitive_topology); + let pipeline = pipelines + .specialize( + &pipeline_cache, + &pipeline, + (!config.on_top, key), + &mesh.layout, + ) + .unwrap(); + phase.add(Opaque3d { + entity, + pipeline, + draw_function, + distance: 0., + }); + } + } + } +} diff --git a/crates/bevy_hierarchy/src/valid_parent_check_plugin.rs b/crates/bevy_hierarchy/src/valid_parent_check_plugin.rs index 557450f9af..ad8c6c1ec3 100644 --- a/crates/bevy_hierarchy/src/valid_parent_check_plugin.rs +++ b/crates/bevy_hierarchy/src/valid_parent_check_plugin.rs @@ -1,6 +1,6 @@ use std::marker::PhantomData; -use bevy_app::{App, CoreSet, Plugin}; +use bevy_app::{App, Last, Plugin}; use bevy_core::Name; use bevy_ecs::prelude::*; use bevy_log::warn; @@ -96,10 +96,10 @@ impl Default for ValidParentCheckPlugin { impl Plugin for ValidParentCheckPlugin { fn build(&self, app: &mut App) { - app.init_resource::>().add_system( + app.init_resource::>().add_systems( + Last, check_hierarchy_component_has_valid_parent:: - .run_if(resource_equals(ReportHierarchyIssue::::new(true))) - .in_base_set(CoreSet::Last), + .run_if(resource_equals(ReportHierarchyIssue::::new(true))), ); } } diff --git a/crates/bevy_input/src/common_conditions.rs b/crates/bevy_input/src/common_conditions.rs index 265b1c0c4d..8b85b8a858 100644 --- a/crates/bevy_input/src/common_conditions.rs +++ b/crates/bevy_input/src/common_conditions.rs @@ -11,7 +11,7 @@ use std::hash::Hash; /// fn main() { /// App::new() /// .add_plugins(DefaultPlugins) -/// .add_system(pause_menu.run_if(input_toggle_active(false, KeyCode::Escape))) +/// .add_systems(Update, pause_menu.run_if(input_toggle_active(false, KeyCode::Escape))) /// .run(); /// } /// @@ -33,7 +33,7 @@ use std::hash::Hash; /// App::new() /// .add_plugins(DefaultPlugins) /// .init_resource::() -/// .add_system(pause_menu.run_if(|paused: Res| paused.0)) +/// .add_systems(Update, pause_menu.run_if(|paused: Res| paused.0)) /// .run(); /// } /// @@ -48,7 +48,7 @@ use std::hash::Hash; /// } /// /// ``` -pub fn input_toggle_active(default: bool, input: T) -> impl FnMut(Res>) -> bool +pub fn input_toggle_active(default: bool, input: T) -> impl FnMut(Res>) -> bool + Clone where T: Copy + Eq + Hash + Send + Sync + 'static, { @@ -60,7 +60,7 @@ where } /// Run condition that is active if [`Input::pressed`] is true for the given input. -pub fn input_pressed(input: T) -> impl FnMut(Res>) -> bool +pub fn input_pressed(input: T) -> impl FnMut(Res>) -> bool + Clone where T: Copy + Eq + Hash + Send + Sync + 'static, { @@ -75,13 +75,13 @@ where /// fn main() { /// App::new() /// .add_plugins(DefaultPlugins) -/// .add_system(jump.run_if(input_just_pressed(KeyCode::Space))) +/// .add_systems(Update, jump.run_if(input_just_pressed(KeyCode::Space))) /// .run(); /// } /// /// # fn jump() {} /// ``` -pub fn input_just_pressed(input: T) -> impl FnMut(Res>) -> bool +pub fn input_just_pressed(input: T) -> impl FnMut(Res>) -> bool + Clone where T: Copy + Eq + Hash + Send + Sync + 'static, { @@ -89,9 +89,29 @@ where } /// Run condition that is active if [`Input::just_released`] is true for the given input. -pub fn input_just_released(input: T) -> impl FnMut(Res>) -> bool +pub fn input_just_released(input: T) -> impl FnMut(Res>) -> bool + Clone where T: Copy + Eq + Hash + Send + Sync + 'static, { move |inputs: Res>| inputs.just_released(input) } + +#[cfg(test)] +mod tests { + use super::*; + use bevy::prelude::{IntoSystemConfigs, KeyCode, Schedule}; + + fn test_system() {} + + // Ensure distributive_run_if compiles with the common conditions. + #[test] + fn distributive_run_if_compiles() { + Schedule::default().add_systems( + (test_system, test_system) + .distributive_run_if(input_toggle_active(false, KeyCode::Escape)) + .distributive_run_if(input_pressed(KeyCode::Escape)) + .distributive_run_if(input_just_pressed(KeyCode::Escape)) + .distributive_run_if(input_just_released(KeyCode::Escape)), + ); + } +} diff --git a/crates/bevy_input/src/lib.rs b/crates/bevy_input/src/lib.rs index 7fc988171c..9da05a2e25 100644 --- a/crates/bevy_input/src/lib.rs +++ b/crates/bevy_input/src/lib.rs @@ -53,18 +53,18 @@ pub struct InputSystem; impl Plugin for InputPlugin { fn build(&self, app: &mut App) { - app.configure_set(InputSystem.in_base_set(CoreSet::PreUpdate)) + app // keyboard .add_event::() .init_resource::>() .init_resource::>() - .add_system(keyboard_input_system.in_set(InputSystem)) + .add_systems(PreUpdate, keyboard_input_system.in_set(InputSystem)) // mouse .add_event::() .add_event::() .add_event::() .init_resource::>() - .add_system(mouse_button_input_system.in_set(InputSystem)) + .add_systems(PreUpdate, mouse_button_input_system.in_set(InputSystem)) // gamepad .add_event::() .add_event::() @@ -76,6 +76,7 @@ impl Plugin for InputPlugin { .init_resource::>() .init_resource::>() .add_systems( + PreUpdate, ( gamepad_event_system, gamepad_connection_system.after(gamepad_event_system), @@ -91,7 +92,7 @@ impl Plugin for InputPlugin { // touch .add_event::() .init_resource::() - .add_system(touch_screen_input_system.in_set(InputSystem)); + .add_systems(PreUpdate, touch_screen_input_system.in_set(InputSystem)); // Register common types app.register_type::(); diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 1eec1d8c5e..5af93188b3 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -71,11 +71,14 @@ subpixel_glyph_atlas = ["bevy_text/subpixel_glyph_atlas"] webgl = ["bevy_core_pipeline?/webgl", "bevy_pbr?/webgl", "bevy_render?/webgl"] # enable systems that allow for automated testing on CI -bevy_ci_testing = ["bevy_app/bevy_ci_testing", "bevy_render/ci_limits"] +bevy_ci_testing = ["bevy_app/bevy_ci_testing", "bevy_render?/ci_limits"] # Enable animation support, and glTF animation loading animation = ["bevy_animation", "bevy_gltf?/bevy_animation"] +bevy_sprite = ["dep:bevy_sprite", "bevy_gizmos?/bevy_sprite"] +bevy_pbr = ["dep:bevy_pbr", "bevy_gizmos?/bevy_pbr"] + # Used to disable code that is unsupported when Bevy is dynamically linked dynamic_linking = ["bevy_diagnostic/dynamic_linking"] @@ -122,3 +125,4 @@ bevy_text = { path = "../bevy_text", optional = true, version = "0.11.0-dev" } bevy_ui = { path = "../bevy_ui", optional = true, version = "0.11.0-dev" } bevy_winit = { path = "../bevy_winit", optional = true, version = "0.11.0-dev" } bevy_gilrs = { path = "../bevy_gilrs", optional = true, version = "0.11.0-dev" } +bevy_gizmos = { path = "../bevy_gizmos", optional = true, version = "0.11.0-dev", default-features = false } diff --git a/crates/bevy_internal/src/default_plugins.rs b/crates/bevy_internal/src/default_plugins.rs index 2c3e637de8..fe3be50bf9 100644 --- a/crates/bevy_internal/src/default_plugins.rs +++ b/crates/bevy_internal/src/default_plugins.rs @@ -135,6 +135,11 @@ impl PluginGroup for DefaultPlugins { group = group.add(bevy_animation::AnimationPlugin::default()); } + #[cfg(feature = "bevy_gizmos")] + { + group = group.add(bevy_gizmos::GizmoPlugin); + } + group } } diff --git a/crates/bevy_internal/src/lib.rs b/crates/bevy_internal/src/lib.rs index b2211a5e3f..4e1c6bcb17 100644 --- a/crates/bevy_internal/src/lib.rs +++ b/crates/bevy_internal/src/lib.rs @@ -172,6 +172,12 @@ pub mod winit { pub use bevy_winit::*; } +#[cfg(feature = "bevy_gizmos")] +pub mod gizmos { + //! Immediate mode debug drawing. + pub use bevy_gizmos::*; +} + #[cfg(feature = "bevy_dynamic_plugin")] pub mod dynamic_plugin { //! Dynamic linking of plugins diff --git a/crates/bevy_internal/src/prelude.rs b/crates/bevy_internal/src/prelude.rs index cc3614544d..f9243382a1 100644 --- a/crates/bevy_internal/src/prelude.rs +++ b/crates/bevy_internal/src/prelude.rs @@ -51,6 +51,10 @@ pub use crate::ui::prelude::*; #[cfg(feature = "bevy_dynamic_plugin")] pub use crate::dynamic_plugin::*; +#[doc(hidden)] +#[cfg(feature = "bevy_gizmos")] +pub use crate::gizmos::prelude::*; + #[doc(hidden)] #[cfg(feature = "bevy_gilrs")] pub use crate::gilrs::*; diff --git a/crates/bevy_macro_utils/Cargo.toml b/crates/bevy_macro_utils/Cargo.toml index 8b9149864a..76cb1d690f 100644 --- a/crates/bevy_macro_utils/Cargo.toml +++ b/crates/bevy_macro_utils/Cargo.toml @@ -12,3 +12,4 @@ keywords = ["bevy"] toml_edit = "0.19" syn = "1.0" quote = "1.0" +rustc-hash = "1.0" diff --git a/crates/bevy_macro_utils/src/lib.rs b/crates/bevy_macro_utils/src/lib.rs index 3fd275ceb9..3a72994510 100644 --- a/crates/bevy_macro_utils/src/lib.rs +++ b/crates/bevy_macro_utils/src/lib.rs @@ -8,10 +8,11 @@ pub use attrs::*; pub use shape::*; pub use symbol::*; -use proc_macro::TokenStream; +use proc_macro::{TokenStream, TokenTree}; use quote::{quote, quote_spanned}; +use rustc_hash::FxHashSet; use std::{env, path::PathBuf}; -use syn::spanned::Spanned; +use syn::{spanned::Spanned, Ident}; use toml_edit::{Document, Item}; pub struct BevyManifest { @@ -108,6 +109,48 @@ impl BevyManifest { } } +/// Finds an identifier that will not conflict with the specified set of tokens. +/// If the identifier is present in `haystack`, extra characters will be added +/// to it until it no longer conflicts with anything. +/// +/// Note that the returned identifier can still conflict in niche cases, +/// such as if an identifier in `haystack` is hidden behind an un-expanded macro. +pub fn ensure_no_collision(value: Ident, haystack: TokenStream) -> Ident { + // Collect all the identifiers in `haystack` into a set. + let idents = { + // List of token streams that will be visited in future loop iterations. + let mut unvisited = vec![haystack]; + // Identifiers we have found while searching tokens. + let mut found = FxHashSet::default(); + while let Some(tokens) = unvisited.pop() { + for t in tokens { + match t { + // Collect any identifiers we encounter. + TokenTree::Ident(ident) => { + found.insert(ident.to_string()); + } + // Queue up nested token streams to be visited in a future loop iteration. + TokenTree::Group(g) => unvisited.push(g.stream()), + TokenTree::Punct(_) | TokenTree::Literal(_) => {} + } + } + } + + found + }; + + let span = value.span(); + + // If there's a collision, add more characters to the identifier + // until it doesn't collide with anything anymore. + let mut value = value.to_string(); + while idents.contains(&value) { + value.push('X'); + } + + Ident::new(&value, span) +} + /// Derive a label trait /// /// # Args diff --git a/crates/bevy_pbr/src/lib.rs b/crates/bevy_pbr/src/lib.rs index 530a2fa71e..b3d7741db1 100644 --- a/crates/bevy_pbr/src/lib.rs +++ b/crates/bevy_pbr/src/lib.rs @@ -55,7 +55,7 @@ use bevy_render::{ render_phase::sort_phase_system, render_resource::Shader, view::{ViewSet, VisibilitySystems}, - ExtractSchedule, RenderApp, RenderSet, + ExtractSchedule, Render, RenderApp, RenderSet, }; use bevy_transform::TransformSystem; use environment_map::EnvironmentMapPlugin; @@ -151,6 +151,7 @@ impl Plugin for PbrPlugin { app.register_asset_reflect::() .register_type::() + .register_type::() .register_type::() .register_type::() .register_type::() @@ -175,68 +176,57 @@ impl Plugin for PbrPlugin { .init_resource::() .add_plugin(ExtractResourcePlugin::::default()) .configure_sets( + PostUpdate, ( SimulationLightSystems::AddClusters, - SimulationLightSystems::AddClustersFlush - .after(SimulationLightSystems::AddClusters) - .before(SimulationLightSystems::AssignLightsToClusters), + SimulationLightSystems::AddClustersFlush, SimulationLightSystems::AssignLightsToClusters, - SimulationLightSystems::CheckLightVisibility, - SimulationLightSystems::UpdateDirectionalLightCascades, - SimulationLightSystems::UpdateLightFrusta, ) - .in_base_set(CoreSet::PostUpdate), + .chain(), ) .add_plugin(FogPlugin) - .add_system(add_clusters.in_set(SimulationLightSystems::AddClusters)) - .add_system(apply_system_buffers.in_set(SimulationLightSystems::AddClustersFlush)) - .add_system( - assign_lights_to_clusters - .in_set(SimulationLightSystems::AssignLightsToClusters) - .after(TransformSystem::TransformPropagate) - .after(VisibilitySystems::CheckVisibility) - .after(CameraUpdateSystem), - ) - .add_system( - update_directional_light_cascades - .in_set(SimulationLightSystems::UpdateDirectionalLightCascades) - .after(TransformSystem::TransformPropagate) - .after(CameraUpdateSystem), - ) - .add_system( - update_directional_light_frusta - .in_set(SimulationLightSystems::UpdateLightFrusta) - // This must run after CheckVisibility because it relies on ComputedVisibility::is_visible() - .after(VisibilitySystems::CheckVisibility) - .after(TransformSystem::TransformPropagate) - .after(SimulationLightSystems::UpdateDirectionalLightCascades) - // We assume that no entity will be both a directional light and a spot light, - // so these systems will run independently of one another. - // FIXME: Add an archetype invariant for this https://github.com/bevyengine/bevy/issues/1481. - .ambiguous_with(update_spot_light_frusta), - ) - .add_system( - update_point_light_frusta - .in_set(SimulationLightSystems::UpdateLightFrusta) - .after(TransformSystem::TransformPropagate) - .after(SimulationLightSystems::AssignLightsToClusters), - ) - .add_system( - update_spot_light_frusta - .in_set(SimulationLightSystems::UpdateLightFrusta) - .after(TransformSystem::TransformPropagate) - .after(SimulationLightSystems::AssignLightsToClusters), - ) - .add_system( - check_light_mesh_visibility - .in_set(SimulationLightSystems::CheckLightVisibility) - .after(VisibilitySystems::CalculateBoundsFlush) - .after(TransformSystem::TransformPropagate) - .after(SimulationLightSystems::UpdateLightFrusta) - // NOTE: This MUST be scheduled AFTER the core renderer visibility check - // because that resets entity ComputedVisibility for the first view - // which would override any results from this otherwise - .after(VisibilitySystems::CheckVisibility), + .add_systems( + PostUpdate, + ( + add_clusters.in_set(SimulationLightSystems::AddClusters), + apply_system_buffers.in_set(SimulationLightSystems::AddClustersFlush), + assign_lights_to_clusters + .in_set(SimulationLightSystems::AssignLightsToClusters) + .after(TransformSystem::TransformPropagate) + .after(VisibilitySystems::CheckVisibility) + .after(CameraUpdateSystem), + update_directional_light_cascades + .in_set(SimulationLightSystems::UpdateDirectionalLightCascades) + .after(TransformSystem::TransformPropagate) + .after(CameraUpdateSystem), + update_directional_light_frusta + .in_set(SimulationLightSystems::UpdateLightFrusta) + // This must run after CheckVisibility because it relies on ComputedVisibility::is_visible() + .after(VisibilitySystems::CheckVisibility) + .after(TransformSystem::TransformPropagate) + .after(SimulationLightSystems::UpdateDirectionalLightCascades) + // We assume that no entity will be both a directional light and a spot light, + // so these systems will run independently of one another. + // FIXME: Add an archetype invariant for this https://github.com/bevyengine/bevy/issues/1481. + .ambiguous_with(update_spot_light_frusta), + update_point_light_frusta + .in_set(SimulationLightSystems::UpdateLightFrusta) + .after(TransformSystem::TransformPropagate) + .after(SimulationLightSystems::AssignLightsToClusters), + update_spot_light_frusta + .in_set(SimulationLightSystems::UpdateLightFrusta) + .after(TransformSystem::TransformPropagate) + .after(SimulationLightSystems::AssignLightsToClusters), + check_light_mesh_visibility + .in_set(SimulationLightSystems::CheckLightVisibility) + .after(VisibilitySystems::CalculateBoundsFlush) + .after(TransformSystem::TransformPropagate) + .after(SimulationLightSystems::UpdateLightFrusta) + // NOTE: This MUST be scheduled AFTER the core renderer visibility check + // because that resets entity ComputedVisibility for the first view + // which would override any results from this otherwise + .after(VisibilitySystems::CheckVisibility), + ), ); app.world @@ -257,35 +247,39 @@ impl Plugin for PbrPlugin { // Extract the required data from the main world render_app - .configure_set(RenderLightSystems::PrepareLights.in_set(RenderSet::Prepare)) - .configure_set(RenderLightSystems::PrepareClusters.in_set(RenderSet::Prepare)) - .configure_set(RenderLightSystems::QueueShadows.in_set(RenderSet::Queue)) + .configure_sets( + Render, + ( + RenderLightSystems::PrepareLights.in_set(RenderSet::Prepare), + RenderLightSystems::PrepareClusters.in_set(RenderSet::Prepare), + RenderLightSystems::QueueShadows.in_set(RenderSet::Queue), + ), + ) .add_systems( + ExtractSchedule, ( render::extract_clusters.in_set(RenderLightSystems::ExtractClusters), render::extract_lights.in_set(RenderLightSystems::ExtractLights), - ) - .in_schedule(ExtractSchedule), + ), ) - .add_system( - render::prepare_lights - .before(ViewSet::PrepareUniforms) - .in_set(RenderLightSystems::PrepareLights), + .add_systems( + Render, + ( + render::prepare_lights + .before(ViewSet::PrepareUniforms) + .in_set(RenderLightSystems::PrepareLights), + // A sync is needed after prepare_lights, before prepare_view_uniforms, + // because prepare_lights creates new views for shadow mapping + apply_system_buffers + .in_set(RenderSet::Prepare) + .after(RenderLightSystems::PrepareLights) + .before(ViewSet::PrepareUniforms), + render::prepare_clusters + .after(render::prepare_lights) + .in_set(RenderLightSystems::PrepareClusters), + sort_phase_system::.in_set(RenderSet::PhaseSort), + ), ) - // A sync is needed after prepare_lights, before prepare_view_uniforms, - // because prepare_lights creates new views for shadow mapping - .add_system( - apply_system_buffers - .in_set(RenderSet::Prepare) - .after(RenderLightSystems::PrepareLights) - .before(ViewSet::PrepareUniforms), - ) - .add_system( - render::prepare_clusters - .after(render::prepare_lights) - .in_set(RenderLightSystems::PrepareClusters), - ) - .add_system(sort_phase_system::.in_set(RenderSet::PhaseSort)) .init_resource::() .init_resource::() .init_resource::(); @@ -300,11 +294,5 @@ impl Plugin for PbrPlugin { draw_3d_graph::node::SHADOW_PASS, bevy_core_pipeline::core_3d::graph::node::MAIN_PASS, ); - draw_3d_graph.add_slot_edge( - draw_3d_graph.input_node().id, - bevy_core_pipeline::core_3d::graph::input::VIEW_ENTITY, - draw_3d_graph::node::SHADOW_PASS, - ShadowPassNode::IN_VIEW, - ); } } diff --git a/crates/bevy_pbr/src/material.rs b/crates/bevy_pbr/src/material.rs index cb57e68f4d..be28f2e13a 100644 --- a/crates/bevy_pbr/src/material.rs +++ b/crates/bevy_pbr/src/material.rs @@ -3,7 +3,7 @@ use crate::{ MeshUniform, PrepassPipelinePlugin, PrepassPlugin, RenderLightSystems, SetMeshBindGroup, SetMeshViewBindGroup, Shadow, }; -use bevy_app::{App, IntoSystemAppConfig, Plugin}; +use bevy_app::{App, Plugin}; use bevy_asset::{AddAsset, AssetEvent, AssetServer, Assets, Handle}; use bevy_core_pipeline::{ core_3d::{AlphaMask3d, Opaque3d, Transparent3d}, @@ -35,7 +35,7 @@ use bevy_render::{ renderer::RenderDevice, texture::FallbackImage, view::{ExtractedView, Msaa, VisibleEntities}, - Extract, ExtractSchedule, RenderApp, RenderSet, + Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; use bevy_utils::{tracing::error, HashMap, HashSet}; use std::hash::Hash; @@ -199,14 +199,17 @@ where .init_resource::>() .init_resource::>() .init_resource::>>() - .add_system(extract_materials::.in_schedule(ExtractSchedule)) - .add_system( - prepare_materials:: - .in_set(RenderSet::Prepare) - .after(PrepareAssetSet::PreAssetPrepare), - ) - .add_system(render::queue_shadows::.in_set(RenderLightSystems::QueueShadows)) - .add_system(queue_material_meshes::.in_set(RenderSet::Queue)); + .add_systems(ExtractSchedule, extract_materials::) + .add_systems( + Render, + ( + prepare_materials:: + .in_set(RenderSet::Prepare) + .after(PrepareAssetSet::PreAssetPrepare), + render::queue_shadows::.in_set(RenderLightSystems::QueueShadows), + queue_material_meshes::.in_set(RenderSet::Queue), + ), + ); } // PrepassPipelinePlugin is required for shadow mapping and the optional PrepassPlugin diff --git a/crates/bevy_pbr/src/prepass/mod.rs b/crates/bevy_pbr/src/prepass/mod.rs index 855d1e4ae8..b2a50c5f37 100644 --- a/crates/bevy_pbr/src/prepass/mod.rs +++ b/crates/bevy_pbr/src/prepass/mod.rs @@ -1,4 +1,4 @@ -use bevy_app::{IntoSystemAppConfig, Plugin}; +use bevy_app::Plugin; use bevy_asset::{load_internal_asset, AssetServer, Handle, HandleUntyped}; use bevy_core_pipeline::{ prelude::Camera3d, @@ -17,6 +17,7 @@ use bevy_ecs::{ use bevy_reflect::TypeUuid; use bevy_render::{ camera::ExtractedCamera, + globals::{GlobalsBuffer, GlobalsUniform}, mesh::MeshVertexBufferLayout, prelude::{Camera, Mesh}, render_asset::RenderAssets, @@ -38,7 +39,7 @@ use bevy_render::{ renderer::RenderDevice, texture::{FallbackImagesDepth, FallbackImagesMsaa, TextureCache}, view::{ExtractedView, Msaa, ViewUniform, ViewUniformOffset, ViewUniforms, VisibleEntities}, - Extract, ExtractSchedule, RenderApp, RenderSet, + Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; use bevy_utils::{tracing::error, HashMap}; @@ -101,7 +102,10 @@ where }; render_app - .add_system(queue_prepass_view_bind_group::.in_set(RenderSet::Queue)) + .add_systems( + Render, + queue_prepass_view_bind_group::.in_set(RenderSet::Queue), + ) .init_resource::>() .init_resource::() .init_resource::>>(); @@ -129,15 +133,18 @@ where }; render_app - .add_system(extract_camera_prepass_phase.in_schedule(ExtractSchedule)) - .add_system( - prepare_prepass_textures - .in_set(RenderSet::Prepare) - .after(bevy_render::view::prepare_windows), + .add_systems(ExtractSchedule, extract_camera_prepass_phase) + .add_systems( + Render, + ( + prepare_prepass_textures + .in_set(RenderSet::Prepare) + .after(bevy_render::view::prepare_windows), + queue_prepass_material_meshes::.in_set(RenderSet::Queue), + sort_phase_system::.in_set(RenderSet::PhaseSort), + sort_phase_system::.in_set(RenderSet::PhaseSort), + ), ) - .add_system(queue_prepass_material_meshes::.in_set(RenderSet::Queue)) - .add_system(sort_phase_system::.in_set(RenderSet::PhaseSort)) - .add_system(sort_phase_system::.in_set(RenderSet::PhaseSort)) .init_resource::>() .init_resource::>() .add_render_command::>() @@ -167,7 +174,7 @@ impl FromWorld for PrepassPipeline { // View BindGroupLayoutEntry { binding: 0, - visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT, + visibility: ShaderStages::VERTEX_FRAGMENT, ty: BindingType::Buffer { ty: BufferBindingType::Uniform, has_dynamic_offset: true, @@ -175,6 +182,17 @@ impl FromWorld for PrepassPipeline { }, count: None, }, + // Globals + BindGroupLayoutEntry { + binding: 1, + visibility: ShaderStages::VERTEX_FRAGMENT, + ty: BindingType::Buffer { + ty: BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: Some(GlobalsUniform::min_size()), + }, + count: None, + }, ], label: Some("prepass_view_layout"), }); @@ -573,19 +591,26 @@ pub fn queue_prepass_view_bind_group( render_device: Res, prepass_pipeline: Res>, view_uniforms: Res, + globals_buffer: Res, mut prepass_view_bind_group: ResMut, ) { - if let Some(view_binding) = view_uniforms.uniforms.binding() { - prepass_view_bind_group.bind_group = - Some(render_device.create_bind_group(&BindGroupDescriptor { - entries: &[BindGroupEntry { + let Some(view_binding) = view_uniforms.uniforms.binding() else { return }; + let Some(globals_binding) = globals_buffer.buffer.binding() else { return }; + prepass_view_bind_group.bind_group = + Some(render_device.create_bind_group(&BindGroupDescriptor { + entries: &[ + BindGroupEntry { binding: 0, resource: view_binding, - }], - label: Some("prepass_view_bind_group"), - layout: &prepass_pipeline.view_layout, - })); - } + }, + BindGroupEntry { + binding: 1, + resource: globals_binding, + }, + ], + label: Some("prepass_view_bind_group"), + layout: &prepass_pipeline.view_layout, + })); } #[allow(clippy::too_many_arguments)] diff --git a/crates/bevy_pbr/src/prepass/prepass_bindings.wgsl b/crates/bevy_pbr/src/prepass/prepass_bindings.wgsl index cd338af0ed..528f2e596d 100644 --- a/crates/bevy_pbr/src/prepass/prepass_bindings.wgsl +++ b/crates/bevy_pbr/src/prepass/prepass_bindings.wgsl @@ -6,6 +6,9 @@ @group(0) @binding(0) var view: View; +@group(0) @binding(1) +var globals: Globals; + // Material bindings will be in @group(1) @group(2) @binding(0) @@ -16,3 +19,4 @@ var mesh: Mesh; var joint_matrices: SkinnedMesh; #import bevy_pbr::skinning #endif + diff --git a/crates/bevy_pbr/src/render/fog.rs b/crates/bevy_pbr/src/render/fog.rs index 2c658bb7a1..efa9a4c15f 100644 --- a/crates/bevy_pbr/src/render/fog.rs +++ b/crates/bevy_pbr/src/render/fog.rs @@ -8,7 +8,7 @@ use bevy_render::{ render_resource::{DynamicUniformBuffer, Shader, ShaderType}, renderer::{RenderDevice, RenderQueue}, view::ExtractedView, - RenderApp, RenderSet, + Render, RenderApp, RenderSet, }; use crate::{FogFalloff, FogSettings}; @@ -142,8 +142,11 @@ impl Plugin for FogPlugin { if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { render_app .init_resource::() - .add_system(prepare_fog.in_set(RenderFogSystems::PrepareFog)) - .configure_set(RenderFogSystems::PrepareFog.in_set(RenderSet::Prepare)); + .add_systems(Render, prepare_fog.in_set(RenderFogSystems::PrepareFog)) + .configure_set( + Render, + RenderFogSystems::PrepareFog.in_set(RenderSet::Prepare), + ); } } } diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index cdf1e7ab15..1df01bfe05 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -15,7 +15,7 @@ use bevy_render::{ color::Color, mesh::Mesh, render_asset::RenderAssets, - render_graph::{Node, NodeRunError, RenderGraphContext, SlotInfo, SlotType}, + render_graph::{Node, NodeRunError, RenderGraphContext}, render_phase::{ CachedRenderPipelinePhaseItem, DrawFunctionId, DrawFunctions, PhaseItem, RenderPhase, }, @@ -472,37 +472,43 @@ pub(crate) struct CubeMapFace { pub(crate) up: Vec3, } -// see https://www.khronos.org/opengl/wiki/Cubemap_Texture +// Cubemap faces are [+X, -X, +Y, -Y, +Z, -Z], per https://www.w3.org/TR/webgpu/#texture-view-creation +// Note: Cubemap coordinates are left-handed y-up, unlike the rest of Bevy. +// See https://registry.khronos.org/vulkan/specs/1.2/html/chap16.html#_cube_map_face_selection +// +// For each cubemap face, we take care to specify the appropriate target/up axis such that the rendered +// texture using Bevy's right-handed y-up coordinate space matches the expected cubemap face in +// left-handed y-up cubemap coordinates. pub(crate) const CUBE_MAP_FACES: [CubeMapFace; 6] = [ - // 0 GL_TEXTURE_CUBE_MAP_POSITIVE_X - CubeMapFace { - target: Vec3::NEG_X, - up: Vec3::NEG_Y, - }, - // 1 GL_TEXTURE_CUBE_MAP_NEGATIVE_X + // +X CubeMapFace { target: Vec3::X, - up: Vec3::NEG_Y, + up: Vec3::Y, }, - // 2 GL_TEXTURE_CUBE_MAP_POSITIVE_Y + // -X CubeMapFace { - target: Vec3::NEG_Y, - up: Vec3::Z, + target: Vec3::NEG_X, + up: Vec3::Y, }, - // 3 GL_TEXTURE_CUBE_MAP_NEGATIVE_Y + // +Y CubeMapFace { target: Vec3::Y, + up: Vec3::Z, + }, + // -Y + CubeMapFace { + target: Vec3::NEG_Y, up: Vec3::NEG_Z, }, - // 4 GL_TEXTURE_CUBE_MAP_POSITIVE_Z + // +Z (with left-handed conventions, pointing forwards) CubeMapFace { target: Vec3::NEG_Z, - up: Vec3::NEG_Y, + up: Vec3::Y, }, - // 5 GL_TEXTURE_CUBE_MAP_NEGATIVE_Z + // -Z (with left-handed conventions, pointing backwards) CubeMapFace { target: Vec3::Z, - up: Vec3::NEG_Y, + up: Vec3::Y, }, ]; @@ -1691,8 +1697,6 @@ pub struct ShadowPassNode { } impl ShadowPassNode { - pub const IN_VIEW: &'static str = "view"; - pub fn new(world: &mut World) -> Self { Self { main_view_query: QueryState::new(world), @@ -1702,10 +1706,6 @@ impl ShadowPassNode { } impl Node for ShadowPassNode { - fn input(&self) -> Vec { - vec![SlotInfo::new(ShadowPassNode::IN_VIEW, SlotType::Entity)] - } - fn update(&mut self, world: &mut World) { self.main_view_query.update_archetypes(world); self.view_light_query.update_archetypes(world); @@ -1717,7 +1717,7 @@ impl Node for ShadowPassNode { render_context: &mut RenderContext, world: &World, ) -> Result<(), NodeRunError> { - let view_entity = graph.get_input_entity(Self::IN_VIEW)?; + let view_entity = graph.view_entity(); if let Ok(view_lights) = self.main_view_query.get_manual(world, view_entity) { for view_light_entity in view_lights.lights.iter().copied() { let (view_light, shadow_phase) = self diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index 4472324421..103b491f21 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -4,7 +4,7 @@ use crate::{ ViewClusterBindings, ViewFogUniformOffset, ViewLightsUniformOffset, ViewShadowBindings, CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT, MAX_CASCADES_PER_LIGHT, MAX_DIRECTIONAL_LIGHTS, }; -use bevy_app::{IntoSystemAppConfigs, Plugin}; +use bevy_app::Plugin; use bevy_asset::{load_internal_asset, Assets, Handle, HandleUntyped}; use bevy_core_pipeline::{ core_3d::ViewTransmissionTexture, @@ -37,7 +37,7 @@ use bevy_render::{ FallbackImagesMsaa, GpuImage, Image, ImageSampler, TextureFormatPixelInfo, }, view::{ComputedVisibility, ViewTarget, ViewUniform, ViewUniformOffset, ViewUniforms}, - Extract, ExtractSchedule, RenderApp, RenderSet, + Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; use bevy_transform::components::GlobalTransform; use std::num::NonZeroU64; @@ -108,10 +108,15 @@ impl Plugin for MeshRenderPlugin { render_app .init_resource::() .init_resource::() - .add_systems((extract_meshes, extract_skinned_meshes).in_schedule(ExtractSchedule)) - .add_system(prepare_skinned_meshes.in_set(RenderSet::Prepare)) - .add_system(queue_mesh_bind_group.in_set(RenderSet::Queue)) - .add_system(queue_mesh_view_bind_groups.in_set(RenderSet::Queue)); + .add_systems(ExtractSchedule, (extract_meshes, extract_skinned_meshes)) + .add_systems( + Render, + ( + prepare_skinned_meshes.in_set(RenderSet::Prepare), + queue_mesh_bind_group.in_set(RenderSet::Queue), + queue_mesh_view_bind_groups.in_set(RenderSet::Queue), + ), + ); } } } diff --git a/crates/bevy_pbr/src/render/shadows.wgsl b/crates/bevy_pbr/src/render/shadows.wgsl index f74733c900..8da216803b 100644 --- a/crates/bevy_pbr/src/render/shadows.wgsl +++ b/crates/bevy_pbr/src/render/shadows.wgsl @@ -1,5 +1,7 @@ #define_import_path bevy_pbr::shadows +const flip_z: vec3 = vec3(1.0, 1.0, -1.0); + fn fetch_point_shadow(light_id: u32, frag_position: vec4, surface_normal: vec3) -> f32 { let light = &point_lights.data[light_id]; @@ -17,7 +19,7 @@ fn fetch_point_shadow(light_id: u32, frag_position: vec4, surface_normal: v let offset_position = frag_position.xyz + normal_offset + depth_offset; // similar largest-absolute-axis trick as above, but now with the offset fragment position - let frag_ls = (*light).position_radius.xyz - offset_position.xyz; + let frag_ls = offset_position.xyz - (*light).position_radius.xyz ; let abs_position_ls = abs(frag_ls); let major_axis_magnitude = max(abs_position_ls.x, max(abs_position_ls.y, abs_position_ls.z)); @@ -28,16 +30,17 @@ fn fetch_point_shadow(light_id: u32, frag_position: vec4, surface_normal: v let zw = -major_axis_magnitude * (*light).light_custom_data.xy + (*light).light_custom_data.zw; let depth = zw.x / zw.y; - // do the lookup, using HW PCF and comparison + // Do the lookup, using HW PCF and comparison. Cubemaps assume a left-handed coordinate space, + // so we have to flip the z-axis when sampling. // NOTE: Due to the non-uniform control flow above, we must use the Level variant of // textureSampleCompare to avoid undefined behaviour due to some of the fragments in // a quad (2x2 fragments) being processed not being sampled, and this messing with // mip-mapping functionality. The shadow maps have no mipmaps so Level just samples // from LOD 0. #ifdef NO_ARRAY_TEXTURES_SUPPORT - return textureSampleCompare(point_shadow_textures, point_shadow_textures_sampler, frag_ls, depth); + return textureSampleCompare(point_shadow_textures, point_shadow_textures_sampler, frag_ls * flip_z, depth); #else - return textureSampleCompareLevel(point_shadow_textures, point_shadow_textures_sampler, frag_ls, i32(light_id), depth); + return textureSampleCompareLevel(point_shadow_textures, point_shadow_textures_sampler, frag_ls * flip_z, i32(light_id), depth); #endif } diff --git a/crates/bevy_pbr/src/wireframe.rs b/crates/bevy_pbr/src/wireframe.rs index 34d93c9185..7e885f9231 100644 --- a/crates/bevy_pbr/src/wireframe.rs +++ b/crates/bevy_pbr/src/wireframe.rs @@ -7,6 +7,7 @@ use bevy_ecs::{prelude::*, reflect::ReflectComponent}; use bevy_reflect::std_traits::ReflectDefault; use bevy_reflect::{Reflect, TypeUuid}; use bevy_render::extract_component::{ExtractComponent, ExtractComponentPlugin}; +use bevy_render::Render; use bevy_render::{ extract_resource::{ExtractResource, ExtractResourcePlugin}, mesh::{Mesh, MeshVertexBufferLayout}, @@ -47,7 +48,7 @@ impl Plugin for WireframePlugin { .add_render_command::() .init_resource::() .init_resource::>() - .add_system(queue_wireframes.in_set(RenderSet::Queue)); + .add_systems(Render, queue_wireframes.in_set(RenderSet::Queue)); } } } diff --git a/crates/bevy_reflect/bevy_reflect_derive/src/utility.rs b/crates/bevy_reflect/bevy_reflect_derive/src/utility.rs index cb9b11957c..c0675d5363 100644 --- a/crates/bevy_reflect/bevy_reflect_derive/src/utility.rs +++ b/crates/bevy_reflect/bevy_reflect_derive/src/utility.rs @@ -92,7 +92,7 @@ impl Default for WhereClauseOptions { /// # Arguments /// /// * `where_clause`: existing `where` clause present on the object to be derived -/// * `where_clause_options`: additional paramters defining which trait bounds to add to the `where` clause +/// * `where_clause_options`: additional parameters defining which trait bounds to add to the `where` clause /// /// # Example /// diff --git a/crates/bevy_reflect/src/enums/dynamic_enum.rs b/crates/bevy_reflect/src/enums/dynamic_enum.rs index fd3a8585b7..db9e050fbe 100644 --- a/crates/bevy_reflect/src/enums/dynamic_enum.rs +++ b/crates/bevy_reflect/src/enums/dynamic_enum.rs @@ -8,8 +8,9 @@ use std::any::Any; use std::fmt::Formatter; /// A dynamic representation of an enum variant. -#[derive(Debug)] +#[derive(Debug, Default)] pub enum DynamicVariant { + #[default] Unit, Tuple(DynamicTuple), Struct(DynamicStruct), @@ -25,12 +26,6 @@ impl Clone for DynamicVariant { } } -impl Default for DynamicVariant { - fn default() -> Self { - DynamicVariant::Unit - } -} - impl From for DynamicVariant { fn from(dyn_tuple: DynamicTuple) -> Self { Self::Tuple(dyn_tuple) diff --git a/crates/bevy_reflect_compile_fail_tests/tests/reflect_derive/generics.fail.stderr b/crates/bevy_reflect_compile_fail_tests/tests/reflect_derive/generics.fail.stderr index f59304dbd4..0f568ed9c5 100644 --- a/crates/bevy_reflect_compile_fail_tests/tests/reflect_derive/generics.fail.stderr +++ b/crates/bevy_reflect_compile_fail_tests/tests/reflect_derive/generics.fail.stderr @@ -24,7 +24,7 @@ note: required for `Foo` to implement `Reflect` --> tests/reflect_derive/generics.fail.rs:3:10 | 3 | #[derive(Reflect)] - | ^^^^^^^ + | ^^^^^^^ unsatisfied trait bound introduced in this `derive` macro 4 | struct Foo { | ^^^^^^ = note: required for the cast from `Foo` to the object type `dyn Reflect` diff --git a/crates/bevy_render/src/camera/camera.rs b/crates/bevy_render/src/camera/camera.rs index 27687fc863..8b0e44ab0b 100644 --- a/crates/bevy_render/src/camera/camera.rs +++ b/crates/bevy_render/src/camera/camera.rs @@ -492,7 +492,7 @@ impl NormalizedRenderTarget { /// The system function is generic over the camera projection type, and only instances of /// [`OrthographicProjection`] and [`PerspectiveProjection`] are automatically added to /// the app, as well as the runtime-selected [`Projection`]. -/// The system runs during [`CoreSet::PostUpdate`]. +/// The system runs during [`PostUpdate`](bevy_app::PostUpdate). /// /// ## World Resources /// @@ -502,7 +502,6 @@ impl NormalizedRenderTarget { /// [`OrthographicProjection`]: crate::camera::OrthographicProjection /// [`PerspectiveProjection`]: crate::camera::PerspectiveProjection /// [`Projection`]: crate::camera::Projection -/// [`CoreSet::PostUpdate`]: bevy_app::CoreSet::PostUpdate pub fn camera_system( mut window_resized_events: EventReader, mut window_created_events: EventReader, diff --git a/crates/bevy_render/src/camera/camera_driver_node.rs b/crates/bevy_render/src/camera/camera_driver_node.rs index dcee0cba45..84ff1896fc 100644 --- a/crates/bevy_render/src/camera/camera_driver_node.rs +++ b/crates/bevy_render/src/camera/camera_driver_node.rs @@ -1,6 +1,6 @@ use crate::{ camera::{ExtractedCamera, NormalizedRenderTarget, SortedCameras}, - render_graph::{Node, NodeRunError, RenderGraphContext, SlotValue}, + render_graph::{Node, NodeRunError, RenderGraphContext}, renderer::RenderContext, view::ExtractedWindows, }; @@ -39,7 +39,8 @@ impl Node for CameraDriverNode { } graph.run_sub_graph( camera.render_graph.clone(), - vec![SlotValue::Entity(sorted_camera.entity)], + vec![], + Some(sorted_camera.entity), )?; } } diff --git a/crates/bevy_render/src/camera/mod.rs b/crates/bevy_render/src/camera/mod.rs index 91ea69d941..44d72cb03e 100644 --- a/crates/bevy_render/src/camera/mod.rs +++ b/crates/bevy_render/src/camera/mod.rs @@ -7,9 +7,9 @@ pub use camera::*; pub use camera_driver_node::*; pub use projection::*; -use crate::{render_graph::RenderGraph, ExtractSchedule, RenderApp, RenderSet}; -use bevy_app::{App, IntoSystemAppConfig, Plugin}; -use bevy_ecs::schedule::IntoSystemConfig; +use crate::{render_graph::RenderGraph, ExtractSchedule, Render, RenderApp, RenderSet}; +use bevy_app::{App, Plugin}; +use bevy_ecs::schedule::IntoSystemConfigs; #[derive(Default)] pub struct CameraPlugin; @@ -29,8 +29,8 @@ impl Plugin for CameraPlugin { if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { render_app .init_resource::() - .add_system(extract_cameras.in_schedule(ExtractSchedule)) - .add_system(sort_cameras.in_set(RenderSet::Prepare)); + .add_systems(ExtractSchedule, extract_cameras) + .add_systems(Render, sort_cameras.in_set(RenderSet::Prepare)); let camera_driver_node = CameraDriverNode::new(&mut render_app.world); let mut render_graph = render_app.world.resource_mut::(); render_graph.add_node(crate::main_graph::node::CAMERA_DRIVER, camera_driver_node); diff --git a/crates/bevy_render/src/camera/projection.rs b/crates/bevy_render/src/camera/projection.rs index 34757708f3..ad3731be45 100644 --- a/crates/bevy_render/src/camera/projection.rs +++ b/crates/bevy_render/src/camera/projection.rs @@ -1,6 +1,6 @@ use std::marker::PhantomData; -use bevy_app::{App, CoreSchedule, CoreSet, Plugin, StartupSet}; +use bevy_app::{App, Plugin, PostStartup, PostUpdate}; use bevy_ecs::{prelude::*, reflect::ReflectComponent}; use bevy_math::{Mat4, Rect, Vec2}; use bevy_reflect::{ @@ -27,11 +27,8 @@ pub struct CameraUpdateSystem; impl Plugin for CameraProjectionPlugin { fn build(&self, app: &mut App) { app.register_type::() - .edit_schedule(CoreSchedule::Startup, |schedule| { - schedule.configure_set(CameraUpdateSystem.in_base_set(StartupSet::PostStartup)); - }) - .configure_set(CameraUpdateSystem.in_base_set(CoreSet::PostUpdate)) - .add_startup_system( + .add_systems( + PostStartup, crate::camera::camera_system:: .in_set(CameraUpdateSystem) // We assume that each camera will only have one projection, @@ -39,7 +36,8 @@ impl Plugin for CameraPro // FIXME: Add an archetype invariant for this https://github.com/bevyengine/bevy/issues/1481. .ambiguous_with(CameraUpdateSystem), ) - .add_system( + .add_systems( + PostUpdate, crate::camera::camera_system:: .in_set(CameraUpdateSystem) // We assume that each camera will only have one projection, diff --git a/crates/bevy_render/src/color/mod.rs b/crates/bevy_render/src/color/mod.rs index 770a7bee9c..d66a455cba 100644 --- a/crates/bevy_render/src/color/mod.rs +++ b/crates/bevy_render/src/color/mod.rs @@ -253,6 +253,43 @@ impl Color { } } + /// New `Color` with LCH representation in sRGB colorspace. + /// + /// # Arguments + /// + /// * `lightness` - Lightness channel. [0.0, 1.5] + /// * `chroma` - Chroma channel. [0.0, 1.5] + /// * `hue` - Hue channel. [0.0, 360.0] + /// + /// See also [`Color::lcha`]. + pub const fn lch(lightness: f32, chroma: f32, hue: f32) -> Color { + Color::Lcha { + lightness, + chroma, + hue, + alpha: 1.0, + } + } + + /// New `Color` with LCH representation in sRGB colorspace. + /// + /// # Arguments + /// + /// * `lightness` - Lightness channel. [0.0, 1.5] + /// * `chroma` - Chroma channel. [0.0, 1.5] + /// * `hue` - Hue channel. [0.0, 360.0] + /// * `alpha` - Alpha channel. [0.0, 1.0] + /// + /// See also [`Color::lch`]. + pub const fn lcha(lightness: f32, chroma: f32, hue: f32, alpha: f32) -> Color { + Color::Lcha { + lightness, + chroma, + hue, + alpha, + } + } + /// New `Color` from sRGB colorspace. /// /// # Examples @@ -525,7 +562,7 @@ impl Color { let [red, green, blue] = LchRepresentation::lch_to_nonlinear_srgb(*lightness, *chroma, *hue); - Color::Rgba { + Color::RgbaLinear { red: red.nonlinear_to_linear_srgb(), green: green.nonlinear_to_linear_srgb(), blue: blue.nonlinear_to_linear_srgb(), @@ -1858,4 +1895,17 @@ mod tests { assert_eq!(starting_color * transformation, mutated_color,); } + + // regression test for https://github.com/bevyengine/bevy/pull/8040 + #[test] + fn convert_to_rgba_linear() { + let rgba = Color::rgba(0., 0., 0., 0.); + let rgba_l = Color::rgba_linear(0., 0., 0., 0.); + let hsla = Color::hsla(0., 0., 0., 0.); + let lcha = Color::lcha(0., 0., 0., 0.); + assert_eq!(rgba_l, rgba_l.as_rgba_linear()); + let Color::RgbaLinear { .. } = rgba.as_rgba_linear() else { panic!("from Rgba") }; + let Color::RgbaLinear { .. } = hsla.as_rgba_linear() else { panic!("from Hsla") }; + let Color::RgbaLinear { .. } = lcha.as_rgba_linear() else { panic!("from Lcha") }; + } } diff --git a/crates/bevy_render/src/extract_component.rs b/crates/bevy_render/src/extract_component.rs index 2b70a897a1..606c8ccbe0 100644 --- a/crates/bevy_render/src/extract_component.rs +++ b/crates/bevy_render/src/extract_component.rs @@ -2,9 +2,9 @@ use crate::{ render_resource::{encase::internal::WriteInto, DynamicUniformBuffer, ShaderType}, renderer::{RenderDevice, RenderQueue}, view::ComputedVisibility, - Extract, ExtractSchedule, RenderApp, RenderSet, + Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; -use bevy_app::{App, IntoSystemAppConfig, Plugin}; +use bevy_app::{App, Plugin}; use bevy_asset::{Asset, Handle}; use bevy_ecs::{ component::Component, @@ -83,7 +83,10 @@ impl Plugin for UniformComponentP if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { render_app .insert_resource(ComponentUniforms::::default()) - .add_system(prepare_uniform_components::.in_set(RenderSet::Prepare)); + .add_systems( + Render, + prepare_uniform_components::.in_set(RenderSet::Prepare), + ); } } } @@ -180,9 +183,9 @@ impl Plugin for ExtractComponentPlugin { fn build(&self, app: &mut App) { if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { if self.only_extract_visible { - render_app.add_system(extract_visible_components::.in_schedule(ExtractSchedule)); + render_app.add_systems(ExtractSchedule, extract_visible_components::); } else { - render_app.add_system(extract_components::.in_schedule(ExtractSchedule)); + render_app.add_systems(ExtractSchedule, extract_components::); } } } diff --git a/crates/bevy_render/src/extract_param.rs b/crates/bevy_render/src/extract_param.rs index 92290407f5..898dfe0e28 100644 --- a/crates/bevy_render/src/extract_param.rs +++ b/crates/bevy_render/src/extract_param.rs @@ -1,5 +1,6 @@ use crate::MainWorld; use bevy_ecs::{ + component::Tick, prelude::*, system::{ReadOnlySystemParam, SystemMeta, SystemParam, SystemParamItem, SystemState}, }; @@ -76,7 +77,7 @@ where state: &'s mut Self::State, system_meta: &SystemMeta, world: &'w World, - change_tick: u32, + change_tick: Tick, ) -> Self::Item<'w, 's> { // SAFETY: // - The caller ensures that `world` is the same one that `init_state` was called with. diff --git a/crates/bevy_render/src/extract_resource.rs b/crates/bevy_render/src/extract_resource.rs index a1e8b122f0..ea8f7e81fd 100644 --- a/crates/bevy_render/src/extract_resource.rs +++ b/crates/bevy_render/src/extract_resource.rs @@ -1,6 +1,6 @@ use std::marker::PhantomData; -use bevy_app::{App, IntoSystemAppConfig, Plugin}; +use bevy_app::{App, Plugin}; use bevy_ecs::prelude::*; pub use bevy_render_macros::ExtractResource; @@ -32,7 +32,7 @@ impl Default for ExtractResourcePlugin { impl Plugin for ExtractResourcePlugin { fn build(&self, app: &mut App) { if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { - render_app.add_system(extract_resource::.in_schedule(ExtractSchedule)); + render_app.add_systems(ExtractSchedule, extract_resource::); } } } diff --git a/crates/bevy_render/src/globals.rs b/crates/bevy_render/src/globals.rs index 6b9fbdbc7f..ef585000b8 100644 --- a/crates/bevy_render/src/globals.rs +++ b/crates/bevy_render/src/globals.rs @@ -3,9 +3,9 @@ use crate::{ prelude::Shader, render_resource::{ShaderType, UniformBuffer}, renderer::{RenderDevice, RenderQueue}, - Extract, ExtractSchedule, RenderApp, RenderSet, + Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; -use bevy_app::{App, IntoSystemAppConfigs, Plugin}; +use bevy_app::{App, Plugin}; use bevy_asset::{load_internal_asset, HandleUntyped}; use bevy_core::FrameCount; use bevy_ecs::prelude::*; @@ -26,8 +26,8 @@ impl Plugin for GlobalsPlugin { render_app .init_resource::() .init_resource:: { fn build(&self, app: &mut App) { if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { render_app + .init_resource::>() + .init_resource::>() + .init_resource::>() + .add_systems(ExtractSchedule, extract_render_asset::) .configure_sets( + Render, ( PrepareAssetSet::PreAssetPrepare, PrepareAssetSet::AssetPrepare, @@ -89,11 +94,10 @@ impl Plugin for RenderAssetPlugin { .chain() .in_set(RenderSet::Prepare), ) - .init_resource::>() - .init_resource::>() - .init_resource::>() - .add_system(extract_render_asset::.in_schedule(ExtractSchedule)) - .add_system(prepare_assets::.in_set(self.prepare_asset_set.clone())); + .add_systems( + Render, + prepare_assets::.in_set(self.prepare_asset_set.clone()), + ); } } } diff --git a/crates/bevy_render/src/render_graph/context.rs b/crates/bevy_render/src/render_graph/context.rs index 6a4ee01afa..da983fdc7f 100644 --- a/crates/bevy_render/src/render_graph/context.rs +++ b/crates/bevy_render/src/render_graph/context.rs @@ -11,6 +11,7 @@ use thiserror::Error; pub struct RunSubGraph { pub name: Cow<'static, str>, pub inputs: Vec, + pub view_entity: Option, } /// The context with all graph information required to run a [`Node`](super::Node). @@ -27,6 +28,11 @@ pub struct RenderGraphContext<'a> { inputs: &'a [SlotValue], outputs: &'a mut [Option], run_sub_graphs: Vec, + /// The view_entity associated with the render graph being executed + /// This is optional because you aren't required to have a view_entity for a node. + /// For example, compute shader nodes don't have one. + /// It should always be set when the RenderGraph is running on a View. + view_entity: Option, } impl<'a> RenderGraphContext<'a> { @@ -43,6 +49,7 @@ impl<'a> RenderGraphContext<'a> { inputs, outputs, run_sub_graphs: Vec::new(), + view_entity: None, } } @@ -158,11 +165,24 @@ impl<'a> RenderGraphContext<'a> { Ok(()) } + pub fn view_entity(&self) -> Entity { + self.view_entity.unwrap() + } + + pub fn get_view_entity(&self) -> Option { + self.view_entity + } + + pub fn set_view_entity(&mut self, view_entity: Entity) { + self.view_entity = Some(view_entity); + } + /// Queues up a sub graph for execution after the node has finished running. pub fn run_sub_graph( &mut self, name: impl Into>, inputs: Vec, + view_entity: Option, ) -> Result<(), RunSubGraphError> { let name = name.into(); let sub_graph = self @@ -193,7 +213,11 @@ impl<'a> RenderGraphContext<'a> { return Err(RunSubGraphError::SubGraphHasNoInputs(name)); } - self.run_sub_graphs.push(RunSubGraph { name, inputs }); + self.run_sub_graphs.push(RunSubGraph { + name, + inputs, + view_entity, + }); Ok(()) } diff --git a/crates/bevy_render/src/render_graph/node.rs b/crates/bevy_render/src/render_graph/node.rs index 11db4aca83..fea7eee2d3 100644 --- a/crates/bevy_render/src/render_graph/node.rs +++ b/crates/bevy_render/src/render_graph/node.rs @@ -2,7 +2,7 @@ use crate::{ define_atomic_id, render_graph::{ Edge, InputSlotError, OutputSlotError, RenderGraphContext, RenderGraphError, - RunSubGraphError, SlotInfo, SlotInfos, SlotType, SlotValue, + RunSubGraphError, SlotInfo, SlotInfos, }, renderer::RenderContext, }; @@ -306,14 +306,13 @@ impl Node for EmptyNode { } } -/// A [`RenderGraph`](super::RenderGraph) [`Node`] that takes a view entity as input and runs the configured graph name once. +/// A [`RenderGraph`](super::RenderGraph) [`Node`] that runs the configured graph name once. /// This makes it easier to insert sub-graph runs into a graph. pub struct RunGraphOnViewNode { graph_name: Cow<'static, str>, } impl RunGraphOnViewNode { - pub const IN_VIEW: &'static str = "view"; pub fn new>>(graph_name: T) -> Self { Self { graph_name: graph_name.into(), @@ -322,20 +321,13 @@ impl RunGraphOnViewNode { } impl Node for RunGraphOnViewNode { - fn input(&self) -> Vec { - vec![SlotInfo::new(Self::IN_VIEW, SlotType::Entity)] - } fn run( &self, graph: &mut RenderGraphContext, _render_context: &mut RenderContext, _world: &World, ) -> Result<(), NodeRunError> { - let view_entity = graph.get_input_entity(Self::IN_VIEW)?; - graph.run_sub_graph( - self.graph_name.clone(), - vec![SlotValue::Entity(view_entity)], - )?; + graph.run_sub_graph(self.graph_name.clone(), vec![], Some(graph.view_entity()))?; Ok(()) } } diff --git a/crates/bevy_render/src/render_resource/shader.rs b/crates/bevy_render/src/render_resource/shader.rs index c45c687e90..a0b874c092 100644 --- a/crates/bevy_render/src/render_resource/shader.rs +++ b/crates/bevy_render/src/render_resource/shader.rs @@ -296,8 +296,6 @@ pub enum ProcessShaderError { "Not enough '# endif' lines. Each if statement should be followed by an endif statement." )] NotEnoughEndIfs, - #[error("This Shader's format does not support processing shader defs.")] - ShaderFormatDoesNotSupportShaderDefs, #[error("This Shader's format does not support imports.")] ShaderFormatDoesNotSupportImports, #[error("Unresolved import: {0:?}.")] @@ -477,10 +475,7 @@ impl ShaderProcessor { Source::Wgsl(source) => source.deref(), Source::Glsl(source, _stage) => source.deref(), Source::SpirV(source) => { - if shader_defs_unique.is_empty() { - return Ok(ProcessedShader::SpirV(source.clone())); - } - return Err(ProcessShaderError::ShaderFormatDoesNotSupportShaderDefs); + return Ok(ProcessedShader::SpirV(source.clone())); } }; diff --git a/crates/bevy_render/src/renderer/graph_runner.rs b/crates/bevy_render/src/renderer/graph_runner.rs index e11c61f6ca..478298dd42 100644 --- a/crates/bevy_render/src/renderer/graph_runner.rs +++ b/crates/bevy_render/src/renderer/graph_runner.rs @@ -1,4 +1,4 @@ -use bevy_ecs::world::World; +use bevy_ecs::{prelude::Entity, world::World}; #[cfg(feature = "trace")] use bevy_utils::tracing::info_span; use bevy_utils::HashMap; @@ -59,7 +59,7 @@ impl RenderGraphRunner { world: &World, ) -> Result<(), RenderGraphRunnerError> { let mut render_context = RenderContext::new(render_device); - Self::run_graph(graph, None, &mut render_context, world, &[])?; + Self::run_graph(graph, None, &mut render_context, world, &[], None)?; { #[cfg(feature = "trace")] let _span = info_span!("submit_graph_commands").entered(); @@ -74,6 +74,7 @@ impl RenderGraphRunner { render_context: &mut RenderContext, world: &World, inputs: &[SlotValue], + view_entity: Option, ) -> Result<(), RenderGraphRunnerError> { let mut node_outputs: HashMap> = HashMap::default(); #[cfg(feature = "trace")] @@ -175,6 +176,10 @@ impl RenderGraphRunner { smallvec![None; node_state.output_slots.len()]; { let mut context = RenderGraphContext::new(graph, node_state, &inputs, &mut outputs); + if let Some(view_entity) = view_entity { + context.set_view_entity(view_entity); + } + { #[cfg(feature = "trace")] let _span = info_span!("node", name = node_state.type_name).entered(); @@ -192,6 +197,7 @@ impl RenderGraphRunner { render_context, world, &run_sub_graph.inputs, + run_sub_graph.view_entity, )?; } } diff --git a/crates/bevy_render/src/texture/mod.rs b/crates/bevy_render/src/texture/mod.rs index f9b32a1eb7..1f4f45a83b 100644 --- a/crates/bevy_render/src/texture/mod.rs +++ b/crates/bevy_render/src/texture/mod.rs @@ -33,7 +33,7 @@ pub use texture_cache::*; use crate::{ render_asset::{PrepareAssetSet, RenderAssetPlugin}, renderer::RenderDevice, - RenderApp, RenderSet, + Render, RenderApp, RenderSet, }; use bevy_app::{App, Plugin}; use bevy_asset::{AddAsset, Assets}; @@ -115,7 +115,10 @@ impl Plugin for ImagePlugin { .init_resource::() .init_resource::() .init_resource::() - .add_system(update_texture_cache_system.in_set(RenderSet::Cleanup)); + .add_systems( + Render, + update_texture_cache_system.in_set(RenderSet::Cleanup), + ); } } } diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index 7a709cb3a8..246aa21ed0 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -14,7 +14,7 @@ use crate::{ render_resource::{DynamicUniformBuffer, ShaderType, Texture, TextureView}, renderer::{RenderDevice, RenderQueue}, texture::{BevyDefault, TextureCache}, - RenderApp, RenderSet, + Render, RenderApp, RenderSet, }; use bevy_app::{App, Plugin}; use bevy_ecs::prelude::*; @@ -55,13 +55,16 @@ impl Plugin for ViewPlugin { if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { render_app .init_resource::() - .configure_set(ViewSet::PrepareUniforms.in_set(RenderSet::Prepare)) - .add_system(prepare_view_uniforms.in_set(ViewSet::PrepareUniforms)) - .add_system( - prepare_view_targets - .after(WindowSystem::Prepare) - .in_set(RenderSet::Prepare) - .after(crate::render_asset::prepare_assets::), + .configure_set(Render, ViewSet::PrepareUniforms.in_set(RenderSet::Prepare)) + .add_systems( + Render, + ( + prepare_view_uniforms.in_set(ViewSet::PrepareUniforms), + prepare_view_targets + .after(WindowSystem::Prepare) + .in_set(RenderSet::Prepare) + .after(crate::render_asset::prepare_assets::), + ), ); } } diff --git a/crates/bevy_render/src/view/visibility/mod.rs b/crates/bevy_render/src/view/visibility/mod.rs index ca5e8a9df2..25f0fc02fa 100644 --- a/crates/bevy_render/src/view/visibility/mod.rs +++ b/crates/bevy_render/src/view/visibility/mod.rs @@ -2,7 +2,7 @@ mod render_layers; pub use render_layers::*; -use bevy_app::{CoreSet, Plugin}; +use bevy_app::{Plugin, PostUpdate}; use bevy_asset::{Assets, Handle}; use bevy_ecs::prelude::*; use bevy_hierarchy::{Children, Parent}; @@ -91,8 +91,8 @@ impl ComputedVisibility { /// Whether this entity is visible to something this frame. This is true if and only if [`Self::is_visible_in_hierarchy`] and [`Self::is_visible_in_view`] /// are true. This is the canonical method to call to determine if an entity should be drawn. - /// This value is updated in [`CoreSet::PostUpdate`] by the [`VisibilitySystems::CheckVisibility`] system set. - /// Reading it during [`CoreSet::Update`] will yield the value from the previous frame. + /// This value is updated in [`PostUpdate`] by the [`VisibilitySystems::CheckVisibility`] system set. + /// Reading it during [`Update`](bevy_app::Update) will yield the value from the previous frame. #[inline] pub fn is_visible(&self) -> bool { self.flags.bits == ComputedVisibilityFlags::all().bits @@ -100,7 +100,7 @@ impl ComputedVisibility { /// Whether this entity is visible in the entity hierarchy, which is determined by the [`Visibility`] component. /// This takes into account "visibility inheritance". If any of this entity's ancestors (see [`Parent`]) are hidden, this entity - /// will be hidden as well. This value is updated in the [`VisibilitySystems::VisibilityPropagate`], which lives under the [`CoreSet::PostUpdate`] set. + /// will be hidden as well. This value is updated in the [`VisibilitySystems::VisibilityPropagate`], which lives in the [`PostUpdate`] schedule. #[inline] pub fn is_visible_in_hierarchy(&self) -> bool { self.flags @@ -110,8 +110,8 @@ impl ComputedVisibility { /// Whether this entity is visible in _any_ view (Cameras, Lights, etc). Each entity type (and view type) should choose how to set this /// value. For cameras and drawn entities, this will take into account [`RenderLayers`]. /// - /// This value is reset to `false` every frame in [`VisibilitySystems::VisibilityPropagate`] during [`CoreSet::PostUpdate`]. - /// Each entity type then chooses how to set this field in the [`VisibilitySystems::CheckVisibility`] system set, under [`CoreSet::PostUpdate`]. + /// This value is reset to `false` every frame in [`VisibilitySystems::VisibilityPropagate`] during [`PostUpdate`]. + /// Each entity type then chooses how to set this field in the [`VisibilitySystems::CheckVisibility`] system set, in [`PostUpdate`]. /// Meshes might use frustum culling to decide if they are visible in a view. /// Other entities might just set this to `true` every frame. #[inline] @@ -210,57 +210,48 @@ impl Plugin for VisibilityPlugin { fn build(&self, app: &mut bevy_app::App) { use VisibilitySystems::*; - app.configure_set(CalculateBounds.in_base_set(CoreSet::PostUpdate)) + app // We add an AABB component in CalculateBounds, which must be ready on the same frame. - .add_system(apply_system_buffers.in_set(CalculateBoundsFlush)) - .configure_set( - CalculateBoundsFlush - .after(CalculateBounds) - .in_base_set(CoreSet::PostUpdate), + .add_systems( + PostUpdate, + apply_system_buffers.in_set(CalculateBoundsFlush), ) - .configure_set(UpdateOrthographicFrusta.in_base_set(CoreSet::PostUpdate)) - .configure_set(UpdatePerspectiveFrusta.in_base_set(CoreSet::PostUpdate)) - .configure_set(UpdateProjectionFrusta.in_base_set(CoreSet::PostUpdate)) - .configure_set(CheckVisibility.in_base_set(CoreSet::PostUpdate)) - .configure_set(VisibilityPropagate.in_base_set(CoreSet::PostUpdate)) - .add_system(calculate_bounds.in_set(CalculateBounds)) - .add_system( - update_frusta:: - .in_set(UpdateOrthographicFrusta) - .after(camera_system::) - .after(TransformSystem::TransformPropagate) - // We assume that no camera will have more than one projection component, - // so these systems will run independently of one another. - // FIXME: Add an archetype invariant for this https://github.com/bevyengine/bevy/issues/1481. - .ambiguous_with(update_frusta::) - .ambiguous_with(update_frusta::), - ) - .add_system( - update_frusta:: - .in_set(UpdatePerspectiveFrusta) - .after(camera_system::) - .after(TransformSystem::TransformPropagate) - // We assume that no camera will have more than one projection component, - // so these systems will run independently of one another. - // FIXME: Add an archetype invariant for this https://github.com/bevyengine/bevy/issues/1481. - .ambiguous_with(update_frusta::), - ) - .add_system( - update_frusta:: - .in_set(UpdateProjectionFrusta) - .after(camera_system::) - .after(TransformSystem::TransformPropagate), - ) - .add_system(visibility_propagate_system.in_set(VisibilityPropagate)) - .add_system( - check_visibility - .in_set(CheckVisibility) - .after(CalculateBoundsFlush) - .after(UpdateOrthographicFrusta) - .after(UpdatePerspectiveFrusta) - .after(UpdateProjectionFrusta) - .after(VisibilityPropagate) - .after(TransformSystem::TransformPropagate), + .configure_set(PostUpdate, CalculateBoundsFlush.after(CalculateBounds)) + .add_systems( + PostUpdate, + ( + calculate_bounds.in_set(CalculateBounds), + update_frusta:: + .in_set(UpdateOrthographicFrusta) + .after(camera_system::) + .after(TransformSystem::TransformPropagate) + // We assume that no camera will have more than one projection component, + // so these systems will run independently of one another. + // FIXME: Add an archetype invariant for this https://github.com/bevyengine/bevy/issues/1481. + .ambiguous_with(update_frusta::) + .ambiguous_with(update_frusta::), + update_frusta:: + .in_set(UpdatePerspectiveFrusta) + .after(camera_system::) + .after(TransformSystem::TransformPropagate) + // We assume that no camera will have more than one projection component, + // so these systems will run independently of one another. + // FIXME: Add an archetype invariant for this https://github.com/bevyengine/bevy/issues/1481. + .ambiguous_with(update_frusta::), + update_frusta:: + .in_set(UpdateProjectionFrusta) + .after(camera_system::) + .after(TransformSystem::TransformPropagate), + visibility_propagate_system.in_set(VisibilityPropagate), + check_visibility + .in_set(CheckVisibility) + .after(CalculateBoundsFlush) + .after(UpdateOrthographicFrusta) + .after(UpdatePerspectiveFrusta) + .after(UpdateProjectionFrusta) + .after(VisibilityPropagate) + .after(TransformSystem::TransformPropagate), + ), ); } } @@ -463,7 +454,7 @@ mod test { #[test] fn visibility_propagation() { let mut app = App::new(); - app.add_system(visibility_propagate_system); + app.add_systems(Update, visibility_propagate_system); let root1 = app .world @@ -582,7 +573,7 @@ mod test { #[test] fn visibility_propagation_unconditional_visible() { let mut app = App::new(); - app.add_system(visibility_propagate_system); + app.add_systems(Update, visibility_propagate_system); let root1 = app .world diff --git a/crates/bevy_render/src/view/window.rs b/crates/bevy_render/src/view/window.rs index ca9fac5aab..8118e4fd06 100644 --- a/crates/bevy_render/src/view/window.rs +++ b/crates/bevy_render/src/view/window.rs @@ -1,9 +1,9 @@ use crate::{ render_resource::TextureView, renderer::{RenderAdapter, RenderDevice, RenderInstance}, - Extract, ExtractSchedule, RenderApp, RenderSet, + Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; -use bevy_app::{App, IntoSystemAppConfig, Plugin}; +use bevy_app::{App, Plugin}; use bevy_ecs::prelude::*; use bevy_utils::{tracing::debug, HashMap, HashSet}; use bevy_window::{ @@ -32,9 +32,9 @@ impl Plugin for WindowRenderPlugin { .init_resource::() .init_resource::() .init_non_send_resource::() - .add_system(extract_windows.in_schedule(ExtractSchedule)) - .configure_set(WindowSystem::Prepare.in_set(RenderSet::Prepare)) - .add_system(prepare_windows.in_set(WindowSystem::Prepare)); + .add_systems(ExtractSchedule, extract_windows) + .configure_set(Render, WindowSystem::Prepare.in_set(RenderSet::Prepare)) + .add_systems(Render, prepare_windows.in_set(WindowSystem::Prepare)); } } } diff --git a/crates/bevy_scene/src/dynamic_scene.rs b/crates/bevy_scene/src/dynamic_scene.rs index cb5bd1a387..adac64c425 100644 --- a/crates/bevy_scene/src/dynamic_scene.rs +++ b/crates/bevy_scene/src/dynamic_scene.rs @@ -10,10 +10,13 @@ use bevy_reflect::{Reflect, TypeRegistryArc, TypeUuid}; #[cfg(feature = "serialize")] use crate::serde::SceneSerializer; +use bevy_ecs::reflect::ReflectResource; #[cfg(feature = "serialize")] use serde::Serialize; -/// A collection of serializable dynamic entities, each with its own run-time defined set of components. +/// A collection of serializable resources and dynamic entities. +/// +/// Each dynamic entity in the collection contains its own run-time defined set of components. /// To spawn a dynamic scene, you can use either: /// * [`SceneSpawner::spawn_dynamic`](crate::SceneSpawner::spawn_dynamic) /// * adding the [`DynamicSceneBundle`](crate::DynamicSceneBundle) to an entity @@ -23,6 +26,7 @@ use serde::Serialize; #[derive(Default, TypeUuid)] #[uuid = "749479b1-fb8c-4ff8-a775-623aa76014f5"] pub struct DynamicScene { + pub resources: Vec>, pub entities: Vec, } @@ -47,15 +51,16 @@ impl DynamicScene { DynamicSceneBuilder::from_world_with_type_registry(world, type_registry.clone()); builder.extract_entities(world.iter_entities().map(|entity| entity.id())); + builder.extract_resources(); builder.build() } - /// Write the dynamic entities and their corresponding components to the given world. + /// Write the resources, the dynamic entities, and their corresponding components to the given world. /// /// This method will return a [`SceneSpawnError`] if a type either is not registered /// in the provided [`AppTypeRegistry`] resource, or doesn't reflect the - /// [`Component`](bevy_ecs::component::Component) trait. + /// [`Component`](bevy_ecs::component::Component) or [`Resource`](bevy_ecs::prelude::Resource) trait. pub fn write_to_world_with( &self, world: &mut World, @@ -64,6 +69,23 @@ impl DynamicScene { ) -> Result<(), SceneSpawnError> { let type_registry = type_registry.read(); + for resource in &self.resources { + let registration = type_registry + .get_with_name(resource.type_name()) + .ok_or_else(|| SceneSpawnError::UnregisteredType { + type_name: resource.type_name().to_string(), + })?; + let reflect_resource = registration.data::().ok_or_else(|| { + SceneSpawnError::UnregisteredResource { + type_name: resource.type_name().to_string(), + } + })?; + + // If the world already contains an instance of the given resource + // just apply the (possibly) new value, otherwise insert the resource + reflect_resource.apply_or_insert(world, &**resource); + } + for scene_entity in &self.entities { // Fetch the entity with the given entity id from the `entity_map` // or spawn a new entity with a transiently unique id if there is @@ -105,7 +127,7 @@ impl DynamicScene { Ok(()) } - /// Write the dynamic entities and their corresponding components to the given world. + /// Write the resources, the dynamic entities, and their corresponding components to the given world. /// /// This method will return a [`SceneSpawnError`] if a type either is not registered /// in the world's [`AppTypeRegistry`] resource, or doesn't reflect the diff --git a/crates/bevy_scene/src/dynamic_scene_builder.rs b/crates/bevy_scene/src/dynamic_scene_builder.rs index 1187f087b6..0f25b1e484 100644 --- a/crates/bevy_scene/src/dynamic_scene_builder.rs +++ b/crates/bevy_scene/src/dynamic_scene_builder.rs @@ -1,10 +1,16 @@ use crate::{DynamicEntity, DynamicScene}; use bevy_app::AppTypeRegistry; -use bevy_ecs::{prelude::Entity, reflect::ReflectComponent, world::World}; +use bevy_ecs::component::ComponentId; +use bevy_ecs::{ + prelude::Entity, + reflect::{ReflectComponent, ReflectResource}, + world::World, +}; +use bevy_reflect::Reflect; use bevy_utils::default; use std::collections::BTreeMap; -/// A [`DynamicScene`] builder, used to build a scene from a [`World`] by extracting some entities. +/// A [`DynamicScene`] builder, used to build a scene from a [`World`] by extracting some entities and resources. /// /// # Entity Order /// @@ -31,6 +37,7 @@ use std::collections::BTreeMap; /// let dynamic_scene = builder.build(); /// ``` pub struct DynamicSceneBuilder<'w> { + extracted_resources: BTreeMap>, extracted_scene: BTreeMap, type_registry: AppTypeRegistry, original_world: &'w World, @@ -41,6 +48,7 @@ impl<'w> DynamicSceneBuilder<'w> { /// All components registered in that world's [`AppTypeRegistry`] resource will be extracted. pub fn from_world(world: &'w World) -> Self { Self { + extracted_resources: default(), extracted_scene: default(), type_registry: world.resource::().clone(), original_world: world, @@ -51,6 +59,7 @@ impl<'w> DynamicSceneBuilder<'w> { /// Only components registered in the given [`AppTypeRegistry`] will be extracted. pub fn from_world_with_type_registry(world: &'w World, type_registry: AppTypeRegistry) -> Self { Self { + extracted_resources: default(), extracted_scene: default(), type_registry, original_world: world, @@ -63,6 +72,7 @@ impl<'w> DynamicSceneBuilder<'w> { /// [`Self::remove_empty_entities`] before building the scene. pub fn build(self) -> DynamicScene { DynamicScene { + resources: self.extracted_resources.into_values().collect(), entities: self.extracted_scene.into_values().collect(), } } @@ -126,17 +136,20 @@ impl<'w> DynamicSceneBuilder<'w> { let entity = self.original_world.entity(entity); for component_id in entity.archetype().components() { - let reflect_component = self - .original_world - .components() - .get_info(component_id) - .and_then(|info| type_registry.get(info.type_id().unwrap())) - .and_then(|registration| registration.data::()) - .and_then(|reflect_component| reflect_component.reflect(entity)); - - if let Some(reflect_component) = reflect_component { - entry.components.push(reflect_component.clone_value()); - } + let mut extract_and_push = || { + let type_id = self + .original_world + .components() + .get_info(component_id)? + .type_id()?; + let component = type_registry + .get(type_id)? + .data::()? + .reflect(entity)?; + entry.components.push(component.clone_value()); + Some(()) + }; + extract_and_push(); } self.extracted_scene.insert(index, entry); } @@ -144,13 +157,59 @@ impl<'w> DynamicSceneBuilder<'w> { drop(type_registry); self } + + /// Extract resources from the builder's [`World`]. + /// + /// Only resources registered in the builder's [`AppTypeRegistry`] will be extracted. + /// Re-extracting a resource that was already extracted will have no effect. + /// ``` + /// # use bevy_scene::DynamicSceneBuilder; + /// # use bevy_app::AppTypeRegistry; + /// # use bevy_ecs::prelude::{ReflectResource, Resource, World}; + /// # use bevy_reflect::Reflect; + /// #[derive(Resource, Default, Reflect)] + /// #[reflect(Resource)] + /// struct MyResource; + /// + /// # let mut world = World::default(); + /// # world.init_resource::(); + /// world.insert_resource(MyResource); + /// + /// let mut builder = DynamicSceneBuilder::from_world(&world); + /// builder.extract_resources(); + /// let scene = builder.build(); + /// ``` + pub fn extract_resources(&mut self) -> &mut Self { + let type_registry = self.type_registry.read(); + for (component_id, _) in self.original_world.storages().resources.iter() { + let mut extract_and_push = || { + let type_id = self + .original_world + .components() + .get_info(component_id)? + .type_id()?; + let resource = type_registry + .get(type_id)? + .data::()? + .reflect(self.original_world)?; + self.extracted_resources + .insert(component_id, resource.clone_value()); + Some(()) + }; + extract_and_push(); + } + + drop(type_registry); + self + } } #[cfg(test)] mod tests { use bevy_app::AppTypeRegistry; use bevy_ecs::{ - component::Component, prelude::Entity, query::With, reflect::ReflectComponent, world::World, + component::Component, prelude::Entity, prelude::Resource, query::With, + reflect::ReflectComponent, reflect::ReflectResource, world::World, }; use bevy_reflect::Reflect; @@ -160,10 +219,15 @@ mod tests { #[derive(Component, Reflect, Default, Eq, PartialEq, Debug)] #[reflect(Component)] struct ComponentA; + #[derive(Component, Reflect, Default, Eq, PartialEq, Debug)] #[reflect(Component)] struct ComponentB; + #[derive(Resource, Reflect, Default, Eq, PartialEq, Debug)] + #[reflect(Resource)] + struct ResourceA; + #[test] fn extract_one_entity() { let mut world = World::default(); @@ -303,4 +367,41 @@ mod tests { assert_eq!(scene.entities.len(), 1); assert_eq!(scene.entities[0].entity, entity_a.index()); } + + #[test] + fn extract_one_resource() { + let mut world = World::default(); + + let atr = AppTypeRegistry::default(); + atr.write().register::(); + world.insert_resource(atr); + + world.insert_resource(ResourceA); + + let mut builder = DynamicSceneBuilder::from_world(&world); + builder.extract_resources(); + let scene = builder.build(); + + assert_eq!(scene.resources.len(), 1); + assert!(scene.resources[0].represents::()); + } + + #[test] + fn extract_one_resource_twice() { + let mut world = World::default(); + + let atr = AppTypeRegistry::default(); + atr.write().register::(); + world.insert_resource(atr); + + world.insert_resource(ResourceA); + + let mut builder = DynamicSceneBuilder::from_world(&world); + builder.extract_resources(); + builder.extract_resources(); + let scene = builder.build(); + + assert_eq!(scene.resources.len(), 1); + assert!(scene.resources[0].represents::()); + } } diff --git a/crates/bevy_scene/src/lib.rs b/crates/bevy_scene/src/lib.rs index 2d7ef33951..3488064f10 100644 --- a/crates/bevy_scene/src/lib.rs +++ b/crates/bevy_scene/src/lib.rs @@ -24,7 +24,6 @@ pub mod prelude { use bevy_app::prelude::*; use bevy_asset::AddAsset; -use bevy_ecs::prelude::*; #[derive(Default)] pub struct ScenePlugin; @@ -36,9 +35,9 @@ impl Plugin for ScenePlugin { .add_asset::() .init_asset_loader::() .init_resource::() - .add_system(scene_spawner_system) + .add_systems(Update, scene_spawner_system) // Systems `*_bundle_spawner` must run before `scene_spawner_system` - .add_system(scene_spawner.in_base_set(CoreSet::PreUpdate)); + .add_systems(PreUpdate, scene_spawner); } } diff --git a/crates/bevy_scene/src/scene.rs b/crates/bevy_scene/src/scene.rs index 4df757aa58..460f7cf8b2 100644 --- a/crates/bevy_scene/src/scene.rs +++ b/crates/bevy_scene/src/scene.rs @@ -1,7 +1,7 @@ use bevy_app::AppTypeRegistry; use bevy_ecs::{ entity::EntityMap, - reflect::{ReflectComponent, ReflectMapEntities}, + reflect::{ReflectComponent, ReflectMapEntities, ReflectResource}, world::World, }; use bevy_reflect::TypeUuid; @@ -61,6 +61,33 @@ impl Scene { }; let type_registry = type_registry.read(); + + // Resources archetype + for (component_id, _) in self.world.storages().resources.iter() { + let component_info = self + .world + .components() + .get_info(component_id) + .expect("component_ids in archetypes should have ComponentInfo"); + + let type_id = component_info + .type_id() + .expect("reflected resources must have a type_id"); + + let registration = + type_registry + .get(type_id) + .ok_or_else(|| SceneSpawnError::UnregisteredType { + type_name: component_info.name().to_string(), + })?; + let reflect_resource = registration.data::().ok_or_else(|| { + SceneSpawnError::UnregisteredResource { + type_name: component_info.name().to_string(), + } + })?; + reflect_resource.copy(&self.world, world); + } + for archetype in self.world.archetypes().iter() { for scene_entity in archetype.entities() { let entity = *instance_info diff --git a/crates/bevy_scene/src/scene_loader.rs b/crates/bevy_scene/src/scene_loader.rs index 2dea805f92..ed5c82443c 100644 --- a/crates/bevy_scene/src/scene_loader.rs +++ b/crates/bevy_scene/src/scene_loader.rs @@ -1,5 +1,5 @@ use crate::serde::SceneDeserializer; -use anyhow::Result; +use anyhow::{anyhow, Result}; use bevy_app::AppTypeRegistry; use bevy_asset::{AssetLoader, LoadContext, LoadedAsset}; use bevy_ecs::world::{FromWorld, World}; @@ -35,7 +35,17 @@ impl AssetLoader for SceneLoader { let scene_deserializer = SceneDeserializer { type_registry: &self.type_registry.read(), }; - let scene = scene_deserializer.deserialize(&mut deserializer)?; + let scene = scene_deserializer + .deserialize(&mut deserializer) + .map_err(|e| { + let span_error = deserializer.span_error(e); + anyhow!( + "{} at {}:{}", + span_error.code, + load_context.path().to_string_lossy(), + span_error.position, + ) + })?; load_context.set_default_asset(LoadedAsset::new(scene)); Ok(()) }) diff --git a/crates/bevy_scene/src/scene_spawner.rs b/crates/bevy_scene/src/scene_spawner.rs index 7a0c8f851b..5a46aa14e2 100644 --- a/crates/bevy_scene/src/scene_spawner.rs +++ b/crates/bevy_scene/src/scene_spawner.rs @@ -45,6 +45,8 @@ pub struct SceneSpawner { pub enum SceneSpawnError { #[error("scene contains the unregistered component `{type_name}`. consider adding `#[reflect(Component)]` to your type")] UnregisteredComponent { type_name: String }, + #[error("scene contains the unregistered resource `{type_name}`. consider adding `#[reflect(Resource)]` to your type")] + UnregisteredResource { type_name: String }, #[error("scene contains the unregistered type `{type_name}`. consider registering the type using `app.register_type::()`")] UnregisteredType { type_name: String }, #[error("scene does not exist")] diff --git a/crates/bevy_scene/src/serde.rs b/crates/bevy_scene/src/serde.rs index 51b8a5ebce..debe7c5451 100644 --- a/crates/bevy_scene/src/serde.rs +++ b/crates/bevy_scene/src/serde.rs @@ -15,6 +15,7 @@ use serde::{ use std::fmt::Formatter; pub const SCENE_STRUCT: &str = "Scene"; +pub const SCENE_RESOURCES: &str = "resources"; pub const SCENE_ENTITIES: &str = "entities"; pub const ENTITY_STRUCT: &str = "Entity"; @@ -36,7 +37,14 @@ impl<'a> Serialize for SceneSerializer<'a> { where S: serde::Serializer, { - let mut state = serializer.serialize_struct(SCENE_STRUCT, 1)?; + let mut state = serializer.serialize_struct(SCENE_STRUCT, 2)?; + state.serialize_field( + SCENE_RESOURCES, + &SceneMapSerializer { + entries: &self.scene.resources, + registry: self.registry, + }, + )?; state.serialize_field( SCENE_ENTITIES, &EntitiesSerializer { @@ -85,8 +93,8 @@ impl<'a> Serialize for EntitySerializer<'a> { let mut state = serializer.serialize_struct(ENTITY_STRUCT, 1)?; state.serialize_field( ENTITY_FIELD_COMPONENTS, - &ComponentsSerializer { - components: &self.entity.components, + &SceneMapSerializer { + entries: &self.entity.components, registry: self.registry, }, )?; @@ -94,21 +102,21 @@ impl<'a> Serialize for EntitySerializer<'a> { } } -pub struct ComponentsSerializer<'a> { - pub components: &'a [Box], +pub struct SceneMapSerializer<'a> { + pub entries: &'a [Box], pub registry: &'a TypeRegistryArc, } -impl<'a> Serialize for ComponentsSerializer<'a> { +impl<'a> Serialize for SceneMapSerializer<'a> { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { - let mut state = serializer.serialize_map(Some(self.components.len()))?; - for component in self.components { + let mut state = serializer.serialize_map(Some(self.entries.len()))?; + for reflect in self.entries { state.serialize_entry( - component.type_name(), - &TypedReflectSerializer::new(&**component, &self.registry.read()), + reflect.type_name(), + &TypedReflectSerializer::new(&**reflect, &self.registry.read()), )?; } state.end() @@ -118,6 +126,7 @@ impl<'a> Serialize for ComponentsSerializer<'a> { #[derive(Deserialize)] #[serde(field_identifier, rename_all = "lowercase")] enum SceneField { + Resources, Entities, } @@ -140,7 +149,7 @@ impl<'a, 'de> DeserializeSeed<'de> for SceneDeserializer<'a> { { deserializer.deserialize_struct( SCENE_STRUCT, - &[SCENE_ENTITIES], + &[SCENE_RESOURCES, SCENE_ENTITIES], SceneVisitor { type_registry: self.type_registry, }, @@ -163,9 +172,18 @@ impl<'a, 'de> Visitor<'de> for SceneVisitor<'a> { where A: MapAccess<'de>, { + let mut resources = None; let mut entities = None; while let Some(key) = map.next_key()? { match key { + SceneField::Resources => { + if resources.is_some() { + return Err(Error::duplicate_field(SCENE_RESOURCES)); + } + resources = Some(map.next_value_seed(SceneMapDeserializer { + registry: self.type_registry, + })?); + } SceneField::Entities => { if entities.is_some() { return Err(Error::duplicate_field(SCENE_ENTITIES)); @@ -177,22 +195,35 @@ impl<'a, 'de> Visitor<'de> for SceneVisitor<'a> { } } + let resources = resources.ok_or_else(|| Error::missing_field(SCENE_RESOURCES))?; let entities = entities.ok_or_else(|| Error::missing_field(SCENE_ENTITIES))?; - Ok(DynamicScene { entities }) + Ok(DynamicScene { + resources, + entities, + }) } fn visit_seq(self, mut seq: A) -> Result where A: SeqAccess<'de>, { + let resources = seq + .next_element_seed(SceneMapDeserializer { + registry: self.type_registry, + })? + .ok_or_else(|| Error::missing_field(SCENE_RESOURCES))?; + let entities = seq .next_element_seed(SceneEntitiesDeserializer { type_registry: self.type_registry, })? .ok_or_else(|| Error::missing_field(SCENE_ENTITIES))?; - Ok(DynamicScene { entities }) + Ok(DynamicScene { + resources, + entities, + }) } } @@ -281,7 +312,7 @@ impl<'a, 'de> Visitor<'de> for SceneEntityVisitor<'a> { A: SeqAccess<'de>, { let components = seq - .next_element_seed(ComponentDeserializer { + .next_element_seed(SceneMapDeserializer { registry: self.registry, })? .ok_or_else(|| Error::missing_field(ENTITY_FIELD_COMPONENTS))?; @@ -304,7 +335,7 @@ impl<'a, 'de> Visitor<'de> for SceneEntityVisitor<'a> { return Err(Error::duplicate_field(ENTITY_FIELD_COMPONENTS)); } - components = Some(map.next_value_seed(ComponentDeserializer { + components = Some(map.next_value_seed(SceneMapDeserializer { registry: self.registry, })?); } @@ -321,32 +352,32 @@ impl<'a, 'de> Visitor<'de> for SceneEntityVisitor<'a> { } } -pub struct ComponentDeserializer<'a> { +pub struct SceneMapDeserializer<'a> { pub registry: &'a TypeRegistry, } -impl<'a, 'de> DeserializeSeed<'de> for ComponentDeserializer<'a> { +impl<'a, 'de> DeserializeSeed<'de> for SceneMapDeserializer<'a> { type Value = Vec>; fn deserialize(self, deserializer: D) -> Result where D: serde::Deserializer<'de>, { - deserializer.deserialize_map(ComponentVisitor { + deserializer.deserialize_map(SceneMapVisitor { registry: self.registry, }) } } -struct ComponentVisitor<'a> { +struct SceneMapVisitor<'a> { pub registry: &'a TypeRegistry, } -impl<'a, 'de> Visitor<'de> for ComponentVisitor<'a> { +impl<'a, 'de> Visitor<'de> for SceneMapVisitor<'a> { type Value = Vec>; fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { - formatter.write_str("map of components") + formatter.write_str("map of reflect types") } fn visit_map(self, mut map: A) -> std::result::Result @@ -354,23 +385,23 @@ impl<'a, 'de> Visitor<'de> for ComponentVisitor<'a> { A: MapAccess<'de>, { let mut added = HashSet::new(); - let mut components = Vec::new(); + let mut entries = Vec::new(); while let Some(registration) = map.next_key_seed(TypeRegistrationDeserializer::new(self.registry))? { if !added.insert(registration.type_id()) { return Err(Error::custom(format_args!( - "duplicate component: `{}`", + "duplicate reflect type: `{}`", registration.type_name() ))); } - components.push( + entries.push( map.next_value_seed(TypedReflectDeserializer::new(registration, self.registry))?, ); } - Ok(components) + Ok(entries) } fn visit_seq(self, mut seq: A) -> Result @@ -394,7 +425,7 @@ mod tests { use crate::{DynamicScene, DynamicSceneBuilder}; use bevy_app::AppTypeRegistry; use bevy_ecs::entity::EntityMap; - use bevy_ecs::prelude::{Component, ReflectComponent, World}; + use bevy_ecs::prelude::{Component, ReflectComponent, ReflectResource, Resource, World}; use bevy_reflect::{FromReflect, Reflect, ReflectSerialize}; use bincode::Options; use serde::de::DeserializeSeed; @@ -429,6 +460,12 @@ mod tests { }, } + #[derive(Resource, Reflect, Default)] + #[reflect(Resource)] + struct MyResource { + foo: i32, + } + fn create_world() -> World { let mut world = World::new(); let registry = AppTypeRegistry::default(); @@ -443,6 +480,7 @@ mod tests { registry.register_type_data::(); registry.register::<[usize; 3]>(); registry.register::<(f32, f32)>(); + registry.register::(); } world.insert_resource(registry); world @@ -456,11 +494,19 @@ mod tests { let b = world.spawn((Foo(123), Bar(345))).id(); let c = world.spawn((Foo(123), Bar(345), Baz(789))).id(); + world.insert_resource(MyResource { foo: 123 }); + let mut builder = DynamicSceneBuilder::from_world(&world); builder.extract_entities([a, b, c].into_iter()); + builder.extract_resources(); let scene = builder.build(); let expected = r#"( + resources: { + "bevy_scene::serde::tests::MyResource": ( + foo: 123, + ), + }, entities: { 0: ( components: { @@ -493,6 +539,11 @@ mod tests { let world = create_world(); let input = r#"( + resources: { + "bevy_scene::serde::tests::MyResource": ( + foo: 123, + ), + }, entities: { 0: ( components: { @@ -520,6 +571,11 @@ mod tests { }; let scene = scene_deserializer.deserialize(&mut deserializer).unwrap(); + assert_eq!( + 1, + scene.resources.len(), + "expected `resources` to contain 1 resource" + ); assert_eq!( 3, scene.entities.len(), @@ -530,6 +586,11 @@ mod tests { let mut dst_world = create_world(); scene.write_to_world(&mut dst_world, &mut map).unwrap(); + let my_resource = dst_world.get_resource::(); + assert!(my_resource.is_some()); + let my_resource = my_resource.unwrap(); + assert_eq!(my_resource.foo, 123); + assert_eq!(3, dst_world.query::<&Foo>().iter(&dst_world).count()); assert_eq!(2, dst_world.query::<&Bar>().iter(&dst_world).count()); assert_eq!(1, dst_world.query::<&Baz>().iter(&dst_world).count()); @@ -554,10 +615,10 @@ mod tests { assert_eq!( vec![ - 1, 0, 1, 37, 98, 101, 118, 121, 95, 115, 99, 101, 110, 101, 58, 58, 115, 101, 114, - 100, 101, 58, 58, 116, 101, 115, 116, 115, 58, 58, 77, 121, 67, 111, 109, 112, 111, - 110, 101, 110, 116, 1, 2, 3, 102, 102, 166, 63, 205, 204, 108, 64, 1, 12, 72, 101, - 108, 108, 111, 32, 87, 111, 114, 108, 100, 33 + 0, 1, 0, 1, 37, 98, 101, 118, 121, 95, 115, 99, 101, 110, 101, 58, 58, 115, 101, + 114, 100, 101, 58, 58, 116, 101, 115, 116, 115, 58, 58, 77, 121, 67, 111, 109, 112, + 111, 110, 101, 110, 116, 1, 2, 3, 102, 102, 166, 63, 205, 204, 108, 64, 1, 12, 72, + 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33 ], serialized_scene ); @@ -594,11 +655,11 @@ mod tests { assert_eq!( vec![ - 145, 129, 0, 145, 129, 217, 37, 98, 101, 118, 121, 95, 115, 99, 101, 110, 101, 58, - 58, 115, 101, 114, 100, 101, 58, 58, 116, 101, 115, 116, 115, 58, 58, 77, 121, 67, - 111, 109, 112, 111, 110, 101, 110, 116, 147, 147, 1, 2, 3, 146, 202, 63, 166, 102, - 102, 202, 64, 108, 204, 205, 129, 165, 84, 117, 112, 108, 101, 172, 72, 101, 108, - 108, 111, 32, 87, 111, 114, 108, 100, 33 + 146, 128, 129, 0, 145, 129, 217, 37, 98, 101, 118, 121, 95, 115, 99, 101, 110, 101, + 58, 58, 115, 101, 114, 100, 101, 58, 58, 116, 101, 115, 116, 115, 58, 58, 77, 121, + 67, 111, 109, 112, 111, 110, 101, 110, 116, 147, 147, 1, 2, 3, 146, 202, 63, 166, + 102, 102, 202, 64, 108, 204, 205, 129, 165, 84, 117, 112, 108, 101, 172, 72, 101, + 108, 108, 111, 32, 87, 111, 114, 108, 100, 33 ], buf ); @@ -635,12 +696,12 @@ mod tests { assert_eq!( vec![ - 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 37, 0, 0, 0, 0, 0, 0, - 0, 98, 101, 118, 121, 95, 115, 99, 101, 110, 101, 58, 58, 115, 101, 114, 100, 101, - 58, 58, 116, 101, 115, 116, 115, 58, 58, 77, 121, 67, 111, 109, 112, 111, 110, 101, - 110, 116, 1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, - 102, 102, 166, 63, 205, 204, 108, 64, 1, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 72, 101, - 108, 108, 111, 32, 87, 111, 114, 108, 100, 33 + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, + 37, 0, 0, 0, 0, 0, 0, 0, 98, 101, 118, 121, 95, 115, 99, 101, 110, 101, 58, 58, + 115, 101, 114, 100, 101, 58, 58, 116, 101, 115, 116, 115, 58, 58, 77, 121, 67, 111, + 109, 112, 111, 110, 101, 110, 116, 1, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, + 3, 0, 0, 0, 0, 0, 0, 0, 102, 102, 166, 63, 205, 204, 108, 64, 1, 0, 0, 0, 12, 0, 0, + 0, 0, 0, 0, 0, 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 33 ], serialized_scene ); diff --git a/crates/bevy_sprite/src/collide_aabb.rs b/crates/bevy_sprite/src/collide_aabb.rs index fa49f13aed..f455c9fe4a 100644 --- a/crates/bevy_sprite/src/collide_aabb.rs +++ b/crates/bevy_sprite/src/collide_aabb.rs @@ -2,7 +2,7 @@ use bevy_math::{Vec2, Vec3}; -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Copy, Clone)] pub enum Collision { Left, Right, diff --git a/crates/bevy_sprite/src/lib.rs b/crates/bevy_sprite/src/lib.rs index d7a02c3ff7..b624b7bad8 100644 --- a/crates/bevy_sprite/src/lib.rs +++ b/crates/bevy_sprite/src/lib.rs @@ -34,7 +34,7 @@ use bevy_reflect::TypeUuid; use bevy_render::{ render_phase::AddRenderCommand, render_resource::{Shader, SpecializedRenderPipelines}, - ExtractSchedule, RenderApp, RenderSet, + ExtractSchedule, Render, RenderApp, RenderSet, }; #[derive(Default)] @@ -71,13 +71,14 @@ impl Plugin for SpritePlugin { .init_resource::() .add_render_command::() .add_systems( + ExtractSchedule, ( extract_sprites.in_set(SpriteSystem::ExtractSprites), extract_sprite_events, - ) - .in_schedule(ExtractSchedule), + ), ) - .add_system( + .add_systems( + Render, queue_sprites .in_set(RenderSet::Queue) .ambiguous_with(queue_material2d_meshes::), diff --git a/crates/bevy_sprite/src/mesh2d/material.rs b/crates/bevy_sprite/src/mesh2d/material.rs index 4982efe381..ed03db9a48 100644 --- a/crates/bevy_sprite/src/mesh2d/material.rs +++ b/crates/bevy_sprite/src/mesh2d/material.rs @@ -1,4 +1,4 @@ -use bevy_app::{App, IntoSystemAppConfig, Plugin}; +use bevy_app::{App, Plugin}; use bevy_asset::{AddAsset, AssetEvent, AssetServer, Assets, Handle}; use bevy_core_pipeline::{ core_2d::Transparent2d, @@ -32,7 +32,7 @@ use bevy_render::{ renderer::RenderDevice, texture::FallbackImage, view::{ComputedVisibility, ExtractedView, Msaa, Visibility, VisibleEntities}, - Extract, ExtractSchedule, RenderApp, RenderSet, + Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; use bevy_transform::components::{GlobalTransform, Transform}; use bevy_utils::{FloatOrd, HashMap, HashSet}; @@ -161,13 +161,16 @@ where .init_resource::>() .init_resource::>() .init_resource::>>() - .add_system(extract_materials_2d::.in_schedule(ExtractSchedule)) - .add_system( - prepare_materials_2d:: - .in_set(RenderSet::Prepare) - .after(PrepareAssetSet::PreAssetPrepare), - ) - .add_system(queue_material2d_meshes::.in_set(RenderSet::Queue)); + .add_systems(ExtractSchedule, extract_materials_2d::) + .add_systems( + Render, + ( + prepare_materials_2d:: + .in_set(RenderSet::Prepare) + .after(PrepareAssetSet::PreAssetPrepare), + queue_material2d_meshes::.in_set(RenderSet::Queue), + ), + ); } } } diff --git a/crates/bevy_sprite/src/mesh2d/mesh.rs b/crates/bevy_sprite/src/mesh2d/mesh.rs index 42030fb52b..070898df55 100644 --- a/crates/bevy_sprite/src/mesh2d/mesh.rs +++ b/crates/bevy_sprite/src/mesh2d/mesh.rs @@ -1,4 +1,4 @@ -use bevy_app::{IntoSystemAppConfig, Plugin}; +use bevy_app::Plugin; use bevy_asset::{load_internal_asset, Handle, HandleUntyped}; use bevy_ecs::{ @@ -22,7 +22,7 @@ use bevy_render::{ view::{ ComputedVisibility, ExtractedView, ViewTarget, ViewUniform, ViewUniformOffset, ViewUniforms, }, - Extract, ExtractSchedule, RenderApp, RenderSet, + Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; use bevy_transform::components::GlobalTransform; @@ -103,9 +103,14 @@ impl Plugin for Mesh2dRenderPlugin { render_app .init_resource::() .init_resource::>() - .add_system(extract_mesh2d.in_schedule(ExtractSchedule)) - .add_system(queue_mesh2d_bind_group.in_set(RenderSet::Queue)) - .add_system(queue_mesh2d_view_bind_groups.in_set(RenderSet::Queue)); + .add_systems(ExtractSchedule, extract_mesh2d) + .add_systems( + Render, + ( + queue_mesh2d_bind_group.in_set(RenderSet::Queue), + queue_mesh2d_view_bind_groups.in_set(RenderSet::Queue), + ), + ); } } } diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index 767b926490..e740e525a2 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -79,9 +79,9 @@ impl Plugin for TextPlugin { .init_resource::() .init_resource::() .insert_resource(TextPipeline::default()) - .add_system( + .add_systems( + PostUpdate, update_text2d_layout - .in_base_set(CoreSet::PostUpdate) // Potential conflict: `Assets` // In practice, they run independently since `bevy_render::camera_update_system` // will only ever observe its own render target, and `update_text2d_layout` @@ -90,10 +90,9 @@ impl Plugin for TextPlugin { ); if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { - render_app.add_system( - extract_text2d_sprite - .after(SpriteSystem::ExtractSprites) - .in_schedule(ExtractSchedule), + render_app.add_systems( + ExtractSchedule, + extract_text2d_sprite.after(SpriteSystem::ExtractSprites), ); } } diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index 13f9d13405..f252406823 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -23,8 +23,8 @@ use bevy_utils::HashSet; use bevy_window::{PrimaryWindow, Window, WindowScaleFactorChanged}; use crate::{ - Font, FontAtlasSet, FontAtlasWarning, Text, TextError, TextLayoutInfo, TextPipeline, - TextSettings, YAxisOrientation, + Font, FontAtlasSet, FontAtlasWarning, PositionedGlyph, Text, TextError, TextLayoutInfo, + TextPipeline, TextSettings, YAxisOrientation, }; /// The maximum width and height of text. The text will wrap according to the specified size. @@ -94,48 +94,42 @@ pub fn extract_text2d_sprite( .get_single() .map(|window| window.resolution.scale_factor() as f32) .unwrap_or(1.0); + let scaling = GlobalTransform::from_scale(Vec3::splat(scale_factor.recip())); - for (entity, computed_visibility, text, text_layout_info, anchor, text_transform) in + for (entity, computed_visibility, text, text_layout_info, anchor, global_transform) in text2d_query.iter() { if !computed_visibility.is_visible() { continue; } - let text_glyphs = &text_layout_info.glyphs; - let text_anchor = anchor.as_vec() * Vec2::new(1., -1.) - 0.5; - let alignment_offset = text_layout_info.size * text_anchor; + let text_anchor = -(anchor.as_vec() + 0.5); + let alignment_translation = text_layout_info.size * text_anchor; + let transform = *global_transform + * scaling + * GlobalTransform::from_translation(alignment_translation.extend(0.)); let mut color = Color::WHITE; let mut current_section = usize::MAX; - for text_glyph in text_glyphs { - if text_glyph.section_index != current_section { - color = text.sections[text_glyph.section_index] - .style - .color - .as_rgba_linear(); - current_section = text_glyph.section_index; + for PositionedGlyph { + position, + atlas_info, + section_index, + .. + } in &text_layout_info.glyphs + { + if *section_index != current_section { + color = text.sections[*section_index].style.color.as_rgba_linear(); + current_section = *section_index; } - let atlas = texture_atlases - .get(&text_glyph.atlas_info.texture_atlas) - .unwrap(); - let handle = atlas.texture.clone_weak(); - let index = text_glyph.atlas_info.glyph_index; - let rect = Some(atlas.textures[index]); - - let glyph_transform = - Transform::from_translation((alignment_offset + text_glyph.position).extend(0.)); - - let transform = *text_transform - * GlobalTransform::from_scale(Vec3::splat(scale_factor.recip())) - * glyph_transform; + let atlas = texture_atlases.get(&atlas_info.texture_atlas).unwrap(); extracted_sprites.sprites.push(ExtractedSprite { entity, - transform, + transform: transform * GlobalTransform::from_translation(position.extend(0.)), color, - rect, + rect: Some(atlas.textures[atlas_info.glyph_index]), custom_size: None, - image_handle_id: handle.id(), + image_handle_id: atlas.texture.id(), flip_x: false, flip_y: false, anchor: Anchor::Center.as_vec(), diff --git a/crates/bevy_time/src/common_conditions.rs b/crates/bevy_time/src/common_conditions.rs index 357f83cbe2..974de24dfa 100644 --- a/crates/bevy_time/src/common_conditions.rs +++ b/crates/bevy_time/src/common_conditions.rs @@ -8,14 +8,14 @@ use bevy_utils::Duration; /// If used for a fixed timestep system, use [`on_fixed_timer`] instead. /// /// ```rust,no_run -/// # use bevy_app::{App, IntoSystemAppConfig, NoopPluginGroup as DefaultPlugins, PluginGroup}; -/// # use bevy_ecs::schedule::IntoSystemConfig; +/// # use bevy_app::{App, NoopPluginGroup as DefaultPlugins, PluginGroup, Update}; +/// # use bevy_ecs::schedule::IntoSystemConfigs; /// # use bevy_utils::Duration; /// # use bevy_time::common_conditions::on_timer; /// fn main() { /// App::new() /// .add_plugins(DefaultPlugins) -/// .add_system(tick.run_if(on_timer(Duration::from_secs(1)))) +/// .add_systems(Update, tick.run_if(on_timer(Duration::from_secs(1)))) /// .run(); /// } /// fn tick() { @@ -32,7 +32,7 @@ use bevy_utils::Duration; /// For more accurate timers, use the [`Timer`] class directly (see /// [`Timer::times_finished_this_tick`] to address the problem mentioned above), or /// use fixed timesteps that allow systems to run multiple times per frame. -pub fn on_timer(duration: Duration) -> impl FnMut(Res