-
-
Notifications
You must be signed in to change notification settings - Fork 3.6k
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
Add the AssetChanged query filter #5080
Conversation
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.
Impressive work. This should be extremely useful.
The world-query impls look correct to me; nicely done.
I'm a bit nervous at how much of the change detection code is duplicated. Can we reuse primitives from bevy_ecs's change detection? I'm fine to make more parts pub as needed for this.
crates/bevy_asset/src/query.rs
Outdated
} | ||
} | ||
|
||
/// Query filter to get all entities which `Handle<T>` component underlying asset changed. |
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 could use some more elaboration. In particular, I would note:
- This is for the asset that that
Handle<T>
points to. - Entities without a
Handle<T>
component will never be found by this filter. - A small doc example :)
- Contrast to the
Changed<Handle<T>>
query filter, which instead asks "has what the handle pointed to changed". - Demonstrate the
Or<(Changed<Handle<T>>, AssetChanged<T>)>
pattern, which will be commonly needed when you need to respond to asset changes. - This is incompatible with
ResMut<Assets<T>>
.
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.
Yes to all this, I'll clean up the docs. In particular, I think (5) is indeed going to be very common. I'm thinking maybe a type alias could help
pub type AssetOrHandleChanged<T> = Or<(
Changed<Handle<T>>,
AssetChanged<T>,
)>
(6) is kinda misleading. I think I might just store the change_ticks
field in its own resource, so that the incompatibility is more visible and isolated.
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 like the idea of a type alias for 5 :)
Can we do the same thing for |
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 needs tests :) One of the ones that's easily overlooked is that we should assert that AssetChanged<T>
is incompatible with ResMut<Assets<T>>
.
That should be a compile fail test; I think it should be fine to put into the bevy_ecs_compile_fail crate.
I think it would fail at runtime when the system is added (or first ran?). Conflicting system params aren't caught at compile time. |
Well, fault of ECS for not newtyping a
Very likely, I'll check and add if possible.
Currently it is! But I can make it not so by splitting
I agree. I'll see to it. What do you think of benchmarks as well? I think this filter might be particularly slow due to the 2 levels of indirection at each iteration |
In discord, an alternative design where |
My preference would be to newtype a Tick(u32) to reduce duplication. IMO we should do this as a seperate uncontroversial PR and then rebase to make this easier to review. It's a good idea regardless. |
3766f35
to
f6998ca
Compare
# Objective * Enable `Res` and `Query` parameter mutual exclusion * Required for #5080 The `FilteredAccessSet::get_conflicts` methods didn't work properly with `Res` and `ResMut` parameters. Because those added their access by using the `combined_access_mut` method and directly modifying the global access state of the FilteredAccessSet. This caused an inconsistency, because get_conflicts assumes that ALL added access have a corresponding `FilteredAccess` added to the `filtered_accesses` field. In practice, that means that SystemParam that adds their access through the `Access` returned by `combined_access_mut` and the ones that add their access using the `add` method lived in two different universes. As a result, they could never be mutually exclusive. ## Solution This commit fixes it by removing the `combined_access_mut` method. This ensures that the `combined_access` field of FilteredAccessSet is always updated consistently with the addition of a filter. When checking for filtered access, it is now possible to account for `Res` and `ResMut` invalid access. This is currently not needed, but might be in the future. We add the `add_unfiltered_{read,write}` methods to replace previous usages of `combined_access_mut`. We also add improved Debug implementations on FixedBitSet so that their meaning is much clearer in debug output. --- ## Changelog * Fix `Res` and `Query` parameter never being mutually exclusive. ## Migration Guide Note: this mostly changes ECS internals, but since the API is public, it is technically breaking: * Removed `FilteredAccessSet::combined_access_mut` * Replace _immutable_ usage of those by `combined_access` * For _mutable_ usages, use the new `add_unfiltered_{read,write}` methods instead of `combined_access_mut` followed by `add_{read,write}`
# Objective * Enable `Res` and `Query` parameter mutual exclusion * Required for #5080 The `FilteredAccessSet::get_conflicts` methods didn't work properly with `Res` and `ResMut` parameters. Because those added their access by using the `combined_access_mut` method and directly modifying the global access state of the FilteredAccessSet. This caused an inconsistency, because get_conflicts assumes that ALL added access have a corresponding `FilteredAccess` added to the `filtered_accesses` field. In practice, that means that SystemParam that adds their access through the `Access` returned by `combined_access_mut` and the ones that add their access using the `add` method lived in two different universes. As a result, they could never be mutually exclusive. ## Solution This commit fixes it by removing the `combined_access_mut` method. This ensures that the `combined_access` field of FilteredAccessSet is always updated consistently with the addition of a filter. When checking for filtered access, it is now possible to account for `Res` and `ResMut` invalid access. This is currently not needed, but might be in the future. We add the `add_unfiltered_{read,write}` methods to replace previous usages of `combined_access_mut`. We also add improved Debug implementations on FixedBitSet so that their meaning is much clearer in debug output. --- ## Changelog * Fix `Res` and `Query` parameter never being mutually exclusive. ## Migration Guide Note: this mostly changes ECS internals, but since the API is public, it is technically breaking: * Removed `FilteredAccessSet::combined_access_mut` * Replace _immutable_ usage of those by `combined_access` * For _mutable_ usages, use the new `add_unfiltered_{read,write}` methods instead of `combined_access_mut` followed by `add_{read,write}`
# Objective * Enable `Res` and `Query` parameter mutual exclusion * Required for bevyengine#5080 The `FilteredAccessSet::get_conflicts` methods didn't work properly with `Res` and `ResMut` parameters. Because those added their access by using the `combined_access_mut` method and directly modifying the global access state of the FilteredAccessSet. This caused an inconsistency, because get_conflicts assumes that ALL added access have a corresponding `FilteredAccess` added to the `filtered_accesses` field. In practice, that means that SystemParam that adds their access through the `Access` returned by `combined_access_mut` and the ones that add their access using the `add` method lived in two different universes. As a result, they could never be mutually exclusive. ## Solution This commit fixes it by removing the `combined_access_mut` method. This ensures that the `combined_access` field of FilteredAccessSet is always updated consistently with the addition of a filter. When checking for filtered access, it is now possible to account for `Res` and `ResMut` invalid access. This is currently not needed, but might be in the future. We add the `add_unfiltered_{read,write}` methods to replace previous usages of `combined_access_mut`. We also add improved Debug implementations on FixedBitSet so that their meaning is much clearer in debug output. --- ## Changelog * Fix `Res` and `Query` parameter never being mutually exclusive. ## Migration Guide Note: this mostly changes ECS internals, but since the API is public, it is technically breaking: * Removed `FilteredAccessSet::combined_access_mut` * Replace _immutable_ usage of those by `combined_access` * For _mutable_ usages, use the new `add_unfiltered_{read,write}` methods instead of `combined_access_mut` followed by `add_{read,write}`
I believe we could store change ticks on Handles, if we're willing to store ticks as something like In general the current approach in this PR should be compatible with the Asset V2 effort. And I do like that this is currently a "only pay the price if you use it" abstraction. Probably worth waiting to merge until after Asset V2 lands though and then updating this. This isn't the kind of port I'd block an Asset V2 merge on, so it might get lost in mix if we merge it now. |
Thank you for the feedback. I agree with you, I think waiting is the good idea. I'll put the PR back into draft mode. |
3fa5e8c
to
10443ce
Compare
06e5f0e
to
137736c
Compare
We were discussing uniformizing components and assets change detection under This PR was linked as a related work, however it looks from my untrained eye like it's going in the opposite direction and making assets and components even more different. I think it would be useful if possible to explain how this change detection mechanism differs from the one of components (based on ticks and I understand that the objective of this change is more about query filtering, however if we could converge asset change detection toward |
All the changes relevant to your question are in the Ticks for components are stored in Unlike the ECS, an In Consider reading the "Solution" section of the PR description to get the rest of the story. The design you describeI'm not sure I follow you, but I gather you want an I think it would be possible. You say that this PR makes "assets and components even more different". But I don't think that's true. It would move forward the design space toward an unified query API for assets and components IMO. Though I think, indeed, most of the energy required to be spent for an unifying design would be spent in orthogonal sections of the code; Notably, by system-paramifying |
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 that's mostly OK overall, but there's a couple of TODOs that should probably be fixed, like the change detection at start that @alice-i-cecile highlighted in comment (although not sure since they approved anyway).
|
||
use crate::{Asset, AssetId, Handle}; | ||
|
||
#[doc(hidden)] |
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.
Why hidden docs? This is a public type.
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 want this to be part of the public API. It has to be public because the asset_events
system accesses it, and is itself public. But otherwise it shouldn't be touched by 3rd parties. Bereft the choice of making the type pub(crate)
, the second best option is making it #[doc(hidden)]
.
We could probably make asset_events
private though. What do you think?
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.
My two cents rule for my own projects is:
- label all systems so users can order theirs relative to mine
- make systems themselves non public
That works around the issue here I think. But not sure this is the best approach.
Otherwise I'm fine with the explanation. Just wanted to confirm this was intended. Maybe leave a comment to explain?
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.
That's generally my preferred approach as well.
/// # Quirks | ||
/// | ||
/// - Removed assets are not detected. | ||
/// - Asset update tracking only starts after the first system with an |
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 that sounds like a major footgun. Even if documented here, this is quite the unexpected behavior for users.
Thanks @nicopap for the detailed answer. I was skeptical about whether the change goes in the direction of uniform component/asset handling because of the various new |
I see that
Looking at this PR it looks like this would help a few things like query filters and change detection via smart pointer. This also makes the mental model for change detection consistent between assets and components (even if we don't immediately use exactly the same types). Is there any catch you can think of already? Or is it just about due diligence before diving into that path?
The ideal for me (for Bevy Tweening, but I'm sure there are other uses) would be if we can use exactly |
137736c
to
b052585
Compare
Backlog cleanup: from what I can tell, this 2022 PR could still prove valuable and probably should be adopted. I'll close the PR but mention as much on #5069. |
Objective
Blocked on: [Merged by Bors] - Fix FilteredAccessSet get_conflicts inconsistency #5105Solution
AssetChanges<T>
resource.AssetChanged<T>
world queryAssetOrHandleChanged<T>
world query type aliasAssetChanges<T>
resource does the following:HashMap<HandleId, Tick>
AssetChanges<T>
is present, eachAssetEvent<T>
emitted inAssets::<T>::asset_event_system
will also update the relevant entry in theAssetChanges
hash map.AssetChanged<T>
does the following:&Handle<T>
with one difference:init_state
adds theAssetChanges<T>
resource in the world if not presentinit_fetch
fetches theAssetChanges<T>
resource in addition to the&Handle<T>
. (panics if the resource was removed)Design decisions
Performance
AssetChanges<T>
is a fairly costly operation, since it does a hashmap lookup per entity iterated.AssetChanges<T>
for assets that do not have aAssetChanged<T>
world query.AssetChanged
both accesses (and locks) a componentHandle<T>
and a resourceAssetChanges
is unique and might come off as surprising. But I don't see how this could introduce a soundness issue.Relation with Asset v2 (#8624)
The "Single Arc tree" bullet point in "Bevy Assets v2" section mentions we could attach metadata to handles. Using this for change ticks would make the implementation much more performant, and simplify the implementation.
AssetChanges
Update timeI chose to update the
change_ticks
hash map during asset eventpropagation rather than when accessing the asset, because we need access
somehow to the current
change_tick
. Updating the tick hash map duringmodification would require passing the
change_tick
to all methods ofAssets
that accepts a&mut self
. Which would be a huge ergonomicloss.
As a result, the asset change expressed by
AssetChanged
is onlyeffective after
asset_event_system
ran. This is in line with howAssetEvents
currently works, but might cause a 1 frame lag for assetchange detection.
Alternatives
Changelog
AssetChanged<A>
world query, it filtersHandle<A>
for which the underlying asset got modified since the last time the system ran.AssetOrHandleChanged<A>
, a type alias forOr<(Changed<Handle<T>>, AssetChanged<T>)>
, which accounts for case where theHandle
changed instead of the underlying asset.