Add tools to avoid unnecessary AssetEvent::Modified events that lead to rendering performance costs (#16751) (#22460)

# Objective

- Fixes #16751

## Solution

- `Assets::get_mut` now returns a wrapper `AssetMut` type instead of
`&mut impl Asset`.
- `AssetMut` implements `Deref` and `DerefMut`.
- `DerefMut` marks assets as changed.
- when dropped `AssetMut` will add `AssetEvent::Modified` event to a
queue only in case asset was marked as changed.

## Testing

- Did you test these changes? If so, how?
  - No unit tests were added, change is pretty straightforward.
  - Test project: https://github.com/MatrixDev/bevy-feature-16751-test.
    - With change: ~100 fps.
    - Without change: ~15 fps.
- Are there any parts that need more testing?
- I don't really see how this can break anything or add a measurable
overhead.
- `AssetEvent::Modified` will now be sent after the asset was modified
instead of before. It should not affect anything but still worth noting.
- How can other people (reviewers) test your changes? Is there anything
specific they need to know?
- Have a big amount of entities that constantly update their materials.
- Properties of those materials should be animated in a stepped maned
(like changing color every 0.1 seconds).
  - Update material only if value has actually changed:
```rust
if material.base_color != new_color {
    material.base_color = new_color;
}
```
- If relevant, what platforms did you test these changes on, and are
there any important ones you can't test?
  - tested on macos (Mackbook M1)
  - not a platform-specific issue

PS: This is my first PR, so please don’t judge.
This commit is contained in:
Rostyslav Lesovyi
2026-02-10 20:39:37 +02:00
committed by GitHub
parent c062d92353
commit 9fd2637846
40 changed files with 232 additions and 61 deletions
+1 -1
View File
@@ -363,7 +363,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";
};
+142 -7
View File
@@ -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<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())?;
@@ -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.
@@ -618,6 +623,73 @@ 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
}
/// 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<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>>,
@@ -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::<TestAsset>();
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();
// 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::<Assets<TestAsset>>();
let mut asset = assets.get_mut(my_asset_id).unwrap();
asset.value += 1;
}
app.update();
let modified_count = app
.world_mut()
.resource_mut::<Messages<AssetEvent<TestAsset>>>()
.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::<Assets<TestAsset>>();
let asset = assets.get_mut(my_asset_id).unwrap();
let _temp = asset.value;
}
app.update();
let modified_count = app
.world_mut()
.resource_mut::<Messages<AssetEvent<TestAsset>>>()
.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",
);
}
}
}
+1 -1
View File
@@ -1227,7 +1227,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();
}
+1 -1
View File
@@ -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>>();
@@ -114,7 +114,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());
}
@@ -201,7 +201,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;
+1 -1
View File
@@ -287,7 +287,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);
+1 -1
View File
@@ -558,7 +558,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;
}
}
+2 -2
View File
@@ -433,9 +433,9 @@ mod tests {
let mut fonts = world.resource_mut::<Assets<Font>>();
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::<CosmicFontSystem>()
@@ -546,7 +546,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;
}
}
@@ -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
+1 -1
View File
@@ -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
+7 -6
View File
@@ -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 {
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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);
+1 -1
View File
@@ -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 {
+1 -1
View File
@@ -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 {
+2 -2
View File
@@ -371,7 +371,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
@@ -419,7 +419,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;
}
}
+3 -2
View File
@@ -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),
+5 -5
View File
@@ -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 {
+2 -2
View File
@@ -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;
};
+1 -1
View File
@@ -213,7 +213,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
+1 -1
View File
@@ -431,7 +431,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;
+1 -1
View File
@@ -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 {
+1 -1
View File
@@ -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;
};
+1 -1
View File
@@ -470,7 +470,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,
+1 -1
View File
@@ -174,7 +174,7 @@ fn alter_mesh(
mut meshes: ResMut<Assets<Mesh>>,
) {
// 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;
};
+1 -1
View File
@@ -123,7 +123,7 @@ fn alter_handle(
fn alter_asset(mut images: ResMut<Assets<Image>>, left_bird: Single<&Sprite, With<Left>>) {
// 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;
};
+1 -1
View File
@@ -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;
};
+2 -2
View File
@@ -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)
{
+1 -1
View File
@@ -349,7 +349,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 {
@@ -271,7 +271,7 @@ pub fn generate_mipmaps<M: Material + GetImages>(
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<M: Material + GetImages>(
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<M: Material + GetImages>(
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);
+1 -1
View File
@@ -77,7 +77,7 @@ fn update(
keys: Res<ButtonInput<KeyCode>>,
) {
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;
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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| {
+1 -1
View File
@@ -92,7 +92,7 @@ fn animate_materials(
mut materials: ResMut<Assets<StandardMaterial>>,
) {
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,
@@ -132,7 +132,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
+1 -1
View File
@@ -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);
@@ -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<A: Asset>` 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<MeshMaterial3d<StandardMaterial>>,
materials: ResMut<Assets<StandardMaterial>>,
time: Res<Time>,
) {
for material_handle in query.iter_mut() {
// material variable now needs to be marked as mut
let Some(mut material) = materials.get_mut(material_handle) else {
continue;
};
let new_color = compute_new_color(&time);
if material.base_color != new_color {
// material will be marked as changed and extracted down the line
// only if the color has actually changed
material.base_color = new_color;
}
}
}
```