From e6e32caa3bd78cf5bb43c07db484aa5f1ba39b40 Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Wed, 5 Nov 2025 21:13:52 -0800 Subject: [PATCH 1/9] Add SystemRunner --- crates/bevy_ecs/src/system/builder.rs | 37 ++++++- crates/bevy_ecs/src/system/system_param.rs | 115 ++++++++++++++++++++- 2 files changed, 148 insertions(+), 4 deletions(-) diff --git a/crates/bevy_ecs/src/system/builder.rs b/crates/bevy_ecs/src/system/builder.rs index 937911ca834c1..45599d455c6f4 100644 --- a/crates/bevy_ecs/src/system/builder.rs +++ b/crates/bevy_ecs/src/system/builder.rs @@ -7,8 +7,8 @@ use crate::{ query::{QueryData, QueryFilter, QueryState}, resource::Resource, system::{ - DynSystemParam, DynSystemParamState, If, Local, ParamSet, Query, SystemParam, - SystemParamValidationError, + BoxedSystem, DynSystemParam, DynSystemParamState, If, IntoSystem, Local, ParamSet, Query, + SystemInput, SystemParam, SystemParamValidationError, SystemRunner, SystemRunnerState, }, world::{ FilteredResources, FilteredResourcesBuilder, FilteredResourcesMut, @@ -202,6 +202,13 @@ impl ParamBuilder { ) -> impl SystemParamBuilder> { Self } + + /// Helper method for adding a [`SystemRunner`] as a param, equivalent to [`SystemRunnerBuilder::from_system`] + pub fn system<'w, 's, In: SystemInput + 'static, Out: 'static, Marker>( + system: impl IntoSystem, + ) -> impl SystemParamBuilder> { + SystemRunnerBuilder::from_system(system) + } } // SAFETY: Any `QueryState` for the correct world is valid for `Query::State`, @@ -616,6 +623,32 @@ unsafe impl> SystemParamBuilder> } } +/// A [`SystemParamBuilder`] for a [`SystemRunner`] +pub struct SystemRunnerBuilder( + BoxedSystem, +); + +impl SystemRunnerBuilder { + /// Returns a `SystemRunnerBuilder` created from a given system. + pub fn from_system, M>(system: S) -> Self { + Self(Box::new(S::into_system(system))) + } +} + +// SAFETY: the state returned is always valid. In particular the access always +// matches the contained system. +unsafe impl<'w, 's, In: SystemInput + 'static, Out: 'static> + SystemParamBuilder> for SystemRunnerBuilder +{ + fn build(mut self, world: &mut World) -> SystemRunnerState { + let access = self.0.initialize(world); + SystemRunnerState { + system: Some(self.0), + access, + } + } +} + #[cfg(test)] mod tests { use crate::{ diff --git a/crates/bevy_ecs/src/system/system_param.rs b/crates/bevy_ecs/src/system/system_param.rs index 80d449017d2eb..e829dd1261283 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::Archetypes, bundle::Bundles, change_detection::{ComponentTicksMut, ComponentTicksRef, Tick}, - component::{ComponentId, Components}, + component::{self, ComponentId, Components}, entity::{Entities, EntityAllocator}, query::{ Access, FilteredAccess, FilteredAccessSet, QueryData, QueryFilter, QuerySingleError, @@ -11,7 +11,7 @@ use crate::{ }, resource::Resource, storage::ResourceData, - system::{Query, Single, SystemMeta}, + system::{BoxedSystem, Query, RunSystemError, Single, System, SystemInput, SystemMeta}, world::{ unsafe_world_cell::UnsafeWorldCell, DeferredWorld, FilteredResources, FilteredResourcesMut, FromWorld, World, @@ -26,6 +26,7 @@ use core::{ any::Any, fmt::{Debug, Display}, marker::PhantomData, + mem, ops::{Deref, DerefMut}, }; use thiserror::Error; @@ -2765,6 +2766,116 @@ unsafe impl SystemParam for FilteredResourcesMut<'_, '_> { } } +pub struct SystemRunner<'w, 's, In: SystemInput = (), Out = ()> { + world: UnsafeWorldCell<'w>, + system: &'s mut dyn System, +} + +impl<'w, 's, In: SystemInput + 'static, Out: 'static> SystemRunner<'w, 's, In, Out> { + #[inline] + pub fn run_with(&mut self, input: In::Inner<'_>) -> Result { + // SAFETY: + // - all accesses are properly declared in `init_access` + unsafe { self.system.run_unsafe(input, self.world) } + } +} + +impl<'w, 's, Out: 'static> SystemRunner<'w, 's, (), Out> { + #[inline] + pub fn run(&mut self) -> Result { + self.run_with(()) + } +} + +/// The [`SystemParam::State`] for a [`SystemRunner`]. +pub struct SystemRunnerState { + pub(crate) system: Option>, + pub(crate) access: FilteredAccessSet, +} + +// SAFETY: SystemRunner registers all accesses and panics if a conflict is detected. +unsafe impl<'w, 's, In: SystemInput + 'static, Out: 'static> SystemParam + for SystemRunner<'w, 's, In, Out> +{ + type State = SystemRunnerState; + + type Item<'world, 'state> = SystemRunner<'world, 'state, In, Out>; + + #[inline] + fn init_state(_world: &mut World) -> Self::State { + SystemRunnerState { + system: None, + access: FilteredAccessSet::default(), + } + } + + fn init_access( + state: &Self::State, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + world: &mut World, + ) { + //TODO: does this handle exclusive systems properly? + let conflicts = component_access_set.get_conflicts(&state.access); + if !conflicts.is_empty() { + let system_name = system_meta.name(); + let mut accesses = conflicts.format_conflict_list(world); + // Access list may be empty (if access to all components requested) + if !accesses.is_empty() { + accesses.push(' '); + } + panic!("error[B0001]: SystemRunner({}) in system {system_name} accesses component(s) {accesses}in a way that conflicts with a previous system parameter. Consider using `Without` to create disjoint system accesses or using a `ParamSet`. See: https://bevy.org/learn/errors/b0001", state.system.as_ref().map(|sys| sys.name()).unwrap_or(DebugName::borrowed(""))); + } + + component_access_set.extend(state.access.clone()); + } + + #[inline] + unsafe fn get_param<'world, 'state>( + state: &'state mut Self::State, + _system_meta: &SystemMeta, + world: UnsafeWorldCell<'world>, + _change_tick: Tick, + ) -> Self::Item<'world, 'state> { + SystemRunner { + world, + system: state.system.as_deref_mut().expect("SystemRunner accessed with invalid state. This should have been caught by `validate_param`"), + } + } + + #[inline] + fn apply(state: &mut Self::State, _system_meta: &SystemMeta, world: &mut World) { + if let Some(sys) = &mut state.system { + sys.apply_deferred(world); + } + } + + #[inline] + fn queue(state: &mut Self::State, _system_meta: &SystemMeta, world: DeferredWorld) { + if let Some(sys) = &mut state.system { + sys.queue_deferred(world); + } + } + + unsafe fn validate_param( + state: &mut Self::State, + _system_meta: &SystemMeta, + world: UnsafeWorldCell, + ) -> Result<(), SystemParamValidationError> { + state.system.as_mut().ok_or(SystemParamValidationError { + skipped: false, + message: "SystemRunner must be manually initialized, for example with the system builder API.".into(), + param: DebugName::type_name::(), + field: "".into(), + }).and_then(|sys| { + // SAFETY: caller asserts that `world` has all accesses declared in `init_access`, + // which match the underlying system when `state.system` is `Ok`, and otherwise + // this branch will not be entered. + unsafe { sys.validate_param_unsafe(world) } + }) + } +} + /// An error that occurs when a system parameter is not valid, /// used by system executors to determine what to do with a system. /// From 4e1b13948cc874b44b294d32f0c8e9add259b8c6 Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Thu, 6 Nov 2025 21:45:07 -0800 Subject: [PATCH 2/9] Simplify system building with BuilderSystem --- crates/bevy_ecs/src/system/builder.rs | 208 +++++++++++++++++- crates/bevy_ecs/src/system/function_system.rs | 17 ++ 2 files changed, 218 insertions(+), 7 deletions(-) diff --git a/crates/bevy_ecs/src/system/builder.rs b/crates/bevy_ecs/src/system/builder.rs index 937911ca834c1..e086fde83d259 100644 --- a/crates/bevy_ecs/src/system/builder.rs +++ b/crates/bevy_ecs/src/system/builder.rs @@ -1,23 +1,26 @@ use alloc::{boxed::Box, vec::Vec}; use bevy_platform::cell::SyncCell; +use bevy_utils::prelude::DebugName; use variadics_please::all_tuples; use crate::{ + change_detection::{CheckChangeTicks, Tick}, prelude::QueryBuilder, - query::{QueryData, QueryFilter, QueryState}, + query::{FilteredAccessSet, QueryData, QueryFilter, QueryState}, resource::Resource, system::{ - DynSystemParam, DynSystemParamState, If, Local, ParamSet, Query, SystemParam, - SystemParamValidationError, + DynSystemParam, DynSystemParamState, FunctionSystem, If, Local, ParamSet, Query, System, + SystemMeta, SystemParam, SystemParamFunction, SystemParamValidationError, }, world::{ - FilteredResources, FilteredResourcesBuilder, FilteredResourcesMut, - FilteredResourcesMutBuilder, FromWorld, World, + unsafe_world_cell::UnsafeWorldCell, DeferredWorld, FilteredResources, + FilteredResourcesBuilder, FilteredResourcesMut, FilteredResourcesMutBuilder, FromWorld, + World, }, }; -use core::fmt::Debug; +use core::{any::TypeId, fmt::Debug, mem, ptr}; -use super::{Res, ResMut, SystemState}; +use super::{Res, ResMut, RunSystemError, SystemState, SystemStateFlags}; /// A builder that can create a [`SystemParam`]. /// @@ -119,6 +122,15 @@ pub unsafe trait SystemParamBuilder: Sized { fn build_state(self, world: &mut World) -> SystemState

{ SystemState::from_builder(world, self) } + + /// Create a [`System`] from a [`SystemParamBuilder`] + fn build_system(self, func: Func) -> BuilderSystem + where + Self: Send + Sync + 'static, + Func: SystemParamFunction + Send + Sync + 'static, + { + BuilderSystem::new(self, func) + } } /// A [`SystemParamBuilder`] for any [`SystemParam`] that uses its default initialization. @@ -204,6 +216,188 @@ impl ParamBuilder { } } +/// A [`System`] created from a [`SystemParamBuilder`] whose state is not +/// initialized until the first run. +pub struct BuilderSystem +where + Marker: 'static, + Builder: SystemParamBuilder + Send + Sync + 'static, + Func: SystemParamFunction, +{ + inner: BuilderSystemInner, +} + +impl BuilderSystem +where + Marker: 'static, + Builder: SystemParamBuilder + Send + Sync + 'static, + Func: SystemParamFunction, +{ + /// Returns a new `BuilderSystem` given a system param builder and a system function + pub fn new(builder: Builder, func: Func) -> Self { + Self { + inner: BuilderSystemInner::Uninitialized { + builder, + func, + meta: SystemMeta::new::(), + }, + } + } +} + +#[derive(Default)] +enum BuilderSystemInner +where + Marker: 'static, + Builder: SystemParamBuilder + Send + Sync + 'static, + Func: SystemParamFunction, +{ + Initialized(FunctionSystem), + Uninitialized { + builder: Builder, + func: Func, + meta: SystemMeta, + }, + // Only here as a `Default` for swapping in `initialize` + #[default] + ReallyUninitialized, +} + +impl System for BuilderSystem +where + Marker: 'static, + Builder: SystemParamBuilder + Send + Sync + 'static, + Func: SystemParamFunction, +{ + type In = Func::In; + + type Out = Func::Out; + + #[inline] + fn name(&self) -> DebugName { + match &self.inner { + BuilderSystemInner::Initialized(system) => system.name(), + BuilderSystemInner::Uninitialized { meta, .. } => meta.name().clone(), + BuilderSystemInner::ReallyUninitialized => unreachable!(), + } + } + + #[inline] + fn flags(&self) -> SystemStateFlags { + match &self.inner { + BuilderSystemInner::Initialized(system) => system.flags(), + BuilderSystemInner::Uninitialized { meta, .. } => meta.flags(), + BuilderSystemInner::ReallyUninitialized => unreachable!(), + } + } + + #[inline] + unsafe fn run_unsafe( + &mut self, + input: super::SystemIn<'_, Self>, + world: UnsafeWorldCell, + ) -> Result { + match &mut self.inner { + // SAFETY: requirements upheld by the caller. + BuilderSystemInner::Initialized(system) => unsafe { system.run_unsafe(input, world) }, + BuilderSystemInner::Uninitialized { .. } => panic!( + "BuilderSystem {} was not initialized before calling run_unsafe.", + self.name() + ), + BuilderSystemInner::ReallyUninitialized => unreachable!(), + } + } + + #[inline] + fn refresh_hotpatch(&mut self) { + match &mut self.inner { + BuilderSystemInner::Initialized(system) => system.refresh_hotpatch(), + BuilderSystemInner::Uninitialized { .. } => {} + BuilderSystemInner::ReallyUninitialized => unreachable!(), + } + } + + #[inline] + fn apply_deferred(&mut self, world: &mut World) { + match &mut self.inner { + BuilderSystemInner::Initialized(system) => system.apply_deferred(world), + BuilderSystemInner::Uninitialized { .. } => {} + BuilderSystemInner::ReallyUninitialized => unreachable!(), + } + } + + #[inline] + fn queue_deferred(&mut self, world: DeferredWorld) { + match &mut self.inner { + BuilderSystemInner::Initialized(system) => system.queue_deferred(world), + BuilderSystemInner::Uninitialized { .. } => {} + BuilderSystemInner::ReallyUninitialized => unreachable!(), + } + } + + #[inline] + unsafe fn validate_param_unsafe( + &mut self, + world: UnsafeWorldCell, + ) -> Result<(), SystemParamValidationError> { + match &mut self.inner { + // SAFETY: requirements upheld by the caller. + BuilderSystemInner::Initialized(system) => unsafe { + system.validate_param_unsafe(world) + }, + BuilderSystemInner::Uninitialized { .. } => Ok(()), + BuilderSystemInner::ReallyUninitialized => unreachable!(), + } + } + + #[inline] + fn initialize(&mut self, world: &mut World) -> FilteredAccessSet { + let inner = mem::take(&mut self.inner); + match inner { + BuilderSystemInner::Initialized(mut system) => { + let access = system.initialize(world); + self.inner = BuilderSystemInner::Initialized(system); + access + } + BuilderSystemInner::Uninitialized { builder, func, .. } => { + // SAFETY: this is wildly unsafe please change this. + let mut system = builder.build_state(world).build_any_system(func); + let access = system.initialize(world); + self.inner = BuilderSystemInner::Initialized(system); + access + } + BuilderSystemInner::ReallyUninitialized => unreachable!(), + } + } + + #[inline] + fn check_change_tick(&mut self, check: CheckChangeTicks) { + match &mut self.inner { + BuilderSystemInner::Initialized(system) => system.check_change_tick(check), + BuilderSystemInner::Uninitialized { .. } => {} + BuilderSystemInner::ReallyUninitialized => unreachable!(), + } + } + + #[inline] + fn get_last_run(&self) -> Tick { + match &self.inner { + BuilderSystemInner::Initialized(system) => system.get_last_run(), + BuilderSystemInner::Uninitialized { meta, .. } => meta.get_last_run(), + BuilderSystemInner::ReallyUninitialized => unreachable!(), + } + } + + #[inline] + fn set_last_run(&mut self, last_run: Tick) { + match &mut self.inner { + BuilderSystemInner::Initialized(system) => system.set_last_run(last_run), + BuilderSystemInner::Uninitialized { meta, .. } => meta.set_last_run(last_run), + BuilderSystemInner::ReallyUninitialized => unreachable!(), + } + } +} + // SAFETY: Any `QueryState` for the correct world is valid for `Query::State`, // and we check the world during `build`. unsafe impl<'w, 's, D: QueryData + 'static, F: QueryFilter + 'static> diff --git a/crates/bevy_ecs/src/system/function_system.rs b/crates/bevy_ecs/src/system/function_system.rs index 5f6653be39834..48e64819ed693 100644 --- a/crates/bevy_ecs/src/system/function_system.rs +++ b/crates/bevy_ecs/src/system/function_system.rs @@ -64,6 +64,11 @@ impl SystemMeta { &self.name } + /// Returns the system's state flags + pub fn flags(&self) -> SystemStateFlags { + self.flags + } + /// Sets the name of this system. /// /// Useful to give closure systems more readable and unique names for debugging and tracing. @@ -79,6 +84,18 @@ impl SystemMeta { self.name = new_name.into(); } + /// Gets the last time this system was run. + #[inline] + pub fn get_last_run(&self) -> Tick { + self.last_run + } + + /// Sets the last time this system was run. + #[inline] + pub fn set_last_run(&mut self, last_run: Tick) { + self.last_run = last_run; + } + /// Returns true if the system is [`Send`]. #[inline] pub fn is_send(&self) -> bool { From 38c9b2947cdd62799687ab6cbefad0dcb7a4c461 Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Fri, 7 Nov 2025 12:35:27 -0800 Subject: [PATCH 3/9] system composition macro --- crates/bevy_ecs/macros/src/lib.rs | 11 ++ .../bevy_ecs/macros/src/system_composition.rs | 121 ++++++++++++++++++ crates/bevy_ecs/src/system/mod.rs | 2 + 3 files changed, 134 insertions(+) create mode 100644 crates/bevy_ecs/macros/src/system_composition.rs diff --git a/crates/bevy_ecs/macros/src/lib.rs b/crates/bevy_ecs/macros/src/lib.rs index f968d36e50ea5..7e11fdc772e53 100644 --- a/crates/bevy_ecs/macros/src/lib.rs +++ b/crates/bevy_ecs/macros/src/lib.rs @@ -9,6 +9,7 @@ mod event; mod message; mod query_data; mod query_filter; +mod system_composition; mod world_query; use crate::{ @@ -719,3 +720,13 @@ pub fn derive_from_world(input: TokenStream) -> TokenStream { } }) } + +#[proc_macro] +pub fn compose(input: TokenStream) -> TokenStream { + system_composition::compose(input, false) +} + +#[proc_macro] +pub fn compose_with(input: TokenStream) -> TokenStream { + system_composition::compose(input, true) +} diff --git a/crates/bevy_ecs/macros/src/system_composition.rs b/crates/bevy_ecs/macros/src/system_composition.rs new file mode 100644 index 0000000000000..cbfe83c57c0c3 --- /dev/null +++ b/crates/bevy_ecs/macros/src/system_composition.rs @@ -0,0 +1,121 @@ +use core::mem; + +use bevy_macro_utils::ensure_no_collision; +use proc_macro::TokenStream; +use quote::{format_ident, quote}; +use syn::{ + parse::{Parse, ParseStream}, + parse_macro_input, parse_quote, parse_quote_spanned, + spanned::Spanned, + visit_mut::VisitMut, + Expr, ExprMacro, Ident, Pat, Path, Token, +}; + +use crate::bevy_ecs_path; + +struct ExpandSystemCalls { + call_ident: Ident, + systems_ident: Ident, + system_paths: Vec, +} + +struct SystemCall { + path: Path, + _comma: Option, + input: Option>, +} + +impl Parse for SystemCall { + fn parse(input: ParseStream) -> syn::Result { + let path = input.parse()?; + let comma: Option = input.parse()?; + let input = comma.as_ref().and_then(|_| input.parse().ok()); + Ok(Self { + path, + _comma: comma, + input, + }) + } +} + +impl VisitMut for ExpandSystemCalls { + fn visit_expr_mut(&mut self, i: &mut Expr) { + if let Expr::Macro(ExprMacro { attrs: _, mac }) = i + && mac.path.is_ident(&self.call_ident) + { + let call = match mac.parse_body::() { + Ok(call) => call, + Err(err) => { + *i = Expr::Verbatim(err.into_compile_error()); + return; + } + }; + + let call_index = match self.system_paths.iter().position(|p| p == &call.path) { + Some(i) => i, + None => { + let len = self.system_paths.len(); + self.system_paths.push(call.path.clone()); + len + } + }; + + let systems_ident = &self.systems_ident; + let system_accessor = format_ident!("p{}", call_index); + let expr: Expr = match &call.input { + Some(input) => { + parse_quote_spanned!(mac.span()=> #systems_ident.#system_accessor().run_with(#input)) + } + None => { + parse_quote_spanned!(mac.span()=> #systems_ident.#system_accessor().run()) + } + }; + *i = expr; + } else { + syn::visit_mut::visit_expr_mut(self, i); + } + } +} + +pub fn compose(input: TokenStream, has_input: bool) -> TokenStream { + let bevy_ecs_path = bevy_ecs_path(); + let call_ident = format_ident!("run"); + let systems_ident = ensure_no_collision(format_ident!("__systems"), input.clone()); + let mut expr_closure = parse_macro_input!(input as syn::ExprClosure); + + let mut visitor = ExpandSystemCalls { + call_ident, + systems_ident: systems_ident.clone(), + system_paths: Vec::new(), + }; + + syn::visit_mut::visit_expr_closure_mut(&mut visitor, &mut expr_closure); + + let runner_types: Vec = visitor + .system_paths + .iter() + .map(|path| parse_quote_spanned!(path.span()=> #bevy_ecs_path::system::SystemRunner<_, _>)) + .collect(); + + let mut builders: Vec = vec![ + parse_quote!(#bevy_ecs_path::system::builder::ParamBuilder); + if has_input { + expr_closure.inputs.len() - 1 + } else { + expr_closure.inputs.len() + } + ]; + let system_builders: Vec = visitor.system_paths.iter().map(|path| parse_quote_spanned!(path.span()=> #bevy_ecs_path::system::ParamBuilder::system(#path))).collect(); + builders.push(parse_quote!(#bevy_ecs_path::system::ParamSetBuilder((#(#system_builders,)*)))); + + expr_closure.inputs.push(Pat::Type( + parse_quote!(mut #systems_ident: #bevy_ecs_path::system::ParamSet<(#(#runner_types,)*)>), + )); + + TokenStream::from(quote! { + #bevy_ecs_path::system::SystemParamBuilder::build_system( + (#(#builders,)*), + #expr_closure + ) + }) +} diff --git a/crates/bevy_ecs/src/system/mod.rs b/crates/bevy_ecs/src/system/mod.rs index eeecf4b7ac5db..6e97a69104b31 100644 --- a/crates/bevy_ecs/src/system/mod.rs +++ b/crates/bevy_ecs/src/system/mod.rs @@ -153,6 +153,8 @@ pub use system_name::*; pub use system_param::*; pub use system_registry::*; +pub use bevy_ecs_macros::{compose, compose_with}; + use crate::world::{FromWorld, World}; /// Conversion trait to turn something into a [`System`]. From c2d996a5ed2a0b7a3e540ed22a94de56b2cf8e05 Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Thu, 6 Nov 2025 21:52:14 -0800 Subject: [PATCH 4/9] cleanup --- crates/bevy_ecs/macros/Cargo.toml | 2 +- .../bevy_ecs/macros/src/system_composition.rs | 27 ++++-- crates/bevy_ecs/src/system/builder.rs | 97 +++++++++++++------ crates/bevy_ecs/src/system/system_param.rs | 65 +++++++++---- 4 files changed, 130 insertions(+), 61 deletions(-) diff --git a/crates/bevy_ecs/macros/Cargo.toml b/crates/bevy_ecs/macros/Cargo.toml index c70c258e6d74f..1103128a8b88a 100644 --- a/crates/bevy_ecs/macros/Cargo.toml +++ b/crates/bevy_ecs/macros/Cargo.toml @@ -11,7 +11,7 @@ proc-macro = true [dependencies] bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.18.0-dev" } -syn = { version = "2.0.108", features = ["full", "extra-traits"] } +syn = { version = "2.0.108", features = ["full", "extra-traits", "visit-mut"] } quote = "1.0" proc-macro2 = "1.0" [lints] diff --git a/crates/bevy_ecs/macros/src/system_composition.rs b/crates/bevy_ecs/macros/src/system_composition.rs index cbfe83c57c0c3..3bf1f82dbc79b 100644 --- a/crates/bevy_ecs/macros/src/system_composition.rs +++ b/crates/bevy_ecs/macros/src/system_composition.rs @@ -1,5 +1,3 @@ -use core::mem; - use bevy_macro_utils::ensure_no_collision; use proc_macro::TokenStream; use quote::{format_ident, quote}; @@ -94,17 +92,26 @@ pub fn compose(input: TokenStream, has_input: bool) -> TokenStream { let runner_types: Vec = visitor .system_paths .iter() - .map(|path| parse_quote_spanned!(path.span()=> #bevy_ecs_path::system::SystemRunner<_, _>)) + .map(|path| parse_quote_spanned!(path.span()=> #bevy_ecs_path::system::SystemRunner<_, _, _>)) .collect(); - let mut builders: Vec = vec![ - parse_quote!(#bevy_ecs_path::system::builder::ParamBuilder); - if has_input { - expr_closure.inputs.len() - 1 - } else { - expr_closure.inputs.len() + let param_count = if has_input { + if expr_closure.inputs.is_empty() { + return TokenStream::from( + syn::Error::new_spanned( + &expr_closure.inputs, + "closure must have at least one parameter", + ) + .into_compile_error(), + ); } - ]; + expr_closure.inputs.len() - 1 + } else { + expr_closure.inputs.len() + }; + + let mut builders: Vec = + vec![parse_quote!(#bevy_ecs_path::system::ParamBuilder); param_count]; let system_builders: Vec = visitor.system_paths.iter().map(|path| parse_quote_spanned!(path.span()=> #bevy_ecs_path::system::ParamBuilder::system(#path))).collect(); builders.push(parse_quote!(#bevy_ecs_path::system::ParamSetBuilder((#(#system_builders,)*)))); diff --git a/crates/bevy_ecs/src/system/builder.rs b/crates/bevy_ecs/src/system/builder.rs index 6d8fd8d8dc459..759e8bafaf91f 100644 --- a/crates/bevy_ecs/src/system/builder.rs +++ b/crates/bevy_ecs/src/system/builder.rs @@ -9,9 +9,10 @@ use crate::{ query::{FilteredAccessSet, QueryData, QueryFilter, QueryState}, resource::Resource, system::{ - BoxedSystem, DynSystemParam, DynSystemParamState, FunctionSystem, If, IntoSystem, Local, - ParamSet, Query, System, SystemInput, SystemMeta, SystemParam, SystemParamFunction, - SystemParamValidationError, SystemRunner, SystemRunnerState, + BoxedSystem, DynSystemParam, DynSystemParamState, FunctionSystem, If, IntoResult, + IntoSystem, Local, ParamSet, Query, ReadOnlySystem, System, SystemInput, SystemMeta, + SystemParam, SystemParamFunction, SystemParamValidationError, SystemRunner, + SystemRunnerState, }, world::{ unsafe_world_cell::UnsafeWorldCell, DeferredWorld, FilteredResources, @@ -19,6 +20,7 @@ use crate::{ World, }, }; +use alloc::borrow::Cow; use core::{fmt::Debug, mem}; use super::{Res, ResMut, RunSystemError, SystemState, SystemStateFlags}; @@ -125,10 +127,10 @@ pub unsafe trait SystemParamBuilder: Sized { } /// Create a [`System`] from a [`SystemParamBuilder`] - fn build_system(self, func: Func) -> BuilderSystem + fn build_system(self, func: Func) -> BuilderSystem where Self: Send + Sync + 'static, - Func: SystemParamFunction + Send + Sync + 'static, + Func: SystemParamFunction> + Send + Sync + 'static, { BuilderSystem::new(self, func) } @@ -217,30 +219,37 @@ impl ParamBuilder { } /// Helper method for adding a [`SystemRunner`] as a param, equivalent to [`SystemRunnerBuilder::from_system`] - pub fn system<'w, 's, In: SystemInput + 'static, Out: 'static, Marker>( - system: impl IntoSystem, - ) -> impl SystemParamBuilder> { - SystemRunnerBuilder::from_system(system) + pub fn system<'w, 's, In, Out, Marker, Sys>( + system: Sys, + ) -> impl SystemParamBuilder> + where + In: SystemInput, + Sys: IntoSystem, + { + SystemRunnerBuilder::new(IntoSystem::into_system(system)) } } /// A [`System`] created from a [`SystemParamBuilder`] whose state is not /// initialized until the first run. -pub struct BuilderSystem +pub struct BuilderSystem where Marker: 'static, Builder: SystemParamBuilder + Send + Sync + 'static, - Func: SystemParamFunction, + Func: SystemParamFunction>, + Out: 'static, { - inner: BuilderSystemInner, + inner: BuilderSystemInner, } -impl BuilderSystem +impl BuilderSystem where Marker: 'static, Builder: SystemParamBuilder + Send + Sync + 'static, - Func: SystemParamFunction, + Func: SystemParamFunction>, + Out: 'static, { + #[inline] /// Returns a new `BuilderSystem` given a system param builder and a system function pub fn new(builder: Builder, func: Func) -> Self { Self { @@ -251,16 +260,28 @@ where }, } } + + #[inline] + /// Returns this BuilderSystem with a custom name. + pub fn with_name(mut self, name: impl Into>) -> Self { + if let BuilderSystemInner::Uninitialized { meta, .. } = &mut self.inner { + meta.set_name(name); + } else { + panic!("Called with_name() on an already initialized BuilderSystem"); + } + self + } } #[derive(Default)] -enum BuilderSystemInner +enum BuilderSystemInner where Marker: 'static, Builder: SystemParamBuilder + Send + Sync + 'static, - Func: SystemParamFunction, + Func: SystemParamFunction>, + Out: 'static, { - Initialized(FunctionSystem), + Initialized(FunctionSystem), Uninitialized { builder: Builder, func: Func, @@ -271,15 +292,16 @@ where ReallyUninitialized, } -impl System for BuilderSystem +impl System for BuilderSystem where Marker: 'static, Builder: SystemParamBuilder + Send + Sync + 'static, - Func: SystemParamFunction, + Func: SystemParamFunction>, + Out: 'static, { type In = Func::In; - type Out = Func::Out; + type Out = Out; #[inline] fn name(&self) -> DebugName { @@ -316,6 +338,7 @@ where } } + #[cfg(feature = "hotpatching")] #[inline] fn refresh_hotpatch(&mut self) { match &mut self.inner { @@ -406,6 +429,17 @@ where } } +// SAFETY: if the inner system is readonly, so is this this wrapper +unsafe impl ReadOnlySystem for BuilderSystem +where + Marker: 'static, + Builder: SystemParamBuilder + Send + Sync + 'static, + Func: SystemParamFunction>, + Out: 'static, + for<'a> FunctionSystem: ReadOnlySystem, +{ +} + // SAFETY: Any `QueryState` for the correct world is valid for `Query::State`, // and we check the world during `build`. unsafe impl<'w, 's, D: QueryData + 'static, F: QueryFilter + 'static> @@ -819,23 +853,28 @@ unsafe impl> SystemParamBuilder> } /// A [`SystemParamBuilder`] for a [`SystemRunner`] -pub struct SystemRunnerBuilder( - BoxedSystem, -); +pub struct SystemRunnerBuilder(Box); -impl SystemRunnerBuilder { +impl SystemRunnerBuilder { /// Returns a `SystemRunnerBuilder` created from a given system. - pub fn from_system, M>(system: S) -> Self { - Self(Box::new(S::into_system(system))) + pub fn new(system: Sys) -> Self { + Self(Box::new(system)) + } +} + +impl SystemRunnerBuilder> { + /// Returns a `SystemRunnerBuilder` created from a boxed system. + pub fn boxed(system: BoxedSystem) -> Self { + Self(system) } } // SAFETY: the state returned is always valid. In particular the access always // matches the contained system. -unsafe impl<'w, 's, In: SystemInput + 'static, Out: 'static> - SystemParamBuilder> for SystemRunnerBuilder +unsafe impl<'w, 's, Sys: System + ?Sized> + SystemParamBuilder> for SystemRunnerBuilder { - fn build(mut self, world: &mut World) -> SystemRunnerState { + fn build(mut self, world: &mut World) -> SystemRunnerState { let access = self.0.initialize(world); SystemRunnerState { system: Some(self.0), diff --git a/crates/bevy_ecs/src/system/system_param.rs b/crates/bevy_ecs/src/system/system_param.rs index e829dd1261283..75d87345449df 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::Archetypes, bundle::Bundles, change_detection::{ComponentTicksMut, ComponentTicksRef, Tick}, - component::{self, ComponentId, Components}, + component::{ComponentId, Components}, entity::{Entities, EntityAllocator}, query::{ Access, FilteredAccess, FilteredAccessSet, QueryData, QueryFilter, QuerySingleError, @@ -11,7 +11,7 @@ use crate::{ }, resource::Resource, storage::ResourceData, - system::{BoxedSystem, Query, RunSystemError, Single, System, SystemInput, SystemMeta}, + system::{Query, ReadOnlySystem, RunSystemError, Single, System, SystemInput, SystemMeta}, world::{ unsafe_world_cell::UnsafeWorldCell, DeferredWorld, FilteredResources, FilteredResourcesMut, FromWorld, World, @@ -26,7 +26,6 @@ use core::{ any::Any, fmt::{Debug, Display}, marker::PhantomData, - mem, ops::{Deref, DerefMut}, }; use thiserror::Error; @@ -2766,21 +2765,39 @@ unsafe impl SystemParam for FilteredResourcesMut<'_, '_> { } } -pub struct SystemRunner<'w, 's, In: SystemInput = (), Out = ()> { +pub struct SystemRunner<'w, 's, In = (), Out = (), Sys = dyn System> +where + In: SystemInput, + Sys: System + ?Sized, +{ world: UnsafeWorldCell<'w>, - system: &'s mut dyn System, + system: &'s mut Sys, } -impl<'w, 's, In: SystemInput + 'static, Out: 'static> SystemRunner<'w, 's, In, Out> { +impl<'w, 's, In, Out, Sys> SystemRunner<'w, 's, In, Out, Sys> +where + In: SystemInput, + Sys: System + ?Sized, +{ #[inline] pub fn run_with(&mut self, input: In::Inner<'_>) -> Result { + // SAFETY: + // - all accesses are properly declared in `init_access` + unsafe { + self.system + .validate_param_unsafe(self.world) + .map_err(RunSystemError::Skipped)?; + } // SAFETY: // - all accesses are properly declared in `init_access` unsafe { self.system.run_unsafe(input, self.world) } } } -impl<'w, 's, Out: 'static> SystemRunner<'w, 's, (), Out> { +impl<'w, 's, Out, Sys> SystemRunner<'w, 's, (), Out, Sys> +where + Sys: System + ?Sized, +{ #[inline] pub fn run(&mut self) -> Result { self.run_with(()) @@ -2788,18 +2805,20 @@ impl<'w, 's, Out: 'static> SystemRunner<'w, 's, (), Out> { } /// The [`SystemParam::State`] for a [`SystemRunner`]. -pub struct SystemRunnerState { - pub(crate) system: Option>, +pub struct SystemRunnerState { + pub(crate) system: Option>, pub(crate) access: FilteredAccessSet, } // SAFETY: SystemRunner registers all accesses and panics if a conflict is detected. -unsafe impl<'w, 's, In: SystemInput + 'static, Out: 'static> SystemParam - for SystemRunner<'w, 's, In, Out> +unsafe impl<'w, 's, In, Out, Sys> SystemParam for SystemRunner<'w, 's, In, Out, Sys> +where + In: SystemInput, + Sys: System + ?Sized, { - type State = SystemRunnerState; + type State = SystemRunnerState; - type Item<'world, 'state> = SystemRunner<'world, 'state, In, Out>; + type Item<'world, 'state> = SystemRunner<'world, 'state, In, Out, Sys>; #[inline] fn init_state(_world: &mut World) -> Self::State { @@ -2809,13 +2828,13 @@ unsafe impl<'w, 's, In: SystemInput + 'static, Out: 'static> SystemParam } } + #[inline] fn init_access( state: &Self::State, system_meta: &mut SystemMeta, component_access_set: &mut FilteredAccessSet, world: &mut World, ) { - //TODO: does this handle exclusive systems properly? let conflicts = component_access_set.get_conflicts(&state.access); if !conflicts.is_empty() { let system_name = system_meta.name(); @@ -2857,25 +2876,29 @@ unsafe impl<'w, 's, In: SystemInput + 'static, Out: 'static> SystemParam } } + #[inline] unsafe fn validate_param( state: &mut Self::State, _system_meta: &SystemMeta, - world: UnsafeWorldCell, + _world: UnsafeWorldCell, ) -> Result<(), SystemParamValidationError> { - state.system.as_mut().ok_or(SystemParamValidationError { + state.system.as_ref().map(|_| ()).ok_or(SystemParamValidationError { skipped: false, message: "SystemRunner must be manually initialized, for example with the system builder API.".into(), param: DebugName::type_name::(), field: "".into(), - }).and_then(|sys| { - // SAFETY: caller asserts that `world` has all accesses declared in `init_access`, - // which match the underlying system when `state.system` is `Ok`, and otherwise - // this branch will not be entered. - unsafe { sys.validate_param_unsafe(world) } }) } } +// SAFETY: if the internal system is readonly, this param can't mutate the world. +unsafe impl<'w, 's, In, Out, Sys> ReadOnlySystemParam for SystemRunner<'w, 's, In, Out, Sys> +where + In: SystemInput, + Sys: System + ReadOnlySystem + ?Sized, +{ +} + /// An error that occurs when a system parameter is not valid, /// used by system executors to determine what to do with a system. /// From b986a71e606f121e279df120c3ed3972878c0143 Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Mon, 10 Nov 2025 13:15:55 -0800 Subject: [PATCH 5/9] docs and example --- crates/bevy_ecs/macros/src/lib.rs | 72 +++++++++++++++++++ crates/bevy_ecs/src/system/system_param.rs | 43 +++++++++++ examples/ecs/system_piping.rs | 31 +++++--- .../migration-guides/system_input_unwrap.md | 9 +++ .../release-notes/system_composition.md | 49 +++++++++++++ 5 files changed, 195 insertions(+), 9 deletions(-) create mode 100644 release-content/migration-guides/system_input_unwrap.md create mode 100644 release-content/release-notes/system_composition.md diff --git a/crates/bevy_ecs/macros/src/lib.rs b/crates/bevy_ecs/macros/src/lib.rs index 7e11fdc772e53..8fe193ea43c4e 100644 --- a/crates/bevy_ecs/macros/src/lib.rs +++ b/crates/bevy_ecs/macros/src/lib.rs @@ -721,11 +721,83 @@ pub fn derive_from_world(input: TokenStream) -> TokenStream { }) } +/// Automatically compose systems together with function syntax. +/// +/// This macro provides some nice syntax on top of the `SystemRunner` `SystemParam` +/// to allow running systems inside other systems. Overall, the macro accepts normal +/// closure syntax: +/// +/// ```ignore +/// let system_a = |world: &mut World| { 10 }; +/// let system_b = |a: In, world: &mut World| { println!("{}", *a + 12) }; +/// compose! { +/// || -> Result<(), RunSystemError> { +/// let a = run!(system-a)?; +/// run!(system_b); +/// } +/// } +/// ``` +/// +/// What's special is that the macro will expand any invocations of `run!()` into +/// calls to `SystemRunner::run` or `SystemRunner::run_with`. The `run!()` accepts +/// two parameters: first, a system identifier (or a path to one), and second, an +/// optional input to invoke the system with. +/// +/// Notes: +/// 1. All system runners are passed through a `ParamSet`, so invoked systems will +/// not conflict with each other. However, invoked systems may still conflict +/// with system params in the outer closure. +/// +/// 2. `run!` will not accept expressions that evaluate to systems, only direct +/// identifiers or paths. So, if you want to call something like: +/// +/// ```ignore +/// run!(|query: Query<(&A, &B, &mut C)>| { ... })` +/// ``` +/// +/// Assign the expression to a variable first. #[proc_macro] pub fn compose(input: TokenStream) -> TokenStream { system_composition::compose(input, false) } +/// Automatically compose systems together with function syntax. +/// +/// Unlike [`compose`], this macro allows generating systems that take input. +/// +/// This macro provides some nice syntax on top of the `SystemRunner` `SystemParam` +/// to allow running systems inside other systems. Overall, the macro accepts normal +/// closure syntax: +/// +/// ```ignore +/// let system_a = |input: In, world: &mut World| { *input + 10 }; +/// let system_b = |a: In, world: &mut World| { println!("{}", *a + 12) }; +/// compose_with! { +/// |input: In| -> Result<(), RunSystemError> { +/// let a = run!(system_a, input)?; +/// run!(system_b); +/// } +/// } +/// ``` +/// +/// What's special is that the macro will expand any invocations of `run!()` into +/// calls to `SystemRunner::run` or `SystemRunner::run_with`. The `run!()` accepts +/// two parameters: first, a system identifier (or a path to one), and second, an +/// optional input to invoke the system with. +/// +/// Notes: +/// 1. All system runners are passed through a `ParamSet`, so invoked systems will +/// not conflict with each other. However, invoked systems may still conflict +/// with system params in the outer closure. +/// +/// 2. `run!` will not accept expressions that evaluate to systems, only direct +/// identifiers or paths. So, if you want to call something like: +/// +/// ```ignore +/// run!(|query: Query<(&A, &B, &mut C)>| { ... })` +/// ``` +/// +/// Assign the expression to a variable first. #[proc_macro] pub fn compose_with(input: TokenStream) -> TokenStream { system_composition::compose(input, true) diff --git a/crates/bevy_ecs/src/system/system_param.rs b/crates/bevy_ecs/src/system/system_param.rs index 75d87345449df..b1d0c9e17dd81 100644 --- a/crates/bevy_ecs/src/system/system_param.rs +++ b/crates/bevy_ecs/src/system/system_param.rs @@ -2765,6 +2765,47 @@ unsafe impl SystemParam for FilteredResourcesMut<'_, '_> { } } +/// A [`SystemParam`] that allows running other systems. +/// +/// To be useful, this must be configured using a [`SystemRunnerBuilder`](crate::system::SystemRunnerBuilder) +/// to build the system using a [`SystemParamBuilder`](crate::prelude::SystemParamBuilder). +/// +/// Also see the macros [`compose`](crate::system::compose) and [`compose_with`](crate::system::compose_with) +/// for some nice syntax on top of this API. +/// +/// # Examples +/// +/// ``` +/// # use bevy_ecs::{prelude::*, system::*}; +/// # +/// # #[derive(Component)] +/// # struct A; +/// # +/// # #[derive(Component)] +/// # struct B; +/// # let mut world = World::new(); +/// # +/// fn count_a(a: Query<&A>) -> u32 { +/// a.len() +/// } +/// +/// fn count_b(b: Query<&B>) -> u32 { +/// b.len() +/// } +/// +/// let get_sum = ( +/// ParamBuilder::system(count_a), +/// ParamBuilder::system(count_b) +/// ) +/// .build_state(&mut world) +/// .build_system( +/// |mut run_a: SystemRunner<(), u32>, mut run_b: SystemRunner<(), u32>| -> Result { +/// let a = run_a.run()?; +/// let b = run_b.run()?; +/// Ok(a + b) +/// } +/// ); +/// ``` pub struct SystemRunner<'w, 's, In = (), Out = (), Sys = dyn System> where In: SystemInput, @@ -2779,6 +2820,7 @@ where In: SystemInput, Sys: System + ?Sized, { + /// Run the system with input. #[inline] pub fn run_with(&mut self, input: In::Inner<'_>) -> Result { // SAFETY: @@ -2798,6 +2840,7 @@ impl<'w, 's, Out, Sys> SystemRunner<'w, 's, (), Out, Sys> where Sys: System + ?Sized, { + /// Run the system. #[inline] pub fn run(&mut self) -> Result { self.run_with(()) diff --git a/examples/ecs/system_piping.rs b/examples/ecs/system_piping.rs index a3607c085de6d..9ca2bc727c05e 100644 --- a/examples/ecs/system_piping.rs +++ b/examples/ecs/system_piping.rs @@ -2,6 +2,7 @@ //! passing the output of the first into the input of the next. use bevy::prelude::*; +use bevy_ecs::system::{compose, RunSystemError}; use std::num::ParseIntError; use bevy::log::{debug, error, info, Level, LogPlugin}; @@ -21,17 +22,29 @@ fn main() { parse_message_system.pipe(handler_system), data_pipe_system.map(|out| info!("{out}")), parse_message_system.map(|out| debug!("{out:?}")), - warning_pipe_system.map(|out| { - if let Err(err) = out { - error!("{err}"); + parse_message_system.map(drop), + // You can also use the `compose!` macro to pipe systems together! + // This is even more powerful, but might be harder to use with + // fully generic code. See the docs on `compose!` and `compose_with!` + // for more info. + compose! { + || -> Result<(), RunSystemError> { + let out = run!(warning_pipe_system)?; + if let Err(err) = out { + error!("{err}"); + } + Ok(()) } - }), - parse_error_message_system.map(|out| { - if let Err(err) = out { - error!("{err}"); + }, + compose! { + || -> Result<(), RunSystemError> { + let out = run!(parse_error_message_system)?; + if let Err(err) = out { + error!("{err}"); + } + Ok(()) } - }), - parse_message_system.map(drop), + }, ), ) .run(); diff --git a/release-content/migration-guides/system_input_unwrap.md b/release-content/migration-guides/system_input_unwrap.md new file mode 100644 index 0000000000000..eabbc007d3544 --- /dev/null +++ b/release-content/migration-guides/system_input_unwrap.md @@ -0,0 +1,9 @@ +--- +title: "New required method on SystemInput" +pull_requests: [todo] +--- + +Custom implementations of the `SystemInput` trait will need to implement a new +required method: `unwrap`. Like `wrap`, it converts between the inner input item +and the wrapper, but in the opposite direction. In most cases it should be +trivial to add. diff --git a/release-content/release-notes/system_composition.md b/release-content/release-notes/system_composition.md new file mode 100644 index 0000000000000..e31f1af7e4bbc --- /dev/null +++ b/release-content/release-notes/system_composition.md @@ -0,0 +1,49 @@ +--- +title: System Composition +authors: ["@ecoskey"] +pull_requests: [todo] +--- + +## `SystemRunner` SystemParam + +We've been working on some new tools to make composing multiple ECS systems together +even easier. Bevy 0.18 introduces the `SystemRunner` `SystemParam`, allowing running +systems inside other systems! + +```rust +fn count_a(a: Query<&A>) -> u32 { + a.iter().len() +} + +fn count_b(b: Query<&B>) -> u32 { + b.iter().len() +} + +let get_sum = ( + ParamBuilder::system(count_a), + ParamBuilder::system(count_b) +) +.build_system( + |mut run_a: SystemRunner<(), u32>, mut run_b: SystemRunner<(), u32>| -> Result { + let a = run_a.run()?; + let b = run_b.run()?; + Ok(a + b) + } +); +``` + +## `compose!` and `compose_with!` + +With this new API we've also added some nice macro syntax to go on top. The `compose!` +and `compose_with!` macros will automatically transform a provided closure, making +the new `SystemRunner` params almost seamless to use. + +```rust +compose! { + || -> Result { + let a = run!(count_a)?; + let b = run!(count_b)?; + Ok(a + b) + } +} +``` From e0c905cdfa82c142cfe7bbffe85a71ac4ee78b51 Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Tue, 11 Nov 2025 17:23:28 -0800 Subject: [PATCH 6/9] fix docs --- crates/bevy_ecs/src/system/system_param.rs | 4 ++-- release-content/release-notes/system_composition.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/bevy_ecs/src/system/system_param.rs b/crates/bevy_ecs/src/system/system_param.rs index b1d0c9e17dd81..0f4ccef3b65d5 100644 --- a/crates/bevy_ecs/src/system/system_param.rs +++ b/crates/bevy_ecs/src/system/system_param.rs @@ -2786,11 +2786,11 @@ unsafe impl SystemParam for FilteredResourcesMut<'_, '_> { /// # let mut world = World::new(); /// # /// fn count_a(a: Query<&A>) -> u32 { -/// a.len() +/// a.count() /// } /// /// fn count_b(b: Query<&B>) -> u32 { -/// b.len() +/// b.count() /// } /// /// let get_sum = ( diff --git a/release-content/release-notes/system_composition.md b/release-content/release-notes/system_composition.md index e31f1af7e4bbc..b10c97c383d3c 100644 --- a/release-content/release-notes/system_composition.md +++ b/release-content/release-notes/system_composition.md @@ -12,11 +12,11 @@ systems inside other systems! ```rust fn count_a(a: Query<&A>) -> u32 { - a.iter().len() + a.count() } fn count_b(b: Query<&B>) -> u32 { - b.iter().len() + b.count() } let get_sum = ( From 2b4849ff28baf7c8ca23553f195f0a9b63268fdf Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Tue, 11 Nov 2025 17:25:23 -0800 Subject: [PATCH 7/9] fix ci --- examples/ecs/system_piping.rs | 2 +- release-content/migration-guides/system_input_unwrap.md | 2 +- release-content/release-notes/system_composition.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/ecs/system_piping.rs b/examples/ecs/system_piping.rs index 9ca2bc727c05e..d7e35ef93920b 100644 --- a/examples/ecs/system_piping.rs +++ b/examples/ecs/system_piping.rs @@ -1,8 +1,8 @@ //! Illustrates how to make a single system from multiple functions running in sequence, //! passing the output of the first into the input of the next. +use bevy::ecs::system::{compose, RunSystemError}; use bevy::prelude::*; -use bevy_ecs::system::{compose, RunSystemError}; use std::num::ParseIntError; use bevy::log::{debug, error, info, Level, LogPlugin}; diff --git a/release-content/migration-guides/system_input_unwrap.md b/release-content/migration-guides/system_input_unwrap.md index eabbc007d3544..f555775ffd3f3 100644 --- a/release-content/migration-guides/system_input_unwrap.md +++ b/release-content/migration-guides/system_input_unwrap.md @@ -1,6 +1,6 @@ --- title: "New required method on SystemInput" -pull_requests: [todo] +pull_requests: [21811] --- Custom implementations of the `SystemInput` trait will need to implement a new diff --git a/release-content/release-notes/system_composition.md b/release-content/release-notes/system_composition.md index b10c97c383d3c..07c4ae1343776 100644 --- a/release-content/release-notes/system_composition.md +++ b/release-content/release-notes/system_composition.md @@ -1,7 +1,7 @@ --- title: System Composition authors: ["@ecoskey"] -pull_requests: [todo] +pull_requests: [21811] --- ## `SystemRunner` SystemParam From 6578cfc07c33d71d1fd30cf45d274854a38779cf Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Tue, 11 Nov 2025 17:26:46 -0800 Subject: [PATCH 8/9] fix docs --- crates/bevy_ecs/src/system/builder.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/system/builder.rs b/crates/bevy_ecs/src/system/builder.rs index 759e8bafaf91f..c9a7ccf42bdcb 100644 --- a/crates/bevy_ecs/src/system/builder.rs +++ b/crates/bevy_ecs/src/system/builder.rs @@ -262,7 +262,7 @@ where } #[inline] - /// Returns this BuilderSystem with a custom name. + /// Returns this `BuilderSystem` with a custom name. pub fn with_name(mut self, name: impl Into>) -> Self { if let BuilderSystemInner::Uninitialized { meta, .. } = &mut self.inner { meta.set_name(name); From cf12de983622b24830b99cd652ccb3e933af07c4 Mon Sep 17 00:00:00 2001 From: Emerson Coskey Date: Tue, 11 Nov 2025 21:12:49 -0800 Subject: [PATCH 9/9] fix docs --- crates/bevy_ecs/src/system/system_param.rs | 6 +++--- release-content/release-notes/system_composition.md | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/bevy_ecs/src/system/system_param.rs b/crates/bevy_ecs/src/system/system_param.rs index 0f4ccef3b65d5..193fdbdf7d952 100644 --- a/crates/bevy_ecs/src/system/system_param.rs +++ b/crates/bevy_ecs/src/system/system_param.rs @@ -2785,11 +2785,11 @@ unsafe impl SystemParam for FilteredResourcesMut<'_, '_> { /// # struct B; /// # let mut world = World::new(); /// # -/// fn count_a(a: Query<&A>) -> u32 { +/// fn count_a(a: Query<&A>) -> usize { /// a.count() /// } /// -/// fn count_b(b: Query<&B>) -> u32 { +/// fn count_b(b: Query<&B>) -> usize { /// b.count() /// } /// @@ -2799,7 +2799,7 @@ unsafe impl SystemParam for FilteredResourcesMut<'_, '_> { /// ) /// .build_state(&mut world) /// .build_system( -/// |mut run_a: SystemRunner<(), u32>, mut run_b: SystemRunner<(), u32>| -> Result { +/// |mut run_a: SystemRunner<(), usize>, mut run_b: SystemRunner<(), usize>| -> Result { /// let a = run_a.run()?; /// let b = run_b.run()?; /// Ok(a + b) diff --git a/release-content/release-notes/system_composition.md b/release-content/release-notes/system_composition.md index 07c4ae1343776..1a428d27d06e2 100644 --- a/release-content/release-notes/system_composition.md +++ b/release-content/release-notes/system_composition.md @@ -11,11 +11,11 @@ even easier. Bevy 0.18 introduces the `SystemRunner` `SystemParam`, allowing run systems inside other systems! ```rust -fn count_a(a: Query<&A>) -> u32 { +fn count_a(a: Query<&A>) -> usize { a.count() } -fn count_b(b: Query<&B>) -> u32 { +fn count_b(b: Query<&B>) -> usize { b.count() } @@ -24,7 +24,7 @@ let get_sum = ( ParamBuilder::system(count_b) ) .build_system( - |mut run_a: SystemRunner<(), u32>, mut run_b: SystemRunner<(), u32>| -> Result { + |mut run_a: SystemRunner<(), usize>, mut run_b: SystemRunner<(), usize>| -> Result { let a = run_a.run()?; let b = run_b.run()?; Ok(a + b) @@ -40,7 +40,7 @@ the new `SystemRunner` params almost seamless to use. ```rust compose! { - || -> Result { + || -> Result { let a = run!(count_a)?; let b = run!(count_b)?; Ok(a + b)