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