Use register_dynamic for merging (#18028)
# Objective I found a bug while working on #17871. When required components are registered, ones that are more specific (smaller inheritance depth) are preferred to others. So, if a ComponentA is already required, but it is registered as required again, it will be updated if and only if the new requirement has a smaller inheritance depth (is more specific). However, this logic was not reflected in merging `RequriedComponents`s together. Hence, for complicated requirement trees, the wrong initializer could be used. ## Solution Re-write merging to work by extending the collection via `require_dynamic` instead of blindly combining the inner storage. ## Testing I created this test to ensure this bug doesn't creep back in. This test fails on main, but passes on this branch. ```rs #[test] fn required_components_inheritance_depth_bias() { #[derive(Component, PartialEq, Eq, Clone, Copy, Debug)] struct MyRequired(bool); #[derive(Component, Default)] #[require(MyRequired(|| MyRequired(false)))] struct MiddleMan; #[derive(Component, Default)] #[require(MiddleMan)] struct ConflictingRequire; #[derive(Component, Default)] #[require(MyRequired(|| MyRequired(true)))] struct MyComponent; let mut world = World::new(); let order_a = world .spawn((ConflictingRequire, MyComponent)) .get::<MyRequired>() .cloned(); let order_b = world .spawn((MyComponent, ConflictingRequire)) .get::<MyRequired>() .cloned(); assert_eq!(order_a, Some(MyRequired(true))); assert_eq!(order_b, Some(MyRequired(true))); } ``` Note that when the inheritance depth is 0 (Like if there were no middle man above), the order of the components in the bundle still matters. ## Migration Guide `RequiredComponents::register_dynamic` has been changed to `RequiredComponents::register_dynamic_with`. Old: ```rust required_components.register_dynamic( component_id, component_constructor.clone(), requirement_inheritance_depth, ); ``` New: ```rust required_components.register_dynamic_with( component_id, requirement_inheritance_depth, || component_constructor.clone(), ); ``` This can prevent unnecessary cloning. --------- Co-authored-by: Carter Anderson <mcanders1@gmail.com> Co-authored-by: Joona Aalto <jondolf.dev@gmail.com>
This commit is contained in:
parent
913eb46324
commit
79e7f8ae0c
@ -1412,10 +1412,10 @@ impl Components {
|
|||||||
// SAFETY: Component ID and constructor match the ones on the original requiree.
|
// SAFETY: Component ID and constructor match the ones on the original requiree.
|
||||||
// The original requiree is responsible for making sure the registration is safe.
|
// The original requiree is responsible for making sure the registration is safe.
|
||||||
unsafe {
|
unsafe {
|
||||||
required_components.register_dynamic(
|
required_components.register_dynamic_with(
|
||||||
*component_id,
|
*component_id,
|
||||||
component.constructor.clone(),
|
|
||||||
component.inheritance_depth + depth + 1,
|
component.inheritance_depth + depth + 1,
|
||||||
|
|| component.constructor.clone(),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -1458,21 +1458,21 @@ impl Components {
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
// Register the new required components.
|
// Register the new required components.
|
||||||
for (component_id, component) in inherited_requirements.iter().cloned() {
|
for (component_id, component) in inherited_requirements.iter() {
|
||||||
// Register the required component for the requiree.
|
// Register the required component for the requiree.
|
||||||
// SAFETY: Component ID and constructor match the ones on the original requiree.
|
// SAFETY: Component ID and constructor match the ones on the original requiree.
|
||||||
unsafe {
|
unsafe {
|
||||||
required_components.register_dynamic(
|
required_components.register_dynamic_with(
|
||||||
component_id,
|
*component_id,
|
||||||
component.constructor,
|
|
||||||
component.inheritance_depth,
|
component.inheritance_depth,
|
||||||
|
|| component.constructor.clone(),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add the requiree to the list of components that require the required component.
|
// Add the requiree to the list of components that require the required component.
|
||||||
// SAFETY: The caller ensures that the required components are valid.
|
// SAFETY: The caller ensures that the required components are valid.
|
||||||
let required_by = unsafe {
|
let required_by = unsafe {
|
||||||
self.get_required_by_mut(component_id)
|
self.get_required_by_mut(*component_id)
|
||||||
.debug_checked_unwrap()
|
.debug_checked_unwrap()
|
||||||
};
|
};
|
||||||
required_by.insert(requiree);
|
required_by.insert(requiree);
|
||||||
@ -2028,25 +2028,30 @@ impl RequiredComponents {
|
|||||||
/// `constructor` _must_ initialize a component for `component_id` in such a way that
|
/// `constructor` _must_ initialize a component for `component_id` in such a way that
|
||||||
/// matches the storage type of the component. It must only use the given `table_row` or `Entity` to
|
/// matches the storage type of the component. It must only use the given `table_row` or `Entity` to
|
||||||
/// initialize the storage for `component_id` corresponding to the given entity.
|
/// initialize the storage for `component_id` corresponding to the given entity.
|
||||||
pub unsafe fn register_dynamic(
|
pub unsafe fn register_dynamic_with(
|
||||||
&mut self,
|
&mut self,
|
||||||
component_id: ComponentId,
|
component_id: ComponentId,
|
||||||
constructor: RequiredComponentConstructor,
|
|
||||||
inheritance_depth: u16,
|
inheritance_depth: u16,
|
||||||
|
constructor: impl FnOnce() -> RequiredComponentConstructor,
|
||||||
) {
|
) {
|
||||||
self.0
|
let entry = self.0.entry(component_id);
|
||||||
.entry(component_id)
|
match entry {
|
||||||
.and_modify(|component| {
|
bevy_platform_support::collections::hash_map::Entry::Occupied(mut occupied) => {
|
||||||
if component.inheritance_depth > inheritance_depth {
|
let current = occupied.get_mut();
|
||||||
// New registration is more specific than existing requirement
|
if current.inheritance_depth > inheritance_depth {
|
||||||
component.constructor = constructor.clone();
|
*current = RequiredComponent {
|
||||||
component.inheritance_depth = inheritance_depth;
|
constructor: constructor(),
|
||||||
|
inheritance_depth,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
.or_insert(RequiredComponent {
|
bevy_platform_support::collections::hash_map::Entry::Vacant(vacant) => {
|
||||||
constructor,
|
vacant.insert(RequiredComponent {
|
||||||
inheritance_depth,
|
constructor: constructor(),
|
||||||
});
|
inheritance_depth,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Registers a required component.
|
/// Registers a required component.
|
||||||
@ -2073,64 +2078,66 @@ impl RequiredComponents {
|
|||||||
constructor: fn() -> C,
|
constructor: fn() -> C,
|
||||||
inheritance_depth: u16,
|
inheritance_depth: u16,
|
||||||
) {
|
) {
|
||||||
let erased: RequiredComponentConstructor = RequiredComponentConstructor({
|
let erased = || {
|
||||||
// `portable-atomic-util` `Arc` is not able to coerce an unsized
|
RequiredComponentConstructor({
|
||||||
// type like `std::sync::Arc` can. Creating a `Box` first does the
|
// `portable-atomic-util` `Arc` is not able to coerce an unsized
|
||||||
// coercion.
|
// type like `std::sync::Arc` can. Creating a `Box` first does the
|
||||||
//
|
// coercion.
|
||||||
// This would be resolved by https://github.com/rust-lang/rust/issues/123430
|
//
|
||||||
|
// This would be resolved by https://github.com/rust-lang/rust/issues/123430
|
||||||
|
|
||||||
#[cfg(not(target_has_atomic = "ptr"))]
|
#[cfg(not(target_has_atomic = "ptr"))]
|
||||||
use alloc::boxed::Box;
|
use alloc::boxed::Box;
|
||||||
|
|
||||||
type Constructor = dyn for<'a, 'b> Fn(
|
type Constructor = dyn for<'a, 'b> Fn(
|
||||||
&'a mut Table,
|
&'a mut Table,
|
||||||
&'b mut SparseSets,
|
&'b mut SparseSets,
|
||||||
Tick,
|
Tick,
|
||||||
TableRow,
|
TableRow,
|
||||||
Entity,
|
Entity,
|
||||||
MaybeLocation,
|
MaybeLocation,
|
||||||
);
|
);
|
||||||
|
|
||||||
#[cfg(not(target_has_atomic = "ptr"))]
|
#[cfg(not(target_has_atomic = "ptr"))]
|
||||||
type Intermediate<T> = Box<T>;
|
type Intermediate<T> = Box<T>;
|
||||||
|
|
||||||
#[cfg(target_has_atomic = "ptr")]
|
#[cfg(target_has_atomic = "ptr")]
|
||||||
type Intermediate<T> = Arc<T>;
|
type Intermediate<T> = Arc<T>;
|
||||||
|
|
||||||
let boxed: Intermediate<Constructor> = Intermediate::new(
|
let boxed: Intermediate<Constructor> = Intermediate::new(
|
||||||
move |table, sparse_sets, change_tick, table_row, entity, caller| {
|
move |table, sparse_sets, change_tick, table_row, entity, caller| {
|
||||||
OwningPtr::make(constructor(), |ptr| {
|
OwningPtr::make(constructor(), |ptr| {
|
||||||
// SAFETY: This will only be called in the context of `BundleInfo::write_components`, which will
|
// SAFETY: This will only be called in the context of `BundleInfo::write_components`, which will
|
||||||
// pass in a valid table_row and entity requiring a C constructor
|
// pass in a valid table_row and entity requiring a C constructor
|
||||||
// C::STORAGE_TYPE is the storage type associated with `component_id` / `C`
|
// C::STORAGE_TYPE is the storage type associated with `component_id` / `C`
|
||||||
// `ptr` points to valid `C` data, which matches the type associated with `component_id`
|
// `ptr` points to valid `C` data, which matches the type associated with `component_id`
|
||||||
unsafe {
|
unsafe {
|
||||||
BundleInfo::initialize_required_component(
|
BundleInfo::initialize_required_component(
|
||||||
table,
|
table,
|
||||||
sparse_sets,
|
sparse_sets,
|
||||||
change_tick,
|
change_tick,
|
||||||
table_row,
|
table_row,
|
||||||
entity,
|
entity,
|
||||||
component_id,
|
component_id,
|
||||||
C::STORAGE_TYPE,
|
C::STORAGE_TYPE,
|
||||||
ptr,
|
ptr,
|
||||||
caller,
|
caller,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
Arc::from(boxed)
|
Arc::from(boxed)
|
||||||
});
|
})
|
||||||
|
};
|
||||||
|
|
||||||
// SAFETY:
|
// SAFETY:
|
||||||
// `component_id` matches the type initialized by the `erased` constructor above.
|
// `component_id` matches the type initialized by the `erased` constructor above.
|
||||||
// `erased` initializes a component for `component_id` in such a way that
|
// `erased` initializes a component for `component_id` in such a way that
|
||||||
// matches the storage type of the component. It only uses the given `table_row` or `Entity` to
|
// matches the storage type of the component. It only uses the given `table_row` or `Entity` to
|
||||||
// initialize the storage corresponding to the given entity.
|
// initialize the storage corresponding to the given entity.
|
||||||
unsafe { self.register_dynamic(component_id, erased, inheritance_depth) };
|
unsafe { self.register_dynamic_with(component_id, inheritance_depth, erased) };
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Iterates the ids of all required components. This includes recursive required components.
|
/// Iterates the ids of all required components. This includes recursive required components.
|
||||||
@ -2148,11 +2155,26 @@ impl RequiredComponents {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merges `required_components` into this collection. This only inserts a required component
|
/// Merges `required_components` into this collection. This only inserts a required component
|
||||||
// if it _did not already exist_.
|
/// if it _did not already exist_ *or* if the required component is more specific than the existing one
|
||||||
|
/// (in other words, if the inheritance depth is smaller).
|
||||||
|
///
|
||||||
|
/// See [`register_dynamic_with`](Self::register_dynamic_with) for details.
|
||||||
pub(crate) fn merge(&mut self, required_components: &RequiredComponents) {
|
pub(crate) fn merge(&mut self, required_components: &RequiredComponents) {
|
||||||
for (id, constructor) in &required_components.0 {
|
for (
|
||||||
self.0.entry(*id).or_insert_with(|| constructor.clone());
|
component_id,
|
||||||
|
RequiredComponent {
|
||||||
|
constructor,
|
||||||
|
inheritance_depth,
|
||||||
|
},
|
||||||
|
) in required_components.0.iter()
|
||||||
|
{
|
||||||
|
// SAFETY: This exact registration must have been done on `required_components`, so safety is ensured by that caller.
|
||||||
|
unsafe {
|
||||||
|
self.register_dynamic_with(*component_id, *inheritance_depth, || {
|
||||||
|
constructor.clone()
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2643,6 +2643,37 @@ mod tests {
|
|||||||
assert_eq!(to_vec(required_z), vec![(b, 0), (c, 1)]);
|
assert_eq!(to_vec(required_z), vec![(b, 0), (c, 1)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn required_components_inheritance_depth_bias() {
|
||||||
|
#[derive(Component, PartialEq, Eq, Clone, Copy, Debug)]
|
||||||
|
struct MyRequired(bool);
|
||||||
|
|
||||||
|
#[derive(Component, Default)]
|
||||||
|
#[require(MyRequired(|| MyRequired(false)))]
|
||||||
|
struct MiddleMan;
|
||||||
|
|
||||||
|
#[derive(Component, Default)]
|
||||||
|
#[require(MiddleMan)]
|
||||||
|
struct ConflictingRequire;
|
||||||
|
|
||||||
|
#[derive(Component, Default)]
|
||||||
|
#[require(MyRequired(|| MyRequired(true)))]
|
||||||
|
struct MyComponent;
|
||||||
|
|
||||||
|
let mut world = World::new();
|
||||||
|
let order_a = world
|
||||||
|
.spawn((ConflictingRequire, MyComponent))
|
||||||
|
.get::<MyRequired>()
|
||||||
|
.cloned();
|
||||||
|
let order_b = world
|
||||||
|
.spawn((MyComponent, ConflictingRequire))
|
||||||
|
.get::<MyRequired>()
|
||||||
|
.cloned();
|
||||||
|
|
||||||
|
assert_eq!(order_a, Some(MyRequired(true)));
|
||||||
|
assert_eq!(order_b, Some(MyRequired(true)));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[should_panic = "Recursive required components detected: A → B → C → B\nhelp: If this is intentional, consider merging the components."]
|
#[should_panic = "Recursive required components detected: A → B → C → B\nhelp: If this is intentional, consider merging the components."]
|
||||||
fn required_components_recursion_errors() {
|
fn required_components_recursion_errors() {
|
||||||
|
Loading…
Reference in New Issue
Block a user