-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Run fixed time-step in an exclusive system #5467
Closed
Closed
Changes from 9 commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
d3d94bf
add `SubSchedule` API
joseph-gio 867e7dd
use sub-schedules for fixed timestep
joseph-gio d7f3852
update examples
joseph-gio cb54639
use fallible APIs
joseph-gio b611dfd
improve a panic message
joseph-gio aa54393
fix a soundness issue
joseph-gio b07208b
update fixed timestep
joseph-gio 444c8f0
fix an error message
joseph-gio 89aad3f
remove an unused trait
joseph-gio 7f6118b
use `Duration` for fixed timestep
joseph-gio File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,239 @@ | ||
use std::fmt::{Debug, Display}; | ||
|
||
use bevy_ecs::{ | ||
change_detection::Mut, | ||
schedule::{ScheduleLabel, ScheduleLabelId, Stage, StageLabel, StageLabelId}, | ||
system::IntoExclusiveSystem, | ||
world::World, | ||
}; | ||
use bevy_utils::HashMap; | ||
use thiserror::Error; | ||
|
||
use crate::App; | ||
|
||
/// Methods for converting a schedule into a [sub-Schedule](SubSchedule) descriptor. | ||
pub trait IntoSubSchedule: Sized { | ||
/// The type that controls the behaviour of the exclusive system | ||
/// which runs [`SubSchedule`]s of this type. | ||
type Runner: FnMut(&mut dyn Stage, &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<Self::Runner> { | ||
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<F>(self, stage: impl StageLabel, f: F) -> SubSchedule<F> | ||
where | ||
F: FnMut(&mut dyn Stage, &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<Self::Runner>; | ||
} | ||
|
||
impl<S: Stage> IntoSubSchedule for S { | ||
type Runner = fn(&mut dyn Stage, &mut World); | ||
fn into_sched(schedule: Self) -> SubSchedule<Self::Runner> { | ||
SubSchedule { | ||
schedule: Box::new(schedule), | ||
label: None, | ||
runner: None, | ||
} | ||
} | ||
} | ||
|
||
impl<R> IntoSubSchedule for SubSchedule<R> | ||
where | ||
R: FnMut(&mut dyn Stage, &mut World) + Send + Sync + 'static, | ||
{ | ||
type Runner = R; | ||
#[inline] | ||
fn into_sched(sched: Self) -> SubSchedule<R> { | ||
sched | ||
} | ||
} | ||
|
||
/// A schedule that may run independently of the main app schedule. | ||
pub struct SubSchedule<F> | ||
where | ||
F: FnMut(&mut dyn Stage, &mut World) + Send + Sync + 'static, | ||
{ | ||
schedule: Box<dyn Stage>, | ||
label: Option<ScheduleLabelId>, | ||
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<ScheduleLabelId, SubSlot>, | ||
} | ||
|
||
struct SubSlot(Option<Box<dyn Stage>>); | ||
|
||
/// Error type returned by [`SubSchedules::extract_from`](SubSchedules#method.extract_from). | ||
#[derive(Error)] | ||
#[non_exhaustive] | ||
pub enum ExtractError { | ||
/// Schedule could not be found. | ||
#[error("there is no sub-schedule with label '{0:?}'")] | ||
NotFound(ScheduleLabelId), | ||
/// Schedule is being extracted by someone else right now. | ||
#[error("cannot extract sub-schedule '{0:?}', as it is currently extracted already")] | ||
AlreadyExtracted(ScheduleLabelId), | ||
/// The [`SubSchedules`] resource got removed during the scope. | ||
#[error("the `SubSchedules` resource got removed during the scope for schedule '{0:?}'")] | ||
ResourceLost(ScheduleLabelId), | ||
} | ||
impl Debug for ExtractError { | ||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
Display::fmt(self, f) | ||
} | ||
} | ||
|
||
#[derive(Error)] | ||
/// Error type returned by [`SubSchedules::insert`](SubSchedules#method.insert). | ||
#[non_exhaustive] | ||
pub enum InsertError { | ||
/// A schedule with this label already exists. | ||
#[error("a sub-schedule with label '{0:?}' already exists")] | ||
Duplicate(ScheduleLabelId), | ||
} | ||
impl Debug for InsertError { | ||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||
Display::fmt(self, f) | ||
} | ||
} | ||
|
||
impl SubSchedules { | ||
/// Inserts a new sub-schedule. | ||
/// | ||
/// # Errors | ||
/// If there is already a sub-schedule labeled `label`. | ||
pub fn insert( | ||
&mut self, | ||
label: impl ScheduleLabel, | ||
sched: Box<dyn Stage>, | ||
) -> Result<(), InsertError> { | ||
let label = label.as_label(); | ||
if self.map.contains_key(&label) { | ||
return Err(InsertError::Duplicate(label)); | ||
} | ||
self.map.insert(label, SubSlot(Some(sched))); | ||
Ok(()) | ||
} | ||
|
||
/// 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. | ||
/// | ||
/// # Errors | ||
/// If there is no schedule associated with `label`, or if that schedule | ||
/// is currently already extracted. | ||
pub fn extract_scope<F, T>( | ||
world: &mut World, | ||
label: impl ScheduleLabel, | ||
f: F, | ||
) -> Result<T, ExtractError> | ||
where | ||
F: FnOnce(&mut World, &mut dyn Stage) -> T, | ||
{ | ||
let label = label.as_label(); | ||
|
||
// Extract. | ||
let mut schedules = world.resource_mut::<Self>(); | ||
let mut sched = schedules | ||
.map | ||
.get_mut(&label) | ||
.ok_or(ExtractError::NotFound(label))? | ||
.0 | ||
.take() | ||
.ok_or(ExtractError::AlreadyExtracted(label))?; | ||
|
||
// Execute. | ||
let val = f(world, sched.as_mut()); | ||
|
||
// Re-insert. | ||
world | ||
.get_resource_mut::<Self>() | ||
.ok_or(ExtractError::ResourceLost(label))? | ||
.map | ||
.get_mut(&label) | ||
.expect("schedule must exist in the map since we found it previously") | ||
.0 = Some(sched); | ||
|
||
Ok(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<S: Stage>(&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<SubSchedules> = app.world.get_resource_or_insert_with(Default::default); | ||
res.insert(label, schedule).unwrap(); | ||
|
||
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| { | ||
runner(sched, w); | ||
}) | ||
.unwrap(); | ||
}; | ||
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(schedule.as_mut(), 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") | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I worry about the overloading of the term "extract" in this PR, overlapping with the current use of that term in the bevy Rendering architecture. It may lead to confusion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO the word extract is general enough that it shouldn't cause confusion, but I wonder what other term we could use. Partition? Isolate?