Skip to content

Split EntityClonerBuilder in OptOut and OptIn variants #19649

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

Open
wants to merge 68 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
8934b9d
initial split
urben1680 Jun 15, 2025
34c2038
update
urben1680 Jun 15, 2025
75404b6
update
urben1680 Jun 15, 2025
ea6fc99
update
urben1680 Jun 15, 2025
d223daa
update
urben1680 Jun 15, 2025
bea9031
comments
urben1680 Jun 15, 2025
d618848
update
urben1680 Jun 15, 2025
15d016a
tests and fixes
urben1680 Jun 15, 2025
e10fb9e
Merge branch 'main' into EntityClonerBuilder-split
urben1680 Jun 15, 2025
c9fc163
check source archetype for required component clone
urben1680 Jun 15, 2025
9734504
remove needs_target_archetype field for lazy target archetype
urben1680 Jun 15, 2025
d634814
keep explicit and required allow filters unique
urben1680 Jun 16, 2025
61f8099
docs and bugfix
urben1680 Jun 16, 2025
ef81970
ci
urben1680 Jun 16, 2025
0b030c6
add deny_by_bundle_id
urben1680 Jun 16, 2025
92a5a75
ci
urben1680 Jun 16, 2025
a111fe5
resolved todos and added more tests
urben1680 Jun 16, 2025
2d42ddd
Merge branch 'main' into EntityClonerBuilder-split
urben1680 Jun 16, 2025
963f769
fixed entity_ref.rs test
urben1680 Jun 16, 2025
18bcc25
deny_all + allow benchmarks
urben1680 Jun 16, 2025
79add68
finish deny_all clone when all source components are cloned
urben1680 Jun 16, 2025
c20c2c8
replaced benchmark enum with consts
urben1680 Jun 16, 2025
a265aa8
fmt
urben1680 Jun 16, 2025
be07ccd
fmt
urben1680 Jun 16, 2025
9afe5dc
separate filter and all other fields in EntityCloner, rework benchmarks
urben1680 Jun 17, 2025
384334b
ci
urben1680 Jun 17, 2025
1642708
Update clone_entities.rs
urben1680 Jun 18, 2025
49bcfa8
Update clone_entities.rs
urben1680 Jun 18, 2025
30cbcd3
Update clone_entities.rs
urben1680 Jun 18, 2025
fa48472
rename filters, docs, more tests, InsertMode in OptOut filter, benchm…
urben1680 Jun 19, 2025
d728628
typo
urben1680 Jun 19, 2025
6b254d9
Merge branch 'main' into EntityClonerBuilder-split
urben1680 Jun 19, 2025
1b475d6
ci, without_required_components benchmark
urben1680 Jun 19, 2025
f2ed9f5
inlines
urben1680 Jun 19, 2025
b06aaf9
migration guide
urben1680 Jun 19, 2025
9c2b574
migration guide PR number fix
urben1680 Jun 19, 2025
683f481
migration note adjustmens
urben1680 Jun 19, 2025
eefa60c
migration note adjustmens
urben1680 Jun 19, 2025
eb76d47
typo
urben1680 Jun 19, 2025
7589c90
line breaks in migration guide table
urben1680 Jun 19, 2025
7b16506
line breaks in migration guide table
urben1680 Jun 19, 2025
782f7d4
migration guide ci
urben1680 Jun 19, 2025
9920402
migration guide fix
urben1680 Jun 19, 2025
931d6c4
Merge branch 'main' into EntityClonerBuilder-split
urben1680 Jun 19, 2025
1b7d8d5
migration guide fixes
urben1680 Jun 20, 2025
50f5c2f
Update crates/bevy_ecs/src/world/entity_ref.rs
urben1680 Jun 20, 2025
1a31af7
Update crates/bevy_ecs/src/world/entity_ref.rs
urben1680 Jun 20, 2025
4845d9c
Update crates/bevy_ecs/src/world/entity_ref.rs
urben1680 Jun 20, 2025
ac27d8d
Update crates/bevy_ecs/src/world/entity_ref.rs
urben1680 Jun 20, 2025
b7db4a4
Update crates/bevy_ecs/src/entity/clone_entities.rs
urben1680 Jun 20, 2025
5ef4c72
review feedback including fixes, renames and reorganizizng, new requi…
urben1680 Jun 20, 2025
6e45193
typo
urben1680 Jun 20, 2025
b25f913
Merge branch 'main' into EntityClonerBuilder-split
urben1680 Jun 20, 2025
63b6875
ci
urben1680 Jun 20, 2025
79617a7
ci
urben1680 Jun 20, 2025
a37647f
update
urben1680 Jun 21, 2025
3a78fe2
Merge branch 'main' into EntityClonerBuilder-split
urben1680 Jun 21, 2025
8bcf7c3
one more reset call
urben1680 Jun 21, 2025
3454460
Merge branch 'EntityClonerBuilder-split' of https://github.com/urben1…
urben1680 Jun 21, 2025
995566f
docs
urben1680 Jun 21, 2025
0b061d6
more docs
urben1680 Jun 21, 2025
8ea1522
minor reworks, comments
urben1680 Jun 21, 2025
b7ae68b
Merge branch 'main' into EntityClonerBuilder-split
urben1680 Jun 21, 2025
7bd7283
typo
urben1680 Jun 21, 2025
0033de6
Merge branch 'EntityClonerBuilder-split' of https://github.com/urben1…
urben1680 Jun 21, 2025
92e7acd
Update and rename entity_observer.rs to entity_cloning.rs
urben1680 Jun 23, 2025
3d10562
Merge branch 'main' into EntityClonerBuilder-split
urben1680 Jun 23, 2025
8b9e7aa
ci
urben1680 Jun 23, 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
220 changes: 173 additions & 47 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;
use bevy_ecs::bundle::{Bundle, InsertMode};
use bevy_ecs::component::ComponentCloneBehavior;
use bevy_ecs::entity::EntityCloner;
use bevy_ecs::hierarchy::ChildOf;
Expand All @@ -17,41 +17,15 @@ criterion_group!(
hierarchy_tall,
hierarchy_wide,
hierarchy_many,
filter
);

#[derive(Component, Reflect, Default, Clone)]
struct C1(Mat4);
struct C<const N: usize>(Mat4);

#[derive(Component, Reflect, Default, Clone)]
struct C2(Mat4);

#[derive(Component, Reflect, Default, Clone)]
struct C3(Mat4);

#[derive(Component, Reflect, Default, Clone)]
struct C4(Mat4);

#[derive(Component, Reflect, Default, Clone)]
struct C5(Mat4);

#[derive(Component, Reflect, Default, Clone)]
struct C6(Mat4);

#[derive(Component, Reflect, Default, Clone)]
struct C7(Mat4);

#[derive(Component, Reflect, Default, Clone)]
struct C8(Mat4);

#[derive(Component, Reflect, Default, Clone)]
struct C9(Mat4);
type ComplexBundle = (C<1>, C<2>, C<3>, C<4>, C<5>, C<6>, C<7>, C<8>, C<9>, C<10>);

#[derive(Component, Reflect, Default, Clone)]
struct C10(Mat4);

type ComplexBundle = (C1, C2, C3, C4, C5, C6, C7, C8, C9, C10);

/// Sets the [`ComponentCloneHandler`] for all explicit and required components in a bundle `B` to
/// 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>(
world: &mut World,
Expand All @@ -71,7 +45,7 @@ fn reflection_cloner<B: Bundle + GetTypeRegistration>(
// this bundle are saved.
let component_ids: Vec<_> = world.register_bundle::<B>().contributed_components().into();

let mut builder = EntityCloner::build(world);
let mut builder = EntityCloner::build_opt_out(world);

// Overwrite the clone handler for all components in the bundle to use `Reflect`, not `Clone`.
for component in component_ids {
Expand All @@ -82,16 +56,15 @@ fn reflection_cloner<B: Bundle + GetTypeRegistration>(
builder.finish()
}

/// A helper function that benchmarks running the [`EntityCommands::clone_and_spawn()`] command on a
/// bundle `B`.
/// A helper function that benchmarks running [`EntityCloner::spawn_clone`] with a bundle `B`.
///
/// The bundle must implement [`Default`], which is used to create the first entity that gets cloned
/// in the benchmark.
///
/// If `clone_via_reflect` is false, this will use the default [`ComponentCloneHandler`] for all
/// components (which is usually [`ComponentCloneHandler::clone_handler()`]). If `clone_via_reflect`
/// If `clone_via_reflect` is false, this will use the default [`ComponentCloneBehavior`] for all
/// 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
/// [`ComponentCloneHandler::reflect_handler()`].
/// [`ComponentCloneBehavior::reflect()`].
fn bench_clone<B: Bundle + Default + GetTypeRegistration>(
b: &mut Bencher,
clone_via_reflect: bool,
Expand All @@ -114,8 +87,7 @@ fn bench_clone<B: Bundle + Default + GetTypeRegistration>(
});
}

/// A helper function that benchmarks running the [`EntityCommands::clone_and_spawn()`] command on a
/// bundle `B`.
/// A helper function that benchmarks running [`EntityCloner::spawn_clone`] with a bundle `B`.
///
/// As compared to [`bench_clone()`], this benchmarks recursively cloning an entity with several
/// children. It does so by setting up an entity tree with a given `height` where each entity has a
Expand All @@ -135,7 +107,7 @@ fn bench_clone_hierarchy<B: Bundle + Default + GetTypeRegistration>(
let mut cloner = if clone_via_reflect {
reflection_cloner::<B>(&mut world, true)
} else {
let mut builder = EntityCloner::build(&mut world);
let mut builder = EntityCloner::build_opt_out(&mut world);
builder.linked_cloning(true);
builder.finish()
};
Expand Down Expand Up @@ -169,7 +141,7 @@ fn bench_clone_hierarchy<B: Bundle + Default + GetTypeRegistration>(

// Each benchmark runs twice: using either the `Clone` or `Reflect` traits to clone entities. This
// constant represents this as an easy array that can be used in a `for` loop.
const SCENARIOS: [(&str, bool); 2] = [("clone", false), ("reflect", true)];
const CLONE_SCENARIOS: [(&str, bool); 2] = [("clone", false), ("reflect", true)];

/// Benchmarks cloning a single entity with 10 components and no children.
fn single(c: &mut Criterion) {
Expand All @@ -178,7 +150,7 @@ fn single(c: &mut Criterion) {
// We're cloning 1 entity.
group.throughput(Throughput::Elements(1));

for (id, clone_via_reflect) in SCENARIOS {
for (id, clone_via_reflect) in CLONE_SCENARIOS {
group.bench_function(id, |b| {
bench_clone::<ComplexBundle>(b, clone_via_reflect);
});
Expand All @@ -194,9 +166,9 @@ fn hierarchy_tall(c: &mut Criterion) {
// We're cloning both the root entity and its 50 descendents.
group.throughput(Throughput::Elements(51));

for (id, clone_via_reflect) in SCENARIOS {
for (id, clone_via_reflect) in CLONE_SCENARIOS {
group.bench_function(id, |b| {
bench_clone_hierarchy::<C1>(b, 50, 1, clone_via_reflect);
bench_clone_hierarchy::<C<1>>(b, 50, 1, clone_via_reflect);
});
}

Expand All @@ -210,9 +182,9 @@ fn hierarchy_wide(c: &mut Criterion) {
// We're cloning both the root entity and its 50 direct children.
group.throughput(Throughput::Elements(51));

for (id, clone_via_reflect) in SCENARIOS {
for (id, clone_via_reflect) in CLONE_SCENARIOS {
group.bench_function(id, |b| {
bench_clone_hierarchy::<C1>(b, 1, 50, clone_via_reflect);
bench_clone_hierarchy::<C<1>>(b, 1, 50, clone_via_reflect);
});
}

Expand All @@ -228,11 +200,165 @@ fn hierarchy_many(c: &mut Criterion) {
// of entities spawned in `bench_clone_hierarchy()` with a `println!()` statement. :)
group.throughput(Throughput::Elements(364));

for (id, clone_via_reflect) in SCENARIOS {
for (id, clone_via_reflect) in CLONE_SCENARIOS {
group.bench_function(id, |b| {
bench_clone_hierarchy::<ComplexBundle>(b, 5, 3, clone_via_reflect);
});
}

group.finish();
}

/// Filter scenario variant for bot opt-in and opt-out filters
#[derive(Clone, Copy)]
#[expect(
clippy::enum_variant_names,
reason = "'Opt' is not understood as an prefix but `OptOut'/'OptIn' are"
)]
enum FilterScenario {
OptOutNone,
OptOutNoneKeep(bool),
OptOutAll,
OptInNone,
OptInAll,
OptInAllWithoutRequired,
OptInAllKeep(bool),
OptInAllKeepWithoutRequired(bool),
}

impl From<FilterScenario> for String {
fn from(value: FilterScenario) -> Self {
match value {
FilterScenario::OptOutNone => "opt_out_none",
FilterScenario::OptOutNoneKeep(true) => "opt_out_none_keep_none",
FilterScenario::OptOutNoneKeep(false) => "opt_out_none_keep_all",
FilterScenario::OptOutAll => "opt_out_all",
FilterScenario::OptInNone => "opt_in_none",
FilterScenario::OptInAll => "opt_in_all",
FilterScenario::OptInAllWithoutRequired => "opt_in_all_without_required",
FilterScenario::OptInAllKeep(true) => "opt_in_all_keep_none",
FilterScenario::OptInAllKeep(false) => "opt_in_all_keep_all",
FilterScenario::OptInAllKeepWithoutRequired(true) => {
"opt_in_all_keep_none_without_required"
}
FilterScenario::OptInAllKeepWithoutRequired(false) => {
"opt_in_all_keep_all_without_required"
}
}
.into()
}
}

/// Common scenarios for different filter to be benchmarked.
const FILTER_SCENARIOS: [FilterScenario; 11] = [
FilterScenario::OptOutNone,
FilterScenario::OptOutNoneKeep(true),
FilterScenario::OptOutNoneKeep(false),
FilterScenario::OptOutAll,
FilterScenario::OptInNone,
FilterScenario::OptInAll,
FilterScenario::OptInAllWithoutRequired,
FilterScenario::OptInAllKeep(true),
FilterScenario::OptInAllKeep(false),
FilterScenario::OptInAllKeepWithoutRequired(true),
FilterScenario::OptInAllKeepWithoutRequired(false),
];

/// A helper function that benchmarks running [`EntityCloner::clone_entity`] with a bundle `B`.
///
/// 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) {
let mut world = World::default();
let mut spawn = |empty| match empty {
false => world.spawn(B::default()).id(),
true => world.spawn_empty().id(),
};
let source = spawn(false);
let (target, mut cloner);

match scenario {
FilterScenario::OptOutNone => {
target = spawn(true);
cloner = EntityCloner::default();
}
FilterScenario::OptOutNoneKeep(is_new) => {
target = spawn(is_new);
let mut builder = EntityCloner::build_opt_out(&mut world);
builder.insert_mode(InsertMode::Keep);
cloner = builder.finish();
}
FilterScenario::OptOutAll => {
target = spawn(true);
let mut builder = EntityCloner::build_opt_out(&mut world);
builder.deny::<B>();
cloner = builder.finish();
}
FilterScenario::OptInNone => {
target = spawn(true);
let builder = EntityCloner::build_opt_in(&mut world);
cloner = builder.finish();
}
FilterScenario::OptInAll => {
target = spawn(true);
let mut builder = EntityCloner::build_opt_in(&mut world);
builder.allow::<B>();
cloner = builder.finish();
}
FilterScenario::OptInAllWithoutRequired => {
target = spawn(true);
let mut builder = EntityCloner::build_opt_in(&mut world);
builder.without_required_components(|builder| {
builder.allow::<B>();
});
cloner = builder.finish();
}
FilterScenario::OptInAllKeep(is_new) => {
target = spawn(is_new);
let mut builder = EntityCloner::build_opt_in(&mut world);
builder.allow_if_new::<B>();
cloner = builder.finish();
}
FilterScenario::OptInAllKeepWithoutRequired(is_new) => {
target = spawn(is_new);
let mut builder = EntityCloner::build_opt_in(&mut world);
builder.without_required_components(|builder| {
builder.allow_if_new::<B>();
});
cloner = builder.finish();
}
}

b.iter(|| {
// clones the given entity into the target
cloner.clone_entity(&mut world, black_box(source), black_box(target));
world.flush();
});
}

/// Benchmarks filtering of cloning a single entity with 5 unclonable components (each requiring 1 unclonable component) into a target.
fn filter(c: &mut Criterion) {
#[derive(Component, Default)]
#[component(clone_behavior = Ignore)]
struct C<const N: usize>;

#[derive(Component, Default)]
#[component(clone_behavior = Ignore)]
#[require(C::<N>)]
struct R<const N: usize>;

type RequiringBundle = (R<1>, R<2>, R<3>, R<4>, R<5>);

let mut group = c.benchmark_group(bench!("filter"));

// We're cloning 1 entity into a target.
group.throughput(Throughput::Elements(1));

for scenario in FILTER_SCENARIOS {
group.bench_function(scenario, |b| {
bench_filter::<RequiringBundle>(b, scenario);
});
}

group.finish();
}
Loading