Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Synchronize removed components with the render world #15582

Open
wants to merge 12 commits into
base: main
Choose a base branch
from

Conversation

kristoff3r
Copy link
Contributor

@kristoff3r kristoff3r commented Oct 1, 2024

Objective

Fixes #15560
Fixes (most of) #15570

Currently a lot of examples (and presumably some user code) depend on toggling certain render features by adding/removing a single component to an entity, e.g. SpotLight to toggle a light. Because of the retained render world this no longer works: Extract will add any new components, but when it is removed the entity persists unchanged in the render world.

Solution

Add SyncComponentPlugin<C: Component> that registers SyncToRenderWorld as a required component for C, and adds a component hook that will clear all components from the render world entity when C is removed. We add this plugin to ExtractComponentPlugin which fixes most instances of the problem. For custom extraction logic we can manually add SyncComponentPlugin for that component.

I haven't done so here, but I propose we rename WorldSyncPlugin to SyncWorldPlugin so we start a naming convention like all the Extract plugins.

In this PR I also fixed a bunch of breakage related to the retained render world, stemming from old code that assumed that Entity would be the same in both worlds. I can split these out into a separate PR if needed.

I found that using the RenderEntity wrapper instead of Entity in data structures when referring to render world entities makes intent much clearer, so I propose we make this an official pattern.

Testing

Run examples like

cargo run --features pbr_multi_layer_material_textures --example clearcoat
cargo run --example volumetric_fog

and see that they work, and that toggles work correctly. But really we should test every single example, as we might not even have caught all the breakage yet.


Migration Guide

The retained render world notes should be updated to explain this edge case and SyncComponentPlugin

@alice-i-cecile alice-i-cecile added C-Bug An unexpected or incorrect behavior A-Rendering Drawing game state to the screen S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged labels Oct 1, 2024
@alice-i-cecile alice-i-cecile added this to the 0.15 milestone Oct 1, 2024
@alice-i-cecile alice-i-cecile self-requested a review October 1, 2024 22:44
@kristoff3r
Copy link
Contributor Author

I'll add and update documentation if we decide this is the solution we want

@kristoff3r kristoff3r marked this pull request as ready for review October 2, 2024 20:49
@IceSentry IceSentry added the A-ECS Entities, components, systems, and events label Oct 3, 2024
@alice-i-cecile alice-i-cecile added S-Needs-Review Needs reviewer attention (from anyone!) to move forward and removed S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged labels Oct 3, 2024
Copy link
Contributor

@Trashtalk217 Trashtalk217 left a comment

Choose a reason for hiding this comment

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

Broadly in favour of this, but there's one big exception that would break this system. This is exceedingly rare, but it does need to be documented.

Say that we have two synced and extracted components A and B. That are located on the same entity.

| Main World                   | Render World               |
| -----------------------------|----------------------------|
| e1: (A, B, RenderEntity(e2)) | e2: (A, B, MainEntity(e1)) |

If you now remove A from e1, this will remove both A and B in the render world.

| Main World                   | Render World              |
| -----------------------------|---------------------------|
| e1: (B, RenderEntity(e2))    | e2: (MainEntity(e1))      |

This may not be what the user expects. And while this scenario may be very rare, it should be documented. You could solve this by counting the number of synced components a entity has (and storing it with SyncToRenderWorld), but that might be a step to far.

Also some documentation is still required for the sync_component plugin.

crates/bevy_pbr/src/render/mesh.rs Show resolved Hide resolved
crates/bevy_pbr/src/render/mesh.rs Show resolved Hide resolved
crates/bevy_render/src/extract_component.rs Show resolved Hide resolved
crates/bevy_render/src/gpu_readback.rs Show resolved Hide resolved
ec.despawn();
};
}
EntityRecord::ComponentRemoved(main_entity) => {
// It's difficult to remove only the relevant component because component ids aren't stable across worlds,
// so we just clear the entire render world entity.
Copy link
Contributor

Choose a reason for hiding this comment

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

This is fun and has consequences, but we're gonna figure that out.

Copy link
Contributor Author

@kristoff3r kristoff3r Oct 4, 2024

Choose a reason for hiding this comment

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

After thinking more I think it's what we need to do in any case. Components like SpotLight aren't extracted directly but generate derived values, e.g. ExtractedPointLight. The only reason SyncComponentPlugin works in this case is because we clear everything. Because extract has to work when there's nothing present in the render world the first time it runs, I cannot think of any cases where this would break?

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm deferring on your judgement on this as I don't have much insight in rendering internals. As long as it's correctly documented (and maybe also include I reason why we have to do this, besides it being difficult, because it's strictly not impossible), this should be good.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@IceSentry or another SME should perhaps sanity check this logic, I'm not an expert in the internals either but it makes sense to me :D

Copy link
Contributor

@Trashtalk217 Trashtalk217 left a comment

Choose a reason for hiding this comment

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

Last couple nits, after this it's ready to go.

crates/bevy_render/src/world_sync.rs Outdated Show resolved Hide resolved
crates/bevy_render/src/world_sync.rs Outdated Show resolved Hide resolved
crates/bevy_render/src/sync_component.rs Outdated Show resolved Hide resolved
Co-authored-by: Trashtalk217 <[email protected]>
@kristoff3r
Copy link
Contributor Author

There are still a few open questions:

  • Do we do the WorldSync to SyncWorld rename?
  • Do we want the SyncComponentPlugin to add the required component registration for SyncToRenderWorld component? If yes we should remove all the ones that have been added in the required components migrations. If no we should re-add the ones removed here. In any case we should find some place to document it.

Also it looks like some of the examples broke when I merged main, I'll have to debug that later.

@alice-i-cecile
Copy link
Member

Do we do the WorldSync to SyncWorld rename?

Yes.

Do we want the SyncComponentPlugin to add the required component registration for SyncToRenderWorld component? If yes we should remove all the ones that have been added in the required components migrations. If no we should re-add the ones removed here. In any case we should find some place to document it.

Yes. Document this on SyncComponentPlugin, and also on the SyncToRenderWorld component please :)

@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-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Oct 8, 2024
if let Some(render_world_entity) = render_world.get_entity_mut(render_entity.id()) {
// In order to handle components that extract to derived components, we clear the entity
// and let the extraction system re-add the components.
render_world_entity.despawn();
Copy link
Contributor

Choose a reason for hiding this comment

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

For now this is probably fine since we don't really take advantage of the retained render world yet, but that feels like a major footgun for when we start to do that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

With the current extract system it's mostly a (minor) performance footgun. The extract system needs to handle extracting to an empty entity when it first gets created. Unless that's very performance heavy (which it shouldn't be during extract) or you insert/remove components very often it shouldn't be a problem.

When we use the retained world more we can probably come up with something smarter, but that will probably involve reworking extraction as well to do diffs and having a way to track derived state. If we just removed the same component it wouldn't solve the issue, as e.g. PointLight becomes ExtractedPointLight.

This at least is a much better stopgap than just saying "you're not allowed to remove render components after spawning".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-ECS Entities, components, systems, and events A-Rendering Drawing game state to the screen C-Bug An unexpected or incorrect behavior S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged
Projects
Status: No status
Development

Successfully merging this pull request may close these issues.

Directional light does not disappear after removing the DirectionalLight component
4 participants