Skip to content

Commit 61130d9

Browse files
committed
Add the AssetChanged query filter
1 parent 90f77be commit 61130d9

File tree

4 files changed

+445
-4
lines changed

4 files changed

+445
-4
lines changed
Lines changed: 398 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,398 @@
1+
//! Filter query to limit iteration over [`Handle`]s which content was updated recently.
2+
use std::marker::PhantomData;
3+
4+
use bevy_ecs::{
5+
archetype::{Archetype, ArchetypeComponentId},
6+
component::{ComponentId, Tick},
7+
prelude::{Changed, Entity, Or, World},
8+
query::{Access, FilteredAccess, QueryItem, ReadFetch, ReadOnlyWorldQuery, WorldQuery},
9+
storage::{Table, TableRow},
10+
world::unsafe_world_cell::UnsafeWorldCell,
11+
};
12+
use bevy_utils::HashMap;
13+
14+
use crate::{asset_changes::AssetChanges, Asset, Handle, HandleId};
15+
16+
#[derive(Clone, Copy)]
17+
struct AssetChangeCheck<'w> {
18+
handle_change_map: &'w HashMap<HandleId, Tick>,
19+
last_run: Tick,
20+
this_run: Tick,
21+
}
22+
impl<'w> AssetChangeCheck<'w> {
23+
fn new<T: Asset>(changes: &'w AssetChanges<T>, last_run: Tick, this_run: Tick) -> Self {
24+
Self {
25+
handle_change_map: &changes.change_ticks,
26+
last_run,
27+
this_run,
28+
}
29+
}
30+
// TODO(perf): some sort of caching? Each check has two levels of indirection,
31+
// which is not optimal.
32+
fn has_changed<T: Asset>(&self, handle: &Handle<T>) -> bool {
33+
if let Some(&handle_change_tick) = self.handle_change_map.get(&handle.id()) {
34+
handle_change_tick.is_newer_than(self.last_run, self.this_run)
35+
} else {
36+
false
37+
}
38+
}
39+
}
40+
41+
/// A shortcut for the commonly used `Or<(Changed<Handle<T>>, AssetChanged<T>)>`
42+
/// query.
43+
///
44+
/// If you want to react to changes to `Handle<T>` component of an entity, you have
45+
/// two cases to worry about:
46+
/// - The `Handle<T>` was changed through a `Query<&mut Handle<T>>`.
47+
/// - The `T` in `Assets<T>` pointed by `Handle<T>` was changed through a
48+
/// `ResMut<Assets<T>>::get_mut`.
49+
///
50+
/// To properly handle both those cases, you need to combine the `Changed` and `AssetChanged`
51+
/// filters. This query type does it for you.
52+
pub type AssetOrHandleChanged<T> = Or<(Changed<Handle<T>>, AssetChanged<T>)>;
53+
54+
/// Query filter to get all entities which `Handle<T>` component underlying asset changed.
55+
///
56+
/// Unlike `Changed<Handle<T>>`, this filter returns entities for which **the underlying `T`
57+
/// asset** was changed. For example, when modifying a `Mesh`, you would do so through the
58+
/// [`Assets<Mesh>::get_mut`] method. This does not change the `Handle<T>` that entities have
59+
/// as component, so `Changed<Handle<T>>` **will not do anything**. While with `AssetChanged<T>`,
60+
/// all entities with the modified mesh will be queried.
61+
///
62+
/// # Notes
63+
/// **performance**: This reads a hashmap once per entity with a `Handle<T>` component, this might
64+
/// result in slowdown, particularly if there are a lot of such entities. Note that if there was no
65+
/// updates, it will simply skip everything.
66+
///
67+
/// **frame delay**: The list of changed assets only gets updated during the
68+
/// [`AssetStage::AssetEvents`] stage. Which means asset changes are not immediately visible to
69+
/// this query. Therefore, asset change detection will have one frame of delay.
70+
///
71+
/// Since oftentimes, it is necessary to check for both handle and asset changes, the
72+
/// [`AssetOrHandleChanged`] query is provided for convenience.
73+
///
74+
/// [`AssetStage::AssetEvents`]: crate::AssetStage::AssetEvents
75+
/// [`Assets<Mesh>::get_mut`]: crate::Assets::get_mut
76+
pub struct AssetChanged<T>(PhantomData<T>);
77+
78+
/// Fetch for [`AssetChanged`].
79+
#[doc(hidden)]
80+
pub struct AssetChangedFetch<'w, T: Asset> {
81+
inner: Option<ReadFetch<'w, Handle<T>>>,
82+
check: AssetChangeCheck<'w>,
83+
}
84+
impl<'w, T: Asset> Clone for AssetChangedFetch<'w, T> {
85+
fn clone(&self) -> Self {
86+
Self {
87+
inner: self.inner,
88+
check: self.check,
89+
}
90+
}
91+
}
92+
93+
/// State for [`AssetChanged`].
94+
#[doc(hidden)]
95+
pub struct AssetChangedState<T: Asset> {
96+
handle_id: ComponentId,
97+
asset_changes_id: ComponentId,
98+
asset_changes_archetype_id: ArchetypeComponentId,
99+
_asset: PhantomData<fn(T)>,
100+
}
101+
102+
// SAFETY: `ROQueryFetch<Self>` is the same as `QueryFetch<Self>`
103+
unsafe impl<T: Asset> WorldQuery for AssetChanged<T> {
104+
type ReadOnly = Self;
105+
type Fetch<'w> = AssetChangedFetch<'w, T>;
106+
type State = AssetChangedState<T>;
107+
108+
type Item<'w> = bool;
109+
110+
fn shrink<'wlong: 'wshort, 'wshort>(item: QueryItem<'wlong, Self>) -> QueryItem<'wshort, Self> {
111+
item
112+
}
113+
114+
fn init_state(world: &mut World) -> AssetChangedState<T> {
115+
let asset_changes_resource_id =
116+
if let Some(id) = world.components().resource_id::<AssetChanges<T>>() {
117+
id
118+
} else {
119+
world.init_resource::<AssetChanges<T>>()
120+
};
121+
let asset_changes_archetype_id = world
122+
.storages()
123+
.resources
124+
.get(asset_changes_resource_id)
125+
.unwrap()
126+
.id();
127+
AssetChangedState {
128+
handle_id: world.init_component::<Handle<T>>(),
129+
asset_changes_id: asset_changes_resource_id,
130+
asset_changes_archetype_id,
131+
_asset: PhantomData,
132+
}
133+
}
134+
135+
fn matches_component_set(
136+
state: &Self::State,
137+
set_contains_id: &impl Fn(ComponentId) -> bool,
138+
) -> bool {
139+
set_contains_id(state.handle_id)
140+
}
141+
unsafe fn init_fetch<'w>(
142+
world: UnsafeWorldCell<'w>,
143+
state: &Self::State,
144+
last_run: Tick,
145+
this_run: Tick,
146+
) -> Self::Fetch<'w> {
147+
let err_msg = || {
148+
panic!(
149+
"AssetChanges<{ty}> was removed, please do not remove AssetChanges<{ty}> when using AssetChanged<{ty}>",
150+
ty = std::any::type_name::<T>()
151+
)
152+
};
153+
let changes: &AssetChanges<_> = world.get_resource().unwrap_or_else(err_msg);
154+
let has_updates = changes.last_change_tick.is_newer_than(last_run, this_run);
155+
AssetChangedFetch {
156+
inner: has_updates
157+
.then(|| <&_>::init_fetch(world, &state.handle_id, last_run, this_run)),
158+
check: AssetChangeCheck::new::<T>(changes, last_run, this_run),
159+
}
160+
}
161+
162+
const IS_DENSE: bool = <&Handle<T>>::IS_DENSE;
163+
164+
// This Fetch filters more than just the archetype.
165+
const IS_ARCHETYPAL: bool = false;
166+
167+
unsafe fn set_table<'w>(fetch: &mut Self::Fetch<'w>, state: &Self::State, table: &'w Table) {
168+
if let Some(inner) = &mut fetch.inner {
169+
<&Handle<T>>::set_table(inner, &state.handle_id, table);
170+
}
171+
}
172+
173+
unsafe fn set_archetype<'w>(
174+
fetch: &mut Self::Fetch<'w>,
175+
state: &Self::State,
176+
archetype: &'w Archetype,
177+
table: &'w Table,
178+
) {
179+
if let Some(inner) = &mut fetch.inner {
180+
<&Handle<T>>::set_archetype(inner, &state.handle_id, archetype, table);
181+
}
182+
}
183+
184+
unsafe fn fetch<'w>(
185+
fetch: &mut Self::Fetch<'w>,
186+
entity: Entity,
187+
table_row: TableRow,
188+
) -> Self::Item<'w> {
189+
fetch.inner.as_mut().map_or(false, |inner| {
190+
let handle = <&Handle<T>>::fetch(inner, entity, table_row);
191+
fetch.check.has_changed(handle)
192+
})
193+
}
194+
195+
#[inline]
196+
unsafe fn filter_fetch(
197+
fetch: &mut Self::Fetch<'_>,
198+
entity: Entity,
199+
table_row: TableRow,
200+
) -> bool {
201+
Self::fetch(fetch, entity, table_row)
202+
}
203+
204+
#[inline]
205+
fn update_component_access(state: &Self::State, access: &mut FilteredAccess<ComponentId>) {
206+
<&Handle<T>>::update_component_access(&state.handle_id, access);
207+
access.add_read(state.asset_changes_id);
208+
}
209+
210+
#[inline]
211+
fn update_archetype_component_access(
212+
state: &Self::State,
213+
archetype: &Archetype,
214+
access: &mut Access<ArchetypeComponentId>,
215+
) {
216+
access.add_read(state.asset_changes_archetype_id);
217+
<&Handle<T>>::update_archetype_component_access(&state.handle_id, archetype, access);
218+
}
219+
}
220+
221+
/// SAFETY: read-only access
222+
unsafe impl<T: Asset> ReadOnlyWorldQuery for AssetChanged<T> {}
223+
224+
#[cfg(test)]
225+
mod tests {
226+
use crate::{AddAsset, Assets};
227+
use bevy_app::{App, AppExit, Startup, Update};
228+
use bevy_ecs::{
229+
entity::Entity,
230+
event::EventWriter,
231+
schedule::IntoSystemConfigs,
232+
system::{Commands, IntoSystem, Local, Query, Res, ResMut, Resource},
233+
};
234+
use bevy_reflect::TypePath;
235+
236+
use super::*;
237+
238+
#[derive(bevy_reflect::TypeUuid, TypePath)]
239+
#[uuid = "44115972-f31b-46e5-be5c-2b9aece6a52f"]
240+
struct MyAsset(u32, &'static str);
241+
242+
fn run_app<Marker>(system: impl IntoSystem<(), (), Marker>) {
243+
let mut app = App::new();
244+
app.add_plugins((bevy_core::FrameCountPlugin, crate::AssetPlugin::default()))
245+
.add_asset::<MyAsset>()
246+
.add_systems(Update, system);
247+
app.run();
248+
}
249+
250+
#[test]
251+
#[should_panic]
252+
fn handle_filter_with_other_query_panics() {
253+
fn compatible_filter(
254+
_query1: Query<&mut Handle<MyAsset>>,
255+
_query2: Query<Entity, AssetChanged<MyAsset>>,
256+
) {
257+
println!(
258+
"this line should not run, as &mut Handle<MyAsset> conflicts with AssetChanged<MyAsset>",
259+
);
260+
}
261+
run_app(compatible_filter);
262+
}
263+
264+
#[test]
265+
#[should_panic]
266+
fn handle_query_pos_panics() {
267+
fn incompatible_query(_query: Query<(AssetChanged<MyAsset>, &mut Handle<MyAsset>)>) {
268+
println!(
269+
"this line should not run, as &mut Handle<MyAsset> conflicts with AssetChanged<MyAsset>",
270+
);
271+
}
272+
run_app(incompatible_query);
273+
}
274+
275+
#[test]
276+
#[should_panic]
277+
fn handle_other_query_panics() {
278+
fn incompatible_params(
279+
_query1: Query<&mut Handle<MyAsset>>,
280+
_query2: Query<AssetChanged<MyAsset>>,
281+
) {
282+
println!(
283+
"this line should not run, as AssetChanged<MyAsset> conflicts with &mut Handle<MyAsset>",
284+
);
285+
}
286+
run_app(incompatible_params);
287+
}
288+
289+
// According to a comment in QueryState::new in bevy_ecs, components on filter
290+
// position shouldn't conflict with components on query position.
291+
#[test]
292+
fn handle_filter_pos_ok() {
293+
fn compatible_filter(
294+
_query: Query<&mut Handle<MyAsset>, AssetChanged<MyAsset>>,
295+
mut exit: EventWriter<AppExit>,
296+
) {
297+
exit.send(AppExit);
298+
}
299+
run_app(compatible_filter);
300+
}
301+
302+
#[test]
303+
fn asset_change() {
304+
#[derive(Default, Resource)]
305+
struct Counter(u32, u32);
306+
307+
let mut app = App::new();
308+
309+
fn count_update(
310+
mut counter: ResMut<Counter>,
311+
assets: Res<Assets<MyAsset>>,
312+
query: Query<&Handle<MyAsset>, AssetChanged<MyAsset>>,
313+
) {
314+
for handle in query.iter() {
315+
let asset = assets.get(handle).unwrap();
316+
let to_update = match asset.0 {
317+
0 => &mut counter.0,
318+
1 => &mut counter.1,
319+
_ => continue,
320+
};
321+
*to_update += 1;
322+
}
323+
}
324+
325+
fn update_once(mut assets: ResMut<Assets<MyAsset>>, mut run_count: Local<u32>) {
326+
let mut update_index = |i| {
327+
let handle = assets
328+
.iter()
329+
.find_map(|(h, a)| (a.0 == i).then_some(h))
330+
.unwrap();
331+
let handle = assets.get_handle(handle);
332+
let mut asset = assets.get_mut(&handle).unwrap();
333+
asset.1 = "new_value";
334+
};
335+
match *run_count {
336+
2 => update_index(0),
337+
3.. => update_index(1),
338+
_ => {}
339+
};
340+
*run_count += 1;
341+
}
342+
app.add_plugins((bevy_core::FrameCountPlugin, crate::AssetPlugin::default()))
343+
.add_asset::<MyAsset>()
344+
.init_resource::<Counter>()
345+
.add_systems(
346+
Startup,
347+
|mut cmds: Commands, mut assets: ResMut<Assets<MyAsset>>| {
348+
let asset0 = assets.add(MyAsset(0, "init"));
349+
let asset1 = assets.add(MyAsset(1, "init"));
350+
cmds.spawn(asset0.clone());
351+
cmds.spawn(asset0);
352+
cmds.spawn(asset1.clone());
353+
cmds.spawn(asset1.clone());
354+
cmds.spawn(asset1);
355+
},
356+
)
357+
.add_systems(Update, (count_update, update_once).chain());
358+
let assert_counter = |app: &App, assert: Counter| {
359+
let counter = app.world.get_resource::<Counter>().unwrap();
360+
assert_eq!(counter.0, assert.0);
361+
assert_eq!(counter.1, assert.1);
362+
};
363+
// First run of the app, `add_systems(Startup…)` runs.
364+
app.update(); // run_count == 0
365+
366+
// This might be considered a bug, since it asserts a frame delay. So if you happen
367+
// to break test on the following line, consider removing it.
368+
assert_counter(&app, Counter(0, 0));
369+
370+
// Second run, the asset events are emitted in asset_event_system, and the
371+
// AssetChanges res is updated as well.
372+
// This means, our asset_change_counter are updated.
373+
app.update(); // run_count == 1
374+
assert_counter(&app, Counter(2, 3));
375+
376+
// Third run: `run_count` in `update_once` enables it to update
377+
// the `MyAsset` with value 0. However, since last frame no assets
378+
// were updated, asset_change_counter stays the same
379+
app.update(); // run_count == 2
380+
assert_counter(&app, Counter(2, 3));
381+
382+
// Now that `update_once` and asset_event_system ran, the `MyAsset`
383+
// with value 1 got updated. There are two handles linking to it,
384+
// so that does 4 updates total.
385+
// This cycle, update_once updates the assets of index 1, therefore,
386+
// next cycle, we will detect them
387+
app.update(); // run_count == 3
388+
assert_counter(&app, Counter(4, 3));
389+
390+
// Last cycle we updated the asset with 3 associated entities, so
391+
// we got 3 new changes to 1
392+
app.update(); // run_count == 4
393+
assert_counter(&app, Counter(4, 6));
394+
// ibid
395+
app.update(); // run_count == 5
396+
assert_counter(&app, Counter(4, 9));
397+
}
398+
}

0 commit comments

Comments
 (0)