Skip to content
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

Change ReflectMapEntities to operate on components before insertion #14549

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

jrobsonchase
Copy link

@jrobsonchase jrobsonchase commented Jul 31, 2024

Objective

Fixes #14465

Solution

Alters ReflectMapEntities to operate more like MapEntities by making it operate on the component itself rather than the World post-insertion. As a result, Observers never see the in-between state where the scene component has been added, but hasn't yet had its entities mapped.

Also brings it more in-line with MapEntities by accepting a generic DynEntityMapper rather than relying on the EntityHashMap and SceneEntityMapper implementation.

Testing

  • New unit test for Observers + DynamicScenes
  • New unit test for Observers + Scenes
  • Open to suggestions for other tests!

Migration Guide

  • The existing map_entities and map_all_entities methods have been renamed to map_world_entities and map_all_world_entities respectively and marked as deprecated.
    • Figured I'd leave these in some form since the new method isn't exactly a drop-in replacement.
    • This is in a separate commit so that they'll be easy to fully remove later. Edit: Oops, I forgot I did this and squashed it away. Oh well, should still be easy to remove them if/when desired.

Copy link
Contributor

Welcome, new contributor!

Please make sure you've read our contributing guide and we look forward to reviewing your pull request shortly ✨

@alice-i-cecile alice-i-cecile added C-Bug An unexpected or incorrect behavior A-ECS Entities, components, systems, and events A-Reflection Runtime information about types A-Scenes Serialized ECS data stored on the disk labels Jul 31, 2024
@jrobsonchase jrobsonchase force-pushed the pre_map_entities branch 3 times, most recently from 8a82089 to f8552ce Compare July 31, 2024 12:56
@alice-i-cecile alice-i-cecile added the C-Breaking-Change A breaking change to Bevy's public API that needs to be noted in a migration guide label Jul 31, 2024
@alice-i-cecile
Copy link
Member

Let me know when this is ready for review :) I'm going to want a regression test or two: the logic here is tricky but important to get right.

@jrobsonchase jrobsonchase force-pushed the pre_map_entities branch 2 times, most recently from f410d21 to d18b79c Compare July 31, 2024 13:30
@jrobsonchase
Copy link
Author

Hmm, I added a test which correctly fails before/succeeds after my change, but in the process I noticed that I'm now causing 'dynamic_scene::tests::components_not_defined_in_scene_should_not_be_affected_by_scene_entity_map' has overflowed its stack 🙃

@jrobsonchase
Copy link
Author

Ope, I'm just an idiot 😅 Forgot to use the wrapped EntityMapper rather than the outer UnDynEntityMapper.

Copy link
Author

@jrobsonchase jrobsonchase left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly happy with this, apart from all of the extra component copying that ends up happening. We might be able to get rid of the outer one in the event that we don't have to map entities via some copy-on-write shenanigans, but I'm not sure about the one incurred from the call to the concrete FromReflect and the subsequent write back to the &mut dyn Reflect.

crates/bevy_ecs/src/entity/map_entities.rs Outdated Show resolved Hide resolved
crates/bevy_ecs/src/identifier/mod.rs Outdated Show resolved Hide resolved
});
}
}

impl<C: Component + MapEntities> FromType<C> for ReflectMapEntities {
impl<C: Component + MapEntities + Reflect + FromReflect> FromType<C> for ReflectMapEntities {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is slightly more restrictive than previously, but I'm not sure that it really matters since anything reflecting MapEntities should already be Reflect + FromReflect.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's fine. Ideally we'd only use Reflect and do a similar trick as ReflectComponent, but I’m not sure that's possible without world access.

Copy link
Author

@jrobsonchase jrobsonchase Jul 31, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

World access shouldn't actually be a problem, at least for the two places we're currently using this. I actually had a &mut World parameter previously before swapping the EntityHashMap<Entity> for the &mut dyn DynEntityMapper.

Would also need access to the type registry, which also shouldn't be an issue, but then the arguments are starting to feel cluttered.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, works just fine. I could go either way 🤷

crates/bevy_scene/src/dynamic_scene.rs Outdated Show resolved Hide resolved
Comment on lines 132 to 146
reflect_component.apply_or_insert(
&mut world.entity_mut(entity),
&*component,
&type_registry,
);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is equivalent to the previous reflect_component.copy, but I'm not 100% certain. I don't use regular Scenes all that often. Or at all, really.

@jrobsonchase jrobsonchase marked this pull request as ready for review July 31, 2024 13:55
crates/bevy_ecs/src/reflect/map_entities.rs Outdated Show resolved Hide resolved
crates/bevy_ecs/src/reflect/map_entities.rs Outdated Show resolved Hide resolved
map_entities: |component, entity_mapper| {
let mut concrete = C::from_reflect(&*component).unwrap();
concrete.map_entities(&mut UnDynEntityMapper(entity_mapper));
component.apply(&concrete as _);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only possible concern here is if the user has mapped fields marked with #[reflect(ignore)]. The original implementation didn't rely on reflection so it was fine, but this is a new "hidden" requirement. We should probably document this somewhere.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like that's expected? If a field is invisible to reflection in general, it seems logical that it would be invisible to reflected traits.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm fair point. And when used with scenes specifically I guess those fields won't even be serialized which means it's less of an issue than I originally thought.

@jrobsonchase jrobsonchase force-pushed the pre_map_entities branch 5 times, most recently from 00acaaa to 31b8c97 Compare July 31, 2024 18:03
@jrobsonchase
Copy link
Author

@alice-i-cecile Ready for review!

I'm satisfied that this change fixes the original scenes/observers/mapped entities issue, but I wonder if there isn't a more fundamental problem with observers and their immediacy. This fixes the issue of individual components being in an inconsistent state, but I think the overall scene will still be inconsistent when the observers fire, e.g. a Trigger<OnAdd, Parent> observer may still see an empty parent entity if the child is written before its parent.

@alice-i-cecile
Copy link
Member

Agreed, there's something deeper here that's bothering me but I haven't figured out exactly what yet.

@cart's idea from #14520 (comment) to use a topologically sorted order of resolution might help this problem as well.

@jrobsonchase
Copy link
Author

jrobsonchase commented Aug 1, 2024

Yeah, that sounds like it would definitely help, but it also feels like another band-aid.

In my mind, the real issue is that observers very much look like systems, which have always run on either side of sync points and thus could never observe intermediate state. But in reality, observers can see this intermediate state since they run immediately. Things like cmd.entity(e).insert(Foo).insert(Bar) vs cmd.entity(e).insert(Bar).insert(Foo) didn't previously have much semantic difference, since all systems (and commands later in the queue) only ever saw the final state where they both existed. Queueing component hooks and triggers as extra Commands rather than immediate calls could be a possible solution with the added benefit of avoiding stack overflows resulting from too many nested triggers.

Edit: Delved a little more deeply, and I think I'm conflating a couple of issues. The stack overflow is due to how command application recurses on world.flush() rather than anything trigger-related, and the only observers that are immediately invoked are the On(Add|Remove|Insert|etc.) ones as far as I can tell. So in general, observers behave like systems and fire post-sync, or at least obey the expected queueing semantics, but the component lifecycle ones insert themselves mid-command-queue.

@alice-i-cecile alice-i-cecile added the X-Contentious There are nontrivial implications that should be thought through label Aug 5, 2024
@alice-i-cecile alice-i-cecile added the S-Needs-Review Needs reviewer attention (from anyone!) to move forward label Aug 5, 2024
@mrchantey
Copy link
Contributor

Can we plz schedule this PR for an upcoming milestone?
AFIK this is what allows us to deserialize scenes that involve MapEntities and Obsevers, which I guess will be increasingly common.
I believe it closes:

@alice-i-cecile alice-i-cecile added this to the 0.15 milestone Sep 13, 2024
@jrobsonchase
Copy link
Author

jrobsonchase commented Sep 16, 2024

Happy to get this rebased and mergeable, but I still think it's just a band-aid. Systems with exclusive World access and Commands shouldn't have to worry about how they order operations just to appease some observer, and observers shouldn't have to care about how things are ordered within a command/exclusive system. I expect exclusive World access to be, well, exclusive, and observers/hooks getting to run in the middle of them feels an awful lot like concurrent shared access. I get that it's all technically kosher according to the borrow checker's rules, but it still breaks the expected mental model. I wrote up a similar issue about mid-exclusive command application, but I think even mid-exclusive read-only observer/hook access is wrong.

@alice-i-cecile
Copy link
Member

Yeah, I'd like to get this merged :) I disagree with you on "should hooks and observers operate during exclusive world access", but that's a conversation for another thread.

@jrobsonchase jrobsonchase force-pushed the pre_map_entities branch 2 times, most recently from b7a52aa to 6ed33e8 Compare September 16, 2024 16:36
@alice-i-cecile
Copy link
Member

@mrchantey can I please have your review here?

Copy link
Contributor

@mrchantey mrchantey left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extra test coverage looks great and its really well documented. I left one comment about documenting panics, not sure if thats our style :)

// Fetch the entity with the given entity id from the `entity_map`.
let entity = *entity_map
.get(&scene_entity.entity)
.expect("should have previously spawned an empty entity");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not 100% on Bevy docs style, are we meant to declare these panics in the function docs?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it would be nice to add a #Panics section to the docs.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding is that panics only need to be documented if they can be triggered by the caller due to invariants not being upheld. That isn't the case for this panic as it signals a bug in the method internals.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-ECS Entities, components, systems, and events A-Reflection Runtime information about types A-Scenes Serialized ECS data stored on the disk C-Breaking-Change A breaking change to Bevy's public API that needs to be noted in a migration guide C-Bug An unexpected or incorrect behavior S-Needs-Review Needs reviewer attention (from anyone!) to move forward X-Contentious There are nontrivial implications that should be thought through
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Observers and hooks doesn't pair well with mapped entities
4 participants