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