Skip to content

Conversation

@MatrixDev
Copy link

Objective

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?
  • 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:
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.

@github-actions
Copy link
Contributor

github-actions bot commented Jan 9, 2026

Welcome, new contributor!

Please make sure you've read our contributing guide and we look forward to reviewing your pull request shortly ✨

@alice-i-cecile alice-i-cecile added C-Feature A new feature, making something new possible A-Assets Load files from disk to use for things like images, models, and sounds C-Performance A change motivated by improving speed, memory usage or compile times M-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Jan 9, 2026
@MatrixDev MatrixDev force-pushed the feature/16751-assets-get_mut-change-tracking branch from f5c7534 to 031302f Compare January 9, 2026 19:25
@MatrixDev
Copy link
Author

A little more details on the test app. As mentioned in the original issue, I've noticed this slowdown in the debug mode and above values are also for the debug mode. But it doesn't take much to have similar slowdown also in the release mode as well.

Debug mode, 441 object (21x21 grid):

  • with change: ~100 fps
  • without change: ~15 fps

Release mode, 1 681 object (41x41 grid):

  • with change: ~120 fps
  • without change: ~30 fps

This PR doesn't actually fix the slowdown itself (which is caused by a huge amount of material extractions) but only provides some tools to avoid it.

Copy link
Contributor

@HugoPeters1024 HugoPeters1024 left a comment

Choose a reason for hiding this comment

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

Thanks for your contribution! I think this change makes sense and your implementation is quite reasonable. I did want to point out 2 things though.

You mentioned that you weren't really sure how to add a test for this, but you then actually go on to describe a pretty good way to test this new functionality! Perhaps you can check out how_to_test_systems.rs to get some inspiration on how to write a test for it. While the implementation is quite simple and almost certainly correct, I think a test is still very valuable to make sure things keep working going forward.

Secondly, your approached reminds me a lot of ResMut, which is good because it serves a similar goal. But I did notice that ResMut relies on the Ticks struct for change detection. From what I understand this tracks the last tick that a certain change happened. Because this appears to be a general change detection mechanism, I wonder if it makes sense to re-use it here. That said, I'm not very familiar with that part of the codebase but it's something to consider.

Thanks again for the contribution and congratulations on very first Bevy PR! I hope it's the first of many :)

@MatrixDev
Copy link
Author

@HugoPeters1024 , thanks for your comments.

Test

I've added a test for the AssetMut change detection.

Potentially I can also move slightly modified test from my repo to Stress Tests folder.
Will need to add "enable change detection" toggle in such case to better see if there is some effect.

Ticks approach

I also looked at change detection for Mut/ResMut for inspiration but they work a little different. Both Component (Mut) and Resource (ResMut) are managed directly by the World object. Mut and ResMut are SystemParams, so they have access to Ticks.

Asset on the other hand is just a part of the Assets resource. It cannot be used as a SystemParam and doesn't have access to Ticks. Also, unlike Mut/ResMut, they rely on the events like AssetEvent::Modified for other code to work.

Maybe there is a way to make them use Ticks and DetectChangesMut but it is not straightforward and will require much bigger refactoring.

@MatrixDev MatrixDev force-pushed the feature/16751-assets-get_mut-change-tracking branch from 4a39b8f to 412b115 Compare January 10, 2026 15:11
@MatrixDev
Copy link
Author

MatrixDev commented Jan 11, 2026

@HugoPeters1024, thanks for your review.

  • test is updated to use Resource instead of the arc/atomics. also field names should be more clear now. it did simplify the test a little.
  • not really sure about scheduling and system dependencies because messages become available in the MessageReader only when Messages::update is called. both Update and PostUpdate don't work here (messages are avaialble only on the second frame) but Last did, so I settled with it. I'm not really convinced it is a good solution, ideally we should add dependencies to message_update_system but I don't really know how to do it here. double app.update() was much simpler and more resistant to scheduler changes while still being true to the test.

Copy link
Contributor

@HugoPeters1024 HugoPeters1024 left a comment

Choose a reason for hiding this comment

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

Thanks for incorporating the feedback, this LGTM! (Make sure you check out any CI failures though)

@MatrixDev
Copy link
Author

@HugoPeters1024, thanks for your approval. I've also resolved all of the CI errors that you've mentioned.

what should be my next steps for this issue?

@HugoPeters1024
Copy link
Contributor

HugoPeters1024 commented Jan 13, 2026

what should be my next steps for this issue?

Now we wait for a maintainer to label this as "ready for final review", if all is well it will be merged when deemed convenient. When there are breaking changes like this it might happen that the PR is deferred to the next release, as we are currently quite close to v0.18.

So for now just keep an eye out if there is any followup review :). If nothing happens in the next week or 2 feel free to reach out on the discord.

Copy link
Contributor

@andriyDev andriyDev left a comment

Choose a reason for hiding this comment

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

I don't feel strongly about this change. Users can work around it fairly easily by just using get() and then upgrading to get_mut if they realize they need to mutate. Yes there's a perf cost, but it's probably not that big a deal.

Practically with assets-as-entities we'll get this exact thing for free, since we'll likely just use change detection like normal components. So I think it's fine to do this early.

asset_modified_counter: u32,
}

let mut app = App::new();
Copy link
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
Author

Choose a reason for hiding this comment

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

done

Comment on lines 774 to 794
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
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
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.

// is best to cache the handle somewhere, as having multiple materials
// that are identical is expensive
let mut new_material = material.clone();
let mut new_material = material.into_inner_untracked().clone();
Copy link
Contributor

Choose a reason for hiding this comment

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

This doesn't seem like we should be using into_inner_untracked here. It seems more like we should stop using get_mut at all for this material.

Copy link
Author

Choose a reason for hiding this comment

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

you're correct, it looks like get_mut here was redundant.

@MatrixDev MatrixDev force-pushed the feature/16751-assets-get_mut-change-tracking branch from 8ce97a9 to 6c4eed9 Compare January 15, 2026 10:01
@MatrixDev
Copy link
Author

MatrixDev commented Jan 15, 2026

@andriyDev, thanks for your review. All your comments hopefully should be resolved now.

I agree that "assets-as-entities" feature will solve this problem by default and will make this change obsolete but I don't know what is current progress on it nor when it is expected to be released.

I also agree that users have ability to achieve this functionality by themself (I even wrote how to do it in the original issue) but it is a somewhat cumbersome, not user-friendly and has some overhead (even if very minimal). Personally, on my projects, I use exactly the same implementation, just calling get_mut_untracked to get initial asset and get_mut in Drop and it works fine for my usecase.

Anyways, I'd be happy if this can be a part of the assets API itself even if just as a temporary fix until "assets-as-entities" arrive.

@MatrixDev MatrixDev requested a review from andriyDev January 15, 2026 10:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-Assets Load files from disk to use for things like images, models, and sounds C-Feature A new feature, making something new possible C-Performance A change motivated by improving speed, memory usage or compile times D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes M-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide S-Needs-Review Needs reviewer attention (from anyone!) to move forward

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add tools to avoid unnecessary AssetEvent::Modified events that lead to rendering performance costs

4 participants