-
-
Notifications
You must be signed in to change notification settings - Fork 4.4k
Make WorldQuery use &World for initialization
#22670
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
base: main
Are you sure you want to change the base?
Make WorldQuery use &World for initialization
#22670
Conversation
This required a little bundle work too.
|
Am I right that queueing the reservations is going to be slower? Do we have any idea how much slower this is for initializing the state? |
Regardless of using queued vs un-queued registration, it first looks to see if the component is already registered. In the un-queued case, if not, it starts the whole registration process, which could take a while. In the queued case, it acquires a lock the the queue, reserves an id, and drops the lock. The registration happens after in So, I think the only way this could have perf problems is if a ton of query states are made about components that are not registered, and nothing flushes the world, spawns the component, or does anything in between to register it. And even if all that does happen, the only real perf cost is getting a |
|
When it comes to |
| /// | ||
| /// - Asset changes are registered in the [`AssetEventSystems`] system set. | ||
| /// - Removed assets are not detected. | ||
| /// - The asset must be initialized ([`App::init_asset`](crate::AssetApp::init_asset)). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a totally fine limitation: this filter is useless without initialized assets.
|
It looks like your PR has been selected for a highlight in the next release blog post, but you didn't provide a release note. Please review the instructions for writing release notes, then expand or revise the content in the release notes directory to showcase your changes. |
|
It looks like your PR is a breaking change, but you didn't provide a migration guide. Please review the instructions for writing migration guides, then expand or revise the content in the migration guides directory to reflect your changes. |
|
The ability to do There's also some docs and deprecation work to do, but please ping me for a re-review when those comments are addressed. |
|
I have two concerns:
My suggestion would be: keep |
This is a sensible concern and should be discussed / debated. Tests to verify that this works during multithreaded operation would be appreciated at the least.
I am completely fine with this as a breaking change given Bevy's level of stability. The migration is very easy, and that's what migration guides are for. |
Well, this probably will be used for multi-thread synchronization since systems that take The short answer is that we consider things like component registrations, new tables, etc not as conceptual mutations. That is, even though they typically need From a user's perspective, a At least, this is what I gathered from discord conversations that ultimately led toward #18173.
Multithreading does make things a bit more complex, but I don't think we have to worry about it here. The only I'd be happy to write some tests to double check lock contention, but I don't think there's any way to create that situation in a test. Still, I understand your concern; I'm just not sure how to definitively prove its correctness here. Was there anything in particular in #18173 that looks suspicious to you? |
What I was mentioning is that In fact, from my parallel computing experience, implicit locks and sync points are not desired in the base framework. The API In addition, the query may return If everyone is comfortable with this, then I consider the PR good to go. |
You're absolutely right about the compiler here, but I'm not sure what kind of warning would be appropriate. The only time this locks is the first time a world sees a component type. There's no locking during flushing or anything, and most queries would not lock besides those made during startup. This shouldn't cause any non-determinism because component registration is order independent. I do get your concern though. Choosing which API is best is not up to me, but I think this is a win for most users. If anyone is worried about the order their components are registered in, they are probably registering them manually already.
Maybe I'm not understanding, but there is no way to get mutable world data from |
I have forgotten if we could query |
I think the issue is that this is exactly what happens when initializing the schedule for the first time! Components that get spawned by the startup schedule will be registered on spawn, but most other components are registered for the first time when we initialize a system that has a |
chescock
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yay, I'm glad this is happening!
I think someone needs to check the performance implications, both of always initializing AssetChanges<A> and of the extra atomic RwLocks during system initialization. But I don't feel qualified to know what's acceptable there, so I get to click Approve :).
| )); | ||
| } | ||
| self.insert_resource(assets) | ||
| .init_resource::<AssetChanges<A>>() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the reason this used to be initialized only in the AssetChanged filter is to avoid the overhead of updating the resource if nothing was using it. There's an asset_events system that takes Option<ResMut<AssetChanges<A>>> and only updates it if the resource exists:
bevy/crates/bevy_asset/src/assets.rs
Line 594 in 78166fb
| asset_changes: Option<ResMut<AssetChanges<A>>>, |
I don't know enough about assets to evaluate how important that is, though.
If we do need to preserve that behavior, I think there are still ways to do it with only &World. Maybe a resource with a ConcurrentQueue<fn(&mut World)>? AssetChanged::init_state would push |world| world.init_resource::<AssetChanges<A>>() and an exclusive system that runs before(AssetEventSystems) would drain the queue and run the function pointers? AssetChanged filters are rare enough that the extra atomics during initialization shouldn't be too expensive.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, it's not ideal. I'd like more information about the perf implications here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have a strong opinion here. It's quite unfortunate to have this running for every asset type though - some asset types may mutate frequently (e.g., Materials) but those asset types are also most likely to actually take advantage of this feature.
I wouldn't block on preserving this unless folks complain (which seems unlikely). With assets-as-entities unblocked, we may also throw out this type entirely, so putting the work in to fix it seems a little early.
| let event_key = world.register_event_key::<E>(); | ||
| let components = B::component_ids(&mut world.components_registrator()); | ||
| let components = B::component_ids(&mut world.components_registrator()) | ||
| .collect::<smallvec::SmallVec<[ComponentId; 16]>>(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If I understand correctly, this collect is because component_ids captures the lifetime of the ComponentsRegistrator type now that it's a generic parameter, which means this now conflicts with the world borrow in world.get_mut::<Observer>. And the compiler forces it to capture that lifetime, even though the return types are always actually 'static and the use<> syntax looks like it would support leaving some parameters out.
But this only actually allocates when creating an observer with more than 16 components, which should basically never happen and which is already allocating to box the system, so the cost is pretty low.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Exactly. We could monomorphize component_ids ourselves to fix this, but that didn't feel worth it. And this should be fixable once we can specify the use<> bounds. I also tried adding a 'static bound, but that didn't work either.
|
@andriyDev, can I have your opinions here on the asset changes? |
|
I think at this point we only have three unresolved questions: Performance impact of queued component registrationFor components that have not been registered, this adds a Is this area's performance important? If so, I'll add some benches for it. (Right now, nothing benches app startup costs like registering systems.) We can also consider Does app startup performance matter enough to create benchmarks for it? Performance impact of
|
For Bevy, IMO no. At least not unless we're talking about seconds. I also expect that windowing and rendering costs are going to absolutely dwarf micro-optimizations inside the ECS.
My feeling is that this is in the noise. Virtually every asset type should be using AssetChanged, although not all of them currently are, leading to subtle bugs when they're updated.
We should avoid adding complexity unless we have concrete evidence that this is a problem we need to solve. Even then, we should probably split that into a separate PR.
We could open a draft PR for the crate, targeting this branch, and see if we can get it to compile? |
Objective
Works towards #18276.
This does not do system initialization with
&World.This makes
WorldQuery::init_stateonly take&World, and by extensionQueryState::new, etc.Solution
Bundle::component_idsalso work with queued component registration.WorldQuery::init_statefrom&mut Worldto&World.AssetChangesresource ininit_assetinstead ofAssetChanged::init_state.Testing