bevy icon indicating copy to clipboard operation
bevy copied to clipboard

Add the AssetChanged query filter

Open nicopap opened this issue 2 years ago • 12 comments

Objective

  • Closes #5069
  • ~~Blocked on: #5105~~

Solution

  • Add the AssetChanges<T> resource.
  • Add the AssetChanged<T> world query
  • Add the AssetOrHandleChanged<T> world query type alias

AssetChanges<T> resource does the following:

  • It is a HashMap<HandleId, Tick>
  • If AssetChanges<T> is present, each AssetEvent<T> emitted in Assets::<T>::asset_event_system will also update the relevant entry in the AssetChanges hash map.

AssetChanged<T> does the following:

  • It acts like &Handle<T> with one difference:
  • Its init_state adds the AssetChanges<T> resource in the world if not present
  • Its init_fetch fetches the AssetChanges<T> resource in addition to the &Handle<T>. (panics if the resource was removed)
  • It checks if the handle was updated since last time the system ran, and filters on it

Design decisions

Performance

  • AssetChanges<T> is a fairly costly operation, since it does a hashmap lookup per entity iterated.
  • We do not maintain and update an AssetChanges<T> for assets that do not have a AssetChanged<T> world query.
  • The fact that AssetChanged both accesses (and locks) a component Handle<T> and a resource AssetChanges is unique and might come off as surprising. But I don't see how this could introduce a soundness issue.
  • If using "Single arc tree" in #8624 turns out to be impossible for change tick, a few optimizations are possibles:
    • bloom filters seem promising
    • a coarse-grained asset tick, that we could compare directly instead of fetching individual asset change ticks.

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 time

I chose to update the change_ticks hash map during asset event propagation rather than when accessing the asset, because we need access somehow to the current change_tick. Updating the tick hash map during modification would require passing the change_tick to all methods of Assets that accepts a &mut self. Which would be a huge ergonomic loss.

As a result, the asset change expressed by AssetChanged is only effective after asset_event_system ran. This is in line with how AssetEvents currently works, but might cause a 1 frame lag for asset change detection.

Alternatives

  1. #4944 (reverted due to perf issues)
  2. Potentially (not confirmed) #8624 using the single arc tree as mentioned
  3. Using a component (I will explore this)

Changelog

  • Added AssetChanged<A> world query, it filters Handle<A> for which the underlying asset got modified since the last time the system ran.
  • Added AssetOrHandleChanged<A>, a type alias for Or<(Changed<Handle<T>>, AssetChanged<T>)>, which accounts for case where the Handle changed instead of the underlying asset.

nicopap avatar Jun 23 '22 14:06 nicopap

Can we do the same thing for AssetLoaded ala Added filters? If so, that would be incredibly valuable. I'm not sure if we should make that part of this PR, but I'm curious to hear your thoughts.

alice-i-cecile avatar Jun 25 '22 00:06 alice-i-cecile

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.

jakobhellermann avatar Jun 25 '22 07:06 jakobhellermann

I'm a bit nervous at how much of the change detection code is duplicated.

Well, fault of ECS for not newtyping a Tick(u32) and define the difference checks as methods on those :P I guess I could abstract the checks as extension methods on u32 and use them instead. What do you think?

Can we do the same thing for AssetLoaded ala Added filters?

Very likely, I'll check and add if possible.

AssetChanged<T> is incompatible with ResMut<Assets<T>>

Currently it is! But I can make it not so by splitting change_ticks out of Assets.

This needs tests

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

nicopap avatar Jun 25 '22 07:06 nicopap

In discord, an alternative design where ResMut<Assets<T>> is replaced by AssetsMut<T> was proposed. This in combination with the "mutating methods all require a tick parameter" would ensure same-frame update detection. However it would require a lot of user code change and might make it impossible to conciliate AssetMut with AssetChanged.

nicopap avatar Jun 25 '22 08:06 nicopap

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.

alice-i-cecile avatar Jun 25 '22 11:06 alice-i-cecile

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.

I believe we could store change ticks on Handles, if we're willing to store ticks as something like Arc<AtomicUsize>. Whether we should is a bit of an open question (ill need to spend more time with the problem). We could also consider "smart pointer" change detection that bumps the tick on mutate (much like we do for components). Not (yet) advocating for it, but it would be compatible with going down the Assets as Entities route (see the section in the Asset V2 pr for details on that).

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.

cart avatar Aug 03 '23 19:08 cart

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.

nicopap avatar Aug 03 '23 19:08 nicopap

Currently the "can't detect changes predating the system first run" is a tradeoff to avoid having to pay the price for assets that don't have a AssetChanged<A> filter. But I think it should be possible to fix.

nicopap avatar Sep 18 '23 15:09 nicopap

We were discussing uniformizing components and assets change detection under Mut<T> on Discord here, for the sake of being able to handle both with Mut<T> alone generically without having to know that the thing is a component or an asset (something I've been trying to do for example for several releases in Bevy Tweening, because animating doesn't care where the object to animate comes from, but currently needs 2 duplicates codepaths only because of different change detection mechanisms).

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 Mut<T>), and why it's not possible to use that proven system for assets. I personally don't know enough to see the obstacles, and therefore any solution not going in that direction feels wrong. From a first quick reading it looks like the work does use ticks, but possibly differs in where those ticks are stored (?).

I understand that the objective of this change is more about query filtering, however if we could converge asset change detection toward Mut<T> then we would have an existing familiar pattern for users and would immediately benefit I think (?) from existing query filtering mechanisms like Added<T> which as far as I know are based on ticks like Mut.

djeedai avatar Oct 28 '23 21:10 djeedai

@djeedai

All the changes relevant to your question are in the crates/bevy_asset/src/assets.rs file.

Ticks for components are stored in Column. Tick filters are the Added<T> and Changed<T> world queries. Tick filters read the tick from the tick storage of the column. Each time the query matches a component, the tick filter will fetch the tick in the column and compare it with the SystemChangeTick. The tick filter will skip entities with incompatible change ticks.

Unlike the ECS, an Assets<A> is a HashMap<AssetId, A>. Assets v2 allows for some additional metadata per assets. The "single arc tree" — mentioned in the PR description — let you get metadata from Handle<A>. But the change tick is not part of those metadata.

In bevy_assets, change ticks are events. They are stored in the queued_events field of the Assets struct. The asset_events system pushes those events to an EventWriter. Normally, you would access the asset events through the EventReader<AssetEvent<A>> system parameter. This PR simply modifies the asset_events system to update an additional resource: AssetChanges<A>.

Consider reading the "Solution" section of the PR description to get the rest of the story.

The design you describe

I'm not sure I follow you, but I gather you want an Assets SystemParam with a Assets::get_mut(…) -> Option<Mut<A>> method, rather than get_mut that always marks the asset as changed. Maybe you are also thinking of a sort of WorldQuery that let you access a Mut<A> instead of the current Handle<A>?

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 Assets, so that its methods would have access to SystemChangeTick, though this isn't an exhaustive list of changes required to accomplish unification. Even then, already, if this PR gets merged, we would have started to move toward this unified design.

nicopap avatar Oct 29 '23 11:10 nicopap

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 Asset-prefixed types, which make it look like everything is duplicated. However I do get that this moves the cursor in the direction where both assets and components have a similar query filter mechanism, which could be in the future merged together, so from that perspective I guess it does improve the situation compared to the current version where asset change detection is only event based. So no objection for that change. Reviewed as best as I could; this is a bit out of my knowledge area.

djeedai avatar Oct 29 '23 22:10 djeedai

@cart

I believe we could store change ticks on Handles, if we're willing to store ticks as something like Arc<AtomicUsize>.

I see that Handle<T> is an Arc to a StrongHandle, which as I understand is where any metadata, including change detection ticks, goes. Why would we need another Arc? Can't we just store an AtomicUsize as a field of StrongHandle?

Whether we should is a bit of an open question (ill need to spend more time with the problem).

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?

We could also consider "smart pointer" change detection that bumps the tick on mutate (much like we do for components).

The ideal for me (for Bevy Tweening, but I'm sure there are other uses) would be if we can use exactly Mut itself, so code handling mutations can deal with assets and components uniformly/generically without having to care about the kind of mutated object nor having to carry around Assets<T> like is needed at the minute.

djeedai avatar Oct 30 '23 12:10 djeedai