Skip to content

Commit

Permalink
Type safe retained render world (#15756)
Browse files Browse the repository at this point in the history
# Objective

In the Render World, there are a number of collections that are derived
from Main World entities and are used to drive rendering. The most
notable are:
- `VisibleEntities`, which is generated in the `check_visibility` system
and contains visible entities for a view.
- `ExtractedInstances`, which maps entity ids to asset ids.

In the old model, these collections were trivially kept in sync -- any
extracted phase item could look itself up because the render entity id
was guaranteed to always match the corresponding main world id.

After #15320, this became much more complicated, and was leading to a
number of subtle bugs in the Render World. The main rendering systems,
i.e. `queue_material_meshes` and `queue_material2d_meshes`, follow a
similar pattern:

```rust
for visible_entity in visible_entities.iter::<With<Mesh2d>>() {
    let Some(mesh_instance) = render_mesh_instances.get_mut(visible_entity) else {
        continue;
    };
            
    // Look some more stuff up and specialize the pipeline...
            
    let bin_key = Opaque2dBinKey {
        pipeline: pipeline_id,
        draw_function: draw_opaque_2d,
        asset_id: mesh_instance.mesh_asset_id.into(),
        material_bind_group_id: material_2d.get_bind_group_id().0,
    };
    opaque_phase.add(
        bin_key,
        *visible_entity,
        BinnedRenderPhaseType::mesh(mesh_instance.automatic_batching),
    );
}
```

In this case, `visible_entities` and `render_mesh_instances` are both
collections that are created and keyed by Main World entity ids, and so
this lookup happens to work by coincidence. However, there is a major
unintentional bug here: namely, because `visible_entities` is a
collection of Main World ids, the phase item being queued is created
with a Main World id rather than its correct Render World id.

This happens to not break mesh rendering because the render commands
used for drawing meshes do not access the `ItemQuery` parameter, but
demonstrates the confusion that is now possible: our UI phase items are
correctly being queued with Render World ids while our meshes aren't.

Additionally, this makes it very easy and error prone to use the wrong
entity id to look up things like assets. For example, if instead we
ignored visibility checks and queued our meshes via a query, we'd have
to be extra careful to use `&MainEntity` instead of the natural
`Entity`.

## Solution

Make all collections that are derived from Main World data use
`MainEntity` as their key, to ensure type safety and avoid accidentally
looking up data with the wrong entity id:

```rust
pub type MainEntityHashMap<V> = hashbrown::HashMap<MainEntity, V, EntityHash>;
```

Additionally, we make all `PhaseItem` be able to provide both their Main
and Render World ids, to allow render phase implementors maximum
flexibility as to what id should be used to look up data.

You can think of this like tracking at the type level whether something
in the Render World should use it's "primary key", i.e. entity id, or
needs to use a foreign key, i.e. `MainEntity`.

## Testing

##### TODO:

This will require extensive testing to make sure things didn't break!
Additionally, some extraction logic has become more complicated and
needs to be checked for regressions.

## Migration Guide

With the advent of the retained render world, collections that contain
references to `Entity` that are extracted into the render world have
been changed to contain `MainEntity` in order to prevent errors where a
render world entity id is used to look up an item by accident. Custom
rendering code may need to be changed to query for `&MainEntity` in
order to look up the correct item from such a collection. Additionally,
users who implement their own extraction logic for collections of main
world entity should strongly consider extracting into a different
collection that uses `MainEntity` as a key.

Additionally, render phases now require specifying both the `Entity` and
`MainEntity` for a given `PhaseItem`. Custom render phases should ensure
`MainEntity` is available when queuing a phase item.
  • Loading branch information
tychedelia authored Oct 10, 2024
1 parent 754194f commit dd812b3
Show file tree
Hide file tree
Showing 37 changed files with 584 additions and 290 deletions.
34 changes: 24 additions & 10 deletions crates/bevy_core_pipeline/src/core_2d/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,11 @@ pub use camera_2d::*;
pub use main_opaque_pass_2d_node::*;
pub use main_transparent_pass_2d_node::*;

use crate::{tonemapping::TonemappingNode, upscaling::UpscalingNode};
use bevy_app::{App, Plugin};
use bevy_ecs::{entity::EntityHashSet, prelude::*};
use bevy_math::FloatOrd;
use bevy_render::sync_world::MainEntity;
use bevy_render::{
camera::{Camera, ExtractedCamera},
extract_component::ExtractComponentPlugin,
Expand All @@ -61,8 +63,6 @@ use bevy_render::{
Extract, ExtractSchedule, Render, RenderApp, RenderSet,
};

use crate::{tonemapping::TonemappingNode, upscaling::UpscalingNode};

use self::graph::{Core2d, Node2d};

pub const CORE_2D_DEPTH_FORMAT: TextureFormat = TextureFormat::Depth32Float;
Expand Down Expand Up @@ -129,7 +129,7 @@ pub struct Opaque2d {
pub key: Opaque2dBinKey,
/// An entity from which data will be fetched, including the mesh if
/// applicable.
pub representative_entity: Entity,
pub representative_entity: (Entity, MainEntity),
/// The ranges of instances.
pub batch_range: Range<u32>,
/// An extra index, which is either a dynamic offset or an index in the
Expand All @@ -156,7 +156,11 @@ pub struct Opaque2dBinKey {
impl PhaseItem for Opaque2d {
#[inline]
fn entity(&self) -> Entity {
self.representative_entity
self.representative_entity.0
}

fn main_entity(&self) -> MainEntity {
self.representative_entity.1
}

#[inline]
Expand Down Expand Up @@ -188,7 +192,7 @@ impl BinnedPhaseItem for Opaque2d {

fn new(
key: Self::BinKey,
representative_entity: Entity,
representative_entity: (Entity, MainEntity),
batch_range: Range<u32>,
extra_index: PhaseItemExtraIndex,
) -> Self {
Expand All @@ -214,7 +218,7 @@ pub struct AlphaMask2d {
pub key: AlphaMask2dBinKey,
/// An entity from which data will be fetched, including the mesh if
/// applicable.
pub representative_entity: Entity,
pub representative_entity: (Entity, MainEntity),
/// The ranges of instances.
pub batch_range: Range<u32>,
/// An extra index, which is either a dynamic offset or an index in the
Expand All @@ -241,7 +245,12 @@ pub struct AlphaMask2dBinKey {
impl PhaseItem for AlphaMask2d {
#[inline]
fn entity(&self) -> Entity {
self.representative_entity
self.representative_entity.0
}

#[inline]
fn main_entity(&self) -> MainEntity {
self.representative_entity.1
}

#[inline]
Expand Down Expand Up @@ -273,7 +282,7 @@ impl BinnedPhaseItem for AlphaMask2d {

fn new(
key: Self::BinKey,
representative_entity: Entity,
representative_entity: (Entity, MainEntity),
batch_range: Range<u32>,
extra_index: PhaseItemExtraIndex,
) -> Self {
Expand All @@ -296,7 +305,7 @@ impl CachedRenderPipelinePhaseItem for AlphaMask2d {
/// Transparent 2D [`SortedPhaseItem`]s.
pub struct Transparent2d {
pub sort_key: FloatOrd,
pub entity: Entity,
pub entity: (Entity, MainEntity),
pub pipeline: CachedRenderPipelineId,
pub draw_function: DrawFunctionId,
pub batch_range: Range<u32>,
Expand All @@ -306,7 +315,12 @@ pub struct Transparent2d {
impl PhaseItem for Transparent2d {
#[inline]
fn entity(&self) -> Entity {
self.entity
self.entity.0
}

#[inline]
fn main_entity(&self) -> MainEntity {
self.entity.1
}

#[inline]
Expand Down
39 changes: 29 additions & 10 deletions crates/bevy_core_pipeline/src/core_3d/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ pub use main_transparent_pass_3d_node::*;
use bevy_app::{App, Plugin, PostUpdate};
use bevy_ecs::{entity::EntityHashSet, prelude::*};
use bevy_math::FloatOrd;
use bevy_render::sync_world::MainEntity;
use bevy_render::{
camera::{Camera, ExtractedCamera},
extract_component::ExtractComponentPlugin,
Expand Down Expand Up @@ -214,7 +215,7 @@ pub struct Opaque3d {
pub key: Opaque3dBinKey,
/// An entity from which data will be fetched, including the mesh if
/// applicable.
pub representative_entity: Entity,
pub representative_entity: (Entity, MainEntity),
/// The ranges of instances.
pub batch_range: Range<u32>,
/// An extra index, which is either a dynamic offset or an index in the
Expand Down Expand Up @@ -249,7 +250,12 @@ pub struct Opaque3dBinKey {
impl PhaseItem for Opaque3d {
#[inline]
fn entity(&self) -> Entity {
self.representative_entity
self.representative_entity.0
}

#[inline]
fn main_entity(&self) -> MainEntity {
self.representative_entity.1
}

#[inline]
Expand Down Expand Up @@ -282,7 +288,7 @@ impl BinnedPhaseItem for Opaque3d {
#[inline]
fn new(
key: Self::BinKey,
representative_entity: Entity,
representative_entity: (Entity, MainEntity),
batch_range: Range<u32>,
extra_index: PhaseItemExtraIndex,
) -> Self {
Expand All @@ -304,15 +310,19 @@ impl CachedRenderPipelinePhaseItem for Opaque3d {

pub struct AlphaMask3d {
pub key: OpaqueNoLightmap3dBinKey,
pub representative_entity: Entity,
pub representative_entity: (Entity, MainEntity),
pub batch_range: Range<u32>,
pub extra_index: PhaseItemExtraIndex,
}

impl PhaseItem for AlphaMask3d {
#[inline]
fn entity(&self) -> Entity {
self.representative_entity
self.representative_entity.0
}

fn main_entity(&self) -> MainEntity {
self.representative_entity.1
}

#[inline]
Expand Down Expand Up @@ -347,7 +357,7 @@ impl BinnedPhaseItem for AlphaMask3d {
#[inline]
fn new(
key: Self::BinKey,
representative_entity: Entity,
representative_entity: (Entity, MainEntity),
batch_range: Range<u32>,
extra_index: PhaseItemExtraIndex,
) -> Self {
Expand All @@ -370,7 +380,7 @@ impl CachedRenderPipelinePhaseItem for AlphaMask3d {
pub struct Transmissive3d {
pub distance: f32,
pub pipeline: CachedRenderPipelineId,
pub entity: Entity,
pub entity: (Entity, MainEntity),
pub draw_function: DrawFunctionId,
pub batch_range: Range<u32>,
pub extra_index: PhaseItemExtraIndex,
Expand All @@ -390,7 +400,12 @@ impl PhaseItem for Transmissive3d {

#[inline]
fn entity(&self) -> Entity {
self.entity
self.entity.0
}

#[inline]
fn main_entity(&self) -> MainEntity {
self.entity.1
}

#[inline]
Expand Down Expand Up @@ -444,7 +459,7 @@ impl CachedRenderPipelinePhaseItem for Transmissive3d {
pub struct Transparent3d {
pub distance: f32,
pub pipeline: CachedRenderPipelineId,
pub entity: Entity,
pub entity: (Entity, MainEntity),
pub draw_function: DrawFunctionId,
pub batch_range: Range<u32>,
pub extra_index: PhaseItemExtraIndex,
Expand All @@ -453,7 +468,11 @@ pub struct Transparent3d {
impl PhaseItem for Transparent3d {
#[inline]
fn entity(&self) -> Entity {
self.entity
self.entity.0
}

fn main_entity(&self) -> MainEntity {
self.entity.1
}

#[inline]
Expand Down
25 changes: 17 additions & 8 deletions crates/bevy_core_pipeline/src/deferred/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ pub mod node;

use core::ops::Range;

use crate::prepass::OpaqueNoLightmap3dBinKey;
use bevy_ecs::prelude::*;
use bevy_render::sync_world::MainEntity;
use bevy_render::{
render_phase::{
BinnedPhaseItem, CachedRenderPipelinePhaseItem, DrawFunctionId, PhaseItem,
Expand All @@ -12,8 +14,6 @@ use bevy_render::{
render_resource::{CachedRenderPipelineId, TextureFormat},
};

use crate::prepass::OpaqueNoLightmap3dBinKey;

pub const DEFERRED_PREPASS_FORMAT: TextureFormat = TextureFormat::Rgba32Uint;
pub const DEFERRED_LIGHTING_PASS_ID_FORMAT: TextureFormat = TextureFormat::R8Uint;
pub const DEFERRED_LIGHTING_PASS_ID_DEPTH_FORMAT: TextureFormat = TextureFormat::Depth16Unorm;
Expand All @@ -26,15 +26,19 @@ pub const DEFERRED_LIGHTING_PASS_ID_DEPTH_FORMAT: TextureFormat = TextureFormat:
#[derive(PartialEq, Eq, Hash)]
pub struct Opaque3dDeferred {
pub key: OpaqueNoLightmap3dBinKey,
pub representative_entity: Entity,
pub representative_entity: (Entity, MainEntity),
pub batch_range: Range<u32>,
pub extra_index: PhaseItemExtraIndex,
}

impl PhaseItem for Opaque3dDeferred {
#[inline]
fn entity(&self) -> Entity {
self.representative_entity
self.representative_entity.0
}

fn main_entity(&self) -> MainEntity {
self.representative_entity.1
}

#[inline]
Expand Down Expand Up @@ -69,7 +73,7 @@ impl BinnedPhaseItem for Opaque3dDeferred {
#[inline]
fn new(
key: Self::BinKey,
representative_entity: Entity,
representative_entity: (Entity, MainEntity),
batch_range: Range<u32>,
extra_index: PhaseItemExtraIndex,
) -> Self {
Expand All @@ -96,15 +100,20 @@ impl CachedRenderPipelinePhaseItem for Opaque3dDeferred {
/// Used to render all meshes with a material with an alpha mask.
pub struct AlphaMask3dDeferred {
pub key: OpaqueNoLightmap3dBinKey,
pub representative_entity: Entity,
pub representative_entity: (Entity, MainEntity),
pub batch_range: Range<u32>,
pub extra_index: PhaseItemExtraIndex,
}

impl PhaseItem for AlphaMask3dDeferred {
#[inline]
fn entity(&self) -> Entity {
self.representative_entity
self.representative_entity.0
}

#[inline]
fn main_entity(&self) -> MainEntity {
self.representative_entity.1
}

#[inline]
Expand Down Expand Up @@ -138,7 +147,7 @@ impl BinnedPhaseItem for AlphaMask3dDeferred {

fn new(
key: Self::BinKey,
representative_entity: Entity,
representative_entity: (Entity, MainEntity),
batch_range: Range<u32>,
extra_index: PhaseItemExtraIndex,
) -> Self {
Expand Down
25 changes: 16 additions & 9 deletions crates/bevy_core_pipeline/src/prepass/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,12 @@ pub mod node;

use core::ops::Range;

use crate::deferred::{DEFERRED_LIGHTING_PASS_ID_FORMAT, DEFERRED_PREPASS_FORMAT};
use bevy_asset::UntypedAssetId;
use bevy_ecs::prelude::*;
use bevy_math::Mat4;
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::sync_world::MainEntity;
use bevy_render::{
render_phase::{
BinnedPhaseItem, CachedRenderPipelinePhaseItem, DrawFunctionId, PhaseItem,
Expand All @@ -45,8 +47,6 @@ use bevy_render::{
texture::ColorAttachment,
};

use crate::deferred::{DEFERRED_LIGHTING_PASS_ID_FORMAT, DEFERRED_PREPASS_FORMAT};

pub const NORMAL_PREPASS_FORMAT: TextureFormat = TextureFormat::Rgb10a2Unorm;
pub const MOTION_VECTOR_PREPASS_FORMAT: TextureFormat = TextureFormat::Rg16Float;

Expand Down Expand Up @@ -143,8 +143,7 @@ pub struct Opaque3dPrepass {

/// An entity from which Bevy fetches data common to all instances in this
/// batch, such as the mesh.
pub representative_entity: Entity,

pub representative_entity: (Entity, MainEntity),
pub batch_range: Range<u32>,
pub extra_index: PhaseItemExtraIndex,
}
Expand All @@ -171,7 +170,11 @@ pub struct OpaqueNoLightmap3dBinKey {
impl PhaseItem for Opaque3dPrepass {
#[inline]
fn entity(&self) -> Entity {
self.representative_entity
self.representative_entity.0
}

fn main_entity(&self) -> MainEntity {
self.representative_entity.1
}

#[inline]
Expand Down Expand Up @@ -206,7 +209,7 @@ impl BinnedPhaseItem for Opaque3dPrepass {
#[inline]
fn new(
key: Self::BinKey,
representative_entity: Entity,
representative_entity: (Entity, MainEntity),
batch_range: Range<u32>,
extra_index: PhaseItemExtraIndex,
) -> Self {
Expand All @@ -233,15 +236,19 @@ impl CachedRenderPipelinePhaseItem for Opaque3dPrepass {
/// Used to render all meshes with a material with an alpha mask.
pub struct AlphaMask3dPrepass {
pub key: OpaqueNoLightmap3dBinKey,
pub representative_entity: Entity,
pub representative_entity: (Entity, MainEntity),
pub batch_range: Range<u32>,
pub extra_index: PhaseItemExtraIndex,
}

impl PhaseItem for AlphaMask3dPrepass {
#[inline]
fn entity(&self) -> Entity {
self.representative_entity
self.representative_entity.0
}

fn main_entity(&self) -> MainEntity {
self.representative_entity.1
}

#[inline]
Expand Down Expand Up @@ -276,7 +283,7 @@ impl BinnedPhaseItem for AlphaMask3dPrepass {
#[inline]
fn new(
key: Self::BinKey,
representative_entity: Entity,
representative_entity: (Entity, MainEntity),
batch_range: Range<u32>,
extra_index: PhaseItemExtraIndex,
) -> Self {
Expand Down
Loading

0 comments on commit dd812b3

Please sign in to comment.