Skip to content
Merged
2 changes: 1 addition & 1 deletion crates/bevy_asset/src/asset_changed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -360,7 +360,7 @@ mod tests {
.iter()
.find_map(|(h, a)| (a.0 == i).then_some(h))
.unwrap();
let asset = assets.get_mut(id).unwrap();
let mut asset = assets.get_mut(id).unwrap();
println!("setting new value for {}", asset.0);
asset.1 = "new_value";
};
Expand Down
148 changes: 141 additions & 7 deletions crates/bevy_asset/src/assets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use bevy_reflect::{Reflect, TypePath};
use core::{any::TypeId, iter::Enumerate, marker::PhantomData, sync::atomic::AtomicU32};
use crossbeam_channel::{Receiver, Sender};
use serde::{Deserialize, Serialize};
use std::ops::{Deref, DerefMut};
use thiserror::Error;
use uuid::Uuid;

Expand Down Expand Up @@ -348,7 +349,7 @@ impl<A: Asset> Assets<A> {
&mut self,
id: impl Into<AssetId<A>>,
insert_fn: impl FnOnce() -> A,
) -> Result<&mut A, InvalidGenerationError> {
) -> Result<AssetMut<'_, A>, InvalidGenerationError> {
let id: AssetId<A> = id.into();
if self.get(id).is_none() {
self.insert(id, insert_fn())?;
Expand Down Expand Up @@ -436,16 +437,20 @@ impl<A: Asset> Assets<A> {
/// Retrieves a mutable reference to the [`Asset`] with the given `id`, if it exists.
/// Note that this supports anything that implements `Into<AssetId<A>>`, which includes [`Handle`] and [`AssetId`].
#[inline]
pub fn get_mut(&mut self, id: impl Into<AssetId<A>>) -> Option<&mut A> {
pub fn get_mut(&mut self, id: impl Into<AssetId<A>>) -> Option<AssetMut<'_, A>> {
let id: AssetId<A> = id.into();
let result = match id {
AssetId::Index { index, .. } => self.dense_storage.get_mut(index),
AssetId::Uuid { uuid } => self.hash_map.get_mut(&uuid),
};
if result.is_some() {
self.queued_events.push(AssetEvent::Modified { id });
}
result
Some(AssetMut {
asset: result?,
guard: AssetMutChangeNotifier {
changed: false,
asset_id: id,
queued_events: &mut self.queued_events,
},
})
}

/// Retrieves a mutable reference to the [`Asset`] with the given `id`, if it exists.
Expand Down Expand Up @@ -618,6 +623,62 @@ impl<A: Asset> Assets<A> {
}
}

/// Unique mutable borrow of an asset.
///
/// [AssetEvent::Modified] events will be only triggered if an asset itself is mutably borrowed.
///
/// Just as an example, this allows checking if a material property has changed
/// before modifying it to avoid unnecessary material extraction down the pipeline.
pub struct AssetMut<'a, A: Asset> {
asset: &'a mut A,
guard: AssetMutChangeNotifier<'a, A>,
}

impl<'a, A: Asset> AssetMut<'a, A> {
/// Marks with inner asset as modified and returns reference to it.
pub fn into_inner(mut self) -> &'a mut A {
self.guard.changed = true;
self.asset
}

/// Returns reference to the inner asset but doesn't mark it as modified.
pub fn into_inner_untracked(self) -> &'a mut A {
self.asset
}
}

impl<'a, A: Asset> Deref for AssetMut<'a, A> {
type Target = A;

fn deref(&self) -> &Self::Target {
self.asset
}
}

impl<'a, A: Asset> DerefMut for AssetMut<'a, A> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.guard.changed = true;
self.asset
}
}

/// Helper struct to allow safe destructuring of the [AssetMut::into_inner]
/// while also keeping strong change tracking guarantees.
struct AssetMutChangeNotifier<'a, A: Asset> {
changed: bool,
asset_id: AssetId<A>,
queued_events: &'a mut Vec<AssetEvent<A>>,
}

impl<'a, A: Asset> Drop for AssetMutChangeNotifier<'a, A> {
fn drop(&mut self) {
if self.changed {
self.queued_events
.push(AssetEvent::Modified { id: self.asset_id });
}
}
}

/// A mutable iterator over [`Assets`].
pub struct AssetsMutIterator<'a, A: Asset> {
queued_events: &'a mut Vec<AssetEvent<A>>,
Expand Down Expand Up @@ -673,7 +734,10 @@ pub enum InvalidGenerationError {

#[cfg(test)]
mod test {
use crate::AssetIndex;
use crate::{Asset, AssetApp, AssetEvent, AssetIndex, AssetPlugin, Assets};
use bevy_app::{App, Last, Update};
use bevy_ecs::prelude::*;
use bevy_reflect::TypePath;

#[test]
fn asset_index_round_trip() {
Expand All @@ -684,4 +748,74 @@ mod test {
let roundtripped = AssetIndex::from_bits(asset_index.to_bits());
assert_eq!(asset_index, roundtripped);
}

#[test]
fn assets_mut_change_detection() {
Comment thread
MatrixDev marked this conversation as resolved.
#[derive(Asset, TypePath, Default)]
struct TestAsset {
value: u32,
}

#[derive(Resource, Default)]
struct TestState {
asset_target_value: u32,
asset_modified_counter: u32,
}

let mut app = App::new();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use the create_app function from crates/bevy_asset/src/lib.rs. It prevents you from doing bad stuff accidentally, and hides away some setup code we don't care about.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

app.add_plugins(AssetPlugin::default());
app.init_asset::<TestAsset>();
app.insert_resource(TestState::default());

let mut assets = app.world_mut().resource_mut::<Assets<TestAsset>>();
let my_asset_handle = assets.add(TestAsset::default());
let my_asset_id = my_asset_handle.id();

app.add_systems(
Update,
move |mut assets: ResMut<Assets<TestAsset>>, state: Res<TestState>| {
let mut asset = assets.get_mut(my_asset_id).unwrap();

if asset.value != state.asset_target_value {
asset.value = state.asset_target_value;
}
},
);
app.add_systems(
Last,
move |mut reader: MessageReader<AssetEvent<TestAsset>>,
mut state: ResMut<TestState>| {
for event in reader.read() {
if event.is_modified(my_asset_id) {
state.asset_modified_counter += 1;
}
}
},
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These don't need to be systems for this test. Can we just use app.world_mut().resource::<Assets<TestAsset>>() in the loop and access the asset that way? And then we can just access the Messages<AssetEvent<TestAsset>> resource to drain out any events. I prefer to keep systems out of these tests for simplicity (for example we can delete the TestState resource I think).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done. it is really easier to track what happens without the systems. thanks for suggestion.


// check a few times just in case there are some unexpected leftover events from previous runs
for _ in 0..3 {
let mut state = app.world_mut().resource_mut::<TestState>();
state.asset_target_value += 1;
state.asset_modified_counter = 0;

app.update();

let mut state = app.world_mut().resource_mut::<TestState>();
assert_eq!(
std::mem::take(&mut state.asset_modified_counter),
1,
"Asset value was changed but AssetEvent::Modified was not triggered",
);

app.update();

let mut state = app.world_mut().resource_mut::<TestState>();
assert_eq!(
std::mem::take(&mut state.asset_modified_counter),
0,
"Asset value was not changed but AssetEvent::Modified was triggered",
);
}
}
}
2 changes: 1 addition & 1 deletion crates/bevy_asset/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1229,7 +1229,7 @@ mod tests {

{
let mut texts = app.world_mut().resource_mut::<Assets<CoolText>>();
let a = texts.get_mut(a_id).unwrap();
let mut a = texts.get_mut(a_id).unwrap();
a.text = "Changed".to_string();
}

Expand Down
2 changes: 1 addition & 1 deletion crates/bevy_asset/src/reflect.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ impl<A: Asset + FromReflect> FromType<A> for ReflectAsset {
#[expect(unsafe_code, reason = "Uses `UnsafeWorldCell::get_resource_mut()`.")]
let assets = unsafe { world.get_resource_mut::<Assets<A>>().unwrap().into_inner() };
let asset = assets.get_mut(asset_id.typed_debug_checked());
asset.map(|asset| asset as &mut dyn Reflect)
asset.map(|asset| asset.into_inner() as &mut dyn Reflect)
},
add: |world, value| {
let mut assets = world.resource_mut::<Assets<A>>();
Expand Down
2 changes: 1 addition & 1 deletion crates/bevy_dev_tools/src/frame_time_graph/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ fn update_frame_time_values(
.map(|x| *x as f32 / 1000.0)
.collect::<Vec<_>>();
for (_, material) in frame_time_graph_materials.iter_mut() {
let buffer = buffers.get_mut(&material.values).unwrap();
let mut buffer = buffers.get_mut(&material.values).unwrap();

buffer.set_data(frame_times.clone());
}
Expand Down
2 changes: 1 addition & 1 deletion crates/bevy_feathers/src/controls/color_plane.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ fn update_plane_color(

if let Ok(material_node) = q_material_node.get(*inner_ent) {
// Node component exists, update it
if let Some(material) = r_materials.get_mut(material_node.id()) {
if let Some(mut material) = r_materials.get_mut(material_node.id()) {
// Update properties
material.plane = *plane;
material.fixed_channel = plane_value.0.z;
Expand Down
2 changes: 1 addition & 1 deletion crates/bevy_gizmos/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@ fn update_gizmo_meshes<Config: GizmoConfigGroup>(
handles.handles.insert(TypeId::of::<Config>(), None);
} else if let Some(handle) = handles.handles.get_mut(&TypeId::of::<Config>()) {
if let Some(handle) = handle {
let gizmo = gizmo_assets.get_mut(handle.id()).unwrap();
let mut gizmo = gizmo_assets.get_mut(handle.id()).unwrap();

gizmo.buffer.list_positions = mem::take(&mut storage.list_positions);
gizmo.buffer.list_colors = mem::take(&mut storage.list_colors);
Expand Down
2 changes: 1 addition & 1 deletion crates/bevy_pbr/src/wireframe.rs
Original file line number Diff line number Diff line change
Expand Up @@ -582,7 +582,7 @@ fn global_color_changed(
mut materials: ResMut<Assets<WireframeMaterial>>,
global_material: Res<GlobalWireframeMaterial>,
) {
if let Some(global_material) = materials.get_mut(&global_material.handle) {
if let Some(mut global_material) = materials.get_mut(&global_material.handle) {
global_material.color = config.default_color;
}
}
Expand Down
2 changes: 1 addition & 1 deletion crates/bevy_sprite_render/src/mesh2d/wireframe2d.rs
Original file line number Diff line number Diff line change
Expand Up @@ -571,7 +571,7 @@ fn global_color_changed(
mut materials: ResMut<Assets<Wireframe2dMaterial>>,
global_material: Res<GlobalWireframeMaterial>,
) {
if let Some(global_material) = materials.get_mut(&global_material.handle) {
if let Some(mut global_material) = materials.get_mut(&global_material.handle) {
global_material.color = config.default_color;
}
}
Expand Down
2 changes: 1 addition & 1 deletion crates/bevy_sprite_render/src/tilemap_chunk/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ pub fn update_tilemap_chunk_indices(
);
continue;
};
let Some(tile_data_image) = images.get_mut(&material.tile_data) else {
let Some(mut tile_data_image) = images.get_mut(&material.tile_data) else {
warn!(
"TilemapChunkMaterial tile data image not found for tilemap chunk {}",
chunk_entity
Expand Down
2 changes: 1 addition & 1 deletion crates/bevy_text/src/font.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ pub fn load_font_assets_into_fontdb_system(
let font_system = &mut cosmic_font_system.0;
for event in events.read() {
if let AssetEvent::Added { id } = event
&& let Some(font) = fonts.get_mut(*id)
&& let Some(mut font) = fonts.get_mut(*id)
{
let data = Arc::clone(&font.data);
font.ids = font_system
Expand Down
6 changes: 3 additions & 3 deletions crates/bevy_text/src/font_atlas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,16 +87,16 @@ impl FontAtlas {
texture: &Image,
offset: IVec2,
) -> Result<(), TextError> {
let atlas_layout = atlas_layouts
let mut atlas_layout = atlas_layouts
.get_mut(&self.texture_atlas)
.ok_or(TextError::MissingAtlasLayout)?;
let atlas_texture = textures
let mut atlas_texture = textures
.get_mut(&self.texture)
.ok_or(TextError::MissingAtlasTexture)?;

if let Ok(glyph_index) =
self.dynamic_texture_atlas_builder
.add_texture(atlas_layout, texture, atlas_texture)
.add_texture(&mut atlas_layout, texture, &mut atlas_texture)
{
self.glyph_to_atlas_index.insert(
cache_key,
Expand Down
2 changes: 1 addition & 1 deletion examples/2d/cpu_draw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ fn draw(
}

// Get the image from Bevy's asset storage.
let image = images.get_mut(&my_handle.0).expect("Image not found");
let mut image = images.get_mut(&my_handle.0).expect("Image not found");

// Compute the position of the pixel to draw.

Expand Down
2 changes: 1 addition & 1 deletion examples/2d/texture_atlas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ fn create_texture_atlas(
let texture = textures.add(texture);

// Update the sampling settings of the texture atlas
let image = textures.get_mut(&texture).unwrap();
let mut image = textures.get_mut(&texture).unwrap();
image.sampler = sampling.unwrap_or_default();

(texture_atlas_layout, texture_atlas_sources, texture)
Expand Down
2 changes: 1 addition & 1 deletion examples/3d/animated_material.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ fn animate_materials(
mut materials: ResMut<Assets<StandardMaterial>>,
) {
for material_handle in material_handles.iter() {
if let Some(material) = materials.get_mut(material_handle)
if let Some(mut material) = materials.get_mut(material_handle)
&& let Color::Hsla(ref mut hsla) = material.base_color
{
*hsla = hsla.rotate_hue(time.delta_secs() * 100.0);
Expand Down
2 changes: 1 addition & 1 deletion examples/3d/blend_modes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ fn example_control_system(
let randomize_colors = input.just_pressed(KeyCode::KeyC);

for (material_handle, controls) in &controllable {
let material = materials.get_mut(material_handle).unwrap();
let mut material = materials.get_mut(material_handle).unwrap();

if controls.color && randomize_colors {
material.base_color = Srgba {
Expand Down
2 changes: 1 addition & 1 deletion examples/3d/generate_custom_mesh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ fn input_handler(
if keyboard_input.just_pressed(KeyCode::Space) {
let mesh_handle = mesh_query.single().expect("Query not successful");
let mesh = meshes.get_mut(mesh_handle).unwrap();
toggle_texture(mesh);
toggle_texture(mesh.into_inner());
}
if keyboard_input.pressed(KeyCode::KeyX) {
for mut transform in &mut query {
Expand Down
4 changes: 2 additions & 2 deletions examples/3d/reflection_probes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ impl FromWorld for Cubemaps {
}

fn setup_environment_map_usage(cubemaps: Res<Cubemaps>, mut images: ResMut<Assets<Image>>) {
if let Some(image) = images.get_mut(&cubemaps.specular_environment_map)
if let Some(mut image) = images.get_mut(&cubemaps.specular_environment_map)
&& !image
.texture_descriptor
.usage
Expand Down Expand Up @@ -415,7 +415,7 @@ fn change_sphere_roughness(

// Update the sphere material
for material_handle in sphere_query.iter() {
if let Some(material) = materials.get_mut(&material_handle.0) {
if let Some(mut material) = materials.get_mut(&material_handle.0) {
material.perceptual_roughness = app_status.sphere_roughness;
}
}
Expand Down
2 changes: 1 addition & 1 deletion examples/3d/tonemapping.rs
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ fn drag_drop_image(
};

for mat_h in &image_mat {
if let Some(mat) = materials.get_mut(mat_h) {
if let Some(mut mat) = materials.get_mut(mat_h) {
mat.base_color_texture = Some(new_image.clone());

// Despawn the image viewer instructions
Expand Down
2 changes: 1 addition & 1 deletion examples/3d/transmission.rs
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,7 @@ fn example_control_system(
let randomize_colors = input.just_pressed(KeyCode::KeyC);

for (material_handle, controls) in &controllable {
let material = materials.get_mut(material_handle).unwrap();
let mut material = materials.get_mut(material_handle).unwrap();
if controls.specular_transmission {
material.specular_transmission = state.specular_transmission;
material.thickness = state.thickness;
Expand Down
2 changes: 1 addition & 1 deletion examples/animation/animated_mesh_events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ fn setup_scene_once_loaded(
AnimationNodeType::Clip(handle) => clips.get_mut(handle),
_ => unreachable!(),
};
clip.unwrap()
clip.unwrap().into_inner()
}

for (entity, mut player) in &mut players {
Expand Down
Loading
Loading