From d3d94bf37e989f0927b72ee5b47633efba5a01cd Mon Sep 17 00:00:00 2001 From: JoJoJet Date: Tue, 26 Jul 2022 20:45:26 -0400 Subject: [PATCH 01/10] add `SubSchedule` API --- crates/bevy_app/src/app.rs | 91 ++++++++++- crates/bevy_app/src/lib.rs | 5 +- crates/bevy_app/src/sub_schedule.rs | 216 ++++++++++++++++++++++++++ crates/bevy_ecs/macros/src/lib.rs | 15 ++ crates/bevy_ecs/src/lib.rs | 4 +- crates/bevy_ecs/src/schedule/label.rs | 12 +- 6 files changed, 336 insertions(+), 7 deletions(-) create mode 100644 crates/bevy_app/src/sub_schedule.rs diff --git a/crates/bevy_app/src/app.rs b/crates/bevy_app/src/app.rs index ebdd2ee0df54a..236eef9bebaed 100644 --- a/crates/bevy_app/src/app.rs +++ b/crates/bevy_app/src/app.rs @@ -1,11 +1,15 @@ -use crate::{CoreStage, Plugin, PluginGroup, PluginGroupBuilder, StartupSchedule, StartupStage}; +use crate::{ + CoreStage, IntoSubSchedule, Plugin, PluginGroup, PluginGroupBuilder, StartupSchedule, + StartupStage, SubSchedules, +}; pub use bevy_derive::AppLabel; use bevy_ecs::{ + change_detection::Mut, event::{Event, Events}, prelude::{FromWorld, IntoExclusiveSystem}, schedule::{ - IntoSystemDescriptor, Schedule, ShouldRun, Stage, StageLabel, State, StateData, SystemSet, - SystemStage, + IntoSystemDescriptor, Schedule, ScheduleLabel, ShouldRun, Stage, StageLabel, State, + StateData, SystemSet, SystemStage, }, system::Resource, world::World, @@ -311,6 +315,87 @@ impl App { self } + /// Adds a [`SubSchedule`](crate::SubSchedule) to the app. + /// + /// If a [label](bevy_ecs::schedule::ScheduleLabel) has been applied to it, + /// it can be accessed in the resource [`SubSchedules`]. + /// + /// # Examples + /// ``` + /// # use bevy_app::prelude::*; + /// # use bevy_ecs::prelude::*; + /// # let mut app = App::new(); + /// # fn foo_system() {} + /// # fn bar_system() {} + /// # + /// // Define a label for the a schedule. + /// #[derive(ScheduleLabel)] + /// struct MySched; + /// + /// // Add a new sub-schedule. If the schedule only needs one stage, + /// // you can avoid some overhead by adding the stage directly. + /// app.add_sub_schedule( + /// SystemStage::parallel() + /// .with_system(foo_system) + /// .with_system(bar_system) + /// .label(MySched), + /// ); + /// ``` + #[track_caller] + pub fn add_sub_schedule(&mut self, sched: impl IntoSubSchedule) -> &mut Self { + crate::sub_schedule::add_to_app(self, IntoSubSchedule::into_sched(sched)); + self + } + + /// Allows you to modify a [`SubSchedule`](crate::SubSchedule) by executing a closure on it. + /// + /// # Examples + /// + /// ``` + /// # use bevy_app::prelude::*; + /// # use bevy_ecs::prelude::*; + /// # + /// # #[derive(ScheduleLabel)] + /// # struct FixedUpdate; + /// # + /// # let mut app = App::new(); + /// # app.add_sub_schedule( + /// # Schedule::default() + /// # .with_stage(CoreStage::Update, SystemStage::parallel()) + /// # .label(FixedUpdate), + /// # ); + /// # fn my_system() {} + /// # + /// // Access a previously-added sub-schedule and add a system to it. + /// app.sub_schedule(FixedUpdate, |sched: &mut Schedule| { + /// sched.add_system_to_stage(CoreStage::Update, my_system); + /// }); + /// ``` + /// + /// # Panics + /// If `label` refers to a non-existent `SubSchedule`, or if it is not of type `S`. + pub fn sub_schedule(&mut self, label: impl ScheduleLabel, f: F) -> &mut Self + where + F: FnOnce(&mut S), + { + #[inline(never)] + fn panic(label: impl Debug, ty: impl Debug) -> ! { + panic!("there is no sub-schedule labeled '{label:?}', or it does not match the type `{ty:?}`"); + } + + let label = label.as_label(); + + // Make sure the resource is initialized, so we get a more helpful panic message. + let mut schedules: Mut = + self.world.get_resource_or_insert_with(Default::default); + let sched: &mut S = match schedules.get_mut(label) { + Some(x) => x, + None => panic(label, std::any::type_name::()), + }; + f(sched); + self + } + /// Adds a system to the [update stage](Self::add_default_stages) of the app's [`Schedule`]. /// /// Refer to the [system module documentation](bevy_ecs::system) to see how a system diff --git a/crates/bevy_app/src/lib.rs b/crates/bevy_app/src/lib.rs index c0a9c68f79c3f..604da8e9c9cb9 100644 --- a/crates/bevy_app/src/lib.rs +++ b/crates/bevy_app/src/lib.rs @@ -6,6 +6,7 @@ mod app; mod plugin; mod plugin_group; mod schedule_runner; +mod sub_schedule; #[cfg(feature = "bevy_ci_testing")] mod ci_testing; @@ -15,12 +16,14 @@ pub use bevy_derive::DynamicPlugin; pub use plugin::*; pub use plugin_group::*; pub use schedule_runner::*; +pub use sub_schedule::*; #[allow(missing_docs)] pub mod prelude { #[doc(hidden)] pub use crate::{ - app::App, CoreStage, DynamicPlugin, Plugin, PluginGroup, StartupSchedule, StartupStage, + app::App, CoreStage, DynamicPlugin, IntoSubSchedule, Plugin, PluginGroup, StartupSchedule, + StartupStage, }; } diff --git a/crates/bevy_app/src/sub_schedule.rs b/crates/bevy_app/src/sub_schedule.rs new file mode 100644 index 0000000000000..c29f9e73ed5a0 --- /dev/null +++ b/crates/bevy_app/src/sub_schedule.rs @@ -0,0 +1,216 @@ +use std::fmt::Debug; + +use bevy_ecs::{ + change_detection::Mut, + schedule::{ScheduleLabel, ScheduleLabelId, Stage, StageLabel, StageLabelId}, + system::IntoExclusiveSystem, + world::World, +}; +use bevy_utils::HashMap; + +use crate::App; + +/// Methods for converting a schedule into a [sub-Schedule](SubSchedule) descriptor. +pub trait IntoSubSchedule: Sized { + /// The wrapped schedule type. + type Sched: Stage; + /// The type that controls the behaviour of the exclusive system + /// which runs [`SubSchedule`]s of this type. + type Runner: FnMut(&mut Self::Sched, &mut World) + Send + Sync + 'static; + + /// Applies the specified label to the current schedule. + /// This means it will be accessible in the [`SubSchedules`] resource after + /// being added to the [`App`]. + fn label(self, label: impl ScheduleLabel) -> SubSchedule { + let mut sub = Self::into_sched(self); + sub.label = Some(label.as_label()); + sub + } + /// Defines a function that runs the current schedule. It will be inserted into + /// an exclusive system within the stage `stage`. + /// + /// Overwrites any previously set runner or stage. + fn with_runner(self, stage: impl StageLabel, f: F) -> SubSchedule + where + F: FnMut(&mut Self::Sched, &mut World) + Send + Sync + 'static, + { + let SubSchedule { + schedule, label, .. + } = Self::into_sched(self); + SubSchedule { + schedule, + label, + runner: Some((stage.as_label(), f)), + } + } + + /// Performs the conversion. You usually do not need to call this directly. + fn into_sched(_: Self) -> SubSchedule; +} + +impl IntoSubSchedule for S { + type Sched = Self; + type Runner = fn(&mut Self, &mut World); + fn into_sched(schedule: Self) -> SubSchedule { + SubSchedule { + schedule, + label: None, + runner: None, + } + } +} + +impl IntoSubSchedule for SubSchedule +where + R: FnMut(&mut S, &mut World) + Send + Sync + 'static, +{ + type Sched = S; + type Runner = R; + #[inline] + fn into_sched(sched: Self) -> SubSchedule { + sched + } +} + +/// A schedule that may run independently of the main app schedule. +pub struct SubSchedule +where + F: FnMut(&mut S, &mut World) + Send + Sync + 'static, +{ + schedule: S, + label: Option, + runner: Option<(StageLabelId, F)>, +} + +/// A [resource](bevy_ecs::system::Res) that stores all labeled [`SubSchedule`]s. +#[derive(Default)] +pub struct SubSchedules { + // INVARIANT: A `SubSlot` cannot be removed once added, and is associated with + // a single schedule. Even if a slot gets temporarily emptied, it is guaranteed + // that the slot will always get refilled by the same exact schedule. + map: HashMap, +} + +struct SubSlot(Option>); +impl SubSchedules { + /// Inserts a new sub-schedule. + /// + /// # Panics + /// If there is already a sub-schedule labeled `label`. + #[track_caller] + pub fn insert(&mut self, label: impl ScheduleLabel, sched: Box) { + let label = label.as_label(); + let old = self.map.insert(label, SubSlot(Some(sched))); + if old.is_some() { + panic!("there is already a sub-schedule with label '{label:?}'"); + } + } + + /// Temporarily extracts a [`SubSchedule`] from the world, and provides a scope + /// that has mutable access to both the schedule and the [`World`]. + /// At the end of this scope, the sub-schedule is automatically reinserted. + /// + /// # Panics + /// If there is no schedule associated with `label`, or if that schedule + /// is currently already extracted. + #[track_caller] + pub fn extract_scope(world: &mut World, label: impl ScheduleLabel, f: F) -> T + where + F: FnOnce(&mut World, &mut dyn Stage) -> T, + { + #[inline(never)] + fn panic_none(label: impl Debug) -> ! { + panic!("there is no sub-schedule with label '{label:?}'") + } + #[inline(never)] + fn panic_extracted(label: impl Debug) -> ! { + panic!("cannot extract sub-schedule '{label:?}', as it is currently extracted already") + } + + let label = label.as_label(); + + // Extract. + let mut schedules = world.resource_mut::(); + let mut sched = match schedules.map.get_mut(&label) { + Some(x) => match x.0.take() { + Some(x) => x, + None => panic_extracted(label), + }, + None => panic_none(label), + }; + + // Execute. + let val = f(world, sched.as_mut()); + + // Re-insert. + let mut schedules = world.resource_mut::(); + schedules.map.get_mut(&label).unwrap().0 = Some(sched); + + val + } + + /// Gets a mutable reference to the sub-schedule identified by `label`. + /// + /// # Panics + /// If the schedule is currently [extracted](#method.extract_scope). + pub fn get_mut(&mut self, label: impl ScheduleLabel) -> Option<&mut S> { + #[cold] + fn panic(label: impl Debug) -> ! { + panic!("cannot get sub-schedule '{label:?}', as it is currently extracted") + } + + let label = label.as_label(); + let sched = match self.map.get_mut(&label)?.0.as_deref_mut() { + Some(x) => x, + None => panic(label), + }; + sched.downcast_mut() + } +} + +#[track_caller] +pub(crate) fn add_to_app(app: &mut App, schedule: impl IntoSubSchedule) { + let SubSchedule { + mut schedule, + label, + runner, + } = IntoSubSchedule::into_sched(schedule); + + // If it has a label, insert it to the public resource. + if let Some(label) = label { + let mut res: Mut = app.world.get_resource_or_insert_with(Default::default); + res.insert(label, Box::new(schedule)); + + if let Some((stage, mut runner)) = runner { + // Driver which extracts the schedule from the world and runs it. + let driver = move |w: &mut World| { + SubSchedules::extract_scope(w, label, |w, sched| { + let sched = if let Some(s) = sched.downcast_mut::() { + s + } else { + #[cfg(debug_assertions)] + unreachable!("the sub-schedule '{label:?}' somehow changed type after being inserted!"); + // SAFETY: Due to the invariant on `SubSchedules`, we can be sure that + // `sched` is the same instance that we inserted. + // Thus, we can rely on its type matching `S`. + #[cfg(not(debug_assertions))] + unsafe { + std::hint::unreachable_unchecked() + } + }; + runner(sched, w); + }); + }; + app.add_system_to_stage(stage, driver.exclusive_system()); + } + } else if let Some((stage, mut runner)) = runner { + // If there's no label, then the schedule isn't visible publicly. + // We can just store it locally + let driver = move |w: &mut World| { + runner(&mut schedule, w); + }; + app.add_system_to_stage(stage, driver.exclusive_system()); + } else { + panic!("inserted sub-schedule can never be accessed, as it has neither a label nor a runner function") + } +} diff --git a/crates/bevy_ecs/macros/src/lib.rs b/crates/bevy_ecs/macros/src/lib.rs index 68023e315ddb0..1fa57040e4a57 100644 --- a/crates/bevy_ecs/macros/src/lib.rs +++ b/crates/bevy_ecs/macros/src/lib.rs @@ -492,6 +492,21 @@ pub fn derive_run_criteria_label(input: TokenStream) -> TokenStream { derive_label(input, &trait_path, "run_criteria_label") } +/// Generates an impl of the `ScheduleLabel` trait. +/// +/// This works only for unit structs, or enums with only unit variants. +/// You may force a struct or variant to behave as if it were fieldless with `#[schedule_label(ignore_fields)]`. +#[proc_macro_derive(ScheduleLabel, attributes(schedule_label))] +pub fn derive_schedule_label(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let mut trait_path = bevy_ecs_path(); + trait_path.segments.push(format_ident!("schedule").into()); + trait_path + .segments + .push(format_ident!("ScheduleLabel").into()); + derive_label(input, &trait_path, "schedule_label") +} + pub(crate) fn bevy_ecs_path() -> syn::Path { BevyManifest::default().get_path("bevy_ecs") } diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index cafacc2a2d929..1b7b67bb73023 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -35,8 +35,8 @@ pub mod prelude { query::{Added, AnyOf, ChangeTrackers, Changed, Or, QueryState, With, Without}, schedule::{ AmbiguitySetLabel, ExclusiveSystemDescriptorCoercion, ParallelSystemDescriptorCoercion, - RunCriteria, RunCriteriaDescriptorCoercion, RunCriteriaLabel, Schedule, Stage, - StageLabel, State, SystemLabel, SystemSet, SystemStage, + RunCriteria, RunCriteriaDescriptorCoercion, RunCriteriaLabel, Schedule, ScheduleLabel, + Stage, StageLabel, State, SystemLabel, SystemSet, SystemStage, }, system::{ Commands, In, IntoChainSystem, IntoExclusiveSystem, IntoSystem, Local, NonSend, diff --git a/crates/bevy_ecs/src/schedule/label.rs b/crates/bevy_ecs/src/schedule/label.rs index c9bccf8303084..5a890891ccb54 100644 --- a/crates/bevy_ecs/src/schedule/label.rs +++ b/crates/bevy_ecs/src/schedule/label.rs @@ -1,4 +1,6 @@ -pub use bevy_ecs_macros::{AmbiguitySetLabel, RunCriteriaLabel, StageLabel, SystemLabel}; +pub use bevy_ecs_macros::{ + AmbiguitySetLabel, RunCriteriaLabel, ScheduleLabel, StageLabel, SystemLabel, +}; use bevy_utils::define_label; define_label!( @@ -25,3 +27,11 @@ define_label!( /// Strongly-typed identifier for a [`RunCriteriaLabel`]. RunCriteriaLabelId, ); + +// note: this type won't be necessary come stageless, but we need it for now. +define_label!( + /// A strongly-typed class of labels used to identify a [`Schedule`](crate::schedule::Schedule). + ScheduleLabel, + /// Strongly-typed identifier for a [`ScheduleLabel`]. + ScheduleLabelId, +); From 867e7dd4d36ac4952fdfb54695e4a6b2e7166227 Mon Sep 17 00:00:00 2001 From: JoJoJet Date: Tue, 26 Jul 2022 22:54:13 -0400 Subject: [PATCH 02/10] use sub-schedules for fixed timestep --- crates/bevy_time/src/fixed_timestep.rs | 331 ++++++++++++------------- crates/bevy_time/src/lib.rs | 3 +- 2 files changed, 153 insertions(+), 181 deletions(-) diff --git a/crates/bevy_time/src/fixed_timestep.rs b/crates/bevy_time/src/fixed_timestep.rs index 78fdfca797c50..5dfede95e8e63 100644 --- a/crates/bevy_time/src/fixed_timestep.rs +++ b/crates/bevy_time/src/fixed_timestep.rs @@ -1,17 +1,109 @@ -use crate::Time; +use std::fmt::Debug; + +use bevy_app::{App, CoreStage, IntoSubSchedule}; use bevy_ecs::{ - archetype::ArchetypeComponentId, - component::ComponentId, - query::Access, - schedule::ShouldRun, - system::{IntoSystem, Res, ResMut, System}, + change_detection::Mut, + schedule::{ScheduleLabel, ScheduleLabelId, Stage, StageLabel}, world::World, }; use bevy_utils::HashMap; -use std::borrow::Cow; + +use crate::Time; + +/// Extends [`App`] with methods for constructing schedules with a fixed time-step. +pub trait TimestepAppExt { + /// Adds a new fixed time-step schedule to the stage identified by `stage_label`. + /// If `timestep` has a label, it is applied to the schedule as well. + /// + /// # Panics + /// If there is already a fixed schedule identified by `label`. + fn add_fixed_schedule_to_stage( + &mut self, + stage_label: impl StageLabel, + timestep: FixedTimestep, + schedule: S, + ) -> &mut Self; + /// Adds a new fixed time-step schedule to [`CoreStage::Update`]. + /// If `timestep` has a label, it is applied to the schedule as well. + /// + /// # Panics + /// If there is already a fixed schedule identified by `label`. + fn add_fixed_schedule(&mut self, timestep: FixedTimestep, schedule: impl Stage) -> &mut Self { + self.add_fixed_schedule_to_stage(CoreStage::Update, timestep, schedule) + } +} + +impl TimestepAppExt for App { + #[track_caller] + fn add_fixed_schedule_to_stage( + &mut self, + stage: impl StageLabel, + FixedTimestep { label, step }: FixedTimestep, + schedule: S, + ) -> &mut Self { + // If it has a label, add it to the resource so it can be modified or peeked later. + if let Some(label) = label { + let mut timesteps: Mut = + self.world.get_resource_or_insert_with(Default::default); + let state = FixedTimestepState { + step, + accumulator: 0.0, + }; + + // Insert the state into the map. + // Panic if there already was one. + if timesteps.insert(label, state).is_some() { + #[inline(never)] + #[track_caller] + fn panic(label: impl Debug) -> ! { + panic!("there is already a fixed timestep labeled '{label:?}'"); + } + panic(label) + } + + let runner = move |s: &mut S, w: &mut World| { + let mut state = *w.resource::().get(label).unwrap(); + + // Core looping functionality. + let time = w.resource::