Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_asset/src/asset_changed.rs
9401 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
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
};
17
use bevy_platform::collections::HashMap;
18
use core::marker::PhantomData;
19
use disqualified::ShortName;
20
use 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)]
31
pub(crate) struct AssetChanges<A: Asset> {
32
change_ticks: HashMap<AssetId<A>, Tick>,
33
last_change_tick: Tick,
34
}
35
36
impl<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
46
impl<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
55
struct 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
63
impl<A: AsAssetId> Clone for AssetChangeCheck<'_, A> {
64
fn clone(&self) -> Self {
65
*self
66
}
67
}
68
69
impl<A: AsAssetId> Copy for AssetChangeCheck<'_, A> {}
70
71
impl<'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
125
pub struct AssetChanged<A: AsAssetId>(PhantomData<A>);
126
127
/// [`WorldQuery`] fetch for [`AssetChanged`].
128
#[doc(hidden)]
129
pub struct AssetChangedFetch<'w, A: AsAssetId> {
130
inner: Option<ReadFetch<'w, A>>,
131
check: AssetChangeCheck<'w, A>,
132
}
133
134
impl<'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)]
145
pub 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>`
153
unsafe 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
// - `state.resource_id` was obtained from `world.init_resource::<AssetChanges<A::Asset>>()`,
170
// so the untyped pointer returned by `get_resource_by_id` can safely be dereferenced into that type.
171
// - `update_component_access` declares a read on `state.resource_id`, so it is safe to
172
// read that resource here (see trait-level safety comments on `WorldQuery`, regarding
173
// readonly resource access in `init_fetch`)
174
let Some(changes) = (unsafe {
175
world
176
.get_resource_by_id(state.resource_id)
177
.map(|ptr| ptr.deref::<AssetChanges<A::Asset>>())
178
}) else {
179
error!(
180
"AssetChanges<{ty}> resource was removed, please do not remove \
181
AssetChanges<{ty}> when using the AssetChanged<{ty}> world query",
182
ty = ShortName::of::<A>()
183
);
184
185
return AssetChangedFetch {
186
inner: None,
187
check: AssetChangeCheck {
188
change_ticks: None,
189
last_run,
190
this_run,
191
},
192
};
193
};
194
let has_updates = changes.last_change_tick.is_newer_than(last_run, this_run);
195
196
AssetChangedFetch {
197
inner: has_updates.then(||
198
// SAFETY: We delegate to the inner `init_fetch` for `A`
199
unsafe {
200
<&A>::init_fetch(world, &state.asset_id, last_run, this_run)
201
}),
202
check: AssetChangeCheck::new(changes, last_run, this_run),
203
}
204
}
205
206
const IS_DENSE: bool = <&A>::IS_DENSE;
207
208
unsafe fn set_archetype<'w, 's>(
209
fetch: &mut Self::Fetch<'w>,
210
state: &'s Self::State,
211
archetype: &'w Archetype,
212
table: &'w Table,
213
) {
214
if let Some(inner) = &mut fetch.inner {
215
// SAFETY: We delegate to the inner `set_archetype` for `A`
216
unsafe {
217
<&A>::set_archetype(inner, &state.asset_id, archetype, table);
218
}
219
}
220
}
221
222
unsafe fn set_table<'w, 's>(
223
fetch: &mut Self::Fetch<'w>,
224
state: &Self::State,
225
table: &'w Table,
226
) {
227
if let Some(inner) = &mut fetch.inner {
228
// SAFETY: We delegate to the inner `set_table` for `A`
229
unsafe {
230
<&A>::set_table(inner, &state.asset_id, table);
231
}
232
}
233
}
234
235
#[inline]
236
fn update_component_access(state: &Self::State, access: &mut FilteredAccess) {
237
<&A>::update_component_access(&state.asset_id, access);
238
access.add_resource_read(state.resource_id);
239
}
240
241
fn init_state(world: &mut World) -> AssetChangedState<A> {
242
let resource_id = world.init_resource::<AssetChanges<A::Asset>>();
243
let asset_id = world.register_component::<A>();
244
AssetChangedState {
245
asset_id,
246
resource_id,
247
_asset: PhantomData,
248
}
249
}
250
251
fn get_state(components: &Components) -> Option<Self::State> {
252
let resource_id = components.resource_id::<AssetChanges<A::Asset>>()?;
253
let asset_id = components.component_id::<A>()?;
254
Some(AssetChangedState {
255
asset_id,
256
resource_id,
257
_asset: PhantomData,
258
})
259
}
260
261
fn matches_component_set(
262
state: &Self::State,
263
set_contains_id: &impl Fn(ComponentId) -> bool,
264
) -> bool {
265
set_contains_id(state.asset_id)
266
}
267
}
268
269
#[expect(unsafe_code, reason = "QueryFilter is an unsafe trait.")]
270
// SAFETY: read-only access
271
unsafe impl<A: AsAssetId> QueryFilter for AssetChanged<A> {
272
const IS_ARCHETYPAL: bool = false;
273
274
#[inline]
275
unsafe fn filter_fetch(
276
state: &Self::State,
277
fetch: &mut Self::Fetch<'_>,
278
entity: Entity,
279
table_row: TableRow,
280
) -> bool {
281
fetch.inner.as_mut().is_some_and(|inner| {
282
// SAFETY: We delegate to the inner `fetch` for `A`
283
unsafe {
284
let handle = <&A>::fetch(&state.asset_id, inner, entity, table_row);
285
handle.is_some_and(|handle| fetch.check.has_changed(handle))
286
}
287
})
288
}
289
}
290
291
#[cfg(test)]
292
#[expect(clippy::print_stdout, reason = "Allowed in tests.")]
293
mod tests {
294
use crate::tests::create_app;
295
use crate::{AssetEventSystems, Handle};
296
use alloc::{vec, vec::Vec};
297
use core::num::NonZero;
298
use std::println;
299
300
use crate::{AssetApp, Assets};
301
use bevy_app::{App, AppExit, PostUpdate, Startup, Update};
302
use bevy_ecs::schedule::IntoScheduleConfigs;
303
use bevy_ecs::{
304
component::Component,
305
message::MessageWriter,
306
resource::Resource,
307
system::{Commands, IntoSystem, Local, Query, Res, ResMut},
308
};
309
use bevy_reflect::TypePath;
310
311
use super::*;
312
313
#[derive(Asset, TypePath, Debug)]
314
struct MyAsset(usize, &'static str);
315
316
#[derive(Component)]
317
struct MyComponent(Handle<MyAsset>);
318
319
impl AsAssetId for MyComponent {
320
type Asset = MyAsset;
321
322
fn as_asset_id(&self) -> AssetId<Self::Asset> {
323
self.0.id()
324
}
325
}
326
327
fn run_app<Marker>(system: impl IntoSystem<(), (), Marker>) {
328
let mut app = create_app().0;
329
app.init_asset::<MyAsset>().add_systems(Update, system);
330
app.update();
331
}
332
333
// According to a comment in QueryState::new in bevy_ecs, components on filter
334
// position shouldn't conflict with components on query position.
335
#[test]
336
fn handle_filter_pos_ok() {
337
fn compatible_filter(
338
_query: Query<&mut MyComponent, AssetChanged<MyComponent>>,
339
mut exit: MessageWriter<AppExit>,
340
) {
341
exit.write(AppExit::Error(NonZero::<u8>::MIN));
342
}
343
run_app(compatible_filter);
344
}
345
346
#[derive(Default, PartialEq, Debug, Resource)]
347
struct Counter(Vec<u32>);
348
349
fn count_update(
350
mut counter: ResMut<Counter>,
351
assets: Res<Assets<MyAsset>>,
352
query: Query<&MyComponent, AssetChanged<MyComponent>>,
353
) {
354
for handle in query.iter() {
355
let asset = assets.get(&handle.0).unwrap();
356
counter.0[asset.0] += 1;
357
}
358
}
359
360
fn update_some(mut assets: ResMut<Assets<MyAsset>>, mut run_count: Local<u32>) {
361
let mut update_index = |i| {
362
let id = assets
363
.iter()
364
.find_map(|(h, a)| (a.0 == i).then_some(h))
365
.unwrap();
366
let mut asset = assets.get_mut(id).unwrap();
367
println!("setting new value for {}", asset.0);
368
asset.1 = "new_value";
369
};
370
match *run_count {
371
0 | 1 => update_index(0),
372
2 => {}
373
3 => {
374
update_index(0);
375
update_index(1);
376
}
377
4.. => update_index(1),
378
};
379
*run_count += 1;
380
}
381
382
fn add_some(
383
mut assets: ResMut<Assets<MyAsset>>,
384
mut cmds: Commands,
385
mut run_count: Local<u32>,
386
) {
387
match *run_count {
388
1 => {
389
cmds.spawn(MyComponent(assets.add(MyAsset(0, "init"))));
390
}
391
0 | 2 => {}
392
3 => {
393
cmds.spawn(MyComponent(assets.add(MyAsset(1, "init"))));
394
cmds.spawn(MyComponent(assets.add(MyAsset(2, "init"))));
395
}
396
4.. => {
397
cmds.spawn(MyComponent(assets.add(MyAsset(3, "init"))));
398
}
399
};
400
*run_count += 1;
401
}
402
403
#[track_caller]
404
fn assert_counter(app: &App, assert: Counter) {
405
assert_eq!(&assert, app.world().resource::<Counter>());
406
}
407
408
#[test]
409
fn added() {
410
let mut app = create_app().0;
411
412
app.init_asset::<MyAsset>()
413
.insert_resource(Counter(vec![0, 0, 0, 0]))
414
.add_systems(Update, add_some)
415
.add_systems(PostUpdate, count_update.after(AssetEventSystems));
416
417
// First run of the app, `add_systems(Startup…)` runs.
418
app.update(); // run_count == 0
419
assert_counter(&app, Counter(vec![0, 0, 0, 0]));
420
app.update(); // run_count == 1
421
assert_counter(&app, Counter(vec![1, 0, 0, 0]));
422
app.update(); // run_count == 2
423
assert_counter(&app, Counter(vec![1, 0, 0, 0]));
424
app.update(); // run_count == 3
425
assert_counter(&app, Counter(vec![1, 1, 1, 0]));
426
app.update(); // run_count == 4
427
assert_counter(&app, Counter(vec![1, 1, 1, 1]));
428
}
429
430
#[test]
431
fn changed() {
432
let mut app = create_app().0;
433
434
app.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
}
478
479