//! Defines the [`AssetChanged`] query filter. //! //! Like [`Changed`](bevy_ecs::prelude::Changed), but for [`Asset`]s, //! and triggers whenever the handle or the underlying asset changes. use crate::{AsAssetId, Asset, AssetId}; use bevy_ecs::component::Components; use bevy_ecs::{ archetype::Archetype, component::{ComponentId, Tick}, prelude::{Entity, Resource, World}, query::{FilteredAccess, QueryFilter, QueryItem, ReadFetch, WorldQuery}, storage::{Table, TableRow}, world::unsafe_world_cell::UnsafeWorldCell, }; use bevy_utils::tracing::error; use bevy_utils::HashMap; use core::marker::PhantomData; use disqualified::ShortName; /// A resource that stores the last tick an asset was changed. This is used by /// the [`AssetChanged`] filter to determine if an asset has changed since the last time /// a query ran. /// /// This resource is automatically managed by the [`AssetEvents`](crate::AssetEvents) schedule and /// should not be exposed to the user in order to maintain safety guarantees. Any additional uses of /// this resource should be carefully audited to ensure that they do not introduce any safety /// issues. #[derive(Resource)] pub(crate) struct AssetChanges { change_ticks: HashMap, Tick>, last_change_tick: Tick, } impl AssetChanges { pub(crate) fn insert(&mut self, asset_id: AssetId, tick: Tick) { self.last_change_tick = tick; self.change_ticks.insert(asset_id, tick); } pub(crate) fn remove(&mut self, asset_id: &AssetId) { self.change_ticks.remove(asset_id); } } impl Default for AssetChanges { fn default() -> Self { Self { change_ticks: Default::default(), last_change_tick: Tick::new(0), } } } struct AssetChangeCheck<'w, A: AsAssetId> { // This should never be `None` in practice, but we need to handle the case // where the `AssetChanges` resource was removed. change_ticks: Option<&'w HashMap, Tick>>, last_run: Tick, this_run: Tick, } impl Clone for AssetChangeCheck<'_, A> { fn clone(&self) -> Self { *self } } impl Copy for AssetChangeCheck<'_, A> {} impl<'w, A: AsAssetId> AssetChangeCheck<'w, A> { fn new(changes: &'w AssetChanges, last_run: Tick, this_run: Tick) -> Self { Self { change_ticks: Some(&changes.change_ticks), last_run, this_run, } } // TODO(perf): some sort of caching? Each check has two levels of indirection, // which is not optimal. fn has_changed(&self, handle: &A) -> bool { let is_newer = |tick: &Tick| tick.is_newer_than(self.last_run, self.this_run); let id = handle.as_asset_id(); self.change_ticks .is_some_and(|change_ticks| change_ticks.get(&id).is_some_and(is_newer)) } } /// Filter that selects entities with a `A` for an asset that changed /// after the system last ran, where `A` is a component that implements /// [`AsAssetId`]. /// /// Unlike `Changed`, this is true whenever the asset for the `A` /// in `ResMut>` changed. For example, when a mesh changed through the /// [`Assets::get_mut`] method, `AssetChanged` will iterate over all /// entities with the `Handle` for that mesh. Meanwhile, `Changed>` /// will iterate over no entities. /// /// Swapping the actual `A` component is a common pattern. So you /// should check for _both_ `AssetChanged` and `Changed` with /// `Or<(Changed, AssetChanged)>`. /// /// # Quirks /// /// - Asset changes are registered in the [`AssetEvents`] schedule. /// - Removed assets are not detected. /// /// The list of changed assets only gets updated in the /// [`AssetEvents`] schedule which runs in `Last`. Therefore, `AssetChanged` /// will only pick up asset changes in schedules following `AssetEvents` or the /// next frame. Consider adding the system in the `Last` schedule after [`AssetEvents`] if you need /// to react without frame delay to asset changes. /// /// # Performance /// /// When at least one `A` is updated, this will /// read a hashmap once per entity with a `A` component. The /// runtime of the query is proportional to how many entities with a `A` /// it matches. /// /// If no `A` asset updated since the last time the system ran, then no lookups occur. /// /// [`AssetEvents`]: crate::AssetEvents /// [`Assets::get_mut`]: crate::Assets::get_mut pub struct AssetChanged(PhantomData); /// [`WorldQuery`] fetch for [`AssetChanged`]. #[doc(hidden)] pub struct AssetChangedFetch<'w, A: AsAssetId> { inner: Option>, check: AssetChangeCheck<'w, A>, } impl<'w, A: AsAssetId> Clone for AssetChangedFetch<'w, A> { fn clone(&self) -> Self { Self { inner: self.inner, check: self.check, } } } /// [`WorldQuery`] state for [`AssetChanged`]. #[doc(hidden)] pub struct AssetChangedState { asset_id: ComponentId, resource_id: ComponentId, _asset: PhantomData, } #[allow(unsafe_code)] /// SAFETY: `ROQueryFetch` is the same as `QueryFetch` unsafe impl WorldQuery for AssetChanged { type Item<'w> = (); type Fetch<'w> = AssetChangedFetch<'w, A>; type State = AssetChangedState; fn shrink<'wlong: 'wshort, 'wshort>(_: QueryItem<'wlong, Self>) -> QueryItem<'wshort, Self> {} fn shrink_fetch<'wlong: 'wshort, 'wshort>(fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { fetch } unsafe fn init_fetch<'w>( world: UnsafeWorldCell<'w>, state: &Self::State, last_run: Tick, this_run: Tick, ) -> Self::Fetch<'w> { // SAFETY: // - `AssetChanges` is private and only accessed mutably in the `AssetEvents` schedule // - `resource_id` was obtained from the type ID of `AssetChanges`. let Some(changes) = (unsafe { world .get_resource_by_id(state.resource_id) .map(|ptr| ptr.deref::>()) }) else { error!( "AssetChanges<{ty}> resource was removed, please do not remove \ AssetChanges<{ty}> when using the AssetChanged<{ty}> world query", ty = ShortName::of::() ); return AssetChangedFetch { inner: None, check: AssetChangeCheck { change_ticks: None, last_run, this_run, }, }; }; let has_updates = changes.last_change_tick.is_newer_than(last_run, this_run); AssetChangedFetch { inner: has_updates.then(|| // SAFETY: We delegate to the inner `init_fetch` for `A` unsafe { <&A>::init_fetch(world, &state.asset_id, last_run, this_run) }), check: AssetChangeCheck::new(changes, last_run, this_run), } } const IS_DENSE: bool = <&A>::IS_DENSE; unsafe fn set_archetype<'w>( fetch: &mut Self::Fetch<'w>, state: &Self::State, archetype: &'w Archetype, table: &'w Table, ) { if let Some(inner) = &mut fetch.inner { // SAFETY: We delegate to the inner `set_archetype` for `A` unsafe { <&A>::set_archetype(inner, &state.asset_id, archetype, table); } } } unsafe fn set_table<'w>(fetch: &mut Self::Fetch<'w>, state: &Self::State, table: &'w Table) { if let Some(inner) = &mut fetch.inner { // SAFETY: We delegate to the inner `set_table` for `A` unsafe { <&A>::set_table(inner, &state.asset_id, table); } } } unsafe fn fetch<'w>(_: &mut Self::Fetch<'w>, _: Entity, _: TableRow) -> Self::Item<'w> {} #[inline] fn update_component_access(state: &Self::State, access: &mut FilteredAccess) { <&A>::update_component_access(&state.asset_id, access); assert!( !access.access().has_resource_write(state.resource_id), "AssetChanged<{ty}> requires read-only access to AssetChanges<{ty}>", ty = ShortName::of::() ); access.add_resource_read(state.resource_id); } fn init_state(world: &mut World) -> AssetChangedState { let resource_id = world.init_resource::>(); let asset_id = world.register_component::(); AssetChangedState { asset_id, resource_id, _asset: PhantomData, } } fn get_state(components: &Components) -> Option { let resource_id = components.resource_id::>()?; let asset_id = components.component_id::()?; Some(AssetChangedState { asset_id, resource_id, _asset: PhantomData, }) } fn matches_component_set( state: &Self::State, set_contains_id: &impl Fn(ComponentId) -> bool, ) -> bool { set_contains_id(state.asset_id) } } #[allow(unsafe_code)] /// SAFETY: read-only access unsafe impl QueryFilter for AssetChanged { const IS_ARCHETYPAL: bool = false; #[inline] unsafe fn filter_fetch( fetch: &mut Self::Fetch<'_>, entity: Entity, table_row: TableRow, ) -> bool { fetch.inner.as_mut().is_some_and(|inner| { // SAFETY: We delegate to the inner `fetch` for `A` unsafe { let handle = <&A>::fetch(inner, entity, table_row); fetch.check.has_changed(handle) } }) } } #[cfg(test)] mod tests { use crate::{self as bevy_asset, AssetEvents, AssetPlugin, Handle}; use core::num::NonZero; use crate::{AssetApp, Assets}; use bevy_app::{App, AppExit, Last, Startup, TaskPoolPlugin, Update}; use bevy_ecs::schedule::IntoSystemConfigs; use bevy_ecs::{ component::Component, event::EventWriter, system::{Commands, IntoSystem, Local, Query, Res, ResMut, Resource}, }; use bevy_reflect::TypePath; use super::*; #[derive(Asset, TypePath, Debug)] struct MyAsset(usize, &'static str); #[derive(Component)] struct MyComponent(Handle); impl AsAssetId for MyComponent { type Asset = MyAsset; fn as_asset_id(&self) -> AssetId { self.0.id() } } fn run_app(system: impl IntoSystem<(), (), Marker>) { let mut app = App::new(); app.add_plugins((TaskPoolPlugin::default(), AssetPlugin::default())) .init_asset::() .add_systems(Update, system); app.update(); } // According to a comment in QueryState::new in bevy_ecs, components on filter // position shouldn't conflict with components on query position. #[test] fn handle_filter_pos_ok() { fn compatible_filter( _query: Query<&mut MyComponent, AssetChanged>, mut exit: EventWriter, ) { exit.send(AppExit::Error(NonZero::::MIN)); } run_app(compatible_filter); } #[derive(Default, PartialEq, Debug, Resource)] struct Counter(Vec); fn count_update( mut counter: ResMut, assets: Res>, query: Query<&MyComponent, AssetChanged>, ) { for handle in query.iter() { let asset = assets.get(&handle.0).unwrap(); counter.0[asset.0] += 1; } } fn update_some(mut assets: ResMut>, mut run_count: Local) { let mut update_index = |i| { let id = assets .iter() .find_map(|(h, a)| (a.0 == i).then_some(h)) .unwrap(); let asset = assets.get_mut(id).unwrap(); println!("setting new value for {}", asset.0); asset.1 = "new_value"; }; match *run_count { 0 | 1 => update_index(0), 2 => {} 3 => { update_index(0); update_index(1); } 4.. => update_index(1), }; *run_count += 1; } fn add_some( mut assets: ResMut>, mut cmds: Commands, mut run_count: Local, ) { match *run_count { 1 => { cmds.spawn(MyComponent(assets.add(MyAsset(0, "init")))); } 0 | 2 => {} 3 => { cmds.spawn(MyComponent(assets.add(MyAsset(1, "init")))); cmds.spawn(MyComponent(assets.add(MyAsset(2, "init")))); } 4.. => { cmds.spawn(MyComponent(assets.add(MyAsset(3, "init")))); } }; *run_count += 1; } #[track_caller] fn assert_counter(app: &App, assert: Counter) { assert_eq!(&assert, app.world().resource::()); } #[test] fn added() { let mut app = App::new(); app.add_plugins((TaskPoolPlugin::default(), AssetPlugin::default())) .init_asset::() .insert_resource(Counter(vec![0, 0, 0, 0])) .add_systems(Update, add_some) .add_systems(Last, count_update.after(AssetEvents)); // First run of the app, `add_systems(Startup…)` runs. app.update(); // run_count == 0 assert_counter(&app, Counter(vec![0, 0, 0, 0])); app.update(); // run_count == 1 assert_counter(&app, Counter(vec![1, 0, 0, 0])); app.update(); // run_count == 2 assert_counter(&app, Counter(vec![1, 0, 0, 0])); app.update(); // run_count == 3 assert_counter(&app, Counter(vec![1, 1, 1, 0])); app.update(); // run_count == 4 assert_counter(&app, Counter(vec![1, 1, 1, 1])); } #[test] fn changed() { let mut app = App::new(); app.add_plugins((TaskPoolPlugin::default(), AssetPlugin::default())) .init_asset::() .insert_resource(Counter(vec![0, 0])) .add_systems( Startup, |mut cmds: Commands, mut assets: ResMut>| { let asset0 = assets.add(MyAsset(0, "init")); let asset1 = assets.add(MyAsset(1, "init")); cmds.spawn(MyComponent(asset0.clone())); cmds.spawn(MyComponent(asset0)); cmds.spawn(MyComponent(asset1.clone())); cmds.spawn(MyComponent(asset1.clone())); cmds.spawn(MyComponent(asset1)); }, ) .add_systems(Update, update_some) .add_systems(Last, count_update.after(AssetEvents)); // First run of the app, `add_systems(Startup…)` runs. app.update(); // run_count == 0 // First run: We count the entities that were added in the `Startup` schedule assert_counter(&app, Counter(vec![2, 3])); // Second run: `update_once` updates the first asset, which is // associated with two entities, so `count_update` picks up two updates app.update(); // run_count == 1 assert_counter(&app, Counter(vec![4, 3])); // Third run: `update_once` doesn't update anything, same values as last app.update(); // run_count == 2 assert_counter(&app, Counter(vec![4, 3])); // Fourth run: We update the two assets (asset 0: 2 entities, asset 1: 3) app.update(); // run_count == 3 assert_counter(&app, Counter(vec![6, 6])); // Fifth run: only update second asset app.update(); // run_count == 4 assert_counter(&app, Counter(vec![6, 9])); // ibid app.update(); // run_count == 5 assert_counter(&app, Counter(vec![6, 12])); } }