Synchronize removed components with the render world (#15582)

# 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.

We also 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 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`

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Co-authored-by: Trashtalk217 <trashtalk217@gmail.com>
This commit is contained in:
Kristoffer Søholm 2024-10-09 00:23:17 +02:00 committed by GitHub
parent 45eff09213
commit 2d1b4939d2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 151 additions and 84 deletions

View file

@ -2,7 +2,7 @@ use bevy_ecs::prelude::*;
use bevy_render::{ use bevy_render::{
render_resource::{StorageBuffer, UniformBuffer}, render_resource::{StorageBuffer, UniformBuffer},
renderer::{RenderDevice, RenderQueue}, renderer::{RenderDevice, RenderQueue},
world_sync::RenderEntity, sync_world::RenderEntity,
Extract, Extract,
}; };
use bevy_utils::{Entry, HashMap}; use bevy_utils::{Entry, HashMap};

View file

@ -6,7 +6,7 @@ use crate::{
}; };
use bevy_ecs::prelude::*; use bevy_ecs::prelude::*;
use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::world_sync::SyncToRenderWorld; use bevy_render::sync_world::SyncToRenderWorld;
use bevy_render::{ use bevy_render::{
camera::{ camera::{
Camera, CameraMainTextureUsages, CameraProjection, CameraRenderGraph, Camera, CameraMainTextureUsages, CameraProjection, CameraRenderGraph,

View file

@ -55,9 +55,9 @@ use bevy_render::{
TextureFormat, TextureUsages, TextureFormat, TextureUsages,
}, },
renderer::RenderDevice, renderer::RenderDevice,
sync_world::RenderEntity,
texture::TextureCache, texture::TextureCache,
view::{Msaa, ViewDepthTexture}, view::{Msaa, ViewDepthTexture},
world_sync::RenderEntity,
Extract, ExtractSchedule, Render, RenderApp, RenderSet, Extract, ExtractSchedule, Render, RenderApp, RenderSet,
}; };

View file

@ -11,8 +11,8 @@ use bevy_render::{
extract_component::ExtractComponent, extract_component::ExtractComponent,
primitives::Frustum, primitives::Frustum,
render_resource::{LoadOp, TextureUsages}, render_resource::{LoadOp, TextureUsages},
sync_world::SyncToRenderWorld,
view::{ColorGrading, Msaa, VisibleEntities}, view::{ColorGrading, Msaa, VisibleEntities},
world_sync::SyncToRenderWorld,
}; };
use bevy_transform::prelude::{GlobalTransform, Transform}; use bevy_transform::prelude::{GlobalTransform, Transform};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};

View file

@ -89,9 +89,9 @@ use bevy_render::{
Texture, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, TextureView, Texture, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, TextureView,
}, },
renderer::RenderDevice, renderer::RenderDevice,
sync_world::RenderEntity,
texture::{BevyDefault, ColorAttachment, Image, TextureCache}, texture::{BevyDefault, ColorAttachment, Image, TextureCache},
view::{ExtractedView, ViewDepthTexture, ViewTarget}, view::{ExtractedView, ViewDepthTexture, ViewTarget},
world_sync::RenderEntity,
Extract, ExtractSchedule, Render, RenderApp, RenderSet, Extract, ExtractSchedule, Render, RenderApp, RenderSet,
}; };
use bevy_utils::{tracing::warn, HashMap}; use bevy_utils::{tracing::warn, HashMap};

View file

@ -46,12 +46,12 @@ use bevy_render::{
TextureDescriptor, TextureDimension, TextureFormat, TextureSampleType, TextureUsages, TextureDescriptor, TextureDimension, TextureFormat, TextureSampleType, TextureUsages,
}, },
renderer::{RenderContext, RenderDevice}, renderer::{RenderContext, RenderDevice},
sync_world::RenderEntity,
texture::{BevyDefault, CachedTexture, TextureCache}, texture::{BevyDefault, CachedTexture, TextureCache},
view::{ view::{
prepare_view_targets, ExtractedView, Msaa, ViewDepthTexture, ViewTarget, ViewUniform, prepare_view_targets, ExtractedView, Msaa, ViewDepthTexture, ViewTarget, ViewUniform,
ViewUniformOffset, ViewUniforms, ViewUniformOffset, ViewUniforms,
}, },
world_sync::RenderEntity,
Extract, ExtractSchedule, Render, RenderApp, RenderSet, Extract, ExtractSchedule, Render, RenderApp, RenderSet,
}; };
use bevy_utils::{info_once, prelude::default, warn_once}; use bevy_utils::{info_once, prelude::default, warn_once};

View file

@ -32,9 +32,9 @@ use bevy_render::{
TextureDimension, TextureFormat, TextureSampleType, TextureUsages, TextureDimension, TextureFormat, TextureSampleType, TextureUsages,
}, },
renderer::{RenderContext, RenderDevice}, renderer::{RenderContext, RenderDevice},
sync_world::RenderEntity,
texture::{BevyDefault, CachedTexture, TextureCache}, texture::{BevyDefault, CachedTexture, TextureCache},
view::{ExtractedView, Msaa, ViewTarget}, view::{ExtractedView, Msaa, ViewTarget},
world_sync::RenderEntity,
ExtractSchedule, MainWorld, Render, RenderApp, RenderSet, ExtractSchedule, MainWorld, Render, RenderApp, RenderSet,
}; };
use bevy_utils::tracing::warn; use bevy_utils::tracing::warn;

View file

@ -105,7 +105,7 @@ use {
ShaderStages, ShaderType, VertexFormat, ShaderStages, ShaderType, VertexFormat,
}, },
renderer::RenderDevice, renderer::RenderDevice,
world_sync::TemporaryRenderEntity, sync_world::TemporaryRenderEntity,
Extract, ExtractSchedule, Render, RenderApp, RenderSet, Extract, ExtractSchedule, Render, RenderApp, RenderSet,
}, },
bytemuck::cast_slice, bytemuck::cast_slice,

View file

@ -15,8 +15,8 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::{ use bevy_render::{
mesh::Mesh3d, mesh::Mesh3d,
primitives::{CascadesFrusta, CubemapFrusta, Frustum}, primitives::{CascadesFrusta, CubemapFrusta, Frustum},
sync_world::SyncToRenderWorld,
view::{InheritedVisibility, ViewVisibility, Visibility}, view::{InheritedVisibility, ViewVisibility, Visibility},
world_sync::SyncToRenderWorld,
}; };
use bevy_transform::components::{GlobalTransform, Transform}; use bevy_transform::components::{GlobalTransform, Transform};

View file

@ -20,7 +20,7 @@ use bevy_render::{
UniformBuffer, UniformBuffer,
}, },
renderer::{RenderDevice, RenderQueue}, renderer::{RenderDevice, RenderQueue},
world_sync::RenderEntity, sync_world::RenderEntity,
Extract, Extract,
}; };
use bevy_utils::{hashbrown::HashSet, tracing::warn}; use bevy_utils::{hashbrown::HashSet, tracing::warn};

View file

@ -127,6 +127,7 @@ use bevy_render::{
render_asset::prepare_assets, render_asset::prepare_assets,
render_graph::RenderGraph, render_graph::RenderGraph,
render_resource::Shader, render_resource::Shader,
sync_component::SyncComponentPlugin,
texture::{GpuImage, Image}, texture::{GpuImage, Image},
view::{check_visibility, VisibilitySystems}, view::{check_visibility, VisibilitySystems},
ExtractSchedule, Render, RenderApp, RenderSet, ExtractSchedule, Render, RenderApp, RenderSet,
@ -314,7 +315,6 @@ impl Plugin for PbrPlugin {
.register_type::<PointLight>() .register_type::<PointLight>()
.register_type::<PointLightShadowMap>() .register_type::<PointLightShadowMap>()
.register_type::<SpotLight>() .register_type::<SpotLight>()
.register_type::<DistanceFog>()
.register_type::<ShadowFilteringMethod>() .register_type::<ShadowFilteringMethod>()
.init_resource::<AmbientLight>() .init_resource::<AmbientLight>()
.init_resource::<GlobalVisibleClusterableObjects>() .init_resource::<GlobalVisibleClusterableObjects>()
@ -346,6 +346,11 @@ impl Plugin for PbrPlugin {
VolumetricFogPlugin, VolumetricFogPlugin,
ScreenSpaceReflectionsPlugin, ScreenSpaceReflectionsPlugin,
)) ))
.add_plugins((
SyncComponentPlugin::<DirectionalLight>::default(),
SyncComponentPlugin::<PointLight>::default(),
SyncComponentPlugin::<SpotLight>::default(),
))
.configure_sets( .configure_sets(
PostUpdate, PostUpdate,
( (

View file

@ -1,4 +1,4 @@
use bevy_render::{view::Visibility, world_sync::SyncToRenderWorld}; use bevy_render::view::Visibility;
use super::*; use super::*;
@ -57,8 +57,7 @@ use super::*;
CascadeShadowConfig, CascadeShadowConfig,
CascadesVisibleEntities, CascadesVisibleEntities,
Transform, Transform,
Visibility, Visibility
SyncToRenderWorld
)] )]
pub struct DirectionalLight { pub struct DirectionalLight {
/// The color of the light. /// The color of the light.

View file

@ -1,4 +1,4 @@
use bevy_render::{view::Visibility, world_sync::SyncToRenderWorld}; use bevy_render::view::Visibility;
use super::*; use super::*;
@ -21,13 +21,7 @@ use super::*;
/// Source: [Wikipedia](https://en.wikipedia.org/wiki/Lumen_(unit)#Lighting) /// Source: [Wikipedia](https://en.wikipedia.org/wiki/Lumen_(unit)#Lighting)
#[derive(Component, Debug, Clone, Copy, Reflect)] #[derive(Component, Debug, Clone, Copy, Reflect)]
#[reflect(Component, Default, Debug)] #[reflect(Component, Default, Debug)]
#[require( #[require(CubemapFrusta, CubemapVisibleEntities, Transform, Visibility)]
CubemapFrusta,
CubemapVisibleEntities,
Transform,
Visibility,
SyncToRenderWorld
)]
pub struct PointLight { pub struct PointLight {
/// The color of this light source. /// The color of this light source.
pub color: Color, pub color: Color,

View file

@ -1,4 +1,4 @@
use bevy_render::{view::Visibility, world_sync::SyncToRenderWorld}; use bevy_render::view::Visibility;
use super::*; use super::*;
@ -9,7 +9,7 @@ use super::*;
/// the transform, and can be specified with [`Transform::looking_at`](Transform::looking_at). /// the transform, and can be specified with [`Transform::looking_at`](Transform::looking_at).
#[derive(Component, Debug, Clone, Copy, Reflect)] #[derive(Component, Debug, Clone, Copy, Reflect)]
#[reflect(Component, Default, Debug)] #[reflect(Component, Default, Debug)]
#[require(Frustum, VisibleMeshEntities, Transform, Visibility, SyncToRenderWorld)] #[require(Frustum, VisibleMeshEntities, Transform, Visibility)]
pub struct SpotLight { pub struct SpotLight {
/// The color of the light. /// The color of the light.
/// ///

View file

@ -21,9 +21,9 @@ use bevy_render::{
render_resource::{DynamicUniformBuffer, Sampler, Shader, ShaderType, TextureView}, render_resource::{DynamicUniformBuffer, Sampler, Shader, ShaderType, TextureView},
renderer::{RenderDevice, RenderQueue}, renderer::{RenderDevice, RenderQueue},
settings::WgpuFeatures, settings::WgpuFeatures,
sync_world::RenderEntity,
texture::{FallbackImage, GpuImage, Image}, texture::{FallbackImage, GpuImage, Image},
view::ExtractedView, view::ExtractedView,
world_sync::RenderEntity,
Extract, ExtractSchedule, Render, RenderApp, RenderSet, Extract, ExtractSchedule, Render, RenderApp, RenderSet,
}; };
use bevy_transform::{components::Transform, prelude::GlobalTransform}; use bevy_transform::{components::Transform, prelude::GlobalTransform};

View file

@ -3,7 +3,7 @@ mod prepass_bindings;
use bevy_render::{ use bevy_render::{
mesh::{Mesh3d, MeshVertexBufferLayoutRef, RenderMesh}, mesh::{Mesh3d, MeshVertexBufferLayoutRef, RenderMesh},
render_resource::binding_types::uniform_buffer, render_resource::binding_types::uniform_buffer,
world_sync::RenderEntity, sync_world::RenderEntity,
}; };
pub use prepass_bindings::*; pub use prepass_bindings::*;

View file

@ -8,7 +8,7 @@ use bevy_ecs::{
system::lifetimeless::Read, system::lifetimeless::Read,
}; };
use bevy_math::{ops, Mat4, UVec4, Vec2, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles}; use bevy_math::{ops, Mat4, UVec4, Vec2, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles};
use bevy_render::world_sync::RenderEntity; use bevy_render::sync_world::RenderEntity;
use bevy_render::{ use bevy_render::{
diagnostic::RecordDiagnostics, diagnostic::RecordDiagnostics,
mesh::RenderMesh, mesh::RenderMesh,

View file

@ -30,9 +30,9 @@ use bevy_render::{
*, *,
}, },
renderer::{RenderAdapter, RenderContext, RenderDevice, RenderQueue}, renderer::{RenderAdapter, RenderContext, RenderDevice, RenderQueue},
sync_world::RenderEntity,
texture::{CachedTexture, TextureCache}, texture::{CachedTexture, TextureCache},
view::{Msaa, ViewUniform, ViewUniformOffset, ViewUniforms}, view::{Msaa, ViewUniform, ViewUniformOffset, ViewUniforms},
world_sync::RenderEntity,
Extract, ExtractSchedule, Render, RenderApp, RenderSet, Extract, ExtractSchedule, Render, RenderApp, RenderSet,
}; };
use bevy_utils::{ use bevy_utils::{

View file

@ -51,6 +51,7 @@ use bevy_render::{
mesh::{Mesh, Meshable}, mesh::{Mesh, Meshable},
render_graph::{RenderGraphApp, ViewNodeRunner}, render_graph::{RenderGraphApp, ViewNodeRunner},
render_resource::{Shader, SpecializedRenderPipelines}, render_resource::{Shader, SpecializedRenderPipelines},
sync_component::SyncComponentPlugin,
texture::Image, texture::Image,
view::{InheritedVisibility, ViewVisibility, Visibility}, view::{InheritedVisibility, ViewVisibility, Visibility},
ExtractSchedule, Render, RenderApp, RenderSet, ExtractSchedule, Render, RenderApp, RenderSet,
@ -231,6 +232,8 @@ impl Plugin for VolumetricFogPlugin {
app.register_type::<VolumetricFog>() app.register_type::<VolumetricFog>()
.register_type::<VolumetricLight>(); .register_type::<VolumetricLight>();
app.add_plugins(SyncComponentPlugin::<FogVolume>::default());
let Some(render_app) = app.get_sub_app_mut(RenderApp) else { let Some(render_app) = app.get_sub_app_mut(RenderApp) else {
return; return;
}; };

View file

@ -36,9 +36,9 @@ use bevy_render::{
TextureSampleType, TextureUsages, VertexState, TextureSampleType, TextureUsages, VertexState,
}, },
renderer::{RenderContext, RenderDevice, RenderQueue}, renderer::{RenderContext, RenderDevice, RenderQueue},
sync_world::RenderEntity,
texture::{BevyDefault as _, GpuImage, Image}, texture::{BevyDefault as _, GpuImage, Image},
view::{ExtractedView, Msaa, ViewDepthTexture, ViewTarget, ViewUniformOffset}, view::{ExtractedView, Msaa, ViewDepthTexture, ViewTarget, ViewUniformOffset},
world_sync::RenderEntity,
Extract, Extract,
}; };
use bevy_transform::components::GlobalTransform; use bevy_transform::components::GlobalTransform;

View file

@ -6,12 +6,12 @@ use crate::{
render_asset::RenderAssets, render_asset::RenderAssets,
render_graph::{InternedRenderSubGraph, RenderSubGraph}, render_graph::{InternedRenderSubGraph, RenderSubGraph},
render_resource::TextureView, render_resource::TextureView,
sync_world::{RenderEntity, SyncToRenderWorld},
texture::GpuImage, texture::GpuImage,
view::{ view::{
ColorGrading, ExtractedView, ExtractedWindows, GpuCulling, Msaa, RenderLayers, Visibility, ColorGrading, ExtractedView, ExtractedWindows, GpuCulling, Msaa, RenderLayers, Visibility,
VisibleEntities, VisibleEntities,
}, },
world_sync::{RenderEntity, SyncToRenderWorld},
Extract, Extract,
}; };
use bevy_asset::{AssetEvent, AssetId, Assets, Handle}; use bevy_asset::{AssetEvent, AssetId, Assets, Handle};

View file

@ -1,8 +1,9 @@
use crate::{ use crate::{
render_resource::{encase::internal::WriteInto, DynamicUniformBuffer, ShaderType}, render_resource::{encase::internal::WriteInto, DynamicUniformBuffer, ShaderType},
renderer::{RenderDevice, RenderQueue}, renderer::{RenderDevice, RenderQueue},
sync_component::SyncComponentPlugin,
sync_world::RenderEntity,
view::ViewVisibility, view::ViewVisibility,
world_sync::{RenderEntity, SyncToRenderWorld},
Extract, ExtractSchedule, Render, RenderApp, RenderSet, Extract, ExtractSchedule, Render, RenderApp, RenderSet,
}; };
use bevy_app::{App, Plugin}; use bevy_app::{App, Plugin};
@ -12,7 +13,6 @@ use bevy_ecs::{
prelude::*, prelude::*,
query::{QueryFilter, QueryItem, ReadOnlyQueryData}, query::{QueryFilter, QueryItem, ReadOnlyQueryData},
system::lifetimeless::Read, system::lifetimeless::Read,
world::OnAdd,
}; };
use core::{marker::PhantomData, ops::Deref}; use core::{marker::PhantomData, ops::Deref};
@ -160,15 +160,6 @@ fn prepare_uniform_components<C>(
/// This plugin extracts the components into the render world for synced entities. /// This plugin extracts the components into the render world for synced entities.
/// ///
/// To do so, it sets up the [`ExtractSchedule`] step for the specified [`ExtractComponent`]. /// To do so, it sets up the [`ExtractSchedule`] step for the specified [`ExtractComponent`].
///
/// # Warning
///
/// Be careful when removing the [`ExtractComponent`] from an entity. When an [`ExtractComponent`]
/// is added to an entity, that entity is automatically synced with the render world (see also
/// [`WorldSyncPlugin`](crate::world_sync::WorldSyncPlugin)). When removing the entity in the main
/// world, the synced entity also gets removed. However, if only the [`ExtractComponent`] is removed
/// this *doesn't* happen, and the synced entity stays around with the old extracted data.
/// We recommend despawning the entire entity, instead of only removing [`ExtractComponent`].
pub struct ExtractComponentPlugin<C, F = ()> { pub struct ExtractComponentPlugin<C, F = ()> {
only_extract_visible: bool, only_extract_visible: bool,
marker: PhantomData<fn() -> (C, F)>, marker: PhantomData<fn() -> (C, F)>,
@ -194,10 +185,8 @@ impl<C, F> ExtractComponentPlugin<C, F> {
impl<C: ExtractComponent> Plugin for ExtractComponentPlugin<C> { impl<C: ExtractComponent> Plugin for ExtractComponentPlugin<C> {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
// TODO: use required components app.add_plugins(SyncComponentPlugin::<C>::default());
app.observe(|trigger: Trigger<OnAdd, C>, mut commands: Commands| {
commands.entity(trigger.entity()).insert(SyncToRenderWorld);
});
if let Some(render_app) = app.get_sub_app_mut(RenderApp) { if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
if self.only_extract_visible { if self.only_extract_visible {
render_app.add_systems(ExtractSchedule, extract_visible_components::<C>); render_app.add_systems(ExtractSchedule, extract_visible_components::<C>);
@ -219,7 +208,7 @@ impl<T: Asset> ExtractComponent for Handle<T> {
} }
} }
/// This system extracts all components of the corresponding [`ExtractComponent`], for entities that are synced via [`SyncToRenderWorld`]. /// This system extracts all components of the corresponding [`ExtractComponent`], for entities that are synced via [`crate::sync_world::SyncToRenderWorld`].
fn extract_components<C: ExtractComponent>( fn extract_components<C: ExtractComponent>(
mut commands: Commands, mut commands: Commands,
mut previous_len: Local<usize>, mut previous_len: Local<usize>,
@ -235,7 +224,7 @@ fn extract_components<C: ExtractComponent>(
commands.insert_or_spawn_batch(values); commands.insert_or_spawn_batch(values);
} }
/// This system extracts all components of the corresponding [`ExtractComponent`], for entities that are visible and synced via [`SyncToRenderWorld`]. /// This system extracts all components of the corresponding [`ExtractComponent`], for entities that are visible and synced via [`crate::sync_world::SyncToRenderWorld`].
fn extract_visible_components<C: ExtractComponent>( fn extract_visible_components<C: ExtractComponent>(
mut commands: Commands, mut commands: Commands,
mut previous_len: Local<usize>, mut previous_len: Local<usize>,

View file

@ -30,7 +30,7 @@ use core::ops::{Deref, DerefMut};
/// ``` /// ```
/// use bevy_ecs::prelude::*; /// use bevy_ecs::prelude::*;
/// use bevy_render::Extract; /// use bevy_render::Extract;
/// use bevy_render::world_sync::RenderEntity; /// use bevy_render::sync_world::RenderEntity;
/// # #[derive(Component)] /// # #[derive(Component)]
/// // Do make sure to sync the cloud entities before extracting them. /// // Do make sure to sync the cloud entities before extracting them.
/// # struct Cloud; /// # struct Cloud;

View file

@ -5,6 +5,7 @@ use crate::{
render_resource::{Buffer, BufferUsages, Extent3d, ImageDataLayout, Texture, TextureFormat}, render_resource::{Buffer, BufferUsages, Extent3d, ImageDataLayout, Texture, TextureFormat},
renderer::{render_system, RenderDevice}, renderer::{render_system, RenderDevice},
storage::{GpuShaderStorageBuffer, ShaderStorageBuffer}, storage::{GpuShaderStorageBuffer, ShaderStorageBuffer},
sync_world::MainEntity,
texture::{GpuImage, TextureFormatPixelInfo}, texture::{GpuImage, TextureFormatPixelInfo},
ExtractSchedule, MainWorld, Render, RenderApp, RenderSet, ExtractSchedule, MainWorld, Render, RenderApp, RenderSet,
}; };
@ -232,7 +233,7 @@ fn prepare_buffers(
mut buffer_pool: ResMut<GpuReadbackBufferPool>, mut buffer_pool: ResMut<GpuReadbackBufferPool>,
gpu_images: Res<RenderAssets<GpuImage>>, gpu_images: Res<RenderAssets<GpuImage>>,
ssbos: Res<RenderAssets<GpuShaderStorageBuffer>>, ssbos: Res<RenderAssets<GpuShaderStorageBuffer>>,
handles: Query<(Entity, &Readback)>, handles: Query<(&MainEntity, &Readback)>,
) { ) {
for (entity, readback) in handles.iter() { for (entity, readback) in handles.iter() {
match readback { match readback {
@ -254,7 +255,7 @@ fn prepare_buffers(
); );
let (tx, rx) = async_channel::bounded(1); let (tx, rx) = async_channel::bounded(1);
readbacks.requested.push(GpuReadback { readbacks.requested.push(GpuReadback {
entity, entity: entity.id(),
src: ReadbackSource::Texture { src: ReadbackSource::Texture {
texture: gpu_image.texture.clone(), texture: gpu_image.texture.clone(),
layout, layout,
@ -272,7 +273,7 @@ fn prepare_buffers(
let buffer = buffer_pool.get(&render_device, size); let buffer = buffer_pool.get(&render_device, size);
let (tx, rx) = async_channel::bounded(1); let (tx, rx) = async_channel::bounded(1);
readbacks.requested.push(GpuReadback { readbacks.requested.push(GpuReadback {
entity, entity: entity.id(),
src: ReadbackSource::Buffer { src: ReadbackSource::Buffer {
src_start: 0, src_start: 0,
dst_start: 0, dst_start: 0,

View file

@ -38,9 +38,10 @@ pub mod renderer;
pub mod settings; pub mod settings;
mod spatial_bundle; mod spatial_bundle;
pub mod storage; pub mod storage;
pub mod sync_component;
pub mod sync_world;
pub mod texture; pub mod texture;
pub mod view; pub mod view;
pub mod world_sync;
/// The render prelude. /// The render prelude.
/// ///
@ -77,8 +78,8 @@ use extract_resource::ExtractResourcePlugin;
use globals::GlobalsPlugin; use globals::GlobalsPlugin;
use render_asset::RenderAssetBytesPerFrame; use render_asset::RenderAssetBytesPerFrame;
use renderer::{RenderAdapter, RenderAdapterInfo, RenderDevice, RenderQueue}; use renderer::{RenderAdapter, RenderAdapterInfo, RenderDevice, RenderQueue};
use world_sync::{ use sync_world::{
despawn_temporary_render_entities, entity_sync_system, SyncToRenderWorld, WorldSyncPlugin, despawn_temporary_render_entities, entity_sync_system, SyncToRenderWorld, SyncWorldPlugin,
}; };
use crate::gpu_readback::GpuReadbackPlugin; use crate::gpu_readback::GpuReadbackPlugin;
@ -371,7 +372,7 @@ impl Plugin for RenderPlugin {
GlobalsPlugin, GlobalsPlugin,
MorphPlugin, MorphPlugin,
BatchingPlugin, BatchingPlugin,
WorldSyncPlugin, SyncWorldPlugin,
StoragePlugin, StoragePlugin,
GpuReadbackPlugin::default(), GpuReadbackPlugin::default(),
)); ));

View file

@ -81,7 +81,7 @@ impl Drop for RenderAppChannels {
/// The plugin is dependent on the [`RenderApp`] added by [`crate::RenderPlugin`] and so must /// The plugin is dependent on the [`RenderApp`] added by [`crate::RenderPlugin`] and so must
/// be added after that plugin. If it is not added after, the plugin will do nothing. /// be added after that plugin. If it is not added after, the plugin will do nothing.
/// ///
/// A single frame of execution looks something like below /// A single frame of execution looks something like below
/// ///
/// ```text /// ```text
/// |---------------------------------------------------------------------------| /// |---------------------------------------------------------------------------|
@ -92,7 +92,7 @@ impl Drop for RenderAppChannels {
/// ``` /// ```
/// ///
/// - `sync` is the step where the entity-entity mapping between the main and render world is updated. /// - `sync` is the step where the entity-entity mapping between the main and render world is updated.
/// This is run on the main app's thread. For more information checkout [`WorldSyncPlugin`]. /// This is run on the main app's thread. For more information checkout [`SyncWorldPlugin`].
/// - `extract` is the step where data is copied from the main world to the render world. /// - `extract` is the step where data is copied from the main world to the render world.
/// This is run on the main app's thread. /// This is run on the main app's thread.
/// - On the render thread, we first apply the `extract commands`. This is not run during extract, so the /// - On the render thread, we first apply the `extract commands`. This is not run during extract, so the
@ -104,7 +104,7 @@ impl Drop for RenderAppChannels {
/// - And finally the `main app schedule` is run. /// - And finally the `main app schedule` is run.
/// - Once both the `main app schedule` and the `render schedule` are finished running, `extract` is run again. /// - Once both the `main app schedule` and the `render schedule` are finished running, `extract` is run again.
/// ///
/// [`WorldSyncPlugin`]: crate::world_sync::WorldSyncPlugin /// [`SyncWorldPlugin`]: crate::sync_world::SyncWorldPlugin
#[derive(Default)] #[derive(Default)]
pub struct PipelinedRenderingPlugin; pub struct PipelinedRenderingPlugin;

View file

@ -0,0 +1,42 @@
use core::marker::PhantomData;
use bevy_app::{App, Plugin};
use bevy_ecs::component::Component;
use crate::sync_world::{EntityRecord, PendingSyncEntity, SyncToRenderWorld};
/// Plugin that registers a component for automatic sync to the render world. See [`SyncWorldPlugin`] for more information.
///
/// This plugin is automatically added by [`ExtractComponentPlugin`], and only needs to be added for manual extraction implementations.
///
/// # Implementation details
///
/// It adds [`SyncToRenderWorld`] as a required component to make the [`SyncWorldPlugin`] aware of the component, and
/// handles cleanup of the component in the render world when it is removed from an entity.
///
/// # Warning
/// When the component is removed from the main world entity, all components are removed from the entity in the render world.
/// This is done in order to handle components with custom extraction logic and derived state.
///
/// [`ExtractComponentPlugin`]: crate::extract_component::ExtractComponentPlugin
/// [`SyncWorldPlugin`]: crate::sync_world::SyncWorldPlugin
pub struct SyncComponentPlugin<C: Component>(PhantomData<C>);
impl<C: Component> Default for SyncComponentPlugin<C> {
fn default() -> Self {
Self(PhantomData)
}
}
impl<C: Component> Plugin for SyncComponentPlugin<C> {
fn build(&self, app: &mut App) {
app.register_required_components::<C, SyncToRenderWorld>();
app.world_mut().register_component_hooks::<C>().on_remove(
|mut world, entity, _component_id| {
let mut pending = world.resource_mut::<PendingSyncEntity>();
pending.push(EntityRecord::ComponentRemoved(entity));
},
);
}
}

View file

@ -13,11 +13,18 @@ use bevy_reflect::Reflect;
/// A plugin that synchronizes entities with [`SyncToRenderWorld`] between the main world and the render world. /// A plugin that synchronizes entities with [`SyncToRenderWorld`] between the main world and the render world.
/// ///
/// All entities with the [`SyncToRenderWorld`] component are kept in sync. It
/// is automatically added as a required component by [`ExtractComponentPlugin`]
/// and [`SyncComponentPlugin`], so it doesn't need to be added manually when
/// spawning or as a required component when either of these plugins are used.
///
/// # Implementation
///
/// Bevy's renderer is architected independently from the main app. /// Bevy's renderer is architected independently from the main app.
/// It operates in its own separate ECS [`World`], so the renderer logic can run in parallel with the main world logic. /// It operates in its own separate ECS [`World`], so the renderer logic can run in parallel with the main world logic.
/// This is called "Pipelined Rendering", see [`PipelinedRenderingPlugin`] for more information. /// This is called "Pipelined Rendering", see [`PipelinedRenderingPlugin`] for more information.
/// ///
/// [`WorldSyncPlugin`] is the first thing that runs every frame and it maintains an entity-to-entity mapping /// [`SyncWorldPlugin`] is the first thing that runs every frame and it maintains an entity-to-entity mapping
/// between the main world and the render world. /// between the main world and the render world.
/// It does so by spawning and despawning entities in the render world, to match spawned and despawned entities in the main world. /// It does so by spawning and despawning entities in the render world, to match spawned and despawned entities in the main world.
/// The link between synced entities is maintained by the [`RenderEntity`] and [`MainEntity`] components. /// The link between synced entities is maintained by the [`RenderEntity`] and [`MainEntity`] components.
@ -66,14 +73,16 @@ use bevy_reflect::Reflect;
/// The render world probably cares about a `Position` component, but not a `Velocity` component. /// The render world probably cares about a `Position` component, but not a `Velocity` component.
/// The extraction happens in its own step, independently from, and after synchronization. /// The extraction happens in its own step, independently from, and after synchronization.
/// ///
/// Moreover, [`WorldSyncPlugin`] only synchronizes *entities*. [`RenderAsset`](crate::render_asset::RenderAsset)s like meshes and textures are handled /// Moreover, [`SyncWorldPlugin`] only synchronizes *entities*. [`RenderAsset`](crate::render_asset::RenderAsset)s like meshes and textures are handled
/// differently. /// differently.
/// ///
/// [`PipelinedRenderingPlugin`]: crate::pipelined_rendering::PipelinedRenderingPlugin /// [`PipelinedRenderingPlugin`]: crate::pipelined_rendering::PipelinedRenderingPlugin
/// [`ExtractComponentPlugin`]: crate::extract_component::ExtractComponentPlugin
/// [`SyncComponentPlugin`]: crate::sync_component::SyncComponentPlugin
#[derive(Default)] #[derive(Default)]
pub struct WorldSyncPlugin; pub struct SyncWorldPlugin;
impl Plugin for WorldSyncPlugin { impl Plugin for SyncWorldPlugin {
fn build(&self, app: &mut bevy_app::App) { fn build(&self, app: &mut bevy_app::App) {
app.init_resource::<PendingSyncEntity>(); app.init_resource::<PendingSyncEntity>();
app.observe( app.observe(
@ -86,22 +95,30 @@ impl Plugin for WorldSyncPlugin {
mut pending: ResMut<PendingSyncEntity>, mut pending: ResMut<PendingSyncEntity>,
query: Query<&RenderEntity>| { query: Query<&RenderEntity>| {
if let Ok(e) = query.get(trigger.entity()) { if let Ok(e) = query.get(trigger.entity()) {
pending.push(EntityRecord::Removed(e.id())); pending.push(EntityRecord::Removed(*e));
}; };
}, },
); );
} }
} }
/// Marker component that indicates that its entity needs to be synchronized to the render world /// Marker component that indicates that its entity needs to be synchronized to the render world.
///
/// This component is automatically added as a required component by [`ExtractComponentPlugin`] and [`SyncComponentPlugin`].
/// For more information see [`SyncWorldPlugin`].
/// ///
/// NOTE: This component should persist throughout the entity's entire lifecycle. /// NOTE: This component should persist throughout the entity's entire lifecycle.
/// If this component is removed from its entity, the entity will be despawned. /// If this component is removed from its entity, the entity will be despawned.
///
/// [`ExtractComponentPlugin`]: crate::extract_component::ExtractComponentPlugin
/// [`SyncComponentPlugin`]: crate::sync_component::SyncComponentPlugin
#[derive(Component, Clone, Debug, Default, Reflect)] #[derive(Component, Clone, Debug, Default, Reflect)]
#[reflect[Component]] #[reflect[Component]]
#[component(storage = "SparseSet")] #[component(storage = "SparseSet")]
pub struct SyncToRenderWorld; pub struct SyncToRenderWorld;
/// Component added on the main world entities that are synced to the Render World in order to keep track of the corresponding render world entity /// Component added on the main world entities that are synced to the Render World in order to keep track of the corresponding render world entity.
///
/// Can also be used as a newtype wrapper for render world entities.
#[derive(Component, Deref, Clone, Debug, Copy)] #[derive(Component, Deref, Clone, Debug, Copy)]
pub struct RenderEntity(Entity); pub struct RenderEntity(Entity);
impl RenderEntity { impl RenderEntity {
@ -111,7 +128,9 @@ impl RenderEntity {
} }
} }
/// Component added on the render world entities to keep track of the corresponding main world entity /// Component added on the render world entities to keep track of the corresponding main world entity.
///
/// Can also be used as a newtype wrapper for main world entities.
#[derive(Component, Deref, Clone, Debug)] #[derive(Component, Deref, Clone, Debug)]
pub struct MainEntity(Entity); pub struct MainEntity(Entity);
impl MainEntity { impl MainEntity {
@ -127,13 +146,17 @@ impl MainEntity {
pub struct TemporaryRenderEntity; pub struct TemporaryRenderEntity;
/// A record enum to what entities with [`SyncToRenderWorld`] have been added or removed. /// A record enum to what entities with [`SyncToRenderWorld`] have been added or removed.
#[derive(Debug)]
pub(crate) enum EntityRecord { pub(crate) enum EntityRecord {
/// When an entity is spawned on the main world, notify the render world so that it can spawn a corresponding /// When an entity is spawned on the main world, notify the render world so that it can spawn a corresponding
/// entity. This contains the main world entity. /// entity. This contains the main world entity.
Added(Entity), Added(Entity),
/// When an entity is despawned on the main world, notify the render world so that the corresponding entity can be /// When an entity is despawned on the main world, notify the render world so that the corresponding entity can be
/// despawned. This contains the render world entity. /// despawned. This contains the render world entity.
Removed(Entity), Removed(RenderEntity),
/// When a component is removed from an entity, notify the render world so that the corresponding component can be
/// removed. This contains the main world entity.
ComponentRemoved(Entity),
} }
// Entity Record in MainWorld pending to Sync // Entity Record in MainWorld pending to Sync
@ -148,8 +171,8 @@ pub(crate) fn entity_sync_system(main_world: &mut World, render_world: &mut Worl
for record in pending.drain(..) { for record in pending.drain(..) {
match record { match record {
EntityRecord::Added(e) => { EntityRecord::Added(e) => {
if let Ok(mut entity) = world.get_entity_mut(e) { if let Ok(mut main_entity) = world.get_entity_mut(e) {
match entity.entry::<RenderEntity>() { match main_entity.entry::<RenderEntity>() {
bevy_ecs::world::Entry::Occupied(_) => { bevy_ecs::world::Entry::Occupied(_) => {
panic!("Attempting to synchronize an entity that has already been synchronized!"); panic!("Attempting to synchronize an entity that has already been synchronized!");
} }
@ -161,11 +184,24 @@ pub(crate) fn entity_sync_system(main_world: &mut World, render_world: &mut Worl
}; };
} }
} }
EntityRecord::Removed(e) => { EntityRecord::Removed(render_entity) => {
if let Ok(ec) = render_world.get_entity_mut(e) { if let Ok(ec) = render_world.get_entity_mut(render_entity.id()) {
ec.despawn(); ec.despawn();
}; };
} }
EntityRecord::ComponentRemoved(main_entity) => {
let Some(mut render_entity) = world.get_mut::<RenderEntity>(main_entity) else {
continue;
};
if let Ok(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();
let id = render_world.spawn(MainEntity(main_entity)).id();
render_entity.0 = id;
}
},
} }
} }
}); });
@ -207,7 +243,7 @@ mod tests {
struct RenderDataComponent; struct RenderDataComponent;
#[test] #[test]
fn world_sync() { fn sync_world() {
let mut main_world = World::new(); let mut main_world = World::new();
let mut render_world = World::new(); let mut render_world = World::new();
main_world.init_resource::<PendingSyncEntity>(); main_world.init_resource::<PendingSyncEntity>();
@ -222,7 +258,7 @@ mod tests {
mut pending: ResMut<PendingSyncEntity>, mut pending: ResMut<PendingSyncEntity>,
query: Query<&RenderEntity>| { query: Query<&RenderEntity>| {
if let Ok(e) = query.get(trigger.entity()) { if let Ok(e) = query.get(trigger.entity()) {
pending.push(EntityRecord::Removed(e.id())); pending.push(EntityRecord::Removed(*e));
}; };
}, },
); );

View file

@ -2,9 +2,9 @@ use crate::Sprite;
use bevy_asset::Handle; use bevy_asset::Handle;
use bevy_ecs::bundle::Bundle; use bevy_ecs::bundle::Bundle;
use bevy_render::{ use bevy_render::{
sync_world::SyncToRenderWorld,
texture::Image, texture::Image,
view::{InheritedVisibility, ViewVisibility, Visibility}, view::{InheritedVisibility, ViewVisibility, Visibility},
world_sync::SyncToRenderWorld,
}; };
use bevy_transform::components::{GlobalTransform, Transform}; use bevy_transform::components::{GlobalTransform, Transform};

View file

@ -31,6 +31,7 @@ use bevy_render::{
*, *,
}, },
renderer::{RenderDevice, RenderQueue}, renderer::{RenderDevice, RenderQueue},
sync_world::{RenderEntity, TemporaryRenderEntity},
texture::{ texture::{
BevyDefault, DefaultImageSampler, FallbackImage, GpuImage, Image, ImageSampler, BevyDefault, DefaultImageSampler, FallbackImage, GpuImage, Image, ImageSampler,
TextureFormatPixelInfo, TextureFormatPixelInfo,
@ -39,7 +40,6 @@ use bevy_render::{
ExtractedView, Msaa, ViewTarget, ViewUniform, ViewUniformOffset, ViewUniforms, ExtractedView, Msaa, ViewTarget, ViewUniform, ViewUniformOffset, ViewUniforms,
ViewVisibility, VisibleEntities, ViewVisibility, VisibleEntities,
}, },
world_sync::{RenderEntity, TemporaryRenderEntity},
Extract, Extract,
}; };
use bevy_transform::components::GlobalTransform; use bevy_transform::components::GlobalTransform;

View file

@ -15,7 +15,7 @@ use bevy_ecs::{
system::{Commands, Local, Query, Res, ResMut}, system::{Commands, Local, Query, Res, ResMut},
}; };
use bevy_math::Vec2; use bevy_math::Vec2;
use bevy_render::world_sync::TemporaryRenderEntity; use bevy_render::sync_world::TemporaryRenderEntity;
use bevy_render::{ use bevy_render::{
primitives::Aabb, primitives::Aabb,
texture::Image, texture::Image,

View file

@ -19,9 +19,9 @@ use bevy_render::{
render_phase::*, render_phase::*,
render_resource::{binding_types::uniform_buffer, *}, render_resource::{binding_types::uniform_buffer, *},
renderer::{RenderDevice, RenderQueue}, renderer::{RenderDevice, RenderQueue},
sync_world::{RenderEntity, TemporaryRenderEntity},
texture::BevyDefault, texture::BevyDefault,
view::*, view::*,
world_sync::{RenderEntity, TemporaryRenderEntity},
Extract, ExtractSchedule, Render, RenderSet, Extract, ExtractSchedule, Render, RenderSet,
}; };
use bevy_transform::prelude::GlobalTransform; use bevy_transform::prelude::GlobalTransform;

View file

@ -32,9 +32,9 @@ use bevy_render::{
}; };
use bevy_render::{ use bevy_render::{
render_phase::{PhaseItem, PhaseItemExtraIndex}, render_phase::{PhaseItem, PhaseItemExtraIndex},
sync_world::{RenderEntity, TemporaryRenderEntity},
texture::GpuImage, texture::GpuImage,
view::ViewVisibility, view::ViewVisibility,
world_sync::{RenderEntity, TemporaryRenderEntity},
ExtractSchedule, Render, ExtractSchedule, Render,
}; };
use bevy_sprite::TextureAtlasLayout; use bevy_sprite::TextureAtlasLayout;

View file

@ -18,9 +18,9 @@ use bevy_render::{
render_phase::*, render_phase::*,
render_resource::{binding_types::uniform_buffer, *}, render_resource::{binding_types::uniform_buffer, *},
renderer::{RenderDevice, RenderQueue}, renderer::{RenderDevice, RenderQueue},
sync_world::{RenderEntity, TemporaryRenderEntity},
texture::BevyDefault, texture::BevyDefault,
view::*, view::*,
world_sync::{RenderEntity, TemporaryRenderEntity},
Extract, ExtractSchedule, Render, RenderSet, Extract, ExtractSchedule, Render, RenderSet,
}; };
use bevy_transform::prelude::GlobalTransform; use bevy_transform::prelude::GlobalTransform;

View file

@ -16,9 +16,9 @@ use bevy_render::{
render_phase::*, render_phase::*,
render_resource::{binding_types::uniform_buffer, *}, render_resource::{binding_types::uniform_buffer, *},
renderer::{RenderDevice, RenderQueue}, renderer::{RenderDevice, RenderQueue},
sync_world::{RenderEntity, TemporaryRenderEntity},
texture::{BevyDefault, GpuImage, Image, TRANSPARENT_IMAGE_HANDLE}, texture::{BevyDefault, GpuImage, Image, TRANSPARENT_IMAGE_HANDLE},
view::*, view::*,
world_sync::{RenderEntity, TemporaryRenderEntity},
Extract, ExtractSchedule, Render, RenderSet, Extract, ExtractSchedule, Render, RenderSet,
}; };
use bevy_sprite::{ use bevy_sprite::{

View file

@ -9,7 +9,6 @@ use bevy::{
math::vec3, math::vec3,
pbr::{FogVolume, VolumetricFog, VolumetricLight}, pbr::{FogVolume, VolumetricFog, VolumetricLight},
prelude::*, prelude::*,
render::world_sync::SyncToRenderWorld,
}; };
/// Entry point. /// Entry point.
@ -44,9 +43,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
// up. // up.
scattering: 1.0, scattering: 1.0,
..default() ..default()
}) });
// indicates that this fog volume needs to be Synchronized to the render world
.insert(SyncToRenderWorld);
// Spawn a bright directional light that illuminates the fog well. // Spawn a bright directional light that illuminates the fog well.
commands.spawn(( commands.spawn((