Skip to main content

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