Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
c53f10a
Introduce StaticBundle
SkiFire13 Jun 19, 2025
a015f7d
Implement StaticBundle for Components
SkiFire13 Jun 19, 2025
fd52029
Implement StaticBundle for tuples of StaticBundles
SkiFire13 Jun 19, 2025
9bb3f3c
Implement StaticBundle in the derive macro
SkiFire13 Jun 19, 2025
ebc4143
Implement StaticBundle for SpawnRelatedBundle and SpawnOneRelated
SkiFire13 Jun 19, 2025
e806f5e
Split Bundles methods for Bundle and StaticBundle
SkiFire13 Jun 20, 2025
07f6d1d
Switch Entity{Ref,Mut}Except to StaticBundle
SkiFire13 Jun 2, 2025
ddd69c5
Switch Observer and Trigger to StaticBundle
SkiFire13 Jun 2, 2025
9b0ab87
Require StaticBundle in ReflectBundle
SkiFire13 Jun 2, 2025
9a176a5
Switch EntityClonerBuilder to StaticBundle
SkiFire13 Jun 2, 2025
bd30a8b
Require StaticBundle in batch spawning
SkiFire13 Jun 2, 2025
28a21bd
Switch component removal to StaticBundle
SkiFire13 Jun 2, 2025
c33033a
Switch EntityWorldMut::retain to StaticBundle
SkiFire13 Jun 2, 2025
a44cf37
Require StaticBundle in ExtractComponent
SkiFire13 Jun 2, 2025
e683a10
Add support for #[bundle(dynamic)] attribute
SkiFire13 Jun 3, 2025
4a0f69e
Add migration guide
SkiFire13 Jun 20, 2025
ef88ad8
Add &self parameter to Bundle methods
SkiFire13 Jun 21, 2025
5942b7a
Fix warning
SkiFire13 Jun 21, 2025
20fef0c
Merge remote-tracking branch 'upstream/main' into split-bundle
SkiFire13 Jun 23, 2025
9711258
Apply suggestions
SkiFire13 Jun 23, 2025
f6fe7e8
Merge remote-tracking branch 'upstream/main' into split-bundle
SkiFire13 Jun 24, 2025
f0d8639
Merge remote-tracking branch 'upstream/main' into split-bundle
SkiFire13 Jul 5, 2025
3141342
Merge remote-tracking branch 'upstream/main' into split-bundle
SkiFire13 Jul 6, 2025
1f56c60
Merge remote-tracking branch 'upstream/main' into split-bundle
SkiFire13 Jul 14, 2025
bbac5b1
Fix docs
SkiFire13 Jul 15, 2025
0309198
Merge remote-tracking branch 'upstream/main' into split-bundle
SkiFire13 Jul 15, 2025
c9fc0e4
Merge remote-tracking branch 'upstream/main' into split-bundle
SkiFire13 Jul 29, 2025
f679bac
Merge remote-tracking branch 'upstream/main' into split-bundle
SkiFire13 Aug 21, 2025
e12cbe9
Merge remote-tracking branch 'upstream/main' into split-bundle
SkiFire13 Sep 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions benches/benches/bevy_ecs/entity_cloning.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use core::hint::black_box;

use benches::bench;
use bevy_ecs::bundle::{Bundle, InsertMode};
use bevy_ecs::bundle::{Bundle, InsertMode, StaticBundle};
use bevy_ecs::component::ComponentCloneBehavior;
use bevy_ecs::entity::EntityCloner;
use bevy_ecs::hierarchy::ChildOf;
Expand All @@ -27,7 +27,7 @@ type ComplexBundle = (C<1>, C<2>, C<3>, C<4>, C<5>, C<6>, C<7>, C<8>, C<9>, C<10

/// Sets the [`ComponentCloneBehavior`] for all explicit and required components in a bundle `B` to
/// use the [`Reflect`] trait instead of [`Clone`].
fn reflection_cloner<B: Bundle + GetTypeRegistration>(
fn reflection_cloner<B: StaticBundle + GetTypeRegistration>(
world: &mut World,
linked_cloning: bool,
) -> EntityCloner {
Expand Down Expand Up @@ -65,7 +65,7 @@ fn reflection_cloner<B: Bundle + GetTypeRegistration>(
/// components (which is usually [`ComponentCloneBehavior::clone()`]). If `clone_via_reflect`
/// is true, it will overwrite the handler for all components in the bundle to be
/// [`ComponentCloneBehavior::reflect()`].
fn bench_clone<B: Bundle + Default + GetTypeRegistration>(
fn bench_clone<B: Bundle + StaticBundle + Default + GetTypeRegistration>(
b: &mut Bencher,
clone_via_reflect: bool,
) {
Expand Down Expand Up @@ -96,7 +96,7 @@ fn bench_clone<B: Bundle + Default + GetTypeRegistration>(
/// For example, setting `height` to 5 and `children` to 1 creates a single chain of entities with
/// no siblings. Alternatively, setting `height` to 1 and `children` to 5 will spawn 5 direct
/// children of the root entity.
fn bench_clone_hierarchy<B: Bundle + Default + GetTypeRegistration>(
fn bench_clone_hierarchy<B: Bundle + StaticBundle + Default + GetTypeRegistration>(
b: &mut Bencher,
height: usize,
children: usize,
Expand Down Expand Up @@ -268,7 +268,7 @@ const FILTER_SCENARIOS: [FilterScenario; 11] = [
///
/// The bundle must implement [`Default`], which is used to create the first entity that gets its components cloned
/// in the benchmark. It may also be used to populate the target entity depending on the scenario.
fn bench_filter<B: Bundle + Default>(b: &mut Bencher, scenario: FilterScenario) {
fn bench_filter<B: Bundle + StaticBundle + Default>(b: &mut Bencher, scenario: FilterScenario) {
let mut world = World::default();
let mut spawn = |empty| match empty {
false => world.spawn(B::default()).id(),
Expand Down
4 changes: 2 additions & 2 deletions benches/benches/bevy_ecs/world/world_get.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use core::hint::black_box;

use bevy_ecs::{
bundle::{Bundle, NoBundleEffect},
bundle::{Bundle, NoBundleEffect, StaticBundle},
component::Component,
entity::Entity,
system::{Query, SystemState},
Expand Down Expand Up @@ -38,7 +38,7 @@ fn setup<T: Component + Default>(entity_count: u32) -> (World, Vec<Entity>) {
black_box((world, entities))
}

fn setup_wide<T: Bundle<Effect: NoBundleEffect> + Default>(
fn setup_wide<T: Bundle<Effect: NoBundleEffect> + StaticBundle + Default>(
entity_count: u32,
) -> (World, Vec<Entity>) {
let mut world = World::default();
Expand Down
3 changes: 2 additions & 1 deletion crates/bevy_app/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use alloc::{
};
pub use bevy_derive::AppLabel;
use bevy_ecs::{
bundle::StaticBundle,
component::RequiredComponentsError,
error::{DefaultErrorHandler, ErrorHandler},
event::Event,
Expand Down Expand Up @@ -1378,7 +1379,7 @@ impl App {
/// }
/// });
/// ```
pub fn add_observer<E: Event, B: Bundle, M>(
pub fn add_observer<E: Event, B: StaticBundle, M>(
&mut self,
observer: impl IntoObserverSystem<E, B, M>,
) -> &mut Self {
Expand Down
42 changes: 38 additions & 4 deletions crates/bevy_ecs/macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,21 @@ enum BundleFieldKind {
}

const BUNDLE_ATTRIBUTE_NAME: &str = "bundle";
const BUNDLE_ATTRIBUTE_DYNAMIC: &str = "dynamic";
const BUNDLE_ATTRIBUTE_IGNORE_NAME: &str = "ignore";
const BUNDLE_ATTRIBUTE_NO_FROM_COMPONENTS: &str = "ignore_from_components";

#[derive(Debug)]
struct BundleAttributes {
impl_from_components: bool,
dynamic: bool,
}

impl Default for BundleAttributes {
fn default() -> Self {
Self {
impl_from_components: true,
dynamic: false,
}
}
}
Expand All @@ -63,8 +66,12 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream {
attributes.impl_from_components = false;
return Ok(());
}
if meta.path.is_ident(BUNDLE_ATTRIBUTE_DYNAMIC) {
attributes.dynamic = true;
return Ok(());
}

Err(meta.error(format!("Invalid bundle container attribute. Allowed attributes: `{BUNDLE_ATTRIBUTE_NO_FROM_COMPONENTS}`")))
Err(meta.error(format!("Invalid bundle container attribute. Allowed attributes: `{BUNDLE_ATTRIBUTE_NO_FROM_COMPONENTS}`, `{BUNDLE_ATTRIBUTE_DYNAMIC}`")))
});

if let Err(error) = parsing {
Expand Down Expand Up @@ -144,6 +151,30 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream {
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
let struct_name = &ast.ident;

let static_bundle_impl = (!attributes.dynamic).then(|| quote! {
// SAFETY:
// - all the active fields must implement `StaticBundle` for the function bodies to compile, and hence
// this bundle also represents a static set of components;
// - `component_ids` and `get_component_ids` delegate to the underlying implementation in the same order
// and hence are coherent;
#[allow(deprecated)]
unsafe impl #impl_generics #ecs_path::bundle::StaticBundle for #struct_name #ty_generics #where_clause {
fn component_ids(
components: &mut #ecs_path::component::ComponentsRegistrator,
ids: &mut impl FnMut(#ecs_path::component::ComponentId)
){
#(<#active_field_types as #ecs_path::bundle::StaticBundle>::component_ids(components, &mut *ids);)*
}

fn get_component_ids(
components: &#ecs_path::component::Components,
ids: &mut impl FnMut(Option<#ecs_path::component::ComponentId>)
){
#(<#active_field_types as #ecs_path::bundle::StaticBundle>::get_component_ids(components, &mut *ids);)*
}
}
});

let bundle_impl = quote! {
// SAFETY:
// - ComponentId is returned in field-definition-order. [get_components] uses field-definition-order
Expand All @@ -152,17 +183,19 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream {
#[allow(deprecated)]
unsafe impl #impl_generics #ecs_path::bundle::Bundle for #struct_name #ty_generics #where_clause {
fn component_ids(
&self,
components: &mut #ecs_path::component::ComponentsRegistrator,
ids: &mut impl FnMut(#ecs_path::component::ComponentId)
) {
#(<#active_field_types as #ecs_path::bundle::Bundle>::component_ids(components, ids);)*
#(<#active_field_types as #ecs_path::bundle::Bundle>::component_ids(&self.#active_field_tokens, components, ids);)*
}

fn get_component_ids(
&self,
components: &#ecs_path::component::Components,
ids: &mut impl FnMut(Option<#ecs_path::component::ComponentId>)
) {
#(<#active_field_types as #ecs_path::bundle::Bundle>::get_component_ids(components, &mut *ids);)*
#(<#active_field_types as #ecs_path::bundle::Bundle>::get_component_ids(&self.#active_field_tokens, components, &mut *ids);)*
}
}
};
Expand Down Expand Up @@ -212,7 +245,7 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream {
}
};

let from_components_impl = attributes.impl_from_components.then(|| quote! {
let from_components_impl = (attributes.impl_from_components && !attributes.dynamic).then(|| quote! {
// SAFETY:
// - ComponentId is returned in field-definition-order. [from_components] uses field-definition-order
#[allow(deprecated)]
Expand All @@ -234,6 +267,7 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream {

TokenStream::from(quote! {
#(#attribute_errors)*
#static_bundle_impl
#bundle_impl
#from_components_impl
#dynamic_bundle_impl
Expand Down
73 changes: 65 additions & 8 deletions crates/bevy_ecs/src/bundle/impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@ use core::mem::MaybeUninit;
use variadics_please::all_tuples_enumerated;

use crate::{
bundle::{Bundle, BundleFromComponents, DynamicBundle, NoBundleEffect},
bundle::{Bundle, BundleFromComponents, DynamicBundle, NoBundleEffect, StaticBundle},
component::{Component, ComponentId, Components, ComponentsRegistrator, StorageType},
query::DebugCheckedUnwrap,
world::EntityWorldMut,
};

// SAFETY:
// - `Bundle::component_ids` calls `ids` for C's component id (and nothing else)
// - `Bundle::get_components` is called exactly once for C and passes the component's storage type based on its associated constant.
unsafe impl<C: Component> Bundle for C {
// - `C` always represents the set of components containing just `C`
// - `component_ids` and `get_component_ids` both call `ids` just once for C's component id (and nothing else).
unsafe impl<C: Component> StaticBundle for C {
fn component_ids(components: &mut ComponentsRegistrator, ids: &mut impl FnMut(ComponentId)) {
ids(components.register_component::<C>());
}
Expand All @@ -24,6 +24,27 @@ unsafe impl<C: Component> Bundle for C {
}
}

// SAFETY:
// - `component_ids` calls `ids` for C's component id (and nothing else)
// - `get_components` is called exactly once for C and passes the component's storage type based on its associated constant.
unsafe impl<C: Component> Bundle for C {
fn component_ids(
&self,
components: &mut ComponentsRegistrator,
ids: &mut impl FnMut(ComponentId),
) {
<Self as StaticBundle>::component_ids(components, ids);
}

fn get_component_ids(
&self,
components: &Components,
ids: &mut impl FnMut(Option<ComponentId>),
) {
<Self as StaticBundle>::get_component_ids(components, ids);
}
}

// SAFETY:
// - `Bundle::from_components` calls `func` exactly once for C, which is the exact value returned by `Bundle::component_ids`.
unsafe impl<C: Component> BundleFromComponents for C {
Expand Down Expand Up @@ -55,6 +76,30 @@ impl<C: Component> DynamicBundle for C {

macro_rules! tuple_impl {
($(#[$meta:meta])* $(($index:tt, $name: ident, $alias: ident)),*) => {
#[expect(
clippy::allow_attributes,
reason = "This is a tuple-related macro; as such, the lints below may not always apply."
)]
#[allow(
unused_mut,
unused_variables,
reason = "Zero-length tuples won't use any of the parameters."
)]
$(#[$meta])*
// SAFETY:
// - all the sub-bundles are static, and hence their combination is static too;
// - `component_ids` and `get_component_ids` both delegate to the sub-bundle's methods
// exactly once per sub-bundle, hence they are coherent.
unsafe impl<$($name: StaticBundle),*> StaticBundle for ($($name,)*) {
fn component_ids(components: &mut ComponentsRegistrator, ids: &mut impl FnMut(ComponentId)){
$(<$name as StaticBundle>::component_ids(components, ids);)*
}

fn get_component_ids(components: &Components, ids: &mut impl FnMut(Option<ComponentId>)){
$(<$name as StaticBundle>::get_component_ids(components, ids);)*
}
}

#[expect(
clippy::allow_attributes,
reason = "This is a tuple-related macro; as such, the lints below may not always apply."
Expand All @@ -72,12 +117,24 @@ macro_rules! tuple_impl {
// - `Bundle::get_components` is called exactly once for each member. Relies on the above implementation to pass the correct
// `StorageType` into the callback.
unsafe impl<$($name: Bundle),*> Bundle for ($($name,)*) {
fn component_ids(components: &mut ComponentsRegistrator, ids: &mut impl FnMut(ComponentId)){
$(<$name as Bundle>::component_ids(components, ids);)*
fn component_ids(&self, components: &mut ComponentsRegistrator, ids: &mut impl FnMut(ComponentId)){
#[allow(
non_snake_case,
reason = "The names of these variables are provided by the caller, not by us."
)]
let ($($name,)*) = self;

$(<$name as Bundle>::component_ids($name, components, ids);)*
}

fn get_component_ids(components: &Components, ids: &mut impl FnMut(Option<ComponentId>)){
$(<$name as Bundle>::get_component_ids(components, ids);)*
fn get_component_ids(&self, components: &Components, ids: &mut impl FnMut(Option<ComponentId>)){
#[allow(
non_snake_case,
reason = "The names of these variables are provided by the caller, not by us."
)]
let ($($name,)*) = self;

$(<$name as Bundle>::get_component_ids($name, components, ids);)*
}
}

Expand Down
40 changes: 36 additions & 4 deletions crates/bevy_ecs/src/bundle/info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use indexmap::{IndexMap, IndexSet};

use crate::{
archetype::{Archetype, BundleComponentStatus, ComponentStatus},
bundle::{Bundle, DynamicBundle},
bundle::{Bundle, DynamicBundle, StaticBundle},
change_detection::MaybeLocation,
component::{
ComponentId, Components, ComponentsRegistrator, RequiredComponentConstructor, StorageType,
Expand Down Expand Up @@ -429,7 +429,7 @@ impl Bundles {
///
/// [`World`]: crate::world::World
#[deny(unsafe_op_in_unsafe_fn)]
pub(crate) unsafe fn register_info<T: Bundle>(
pub(crate) unsafe fn register_static_info<T: StaticBundle>(
&mut self,
components: &mut ComponentsRegistrator,
storages: &mut Storages,
Expand All @@ -450,6 +450,37 @@ impl Bundles {
})
}

/// Registers a new [`BundleInfo`] for a statically known type.
///
/// Also registers all the components in the bundle.
///
/// # Safety
///
/// `components` and `storages` must be from the same [`World`] as `self`.
///
/// [`World`]: crate::world::World
pub(crate) unsafe fn register_info<T: Bundle>(
&mut self,
bundle: &T,
components: &mut ComponentsRegistrator,
storages: &mut Storages,
) -> BundleId {
let bundle_infos = &mut self.bundle_infos;
*self.bundle_ids.entry(TypeId::of::<T>()).or_insert_with(|| {
let mut component_ids= Vec::new();
bundle.component_ids(components, &mut |id| component_ids.push(id));
let id = BundleId(bundle_infos.len());
let bundle_info =
// SAFETY: T::component_id ensures:
// - its info was created
// - appropriate storage for it has been initialized.
// - it was created in the same order as the components in T
unsafe { BundleInfo::new(core::any::type_name::<T>(), storages, components, component_ids, id) };
bundle_infos.push(bundle_info);
id
})
}

/// Registers a new [`BundleInfo`], which contains both explicit and required components for a statically known type.
///
/// Also registers all the components in the bundle.
Expand All @@ -460,7 +491,7 @@ impl Bundles {
///
/// [`World`]: crate::world::World
#[deny(unsafe_op_in_unsafe_fn)]
pub(crate) unsafe fn register_contributed_bundle_info<T: Bundle>(
pub(crate) unsafe fn register_contributed_bundle_info<T: StaticBundle>(
&mut self,
components: &mut ComponentsRegistrator,
storages: &mut Storages,
Expand All @@ -470,7 +501,8 @@ impl Bundles {
} else {
// SAFETY: as per the guarantees of this function, components and
// storages are from the same world as self
let explicit_bundle_id = unsafe { self.register_info::<T>(components, storages) };
let explicit_bundle_id =
unsafe { self.register_static_info::<T>(components, storages) };

// SAFETY: reading from `explicit_bundle_id` and creating new bundle in same time. Its valid because bundle hashmap allow this
let id = unsafe {
Expand Down
Loading
Loading