Add cached run_system API (#14920)
# Objective
Working with `World` is painful due to lifetime issues and a lack of
ergonomics, so you may want to delegate to the system API. Your current
options are:
- `world.run_system_once`, which initializes the system each time it's
called (performance cost) and doesn't support `Local`. The docs
recommend users not use this method outside of diagnostic use cases like
unit tests.
- `world.run_system`, which requires you to register the system and
store the `SystemId` somewhere (made easier by implementing `FromWorld`
for a newtyped `Local`, unless you're in e.g. a custom `Command` impl).
These options work, but you're choosing between a performance cost and
an ergonomic challenge.
## Solution
Provide a cached `run_system` API that accepts an `S: IntoSystem` and
checks for a `CachedSystemId<S::System>(SystemId)` resource. If it
doesn't exist, it will register the system and save its `SystemId` in
that resource.
In other words, it hides the "save the `SystemId` in a `Local` or
`Resource`" pattern as an implementation detail.
Prior work: https://github.com/bevyengine/bevy/pull/10469.
## Testing
This approach worked in a proof-of-concept:
b34ee29531/src/util/patch/run_system_cached.rs (L35).
A new unit test was added and it passes in CI.
This commit is contained in:
parent
4682f33e0c
commit
f78856b3bd
@ -3,7 +3,9 @@ mod parallel_scope;
|
||||
use core::panic::Location;
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use super::{Deferred, IntoObserverSystem, IntoSystem, RegisterSystem, Resource};
|
||||
use super::{
|
||||
Deferred, IntoObserverSystem, IntoSystem, RegisterSystem, Resource, RunSystemCachedWith,
|
||||
};
|
||||
use crate::{
|
||||
self as bevy_ecs,
|
||||
bundle::{Bundle, InsertMode},
|
||||
@ -758,6 +760,30 @@ impl<'w, 's> Commands<'w, 's> {
|
||||
SystemId::from_entity(entity)
|
||||
}
|
||||
|
||||
/// Similar to [`Self::run_system`], but caching the [`SystemId`] in a
|
||||
/// [`CachedSystemId`](crate::system::CachedSystemId) resource.
|
||||
///
|
||||
/// See [`World::register_system_cached`] for more information.
|
||||
pub fn run_system_cached<M: 'static, S: IntoSystem<(), (), M> + 'static>(&mut self, system: S) {
|
||||
self.run_system_cached_with(system, ());
|
||||
}
|
||||
|
||||
/// Similar to [`Self::run_system_with_input`], but caching the [`SystemId`] in a
|
||||
/// [`CachedSystemId`](crate::system::CachedSystemId) resource.
|
||||
///
|
||||
/// See [`World::register_system_cached`] for more information.
|
||||
pub fn run_system_cached_with<
|
||||
I: 'static + Send,
|
||||
M: 'static,
|
||||
S: IntoSystem<I, (), M> + 'static,
|
||||
>(
|
||||
&mut self,
|
||||
system: S,
|
||||
input: I,
|
||||
) {
|
||||
self.queue(RunSystemCachedWith::new(system, input));
|
||||
}
|
||||
|
||||
/// Sends a "global" [`Trigger`] without any targets. This will run any [`Observer`] of the `event` that
|
||||
/// isn't scoped to specific targets.
|
||||
///
|
||||
|
||||
@ -208,8 +208,8 @@ impl<In: 'static, Out: 'static> Debug for dyn System<In = In, Out = Out> {
|
||||
/// world.run_system_once(increment); // still prints 1
|
||||
/// ```
|
||||
///
|
||||
/// If you do need systems to hold onto state between runs, use the [`World::run_system`](World::run_system)
|
||||
/// and run the system by their [`SystemId`](crate::system::SystemId).
|
||||
/// If you do need systems to hold onto state between runs, use [`World::run_system_cached`](World::run_system_cached)
|
||||
/// or [`World::run_system`](World::run_system).
|
||||
///
|
||||
/// # Usage
|
||||
/// Typically, to test a system, or to extract specific diagnostics information from a world,
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
use crate::bundle::Bundle;
|
||||
use crate::change_detection::Mut;
|
||||
use crate::entity::Entity;
|
||||
use crate::system::{BoxedSystem, IntoSystem};
|
||||
use crate::system::{BoxedSystem, IntoSystem, System};
|
||||
use crate::world::{Command, World};
|
||||
use crate::{self as bevy_ecs};
|
||||
use bevy_ecs_macros::Component;
|
||||
use bevy_ecs_macros::{Component, Resource};
|
||||
use thiserror::Error;
|
||||
|
||||
/// A small wrapper for [`BoxedSystem`] that also keeps track whether or not the system has been initialized.
|
||||
@ -102,10 +104,29 @@ impl<I, O> std::fmt::Debug for SystemId<I, O> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A cached [`SystemId`] distinguished by the unique function type of its system.
|
||||
///
|
||||
/// This resource is inserted by [`World::register_system_cached`].
|
||||
#[derive(Resource)]
|
||||
pub struct CachedSystemId<S: System>(pub SystemId<S::In, S::Out>);
|
||||
|
||||
/// Creates a [`Bundle`] for a one-shot system entity.
|
||||
fn system_bundle<I: 'static, O: 'static>(system: BoxedSystem<I, O>) -> impl Bundle {
|
||||
(
|
||||
RegisteredSystem {
|
||||
initialized: false,
|
||||
system,
|
||||
},
|
||||
SystemIdMarker,
|
||||
)
|
||||
}
|
||||
|
||||
impl World {
|
||||
/// Registers a system and returns a [`SystemId`] so it can later be called by [`World::run_system`].
|
||||
///
|
||||
/// It's possible to register the same systems more than once, they'll be stored separately.
|
||||
/// It's possible to register multiple copies of the same system by calling this function
|
||||
/// multiple times. If that's not what you want, consider using [`World::register_system_cached`]
|
||||
/// instead.
|
||||
///
|
||||
/// This is different from adding systems to a [`Schedule`](crate::schedule::Schedule),
|
||||
/// because the [`SystemId`] that is returned can be used anywhere in the [`World`] to run the associated system.
|
||||
@ -122,23 +143,13 @@ impl World {
|
||||
/// Similar to [`Self::register_system`], but allows passing in a [`BoxedSystem`].
|
||||
///
|
||||
/// This is useful if the [`IntoSystem`] implementor has already been turned into a
|
||||
/// [`System`](crate::system::System) trait object and put in a [`Box`].
|
||||
/// [`System`] trait object and put in a [`Box`].
|
||||
pub fn register_boxed_system<I: 'static, O: 'static>(
|
||||
&mut self,
|
||||
system: BoxedSystem<I, O>,
|
||||
) -> SystemId<I, O> {
|
||||
SystemId {
|
||||
entity: self
|
||||
.spawn((
|
||||
RegisteredSystem {
|
||||
initialized: false,
|
||||
system,
|
||||
},
|
||||
SystemIdMarker,
|
||||
))
|
||||
.id(),
|
||||
marker: std::marker::PhantomData,
|
||||
}
|
||||
let entity = self.spawn(system_bundle(system)).id();
|
||||
SystemId::from_entity(entity)
|
||||
}
|
||||
|
||||
/// Removes a registered system and returns the system, if it exists.
|
||||
@ -320,6 +331,89 @@ impl World {
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Registers a system or returns its cached [`SystemId`].
|
||||
///
|
||||
/// If you want to run the system immediately and you don't need its `SystemId`, see
|
||||
/// [`World::run_system_cached`].
|
||||
///
|
||||
/// The first time this function is called for a particular system, it will register it and
|
||||
/// store its [`SystemId`] in a [`CachedSystemId`] resource for later. If you would rather
|
||||
/// manage the `SystemId` yourself, or register multiple copies of the same system, use
|
||||
/// [`World::register_system`] instead.
|
||||
///
|
||||
/// # Limitations
|
||||
///
|
||||
/// This function only accepts ZST (zero-sized) systems to guarantee that any two systems of
|
||||
/// the same type must be equal. This means that closures that capture the environment, and
|
||||
/// function pointers, are not accepted.
|
||||
///
|
||||
/// If you want to access values from the environment within a system, consider passing them in
|
||||
/// as inputs via [`World::run_system_cached_with`]. If that's not an option, consider
|
||||
/// [`World::register_system`] instead.
|
||||
pub fn register_system_cached<I: 'static, O: 'static, M, S: IntoSystem<I, O, M> + 'static>(
|
||||
&mut self,
|
||||
system: S,
|
||||
) -> SystemId<I, O> {
|
||||
const {
|
||||
assert!(
|
||||
size_of::<S>() == 0,
|
||||
"Non-ZST systems (e.g. capturing closures, function pointers) cannot be cached.",
|
||||
);
|
||||
}
|
||||
|
||||
if !self.contains_resource::<CachedSystemId<S::System>>() {
|
||||
let id = self.register_system(system);
|
||||
self.insert_resource(CachedSystemId::<S::System>(id));
|
||||
return id;
|
||||
}
|
||||
|
||||
self.resource_scope(|world, mut id: Mut<CachedSystemId<S::System>>| {
|
||||
if let Some(mut entity) = world.get_entity_mut(id.0.entity()) {
|
||||
if !entity.contains::<RegisteredSystem<I, O>>() {
|
||||
entity.insert(system_bundle(Box::new(IntoSystem::into_system(system))));
|
||||
}
|
||||
} else {
|
||||
id.0 = world.register_system(system);
|
||||
}
|
||||
id.0
|
||||
})
|
||||
}
|
||||
|
||||
/// Removes a cached system and its [`CachedSystemId`] resource.
|
||||
///
|
||||
/// See [`World::register_system_cached`] for more information.
|
||||
pub fn remove_system_cached<I: 'static, O: 'static, M, S: IntoSystem<I, O, M> + 'static>(
|
||||
&mut self,
|
||||
_system: S,
|
||||
) -> Result<RemovedSystem<I, O>, RegisteredSystemError<I, O>> {
|
||||
let id = self
|
||||
.remove_resource::<CachedSystemId<S::System>>()
|
||||
.ok_or(RegisteredSystemError::SystemNotCached)?;
|
||||
self.remove_system(id.0)
|
||||
}
|
||||
|
||||
/// Runs a cached system, registering it if necessary.
|
||||
///
|
||||
/// See [`World::register_system_cached`] for more information.
|
||||
pub fn run_system_cached<O: 'static, M, S: IntoSystem<(), O, M> + 'static>(
|
||||
&mut self,
|
||||
system: S,
|
||||
) -> Result<O, RegisteredSystemError<(), O>> {
|
||||
self.run_system_cached_with(system, ())
|
||||
}
|
||||
|
||||
/// Runs a cached system with an input, registering it if necessary.
|
||||
///
|
||||
/// See [`World::register_system_cached`] for more information.
|
||||
pub fn run_system_cached_with<I: 'static, O: 'static, M, S: IntoSystem<I, O, M> + 'static>(
|
||||
&mut self,
|
||||
system: S,
|
||||
input: I,
|
||||
) -> Result<O, RegisteredSystemError<I, O>> {
|
||||
let id = self.register_system_cached(system);
|
||||
self.run_system_with_input(id, input)
|
||||
}
|
||||
}
|
||||
|
||||
/// The [`Command`] type for [`World::run_system`] or [`World::run_system_with_input`].
|
||||
@ -353,7 +447,7 @@ pub struct RunSystemWithInput<I: 'static> {
|
||||
pub type RunSystem = RunSystemWithInput<()>;
|
||||
|
||||
impl RunSystem {
|
||||
/// Creates a new [`Command`] struct, which can be added to [`Commands`](crate::system::Commands)
|
||||
/// Creates a new [`Command`] struct, which can be added to [`Commands`](crate::system::Commands).
|
||||
pub fn new(system_id: SystemId) -> Self {
|
||||
Self::new_with_input(system_id, ())
|
||||
}
|
||||
@ -374,16 +468,16 @@ impl<I: 'static + Send> Command for RunSystemWithInput<I> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The [`Command`] type for registering one shot systems from [Commands](crate::system::Commands).
|
||||
/// The [`Command`] type for registering one shot systems from [`Commands`](crate::system::Commands).
|
||||
///
|
||||
/// This command needs an already boxed system to register, and an already spawned entity
|
||||
/// This command needs an already boxed system to register, and an already spawned entity.
|
||||
pub struct RegisterSystem<I: 'static, O: 'static> {
|
||||
system: BoxedSystem<I, O>,
|
||||
entity: Entity,
|
||||
}
|
||||
|
||||
impl<I: 'static, O: 'static> RegisterSystem<I, O> {
|
||||
/// Creates a new [Command] struct, which can be added to [Commands](crate::system::Commands)
|
||||
/// Creates a new [`Command`] struct, which can be added to [`Commands`](crate::system::Commands).
|
||||
pub fn new<M, S: IntoSystem<I, O, M> + 'static>(system: S, entity: Entity) -> Self {
|
||||
Self {
|
||||
system: Box::new(IntoSystem::into_system(system)),
|
||||
@ -394,12 +488,38 @@ impl<I: 'static, O: 'static> RegisterSystem<I, O> {
|
||||
|
||||
impl<I: 'static + Send, O: 'static + Send> Command for RegisterSystem<I, O> {
|
||||
fn apply(self, world: &mut World) {
|
||||
let _ = world.get_entity_mut(self.entity).map(|mut entity| {
|
||||
entity.insert(RegisteredSystem {
|
||||
initialized: false,
|
||||
system: self.system,
|
||||
});
|
||||
});
|
||||
if let Some(mut entity) = world.get_entity_mut(self.entity) {
|
||||
entity.insert(system_bundle(self.system));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The [`Command`] type for running a cached one-shot system from
|
||||
/// [`Commands`](crate::system::Commands).
|
||||
///
|
||||
/// See [`World::register_system_cached`] for more information.
|
||||
pub struct RunSystemCachedWith<S: System<Out = ()>> {
|
||||
system: S,
|
||||
input: S::In,
|
||||
}
|
||||
|
||||
impl<S: System<Out = ()>> RunSystemCachedWith<S> {
|
||||
/// Creates a new [`Command`] struct, which can be added to
|
||||
/// [`Commands`](crate::system::Commands).
|
||||
pub fn new<M>(system: impl IntoSystem<S::In, (), M, System = S>, input: S::In) -> Self {
|
||||
Self {
|
||||
system: IntoSystem::into_system(system),
|
||||
input,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: System<Out = ()>> Command for RunSystemCachedWith<S>
|
||||
where
|
||||
S::In: Send,
|
||||
{
|
||||
fn apply(self, world: &mut World) {
|
||||
let _ = world.run_system_cached_with(self.system, self.input);
|
||||
}
|
||||
}
|
||||
|
||||
@ -411,6 +531,11 @@ pub enum RegisteredSystemError<I = (), O = ()> {
|
||||
/// Did you forget to register it?
|
||||
#[error("System {0:?} was not registered")]
|
||||
SystemIdNotRegistered(SystemId<I, O>),
|
||||
/// A cached system was removed by value, but no system with its type was found.
|
||||
///
|
||||
/// Did you forget to register it?
|
||||
#[error("Cached system was not found")]
|
||||
SystemNotCached,
|
||||
/// A system tried to run itself recursively.
|
||||
#[error("System {0:?} tried to run itself recursively")]
|
||||
Recursive(SystemId<I, O>),
|
||||
@ -425,6 +550,7 @@ impl<I, O> std::fmt::Debug for RegisteredSystemError<I, O> {
|
||||
Self::SystemIdNotRegistered(arg0) => {
|
||||
f.debug_tuple("SystemIdNotRegistered").field(arg0).finish()
|
||||
}
|
||||
Self::SystemNotCached => write!(f, "SystemNotCached"),
|
||||
Self::Recursive(arg0) => f.debug_tuple("Recursive").field(arg0).finish(),
|
||||
Self::SelfRemove(arg0) => f.debug_tuple("SelfRemove").field(arg0).finish(),
|
||||
}
|
||||
@ -625,4 +751,35 @@ mod tests {
|
||||
let _ = world.run_system(nested_id);
|
||||
assert_eq!(*world.resource::<Counter>(), Counter(5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cached_system() {
|
||||
use crate::system::RegisteredSystemError;
|
||||
|
||||
fn four() -> i32 {
|
||||
4
|
||||
}
|
||||
|
||||
let mut world = World::new();
|
||||
let old = world.register_system_cached(four);
|
||||
let new = world.register_system_cached(four);
|
||||
assert_eq!(old, new);
|
||||
|
||||
let result = world.remove_system_cached(four);
|
||||
assert!(result.is_ok());
|
||||
let new = world.register_system_cached(four);
|
||||
assert_ne!(old, new);
|
||||
|
||||
let output = world.run_system(old);
|
||||
assert!(matches!(
|
||||
output,
|
||||
Err(RegisteredSystemError::SystemIdNotRegistered(x)) if x == old,
|
||||
));
|
||||
let output = world.run_system(new);
|
||||
assert!(matches!(output, Ok(x) if x == four()));
|
||||
let output = world.run_system_cached(four);
|
||||
assert!(matches!(output, Ok(x) if x == four()));
|
||||
let output = world.run_system_cached_with(four, ());
|
||||
assert!(matches!(output, Ok(x) if x == four()));
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user