diff --git a/crates/bevy_asset/src/asset_changed.rs b/crates/bevy_asset/src/asset_changed.rs index 555861dd9809f..76b40919b335a 100644 --- a/crates/bevy_asset/src/asset_changed.rs +++ b/crates/bevy_asset/src/asset_changed.rs @@ -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"; }; diff --git a/crates/bevy_asset/src/assets.rs b/crates/bevy_asset/src/assets.rs index c315ca05e35e4..905d70c31007b 100644 --- a/crates/bevy_asset/src/assets.rs +++ b/crates/bevy_asset/src/assets.rs @@ -8,6 +8,7 @@ use bevy_ecs::{ }; use bevy_platform::collections::HashMap; use bevy_reflect::{Reflect, TypePath}; +use core::ops::{Deref, DerefMut}; use core::{any::TypeId, iter::Enumerate, marker::PhantomData, sync::atomic::AtomicU32}; use crossbeam_channel::{Receiver, Sender}; use serde::{Deserialize, Serialize}; @@ -348,7 +349,7 @@ impl Assets { &mut self, id: impl Into>, insert_fn: impl FnOnce() -> A, - ) -> Result<&mut A, InvalidGenerationError> { + ) -> Result, InvalidGenerationError> { let id: AssetId = id.into(); if self.get(id).is_none() { self.insert(id, insert_fn())?; @@ -436,16 +437,20 @@ impl Assets { /// Retrieves a mutable reference to the [`Asset`] with the given `id`, if it exists. /// Note that this supports anything that implements `Into>`, which includes [`Handle`] and [`AssetId`]. #[inline] - pub fn get_mut(&mut self, id: impl Into>) -> Option<&mut A> { + pub fn get_mut(&mut self, id: impl Into>) -> Option> { let id: AssetId = 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. @@ -618,6 +623,73 @@ impl Assets { } } +/// 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 + } + + /// Manually bypasses change detection, allowing you to mutate the underlying value + /// without emitting [`AssetEvent::Modified`] event. + /// + /// # Warning + /// This is a risky operation, that can have unexpected consequences on any system relying on this code. + /// However, it can be an essential escape hatch when, for example, + /// you are trying to synchronize representations using change detection and need to avoid infinite recursion. + pub fn bypass_change_detection(&mut self) -> &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, + queued_events: &'a mut Vec>, +} + +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>, @@ -673,7 +745,10 @@ pub enum InvalidGenerationError { #[cfg(test)] mod test { - use crate::AssetIndex; + use crate::tests::create_app; + use crate::{Asset, AssetApp, AssetEvent, AssetIndex, Assets}; + use bevy_ecs::prelude::Messages; + use bevy_reflect::TypePath; #[test] fn asset_index_round_trip() { @@ -684,4 +759,64 @@ mod test { let roundtripped = AssetIndex::from_bits(asset_index.to_bits()); assert_eq!(asset_index, roundtripped); } + + #[test] + fn assets_mut_change_detection() { + #[derive(Asset, TypePath, Default)] + struct TestAsset { + value: u32, + } + + let mut app = create_app().0; + app.init_asset::(); + + let mut assets = app.world_mut().resource_mut::>(); + let my_asset_handle = assets.add(TestAsset::default()); + let my_asset_id = my_asset_handle.id(); + + // check a few times just in case there are some unexpected leftover events from previous runs + for _ in 0..3 { + // check that modifying the asset value triggers an event + { + let mut assets = app.world_mut().resource_mut::>(); + let mut asset = assets.get_mut(my_asset_id).unwrap(); + asset.value += 1; + } + + app.update(); + + let modified_count = app + .world_mut() + .resource_mut::>>() + .drain() + .filter(|event| event.is_modified(my_asset_id)) + .count(); + + assert_eq!( + modified_count, 1, + "Asset value was changed but AssetEvent::Modified was not triggered", + ); + + // check that reading the asset value doesn't trigger an event + { + let mut assets = app.world_mut().resource_mut::>(); + let asset = assets.get_mut(my_asset_id).unwrap(); + let _temp = asset.value; + } + + app.update(); + + let modified_count = app + .world_mut() + .resource_mut::>>() + .drain() + .filter(|event| event.is_modified(my_asset_id)) + .count(); + + assert_eq!( + modified_count, 0, + "Asset value was not changed but AssetEvent::Modified was triggered", + ); + } + } } diff --git a/crates/bevy_asset/src/lib.rs b/crates/bevy_asset/src/lib.rs index 949a0c7529ac4..3cc1d663d01ea 100644 --- a/crates/bevy_asset/src/lib.rs +++ b/crates/bevy_asset/src/lib.rs @@ -1229,7 +1229,7 @@ mod tests { { let mut texts = app.world_mut().resource_mut::>(); - let a = texts.get_mut(a_id).unwrap(); + let mut a = texts.get_mut(a_id).unwrap(); a.text = "Changed".to_string(); } diff --git a/crates/bevy_asset/src/reflect.rs b/crates/bevy_asset/src/reflect.rs index 2fc18d69bdfa1..c61b45ca1b85f 100644 --- a/crates/bevy_asset/src/reflect.rs +++ b/crates/bevy_asset/src/reflect.rs @@ -164,7 +164,7 @@ impl FromType for ReflectAsset { #[expect(unsafe_code, reason = "Uses `UnsafeWorldCell::get_resource_mut()`.")] let assets = unsafe { world.get_resource_mut::>().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::>(); diff --git a/crates/bevy_dev_tools/src/frame_time_graph/mod.rs b/crates/bevy_dev_tools/src/frame_time_graph/mod.rs index 8412b186531d9..6a1870bac8e8b 100644 --- a/crates/bevy_dev_tools/src/frame_time_graph/mod.rs +++ b/crates/bevy_dev_tools/src/frame_time_graph/mod.rs @@ -108,7 +108,7 @@ fn update_frame_time_values( .map(|x| *x as f32 / 1000.0) .collect::>(); 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()); } diff --git a/crates/bevy_feathers/src/controls/color_plane.rs b/crates/bevy_feathers/src/controls/color_plane.rs index c0398fe8b47cb..5f1353f74924d 100644 --- a/crates/bevy_feathers/src/controls/color_plane.rs +++ b/crates/bevy_feathers/src/controls/color_plane.rs @@ -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; diff --git a/crates/bevy_gizmos/src/lib.rs b/crates/bevy_gizmos/src/lib.rs index a693c43d26e80..4efdc2612ae85 100755 --- a/crates/bevy_gizmos/src/lib.rs +++ b/crates/bevy_gizmos/src/lib.rs @@ -274,7 +274,7 @@ fn update_gizmo_meshes( handles.handles.insert(TypeId::of::(), None); } else if let Some(handle) = handles.handles.get_mut(&TypeId::of::()) { 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); diff --git a/crates/bevy_pbr/src/wireframe.rs b/crates/bevy_pbr/src/wireframe.rs index c4b19df63f09f..849b5f3cb026e 100644 --- a/crates/bevy_pbr/src/wireframe.rs +++ b/crates/bevy_pbr/src/wireframe.rs @@ -582,7 +582,7 @@ fn global_color_changed( mut materials: ResMut>, global_material: Res, ) { - 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; } } diff --git a/crates/bevy_sprite/src/text2d.rs b/crates/bevy_sprite/src/text2d.rs index 6544b021f8937..26a72f80c89a8 100644 --- a/crates/bevy_sprite/src/text2d.rs +++ b/crates/bevy_sprite/src/text2d.rs @@ -408,9 +408,9 @@ mod tests { let mut fonts = world.resource_mut::>(); - let font = fonts.get_mut(bevy_asset::AssetId::default()).unwrap(); + let mut font = fonts.get_mut(bevy_asset::AssetId::default()).unwrap(); font.family_name = "Fira Mono".into(); - let data = font.data.as_ref().clone(); + let data = font.into_inner().data.as_ref().clone(); app.world_mut() .resource_mut::() diff --git a/crates/bevy_sprite_render/src/mesh2d/wireframe2d.rs b/crates/bevy_sprite_render/src/mesh2d/wireframe2d.rs index 5f9a0a0e6e54d..da5f64c1655fd 100644 --- a/crates/bevy_sprite_render/src/mesh2d/wireframe2d.rs +++ b/crates/bevy_sprite_render/src/mesh2d/wireframe2d.rs @@ -571,7 +571,7 @@ fn global_color_changed( mut materials: ResMut>, global_material: Res, ) { - 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; } } diff --git a/crates/bevy_sprite_render/src/tilemap_chunk/mod.rs b/crates/bevy_sprite_render/src/tilemap_chunk/mod.rs index 6832b6b5f9bb5..10b69cd3ca2e1 100644 --- a/crates/bevy_sprite_render/src/tilemap_chunk/mod.rs +++ b/crates/bevy_sprite_render/src/tilemap_chunk/mod.rs @@ -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 diff --git a/crates/bevy_text/src/font.rs b/crates/bevy_text/src/font.rs index fdfb100da7f95..f700f256ec618 100644 --- a/crates/bevy_text/src/font.rs +++ b/crates/bevy_text/src/font.rs @@ -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 diff --git a/crates/bevy_text/src/font_atlas.rs b/crates/bevy_text/src/font_atlas.rs index 1e2717106b224..bcac49ac77624 100644 --- a/crates/bevy_text/src/font_atlas.rs +++ b/crates/bevy_text/src/font_atlas.rs @@ -87,17 +87,18 @@ 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) - { + if let Ok(glyph_index) = self.dynamic_texture_atlas_builder.add_texture( + &mut atlas_layout, + texture, + &mut atlas_texture, + ) { self.glyph_to_atlas_index.insert( cache_key, GlyphAtlasLocation { diff --git a/examples/2d/cpu_draw.rs b/examples/2d/cpu_draw.rs index 2baf7d05b91a6..1d410b5443b19 100644 --- a/examples/2d/cpu_draw.rs +++ b/examples/2d/cpu_draw.rs @@ -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. diff --git a/examples/2d/texture_atlas.rs b/examples/2d/texture_atlas.rs index 456ed9107ff70..8dccfb82cc85b 100644 --- a/examples/2d/texture_atlas.rs +++ b/examples/2d/texture_atlas.rs @@ -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) diff --git a/examples/3d/animated_material.rs b/examples/3d/animated_material.rs index d83f48d714aa1..820b18365a434 100644 --- a/examples/3d/animated_material.rs +++ b/examples/3d/animated_material.rs @@ -50,7 +50,7 @@ fn animate_materials( mut materials: ResMut>, ) { 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); diff --git a/examples/3d/blend_modes.rs b/examples/3d/blend_modes.rs index 9a5fc5212b527..bd14c9cf1ca58 100644 --- a/examples/3d/blend_modes.rs +++ b/examples/3d/blend_modes.rs @@ -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 { diff --git a/examples/3d/generate_custom_mesh.rs b/examples/3d/generate_custom_mesh.rs index 7b4219effbd61..bdc3d250a07b4 100644 --- a/examples/3d/generate_custom_mesh.rs +++ b/examples/3d/generate_custom_mesh.rs @@ -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 { diff --git a/examples/3d/reflection_probes.rs b/examples/3d/reflection_probes.rs index 22753d1050139..edbfc2d4ece74 100644 --- a/examples/3d/reflection_probes.rs +++ b/examples/3d/reflection_probes.rs @@ -367,7 +367,7 @@ impl FromWorld for Cubemaps { } fn setup_environment_map_usage(cubemaps: Res, mut images: ResMut>) { - 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 @@ -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; } } diff --git a/examples/3d/skybox.rs b/examples/3d/skybox.rs index 0cfe1cec93607..408c1fabff7f2 100644 --- a/examples/3d/skybox.rs +++ b/examples/3d/skybox.rs @@ -153,12 +153,13 @@ fn asset_loaded( ) { if !cubemap.is_loaded && asset_server.load_state(&cubemap.image_handle).is_loaded() { info!("Swapping to {}...", CUBEMAPS[cubemap.index].0); - let image = images.get_mut(&cubemap.image_handle).unwrap(); + let mut image = images.get_mut(&cubemap.image_handle).unwrap(); // NOTE: PNGs do not have any metadata that could indicate they contain a cubemap texture, // so they appear as one texture. The following code reconfigures the texture as necessary. if image.texture_descriptor.array_layer_count() == 1 { + let layers = image.height() / image.width(); image - .reinterpret_stacked_2d_as_array(image.height() / image.width()) + .reinterpret_stacked_2d_as_array(layers) .expect("asset should be 2d texture and height will always be evenly divisible with the given layers"); image.texture_view_descriptor = Some(TextureViewDescriptor { dimension: Some(TextureViewDimension::Cube), diff --git a/examples/3d/solari.rs b/examples/3d/solari.rs index 4f74e33eceb0e..477522a6b89d1 100644 --- a/examples/3d/solari.rs +++ b/examples/3d/solari.rs @@ -390,7 +390,7 @@ fn add_raytracing_meshes_on_scene_load( .insert(RaytracingMesh3d(mesh_handle.clone())); // Ensure meshes are Solari compatible - let mesh = meshes.get_mut(mesh_handle).unwrap(); + let mut mesh = meshes.get_mut(mesh_handle).unwrap(); if !mesh.contains_attribute(Mesh::ATTRIBUTE_UV_0) { let vertex_count = mesh.count_vertices(); mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, vec![[0.0, 0.0]; vertex_count]); @@ -413,11 +413,11 @@ fn add_raytracing_meshes_on_scene_load( // Adjust scene materials to better demo Solari features if material_name.map(|s| s.0.as_str()) == Some("material") { - let material = materials.get_mut(material_handle).unwrap(); + let mut material = materials.get_mut(material_handle).unwrap(); material.emissive = LinearRgba::BLACK; } if material_name.map(|s| s.0.as_str()) == Some("Lights") { - let material = materials.get_mut(material_handle).unwrap(); + let mut material = materials.get_mut(material_handle).unwrap(); material.emissive = LinearRgba::from(Color::srgb(0.941, 0.714, 0.043)) * 1_000_000.0; material.alpha_mode = AlphaMode::Opaque; @@ -426,7 +426,7 @@ fn add_raytracing_meshes_on_scene_load( commands.insert_resource(RobotLightMaterial(material_handle.clone())); } if material_name.map(|s| s.0.as_str()) == Some("Glass_Dark_01") { - let material = materials.get_mut(material_handle).unwrap(); + let mut material = materials.get_mut(material_handle).unwrap(); material.alpha_mode = AlphaMode::Opaque; material.specular_transmission = 0.0; } @@ -473,7 +473,7 @@ fn toggle_lights( if key_input.just_pressed(KeyCode::Digit2) && let Some(robot_light_material) = robot_light_material { - let material = materials.get_mut(&robot_light_material.0).unwrap(); + let mut material = materials.get_mut(&robot_light_material.0).unwrap(); if material.emissive == LinearRgba::BLACK { material.emissive = LinearRgba::from(Color::srgb(0.941, 0.714, 0.043)) * 1_000_000.0; } else { diff --git a/examples/3d/specular_tint.rs b/examples/3d/specular_tint.rs index 9712500081752..3ee0bdb58f9a2 100644 --- a/examples/3d/specular_tint.rs +++ b/examples/3d/specular_tint.rs @@ -153,7 +153,7 @@ fn shift_hue( app_status.hue += HUE_SHIFT_SPEED; for material_handle in objects_with_materials.iter() { - let Some(material) = standard_materials.get_mut(material_handle) else { + let Some(mut material) = standard_materials.get_mut(material_handle) else { continue; }; material.specular_tint = Color::hsva(app_status.hue, 1.0, 1.0, 1.0); @@ -192,7 +192,7 @@ fn toggle_specular_map( }; for material_handle in objects_with_materials.iter() { - let Some(material) = standard_materials.get_mut(material_handle) else { + let Some(mut material) = standard_materials.get_mut(material_handle) else { continue; }; diff --git a/examples/3d/tonemapping.rs b/examples/3d/tonemapping.rs index 5d8b19e4367e1..aa7e58a03a9d8 100644 --- a/examples/3d/tonemapping.rs +++ b/examples/3d/tonemapping.rs @@ -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 diff --git a/examples/3d/transmission.rs b/examples/3d/transmission.rs index cd9754b70e3b3..b6ccee6a35bf3 100644 --- a/examples/3d/transmission.rs +++ b/examples/3d/transmission.rs @@ -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; diff --git a/examples/animation/animated_mesh_events.rs b/examples/animation/animated_mesh_events.rs index 03c38ed22ad9d..bb655b62fc6d4 100644 --- a/examples/animation/animated_mesh_events.rs +++ b/examples/animation/animated_mesh_events.rs @@ -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 { diff --git a/examples/animation/animation_masks.rs b/examples/animation/animation_masks.rs index d357850a6319d..17acfb4b42898 100644 --- a/examples/animation/animation_masks.rs +++ b/examples/animation/animation_masks.rs @@ -433,7 +433,7 @@ fn handle_button_toggles( // iterate just for clarity's sake.) for animation_graph_handle in animation_players.iter_mut() { // The animation graph needs to have loaded. - let Some(animation_graph) = animation_graphs.get_mut(animation_graph_handle) else { + let Some(mut animation_graph) = animation_graphs.get_mut(animation_graph_handle) else { continue; }; diff --git a/examples/app/headless_renderer.rs b/examples/app/headless_renderer.rs index 2ab79e7660a98..41781e92f1ee7 100644 --- a/examples/app/headless_renderer.rs +++ b/examples/app/headless_renderer.rs @@ -487,7 +487,7 @@ fn update( if !image_data.is_empty() { for image in images_to_save.iter() { // Fill correct data from channel to image - let img_bytes = images.get_mut(image.id()).unwrap(); + let mut img_bytes = images.get_mut(image.id()).unwrap(); // We need to ensure that this works regardless of the image dimensions // If the image became wider when copying from the texture to the buffer, diff --git a/examples/asset/alter_mesh.rs b/examples/asset/alter_mesh.rs index d62178e10cbc5..b96ec8ce55bd9 100644 --- a/examples/asset/alter_mesh.rs +++ b/examples/asset/alter_mesh.rs @@ -174,7 +174,7 @@ fn alter_mesh( mut meshes: ResMut>, ) { // Obtain a mutable reference to the Mesh asset. - let Some(mesh) = meshes.get_mut(*left_shape) else { + let Some(mut mesh) = meshes.get_mut(*left_shape) else { return; }; diff --git a/examples/asset/alter_sprite.rs b/examples/asset/alter_sprite.rs index db596ed73ca1a..e489af12c85dd 100644 --- a/examples/asset/alter_sprite.rs +++ b/examples/asset/alter_sprite.rs @@ -123,7 +123,7 @@ fn alter_handle( fn alter_asset(mut images: ResMut>, left_bird: Single<&Sprite, With>) { // Obtain a mutable reference to the Image asset. - let Some(image) = images.get_mut(&left_bird.image) else { + let Some(mut image) = images.get_mut(&left_bird.image) else { return; }; diff --git a/examples/gltf/edit_material_on_gltf.rs b/examples/gltf/edit_material_on_gltf.rs index 893a20e5813ad..bc3e6ec03698c 100644 --- a/examples/gltf/edit_material_on_gltf.rs +++ b/examples/gltf/edit_material_on_gltf.rs @@ -75,7 +75,7 @@ fn change_material( continue; }; // Get the material of the descendant - let Some(material) = asset_materials.get_mut(id.id()) else { + let Some(material) = asset_materials.get(id.id()) else { continue; }; diff --git a/examples/gltf/query_gltf_primitives.rs b/examples/gltf/query_gltf_primitives.rs index 892d087e2f24a..51d1b6fd7023f 100644 --- a/examples/gltf/query_gltf_primitives.rs +++ b/examples/gltf/query_gltf_primitives.rs @@ -26,7 +26,7 @@ fn find_top_material_and_mesh( for (mat_handle, mesh_handle, name) in mat_query.iter() { // locate a material by material name if name.0 == "Top" { - if let Some(material) = materials.get_mut(mat_handle) { + if let Some(mut material) = materials.get_mut(mat_handle) { if let Color::Hsla(ref mut hsla) = material.base_color { *hsla = hsla.rotate_hue(time.delta_secs() * 100.0); } else { @@ -34,7 +34,7 @@ fn find_top_material_and_mesh( } } - if let Some(mesh) = meshes.get_mut(mesh_handle) + if let Some(mut mesh) = meshes.get_mut(mesh_handle) && let Some(VertexAttributeValues::Float32x3(positions)) = mesh.attribute_mut(Mesh::ATTRIBUTE_POSITION) { diff --git a/examples/large_scenes/bistro/src/main.rs b/examples/large_scenes/bistro/src/main.rs index 304e8a47d74bb..80288a092b724 100644 --- a/examples/large_scenes/bistro/src/main.rs +++ b/examples/large_scenes/bistro/src/main.rs @@ -348,7 +348,7 @@ pub fn proc_scene( for entity in children.iter_descendants(scene_ready.entity) { // Sponza needs flipped normals if let Ok(mat_h) = has_std_mat.get(entity) - && let Some(mat) = materials.get_mut(mat_h) + && let Some(mut mat) = materials.get_mut(mat_h) { mat.flip_normal_map_y = true; match mat.alpha_mode { diff --git a/examples/large_scenes/mipmap_generator/src/lib.rs b/examples/large_scenes/mipmap_generator/src/lib.rs index f57bec192c36f..041ad21271697 100644 --- a/examples/large_scenes/mipmap_generator/src/lib.rs +++ b/examples/large_scenes/mipmap_generator/src/lib.rs @@ -271,7 +271,7 @@ pub fn generate_mipmaps( material_handles.push(*material_h); continue; //There is already a task for this image } - if let Some(image) = images.get_mut(image_h) { + if let Some(mut image) = images.get_mut(image_h) { let mut descriptor = match image.sampler.clone() { ImageSampler::Default => default_sampler.0.clone(), ImageSampler::Descriptor(descriptor) => descriptor, @@ -279,7 +279,7 @@ pub fn generate_mipmaps( descriptor.anisotropy_clamp = settings.anisotropic_filtering; image.sampler = ImageSampler::Descriptor(descriptor); if image.texture_descriptor.mip_level_count == 1 - && check_image_compatible(image).is_ok() + && check_image_compatible(&image).is_ok() { let mut image = image.clone(); let settings = settings.clone(); @@ -313,7 +313,7 @@ pub fn generate_mipmaps( tasks.retain(|image_h, (task, material_handles)| { match future::block_on(future::poll_once(task)) { Some(task_data) => { - if let Some(image) = images.get_mut(image_h) { + if let Some(mut image) = images.get_mut(image_h) { *image = task_data.image; progress.processed += 1; let prev_cached_data_gb = bytes_to_gb(progress.cached_data_size_bytes); diff --git a/examples/shader/shader_material_wesl.rs b/examples/shader/shader_material_wesl.rs index b13d8325401f5..4cade8f42700e 100644 --- a/examples/shader/shader_material_wesl.rs +++ b/examples/shader/shader_material_wesl.rs @@ -77,7 +77,7 @@ fn update( keys: Res>, ) { for (material, mut transform) in query.iter_mut() { - let material = materials.get_mut(material).unwrap(); + let mut material = materials.get_mut(material).unwrap(); material.time.x = time.elapsed_secs(); if keys.just_pressed(KeyCode::Space) { material.party_mode = !material.party_mode; diff --git a/examples/shader/shader_prepass.rs b/examples/shader/shader_prepass.rs index 58b6bf7ed7360..bfbce79d24ece 100644 --- a/examples/shader/shader_prepass.rs +++ b/examples/shader/shader_prepass.rs @@ -233,7 +233,7 @@ fn toggle_prepass_view( color.0 = Color::WHITE; }); - let mat = materials.get_mut(*material_handle).unwrap(); + let mut mat = materials.get_mut(*material_handle).unwrap(); mat.settings.show_depth = (*prepass_view == 1) as u32; mat.settings.show_normals = (*prepass_view == 2) as u32; mat.settings.show_motion_vectors = (*prepass_view == 3) as u32; diff --git a/examples/shader/storage_buffer.rs b/examples/shader/storage_buffer.rs index fc274792dbdf5..cb368021d09ff 100644 --- a/examples/shader/storage_buffer.rs +++ b/examples/shader/storage_buffer.rs @@ -73,7 +73,7 @@ fn update( ) { let material = materials.get_mut(&material_handles.0).unwrap(); - let buffer = buffers.get_mut(&material.colors).unwrap(); + let mut buffer = buffers.get_mut(&material.colors).unwrap(); buffer.set_data( (0..5) .map(|i| { diff --git a/examples/stress_tests/many_materials.rs b/examples/stress_tests/many_materials.rs index 65f5ecc5f545e..64266c5d23d48 100644 --- a/examples/stress_tests/many_materials.rs +++ b/examples/stress_tests/many_materials.rs @@ -95,7 +95,7 @@ fn animate_materials( mut materials: ResMut>, ) { for (i, material_handle) in material_handles.iter().enumerate() { - if let Some(material) = materials.get_mut(material_handle) { + if let Some(mut material) = materials.get_mut(material_handle) { let color = Color::hsl( ((i as f32 * 2.345 + time.elapsed_secs()) * 100.0) % 360.0, 1.0, diff --git a/examples/tools/scene_viewer/scene_viewer_plugin.rs b/examples/tools/scene_viewer/scene_viewer_plugin.rs index 7565307003c66..0c6f6028351bd 100644 --- a/examples/tools/scene_viewer/scene_viewer_plugin.rs +++ b/examples/tools/scene_viewer/scene_viewer_plugin.rs @@ -112,7 +112,7 @@ fn scene_load_check( scene_handle.scene_index ) }); - let scene = scenes.get_mut(gltf_scene_handle).unwrap(); + let mut scene = scenes.get_mut(gltf_scene_handle).unwrap(); let mut query = scene .world diff --git a/examples/ui/ui_material.rs b/examples/ui/ui_material.rs index af6625ae773ec..7ec9fe0c8a298 100644 --- a/examples/ui/ui_material.rs +++ b/examples/ui/ui_material.rs @@ -94,7 +94,7 @@ fn animate( ) { let duration = 2.0; for handle in &q { - if let Some(material) = materials.get_mut(handle) { + if let Some(mut material) = materials.get_mut(handle) { // rainbow color effect let new_color = Color::hsl((time.elapsed_secs() * 60.0) % 360.0, 1., 0.5); let border_color = Color::hsl((time.elapsed_secs() * 60.0) % 360.0, 0.75, 0.75); diff --git a/release-content/migration-guides/asset-mut-change-detection.md b/release-content/migration-guides/asset-mut-change-detection.md new file mode 100644 index 0000000000000..27d8548b92632 --- /dev/null +++ b/release-content/migration-guides/asset-mut-change-detection.md @@ -0,0 +1,34 @@ +--- +title: "Avoiding unnecessary `AssetEvent::Modified` events that lead to rendering performance costs" +pull_requests: [22460] +--- + +`Assets::get_mut` will now return `AssetMut` instead of `&mut Asset`. +Similar to `Mut`/`ResMut`, new implementation will trigger `AssetEvent::Modified` +event only when the asset is actually mutated. + +In some cases (like materials), triggering `AssetEvent::Modified` event might lead to +measurable performance costs. To avoid this, it is now possible to check if the `Asset` +will change before mutating it: + +```rust +fn update( + query: Query>, + materials: ResMut>, + time: Res