Skip to content

Fix Scene hot reloading.#18358

Merged
alice-i-cecile merged 9 commits intobevyengine:mainfrom
andriyDev:fix-scene-reload
Jul 21, 2025
Merged

Fix Scene hot reloading.#18358
alice-i-cecile merged 9 commits intobevyengine:mainfrom
andriyDev:fix-scene-reload

Conversation

@andriyDev
Copy link
Copy Markdown
Contributor

@andriyDev andriyDev commented Mar 17, 2025

Objective

Solution

  • Refactor all the SceneSpawner functions to include a _dynamic suffix where appropriate.
  • Add non-DynamicScene versions of functions on SceneSpawner.
  • Listen for Scene AssetEvents and update the corresponding spawned scenes.
  • Despawn scenes before respawning them.
  • Store the parent of a scene in its InstanceInfo, so that we know where to parent the new entities from a hot-reloaded scene.
  • Make InstanceInfo private (none of our APIs returned it anyway).

Testing

  • Ran the hot_asset_reloading example and mutated the GLTF to see it be updated. It works! Moving around meshes works in Blender works, deleting meshes also seems to work.
  • Wrote a test for scene and dynamic scene to ensure they continue to hot-reload.

Migration Guide

  • Some of the methods on SceneSpawner have been renamed:
    • despawn -> despawn_dynamic
    • despawn_sync -> despawn_dynamic_sync
    • update_spawned_scenes -> update_spawned_dynamic_scenes
  • In their place, we've also added despawn, despawn_sync, and update_spawned_scenes which all act on Scenes (as opposed to DynamicScenes).

@github-actions
Copy link
Copy Markdown
Contributor

Your PR caused a change in the graphical output of an example or rendering test. This might be intentional, but it could also mean that something broke!
You can review it at https://pixel-eagle.com/project/B04F67C0-C054-4A6F-92EC-F599FEC2FD1D?filter=PR-18358

If it's expected, please add the M-Deliberate-Rendering-Change label.

If this change seems unrelated to your PR, you can consider updating your PR to target the latest main branch, either by rebasing or merging main into it.

@alice-i-cecile alice-i-cecile added C-Bug An unexpected or incorrect behavior S-Blocked This cannot move forward until something else changes A-Scenes Composing and serializing ECS objects labels Mar 17, 2025
@alice-i-cecile alice-i-cecile added S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged and removed S-Blocked This cannot move forward until something else changes labels Mar 18, 2025
@andriyDev andriyDev force-pushed the fix-scene-reload branch 2 times, most recently from 3c2f024 to 8ef8178 Compare March 18, 2025 02:51
@andriyDev andriyDev marked this pull request as ready for review March 18, 2025 03:32
@andriyDev andriyDev added S-Needs-Review Needs reviewer attention (from anyone!) to move forward S-Needs-Testing Testing must be done before this is safe to merge and removed S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Mar 18, 2025
@andriyDev
Copy link
Copy Markdown
Contributor Author

Looks like this PR isn't quite ready. I've got two meshes. Adding an empty node and saving after that causes a panic:

thread 'Compute Task Pool (0)' panicked at crates\bevy_transform\src\systems.rs:466:21:
assertion `left == right` failed
  left: 11v1#4294967307
 right: 12v1#4294967308

This also causes the entire app to freeze and blast my CPU usage to 50% (IIRC half the threads are for systems and half for assets, so I guess all the system threads are blasting).

Initial investigation shows the hierarchy is not set up correctly, but I'm not sure why yet.

@schrottkatze
Copy link
Copy Markdown

Hi, uh, are you planning on continuing to work on this?
(if you forgot about this, i guess this comment could serve as a ping like "yes people do still need this" ^^', don't mean to be like passive-aggressive, i've just spent like an hour thinking i've been doing something wrong just to realize it's broken in bevy, actually and the only pr to fix it seems stalled >~<)

@TimJentzsch TimJentzsch added the S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged label Jun 10, 2025
@andriyDev
Copy link
Copy Markdown
Contributor Author

Hi, so sorry I never actually went and updated the status here.

This is somewhat blocked on #18418. I say somewhat because it's not clear to me that entities in a scene should be reused at all (which is what that issue is about).

Entities in a Scene do not have any semantic information. That means when you change the scene, the entities before and after have no correspondence. Imagine you have the following Scene (which comes from loading a GLTF)

Entity 1v0: Cube
Entity 2v0: Sphere

If you go and delete the cube, you get the Scene:

Entity 1v0: Sphere

Note that the sphere now uses the cube's Entity. That means hot reloading would try to turn the cube entity into the sphere. Any components the cube had that the sphere doesn't will not be replaced (for example, if the cube had an IAmAnItemPickup component, now the sphere does. What if the sphere was actually the floor of your world!). This whole problem is exacerbated by relations.

I think one approach is to just delete all the entities and spawn new ones. It makes hot reloading less powerful, but makes it behave more reasonably IMO. I can try drafting that up soon. The alternative is this being blocked on #18418 haha.

@andriyDev
Copy link
Copy Markdown
Contributor Author

Actually, maybe even that alternative is a non-starter since if you had something parented to your scene, it would be despawned when you despawned all your entities... So this PR might just be blocked...

@andriyDev andriyDev added S-Blocked This cannot move forward until something else changes and removed S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged S-Needs-Testing Testing must be done before this is safe to merge labels Jun 11, 2025
@github-actions
Copy link
Copy Markdown
Contributor

Your PR caused a change in the graphical output of an example or rendering test. This might be intentional, but it could also mean that something broke!
You can review it at https://pixel-eagle.com/project/B04F67C0-C054-4A6F-92EC-F599FEC2FD1D?filter=PR-18358

If it's expected, please add the M-Deliberate-Rendering-Change label.

If this change seems unrelated to your PR, you can consider updating your PR to target the latest main branch, either by rebasing or merging main into it.

@andriyDev
Copy link
Copy Markdown
Contributor Author

Alright, after some discussion in Discord, I've been convinced that deleting the scene and respawning it is better than doing nothing. I also added a test for scenes and dynamic scenes to make sure they continue to hot-reload.

Note: I've fixed the rendering change, so we don't need the label!

@andriyDev andriyDev added S-Needs-Review Needs reviewer attention (from anyone!) to move forward and removed S-Blocked This cannot move forward until something else changes labels Jul 16, 2025
@andriyDev andriyDev requested a review from janhohenheim July 16, 2025 07:07
@janhohenheim janhohenheim added this to the 0.17 milestone Jul 16, 2025
Comment on lines +320 to +328
for id in scene_ids {
if let Some(spawned_instances) = self.spawned_scenes.get(id) {
for instance_id in spawned_instances {
if let Some(instance_info) = self.spawned_instances.get_mut(instance_id) {
Self::spawn_sync_internal(world, *id, &mut instance_info.entity_map)?;
}
}
}
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nothing blocking, but this could be simplified a lot by using .filter_map for the spawned scenes, followed by .flat_map for the spawned instances, followed again by filter_map for the instance info ;)

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.

I would prefer not to do this to keep it consistent with the dynamic one, and I'd prefer not to change that one to keep the diff small. bevy_scene could use a style pass IMO

Copy link
Copy Markdown
Member

@janhohenheim janhohenheim left a comment

Choose a reason for hiding this comment

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

Looked through all commits and everything looks correct to me :)
Ping @Noxmore, you'll care about this
Also fixes #3759

@andriyDev andriyDev added 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 labels Jul 16, 2025
@alice-i-cecile alice-i-cecile requested a review from cart July 16, 2025 18:23
Copy link
Copy Markdown
Contributor

@Noxmore Noxmore left a comment

Choose a reason for hiding this comment

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

Code looks good, and works with my test case (bevy_trenchbroom)!

@janhohenheim janhohenheim added S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it and removed S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Jul 16, 2025
@ChristopherBiscardi
Copy link
Copy Markdown
Contributor

ChristopherBiscardi commented Jul 17, 2025

seems to work for blender/skein.

speedblender.mp4

One thing I noticed is that the entity list seems to grow with every reload. The code here loops over all Query<&Character> in SceneInstanceReady (Character is on the Cube object) and in normal code would only see one entity/component. I haven't dug too deep yet on it so it may be a trivial thing.

screenshot-2025-07-17-at-15 39 24@2x

edit: notably the Character Component here is added via basically On<Add, GltfExtras>

@andriyDev
Copy link
Copy Markdown
Contributor Author

@ChristopherBiscardi, could I see your observer code? Is it possible you're spawning an entity instead of adding a component? Hotreloading literally despawns every entity associated with the scene, so I don't know what entities your component could be living off of?

bevy_inspector_egui might also shed some light on this. I mean if the entire hierarchy is duplicated after hot reload, then we'll know this PR isn't working.

@ChristopherBiscardi
Copy link
Copy Markdown
Contributor

@ChristopherBiscardi, could I see your observer code? Is it possible you're spawning an entity instead of adding a component? Hotreloading literally despawns every entity associated with the scene, so I don't know what entities your component could be living off of?

The observer code isn't afaik unless something changed significantly between 0.16 and main. The observer code is: https://github.com/rust-adventure/skein/blob/3d5949b7c21fc1b2e54f8414c3fb99e5c4885759/src/lib.rs#L272-L274 which hasn't substantially changed other than Trigger -> On

bevy_inspector_egui might also shed some light on this. I mean if the entire hierarchy is duplicated after hot reload, then we'll know this PR isn't working.

Is there a version of bevy_inspector_egui that works on main?


I think the issue is that I have a scene_spawner in AssetEvent::LoadedWithDependencies, the LoadedWithDependencies is called every time the reload happens, which then creates another instance. So not an issue with the PR afaict.

on-load -> spawn is a shortcut I use for some examples that I'll just have to take a different approach for.

@ChristopherBiscardi
Copy link
Copy Markdown
Contributor

yeah that was it, I confirmed with a Local that prevented the spawn if already had been spawned. reloading itself is working fine.

@alice-i-cecile
Copy link
Copy Markdown
Member

I'm comfortable enough with this fix to merge it now. There's nothing crazy going on here, and the regression test is really helpful.

Merged via the queue into bevyengine:main with commit 50f0f4c Jul 21, 2025
38 checks passed
@andriyDev andriyDev deleted the fix-scene-reload branch August 2, 2025 15:41
github-merge-queue bot pushed a commit that referenced this pull request Aug 14, 2025
# Objective

- examples `many_foxes` and `morph_targets` have stopped working since
#18358
- any case that load several parts of a complex asset and a scene, and
do setup on asset load will fail
- Fixes #20383

## Solution

- Debounce scene asset events so that we ignore modified event happening
too quickly after each others

## Testing

- run examples `many_foxes` or `morph_targets`

## Alternative

I *don't* think this is a good long term fix, but this should be fixed
in the 0.17 and I think alternatives are worse / too complex.

Alternatives are:
- Revert #18358. Normal scene loading is more important than hot
reloading
- Implement partial asset loading in the asset server and the gltf
loader so that when we load `file.gltf#Animation0`, only the animation
data is loaded. This would be a very good change, but too big to do for
the 0.17
- Have the asset server load parent assets only once, even when loading
subassets. This would be a good change and less complex, but I think
worse than the previous idea and kinda not compatible with it

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-Scenes Composing and serializing ECS objects C-Bug An unexpected or incorrect behavior 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-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Hot asset reloading example does not update when saving torus.gltf

7 participants