diff --git a/crates/bevy_asset/src/asset_changed.rs b/crates/bevy_asset/src/asset_changed.rs new file mode 100644 index 0000000000000..c202a57f1f9f7 --- /dev/null +++ b/crates/bevy_asset/src/asset_changed.rs @@ -0,0 +1,479 @@ +//! 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().map_or(false, |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, Update}; + use bevy_core::TaskPoolPlugin; + 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])); + } +} diff --git a/crates/bevy_asset/src/assets.rs b/crates/bevy_asset/src/assets.rs index 3c33aa43faf91..6e3bf1b5cf51f 100644 --- a/crates/bevy_asset/src/assets.rs +++ b/crates/bevy_asset/src/assets.rs @@ -1,3 +1,4 @@ +use crate::asset_changed::AssetChanges; use crate::{ self as bevy_asset, Asset, AssetEvent, AssetHandleProvider, AssetId, AssetServer, Handle, UntypedHandle, @@ -5,7 +6,7 @@ use crate::{ use alloc::sync::Arc; use bevy_ecs::{ prelude::EventWriter, - system::{Res, ResMut, Resource}, + system::{Res, ResMut, Resource, SystemChangeTick}, }; use bevy_reflect::{Reflect, TypePath}; use bevy_utils::HashMap; @@ -559,7 +560,24 @@ impl Assets { /// A system that applies accumulated asset change events to the [`Events`] resource. /// /// [`Events`]: bevy_ecs::event::Events - pub fn asset_events(mut assets: ResMut, mut events: EventWriter>) { + pub(crate) fn asset_events( + mut assets: ResMut, + mut events: EventWriter>, + asset_changes: Option>>, + ticks: SystemChangeTick, + ) { + use AssetEvent::{Added, LoadedWithDependencies, Modified, Removed}; + + if let Some(mut asset_changes) = asset_changes { + for new_event in &assets.queued_events { + match new_event { + Removed { id } | AssetEvent::Unused { id } => asset_changes.remove(id), + Added { id } | Modified { id } | LoadedWithDependencies { id } => { + asset_changes.insert(*id, ticks.this_run()); + } + }; + } + } events.send_batch(assets.queued_events.drain(..)); } diff --git a/crates/bevy_asset/src/lib.rs b/crates/bevy_asset/src/lib.rs index 21e976179e8e7..afd41d1d18a8f 100644 --- a/crates/bevy_asset/src/lib.rs +++ b/crates/bevy_asset/src/lib.rs @@ -21,7 +21,7 @@ //! Typically, you'll use the [`AssetServer::load`] method to load an asset from disk, which returns a [`Handle`]. //! Note that this method does not attempt to reload the asset if it has already been loaded: as long as at least one handle has not been dropped, //! calling [`AssetServer::load`] on the same path will return the same handle. -//! The handle that's returned can be used to instantiate various [`Component`](bevy_ecs::prelude::Component)s that require asset data to function, +//! The handle that's returned can be used to instantiate various [`Component`]s that require asset data to function, //! which will then be spawned into the world as part of an entity. //! //! To avoid assets "popping" into existence, you may want to check that all of the required assets are loaded before transitioning to a new scene. @@ -146,6 +146,7 @@ )] extern crate alloc; +extern crate core; pub mod io; pub mod meta; @@ -157,6 +158,9 @@ pub mod transformer; /// /// This includes the most common types in this crate, re-exported for your convenience. pub mod prelude { + #[doc(hidden)] + pub use crate::asset_changed::AssetChanged; + #[doc(hidden)] pub use crate::{ Asset, AssetApp, AssetEvent, AssetId, AssetMode, AssetPlugin, AssetServer, Assets, @@ -164,6 +168,7 @@ pub mod prelude { }; } +mod asset_changed; mod assets; mod direct_access_ext; mod event; @@ -203,6 +208,7 @@ use crate::{ }; use alloc::sync::Arc; use bevy_app::{App, Last, Plugin, PreUpdate}; +use bevy_ecs::prelude::Component; use bevy_ecs::{ reflect::AppTypeRegistry, schedule::{IntoSystemConfigs, IntoSystemSetConfigs, SystemSet}, @@ -398,6 +404,15 @@ impl Plugin for AssetPlugin { )] pub trait Asset: VisitAssetDependencies + TypePath + Send + Sync + 'static {} +/// A trait for components that can be used as asset identifiers, e.g. handle wrappers. +pub trait AsAssetId: Component { + /// The underlying asset type. + type Asset: Asset; + + /// Retrieves the asset id from this component. + fn as_asset_id(&self) -> AssetId; +} + /// This trait defines how to visit the dependencies of an asset. /// For example, a 3D model might require both textures and meshes to be loaded. ///