bevy_asset/
asset_changed.rs

1//! Defines the [`AssetChanged`] query filter.
2//!
3//! Like [`Changed`](bevy_ecs::prelude::Changed), but for [`Asset`]s,
4//! and triggers whenever the handle or the underlying asset changes.
5
6use crate::{AsAssetId, Asset, AssetId};
7use bevy_ecs::component::Components;
8use bevy_ecs::{
9    archetype::Archetype,
10    component::{ComponentId, Tick},
11    prelude::{Entity, Resource, World},
12    query::{FilteredAccess, QueryData, QueryFilter, ReadFetch, WorldQuery},
13    storage::{Table, TableRow},
14    world::unsafe_world_cell::UnsafeWorldCell,
15};
16use bevy_platform::collections::HashMap;
17use core::marker::PhantomData;
18use disqualified::ShortName;
19use tracing::error;
20
21/// A resource that stores the last tick an asset was changed. This is used by
22/// the [`AssetChanged`] filter to determine if an asset has changed since the last time
23/// a query ran.
24///
25/// This resource is automatically managed by the [`AssetEventSystems`](crate::AssetEventSystems)
26/// system set and should not be exposed to the user in order to maintain safety guarantees.
27/// Any additional uses of this resource should be carefully audited to ensure that they do not
28/// introduce any safety issues.
29#[derive(Resource)]
30pub(crate) struct AssetChanges<A: Asset> {
31    change_ticks: HashMap<AssetId<A>, Tick>,
32    last_change_tick: Tick,
33}
34
35impl<A: Asset> AssetChanges<A> {
36    pub(crate) fn insert(&mut self, asset_id: AssetId<A>, tick: Tick) {
37        self.last_change_tick = tick;
38        self.change_ticks.insert(asset_id, tick);
39    }
40    pub(crate) fn remove(&mut self, asset_id: &AssetId<A>) {
41        self.change_ticks.remove(asset_id);
42    }
43}
44
45impl<A: Asset> Default for AssetChanges<A> {
46    fn default() -> Self {
47        Self {
48            change_ticks: Default::default(),
49            last_change_tick: Tick::new(0),
50        }
51    }
52}
53
54struct AssetChangeCheck<'w, A: AsAssetId> {
55    // This should never be `None` in practice, but we need to handle the case
56    // where the `AssetChanges` resource was removed.
57    change_ticks: Option<&'w HashMap<AssetId<A::Asset>, Tick>>,
58    last_run: Tick,
59    this_run: Tick,
60}
61
62impl<A: AsAssetId> Clone for AssetChangeCheck<'_, A> {
63    fn clone(&self) -> Self {
64        *self
65    }
66}
67
68impl<A: AsAssetId> Copy for AssetChangeCheck<'_, A> {}
69
70impl<'w, A: AsAssetId> AssetChangeCheck<'w, A> {
71    fn new(changes: &'w AssetChanges<A::Asset>, last_run: Tick, this_run: Tick) -> Self {
72        Self {
73            change_ticks: Some(&changes.change_ticks),
74            last_run,
75            this_run,
76        }
77    }
78    // TODO(perf): some sort of caching? Each check has two levels of indirection,
79    // which is not optimal.
80    fn has_changed(&self, handle: &A) -> bool {
81        let is_newer = |tick: &Tick| tick.is_newer_than(self.last_run, self.this_run);
82        let id = handle.as_asset_id();
83
84        self.change_ticks
85            .is_some_and(|change_ticks| change_ticks.get(&id).is_some_and(is_newer))
86    }
87}
88
89/// Filter that selects entities with an `A` for an asset that changed
90/// after the system last ran, where `A` is a component that implements
91/// [`AsAssetId`].
92///
93/// Unlike `Changed<A>`, this is true whenever the asset for the `A`
94/// in `ResMut<Assets<A>>` changed. For example, when a mesh changed through the
95/// [`Assets<Mesh>::get_mut`] method, `AssetChanged<Mesh>` will iterate over all
96/// entities with the `Handle<Mesh>` for that mesh. Meanwhile, `Changed<Handle<Mesh>>`
97/// will iterate over no entities.
98///
99/// Swapping the actual `A` component is a common pattern. So you
100/// should check for _both_ `AssetChanged<A>` and `Changed<A>` with
101/// `Or<(Changed<A>, AssetChanged<A>)>`.
102///
103/// # Quirks
104///
105/// - Asset changes are registered in the [`AssetEventSystems`] system set.
106/// - Removed assets are not detected.
107///
108/// The list of changed assets only gets updated in the [`AssetEventSystems`] system set,
109/// which runs in `PostUpdate`. Therefore, `AssetChanged` will only pick up asset changes in schedules
110/// following [`AssetEventSystems`] or the next frame. Consider adding the system in the `Last` schedule
111/// after [`AssetEventSystems`] if you need to react without frame delay to asset changes.
112///
113/// # Performance
114///
115/// When at least one `A` is updated, this will
116/// read a hashmap once per entity with an `A` component. The
117/// runtime of the query is proportional to how many entities with an `A`
118/// it matches.
119///
120/// If no `A` asset updated since the last time the system ran, then no lookups occur.
121///
122/// [`AssetEventSystems`]: crate::AssetEventSystems
123/// [`Assets<Mesh>::get_mut`]: crate::Assets::get_mut
124pub struct AssetChanged<A: AsAssetId>(PhantomData<A>);
125
126/// [`WorldQuery`] fetch for [`AssetChanged`].
127#[doc(hidden)]
128pub struct AssetChangedFetch<'w, A: AsAssetId> {
129    inner: Option<ReadFetch<'w, A>>,
130    check: AssetChangeCheck<'w, A>,
131}
132
133impl<'w, A: AsAssetId> Clone for AssetChangedFetch<'w, A> {
134    fn clone(&self) -> Self {
135        Self {
136            inner: self.inner,
137            check: self.check,
138        }
139    }
140}
141
142/// [`WorldQuery`] state for [`AssetChanged`].
143#[doc(hidden)]
144pub struct AssetChangedState<A: AsAssetId> {
145    asset_id: ComponentId,
146    resource_id: ComponentId,
147    _asset: PhantomData<fn(A)>,
148}
149
150#[expect(unsafe_code, reason = "WorldQuery is an unsafe trait.")]
151/// SAFETY: `ROQueryFetch<Self>` is the same as `QueryFetch<Self>`
152unsafe impl<A: AsAssetId> WorldQuery for AssetChanged<A> {
153    type Fetch<'w> = AssetChangedFetch<'w, A>;
154
155    type State = AssetChangedState<A>;
156
157    fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> {
158        fetch
159    }
160
161    unsafe fn init_fetch<'w, 's>(
162        world: UnsafeWorldCell<'w>,
163        state: &'s Self::State,
164        last_run: Tick,
165        this_run: Tick,
166    ) -> Self::Fetch<'w> {
167        // SAFETY:
168        // - `AssetChanges` is private and only accessed mutably in the `AssetEventSystems` system set.
169        // - `resource_id` was obtained from the type ID of `AssetChanges<A::Asset>`.
170        let Some(changes) = (unsafe {
171            world
172                .get_resource_by_id(state.resource_id)
173                .map(|ptr| ptr.deref::<AssetChanges<A::Asset>>())
174        }) else {
175            error!(
176                "AssetChanges<{ty}> resource was removed, please do not remove \
177                AssetChanges<{ty}> when using the AssetChanged<{ty}> world query",
178                ty = ShortName::of::<A>()
179            );
180
181            return AssetChangedFetch {
182                inner: None,
183                check: AssetChangeCheck {
184                    change_ticks: None,
185                    last_run,
186                    this_run,
187                },
188            };
189        };
190        let has_updates = changes.last_change_tick.is_newer_than(last_run, this_run);
191
192        AssetChangedFetch {
193            inner: has_updates.then(||
194                    // SAFETY: We delegate to the inner `init_fetch` for `A`
195                    unsafe {
196                        <&A>::init_fetch(world, &state.asset_id, last_run, this_run)
197                    }),
198            check: AssetChangeCheck::new(changes, last_run, this_run),
199        }
200    }
201
202    const IS_DENSE: bool = <&A>::IS_DENSE;
203
204    unsafe fn set_archetype<'w, 's>(
205        fetch: &mut Self::Fetch<'w>,
206        state: &'s Self::State,
207        archetype: &'w Archetype,
208        table: &'w Table,
209    ) {
210        if let Some(inner) = &mut fetch.inner {
211            // SAFETY: We delegate to the inner `set_archetype` for `A`
212            unsafe {
213                <&A>::set_archetype(inner, &state.asset_id, archetype, table);
214            }
215        }
216    }
217
218    unsafe fn set_table<'w, 's>(
219        fetch: &mut Self::Fetch<'w>,
220        state: &Self::State,
221        table: &'w Table,
222    ) {
223        if let Some(inner) = &mut fetch.inner {
224            // SAFETY: We delegate to the inner `set_table` for `A`
225            unsafe {
226                <&A>::set_table(inner, &state.asset_id, table);
227            }
228        }
229    }
230
231    #[inline]
232    fn update_component_access(state: &Self::State, access: &mut FilteredAccess) {
233        <&A>::update_component_access(&state.asset_id, access);
234        access.add_resource_read(state.resource_id);
235    }
236
237    fn init_state(world: &mut World) -> AssetChangedState<A> {
238        let resource_id = world.init_resource::<AssetChanges<A::Asset>>();
239        let asset_id = world.register_component::<A>();
240        AssetChangedState {
241            asset_id,
242            resource_id,
243            _asset: PhantomData,
244        }
245    }
246
247    fn get_state(components: &Components) -> Option<Self::State> {
248        let resource_id = components.resource_id::<AssetChanges<A::Asset>>()?;
249        let asset_id = components.component_id::<A>()?;
250        Some(AssetChangedState {
251            asset_id,
252            resource_id,
253            _asset: PhantomData,
254        })
255    }
256
257    fn matches_component_set(
258        state: &Self::State,
259        set_contains_id: &impl Fn(ComponentId) -> bool,
260    ) -> bool {
261        set_contains_id(state.asset_id)
262    }
263}
264
265#[expect(unsafe_code, reason = "QueryFilter is an unsafe trait.")]
266/// SAFETY: read-only access
267unsafe impl<A: AsAssetId> QueryFilter for AssetChanged<A> {
268    const IS_ARCHETYPAL: bool = false;
269
270    #[inline]
271    unsafe fn filter_fetch(
272        state: &Self::State,
273        fetch: &mut Self::Fetch<'_>,
274        entity: Entity,
275        table_row: TableRow,
276    ) -> bool {
277        fetch.inner.as_mut().is_some_and(|inner| {
278            // SAFETY: We delegate to the inner `fetch` for `A`
279            unsafe {
280                let handle = <&A>::fetch(&state.asset_id, inner, entity, table_row);
281                fetch.check.has_changed(handle)
282            }
283        })
284    }
285}
286
287#[cfg(test)]
288#[expect(clippy::print_stdout, reason = "Allowed in tests.")]
289mod tests {
290    use crate::{AssetEventSystems, AssetPlugin, Handle};
291    use alloc::{vec, vec::Vec};
292    use core::num::NonZero;
293    use std::println;
294
295    use crate::{AssetApp, Assets};
296    use bevy_app::{App, AppExit, PostUpdate, Startup, TaskPoolPlugin, Update};
297    use bevy_ecs::schedule::IntoScheduleConfigs;
298    use bevy_ecs::{
299        component::Component,
300        message::MessageWriter,
301        resource::Resource,
302        system::{Commands, IntoSystem, Local, Query, Res, ResMut},
303    };
304    use bevy_reflect::TypePath;
305
306    use super::*;
307
308    #[derive(Asset, TypePath, Debug)]
309    struct MyAsset(usize, &'static str);
310
311    #[derive(Component)]
312    struct MyComponent(Handle<MyAsset>);
313
314    impl AsAssetId for MyComponent {
315        type Asset = MyAsset;
316
317        fn as_asset_id(&self) -> AssetId<Self::Asset> {
318            self.0.id()
319        }
320    }
321
322    fn run_app<Marker>(system: impl IntoSystem<(), (), Marker>) {
323        let mut app = App::new();
324        app.add_plugins((TaskPoolPlugin::default(), AssetPlugin::default()))
325            .init_asset::<MyAsset>()
326            .add_systems(Update, system);
327        app.update();
328    }
329
330    // According to a comment in QueryState::new in bevy_ecs, components on filter
331    // position shouldn't conflict with components on query position.
332    #[test]
333    fn handle_filter_pos_ok() {
334        fn compatible_filter(
335            _query: Query<&mut MyComponent, AssetChanged<MyComponent>>,
336            mut exit: MessageWriter<AppExit>,
337        ) {
338            exit.write(AppExit::Error(NonZero::<u8>::MIN));
339        }
340        run_app(compatible_filter);
341    }
342
343    #[derive(Default, PartialEq, Debug, Resource)]
344    struct Counter(Vec<u32>);
345
346    fn count_update(
347        mut counter: ResMut<Counter>,
348        assets: Res<Assets<MyAsset>>,
349        query: Query<&MyComponent, AssetChanged<MyComponent>>,
350    ) {
351        for handle in query.iter() {
352            let asset = assets.get(&handle.0).unwrap();
353            counter.0[asset.0] += 1;
354        }
355    }
356
357    fn update_some(mut assets: ResMut<Assets<MyAsset>>, mut run_count: Local<u32>) {
358        let mut update_index = |i| {
359            let id = assets
360                .iter()
361                .find_map(|(h, a)| (a.0 == i).then_some(h))
362                .unwrap();
363            let asset = assets.get_mut(id).unwrap();
364            println!("setting new value for {}", asset.0);
365            asset.1 = "new_value";
366        };
367        match *run_count {
368            0 | 1 => update_index(0),
369            2 => {}
370            3 => {
371                update_index(0);
372                update_index(1);
373            }
374            4.. => update_index(1),
375        };
376        *run_count += 1;
377    }
378
379    fn add_some(
380        mut assets: ResMut<Assets<MyAsset>>,
381        mut cmds: Commands,
382        mut run_count: Local<u32>,
383    ) {
384        match *run_count {
385            1 => {
386                cmds.spawn(MyComponent(assets.add(MyAsset(0, "init"))));
387            }
388            0 | 2 => {}
389            3 => {
390                cmds.spawn(MyComponent(assets.add(MyAsset(1, "init"))));
391                cmds.spawn(MyComponent(assets.add(MyAsset(2, "init"))));
392            }
393            4.. => {
394                cmds.spawn(MyComponent(assets.add(MyAsset(3, "init"))));
395            }
396        };
397        *run_count += 1;
398    }
399
400    #[track_caller]
401    fn assert_counter(app: &App, assert: Counter) {
402        assert_eq!(&assert, app.world().resource::<Counter>());
403    }
404
405    #[test]
406    fn added() {
407        let mut app = App::new();
408
409        app.add_plugins((TaskPoolPlugin::default(), AssetPlugin::default()))
410            .init_asset::<MyAsset>()
411            .insert_resource(Counter(vec![0, 0, 0, 0]))
412            .add_systems(Update, add_some)
413            .add_systems(PostUpdate, count_update.after(AssetEventSystems));
414
415        // First run of the app, `add_systems(Startup…)` runs.
416        app.update(); // run_count == 0
417        assert_counter(&app, Counter(vec![0, 0, 0, 0]));
418        app.update(); // run_count == 1
419        assert_counter(&app, Counter(vec![1, 0, 0, 0]));
420        app.update(); // run_count == 2
421        assert_counter(&app, Counter(vec![1, 0, 0, 0]));
422        app.update(); // run_count == 3
423        assert_counter(&app, Counter(vec![1, 1, 1, 0]));
424        app.update(); // run_count == 4
425        assert_counter(&app, Counter(vec![1, 1, 1, 1]));
426    }
427
428    #[test]
429    fn changed() {
430        let mut app = App::new();
431
432        app.add_plugins((TaskPoolPlugin::default(), AssetPlugin::default()))
433            .init_asset::<MyAsset>()
434            .insert_resource(Counter(vec![0, 0]))
435            .add_systems(
436                Startup,
437                |mut cmds: Commands, mut assets: ResMut<Assets<MyAsset>>| {
438                    let asset0 = assets.add(MyAsset(0, "init"));
439                    let asset1 = assets.add(MyAsset(1, "init"));
440                    cmds.spawn(MyComponent(asset0.clone()));
441                    cmds.spawn(MyComponent(asset0));
442                    cmds.spawn(MyComponent(asset1.clone()));
443                    cmds.spawn(MyComponent(asset1.clone()));
444                    cmds.spawn(MyComponent(asset1));
445                },
446            )
447            .add_systems(Update, update_some)
448            .add_systems(PostUpdate, count_update.after(AssetEventSystems));
449
450        // First run of the app, `add_systems(Startup…)` runs.
451        app.update(); // run_count == 0
452
453        // First run: We count the entities that were added in the `Startup` schedule
454        assert_counter(&app, Counter(vec![2, 3]));
455
456        // Second run: `update_once` updates the first asset, which is
457        // associated with two entities, so `count_update` picks up two updates
458        app.update(); // run_count == 1
459        assert_counter(&app, Counter(vec![4, 3]));
460
461        // Third run: `update_once` doesn't update anything, same values as last
462        app.update(); // run_count == 2
463        assert_counter(&app, Counter(vec![4, 3]));
464
465        // Fourth run: We update the two assets (asset 0: 2 entities, asset 1: 3)
466        app.update(); // run_count == 3
467        assert_counter(&app, Counter(vec![6, 6]));
468
469        // Fifth run: only update second asset
470        app.update(); // run_count == 4
471        assert_counter(&app, Counter(vec![6, 9]));
472        // ibid
473        app.update(); // run_count == 5
474        assert_counter(&app, Counter(vec![6, 12]));
475    }
476}