Skip to content

Commit 5c80d82

Browse files
pcwaltonmockersf
authored andcommitted
Fix a crash that occurs when an off-screen entity's material type changes and the material then becomes visible. (#21410)
PR #20993 attempted to fix a crash that would occur with the following sequence of events: 1. An entity's material changes from type A to type B. (The material *type* must change, not just the material asset.) 2. The `extract_entities_needs_specialization<B>` system runs and adds a new specialization change tick to the entity. 3. The `extract_entities_needs_specialization<A>` system runs and removes the specialization change tick. 4. We crash in rendering because no specialization change tick was present for the entity. Unfortunately, that PR used the presence of the entity in `RenderMaterialInstances` to detect whether the entity is safe to delete from the specialization change tick table. This is incorrect for meshes that change material types while not visible, because only visible meshes are present in `RenderMaterialInstances`. So the above race can still occur if the mesh changes materials while off-screen, which will lead to a crash when the mesh becomes visible. This PR fixes the issue by dividing the process of adding new specialization ticks and the process of removing old specialization ticks into two systems. First, all specialization ticks for all materials are updated; alongside that, we store the *material instance tick*, which is a tick that's updated once per frame. After that, we run `sweep_entities_needing_specialization`, which traverses the `RemovedComponents` list for each material and prunes dead entities from the table if and only if their material instance ticks haven't changed since the last frame. This ensures that the above race can't happen, regardless of whether the meshes are present in `RenderMaterialInstances`. Having to have two separate specialization ticks, one being the standard Bevy system tick and one being a special material instance tick that's updated once per frame, is unfortunate, but it seemed to me like the least bad option. We should be able to get rid of all of this when we have untyped materials.
1 parent 95d008e commit 5c80d82

5 files changed

Lines changed: 179 additions & 31 deletions

File tree

crates/bevy_pbr/src/material.rs

Lines changed: 117 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -391,9 +391,16 @@ where
391391
early_sweep_material_instances::<M>
392392
.after(MaterialExtractionSystems)
393393
.before(late_sweep_material_instances),
394+
// See the comments in
395+
// `sweep_entities_needing_specialization` for an
396+
// explanation of why the systems are ordered this way.
394397
extract_entities_needs_specialization::<M>
398+
.in_set(MaterialExtractEntitiesNeedingSpecializationSystems),
399+
sweep_entities_needing_specialization::<M>
400+
.after(MaterialExtractEntitiesNeedingSpecializationSystems)
401+
.after(MaterialExtractionSystems)
395402
.after(extract_cameras)
396-
.after(MaterialExtractionSystems),
403+
.before(late_sweep_material_instances),
397404
),
398405
);
399406
}
@@ -608,6 +615,11 @@ pub struct RenderMaterialInstance {
608615
#[derive(SystemSet, Clone, PartialEq, Eq, Debug, Hash)]
609616
pub struct MaterialExtractionSystems;
610617

618+
/// A [`SystemSet`] that contains all `extract_entities_needs_specialization`
619+
/// systems.
620+
#[derive(SystemSet, Clone, PartialEq, Eq, Debug, Hash)]
621+
pub struct MaterialExtractEntitiesNeedingSpecializationSystems;
622+
611623
/// Deprecated alias for [`MaterialExtractionSystems`].
612624
#[deprecated(since = "0.17.0", note = "Renamed to `MaterialExtractionSystems`.")]
613625
pub type ExtractMaterialsSet = MaterialExtractionSystems;
@@ -754,10 +766,10 @@ fn early_sweep_material_instances<M>(
754766
/// Removes mesh materials from [`RenderMaterialInstances`] when their
755767
/// [`ViewVisibility`] components are removed.
756768
///
757-
/// This runs after all invocations of [`early_sweep_material_instances`] and is
769+
/// This runs after all invocations of `early_sweep_material_instances` and is
758770
/// responsible for bumping [`RenderMaterialInstances::current_change_tick`] in
759771
/// preparation for a new frame.
760-
pub(crate) fn late_sweep_material_instances(
772+
pub fn late_sweep_material_instances(
761773
mut material_instances: ResMut<RenderMaterialInstances>,
762774
mut removed_meshes_query: Extract<RemovedComponents<Mesh3d>>,
763775
) {
@@ -781,7 +793,39 @@ pub(crate) fn late_sweep_material_instances(
781793

782794
pub fn extract_entities_needs_specialization<M>(
783795
entities_needing_specialization: Extract<Res<EntitiesNeedingSpecialization<M>>>,
784-
material_instances: Res<RenderMaterialInstances>,
796+
mut entity_specialization_ticks: ResMut<EntitySpecializationTicks>,
797+
render_material_instances: Res<RenderMaterialInstances>,
798+
ticks: SystemChangeTick,
799+
) where
800+
M: Material,
801+
{
802+
for entity in entities_needing_specialization.iter() {
803+
// Update the entity's specialization tick with this run's tick
804+
entity_specialization_ticks.insert(
805+
(*entity).into(),
806+
EntitySpecializationTickPair {
807+
system_tick: ticks.this_run(),
808+
material_instances_tick: render_material_instances.current_change_tick,
809+
},
810+
);
811+
}
812+
}
813+
814+
/// A system that runs after all instances of
815+
/// [`extract_entities_needs_specialization`] in order to delete specialization
816+
/// ticks for entities that are no longer renderable.
817+
///
818+
/// We delete entities from the [`EntitySpecializationTicks`] table *after*
819+
/// updating it with newly-discovered renderable entities in order to handle the
820+
/// case in which a single entity changes material types. If we naïvely removed
821+
/// entities from that table when their [`MeshMaterial3d<M>`] components were
822+
/// removed, and an entity changed material types, we might end up adding a new
823+
/// set of [`EntitySpecializationTickPair`] for the new material and then
824+
/// deleting it upon detecting the removed component for the old material.
825+
/// Deferring [`sweep_entities_needing_specialization`] to the end allows us to
826+
/// detect the case in which another material type updated the entity
827+
/// specialization ticks this frame and avoid deleting it if so.
828+
pub fn sweep_entities_needing_specialization<M>(
785829
mut entity_specialization_ticks: ResMut<EntitySpecializationTicks>,
786830
mut removed_mesh_material_components: Extract<RemovedComponents<MeshMaterial3d<M>>>,
787831
mut specialized_material_pipeline_cache: ResMut<SpecializedMaterialPipelineCache>,
@@ -791,24 +835,31 @@ pub fn extract_entities_needs_specialization<M>(
791835
mut specialized_shadow_material_pipeline_cache: Option<
792836
ResMut<SpecializedShadowMaterialPipelineCache>,
793837
>,
838+
render_material_instances: Res<RenderMaterialInstances>,
794839
views: Query<&ExtractedView>,
795-
ticks: SystemChangeTick,
796840
) where
797841
M: Material,
798842
{
799843
// Clean up any despawned entities, we do this first in case the removed material was re-added
800844
// the same frame, thus will appear both in the removed components list and have been added to
801845
// the `EntitiesNeedingSpecialization` collection by triggering the `Changed` filter
802846
//
803-
// Additionally, we need to make sure that we are careful about materials that could have changed
804-
// type, e.g. from a `StandardMaterial` to a `CustomMaterial`, as this will also appear in the
805-
// removed components list. As such, we make sure that this system runs after `MaterialExtractionSystems`
806-
// so that the `RenderMaterialInstances` bookkeeping has already been done, and we can check if the entity
807-
// still has a valid material instance.
847+
// Additionally, we need to make sure that we are careful about materials
848+
// that could have changed type, e.g. from a `StandardMaterial` to a
849+
// `CustomMaterial`, as this will also appear in the removed components
850+
// list. As such, we make sure that this system runs after
851+
// `extract_entities_needs_specialization` so that the entity specialization
852+
// tick bookkeeping has already been done, and we can check if the entity's
853+
// tick was updated this frame.
808854
for entity in removed_mesh_material_components.read() {
809-
if material_instances
810-
.instances
811-
.contains_key(&MainEntity::from(entity))
855+
// If the entity's specialization tick was updated this frame, that
856+
// means that that entity changed materials this frame. Don't remove the
857+
// entity from the table in that case.
858+
if entity_specialization_ticks
859+
.get(&MainEntity::from(entity))
860+
.is_some_and(|ticks| {
861+
ticks.material_instances_tick == render_material_instances.current_change_tick
862+
})
812863
{
813864
continue;
814865
}
@@ -834,11 +885,6 @@ pub fn extract_entities_needs_specialization<M>(
834885
}
835886
}
836887
}
837-
838-
for entity in entities_needing_specialization.iter() {
839-
// Update the entity's specialization tick with this run's tick
840-
entity_specialization_ticks.insert((*entity).into(), ticks.this_run());
841-
}
842888
}
843889

844890
#[derive(Resource, Deref, DerefMut, Clone, Debug)]
@@ -857,10 +903,58 @@ impl<M> Default for EntitiesNeedingSpecialization<M> {
857903
}
858904
}
859905

906+
/// Stores ticks specifying the last time Bevy specialized the pipelines of each
907+
/// entity.
908+
///
909+
/// Every entity that has a mesh and material must be present in this table,
910+
/// even if that mesh isn't visible.
860911
#[derive(Resource, Deref, DerefMut, Default, Clone, Debug)]
861912
pub struct EntitySpecializationTicks {
913+
/// A mapping from each main entity to ticks that specify the last time this
914+
/// entity's pipeline was specialized.
915+
///
916+
/// Every entity that has a mesh and material must be present in this table,
917+
/// even if that mesh isn't visible.
862918
#[deref]
863-
pub entities: MainEntityHashMap<Tick>,
919+
pub entities: MainEntityHashMap<EntitySpecializationTickPair>,
920+
}
921+
922+
/// Ticks that specify the last time an entity's pipeline was specialized.
923+
///
924+
/// We need two different types of ticks here for a subtle reason. First, we
925+
/// need the [`Self::system_tick`], which maps to Bevy's [`SystemChangeTick`],
926+
/// because that's what we use in [`specialize_material_meshes`] to check
927+
/// whether pipelines need specialization. But we also need
928+
/// [`Self::material_instances_tick`], which maps to the
929+
/// [`RenderMaterialInstances::current_change_tick`]. That's because the latter
930+
/// only changes once per frame, which is a guarantee we need to handle the
931+
/// following case:
932+
///
933+
/// 1. The app removes material A from a mesh and replaces it with material B.
934+
/// Both A and B are of different [`Material`] types entirely.
935+
///
936+
/// 2. [`extract_entities_needs_specialization`] runs for material B and marks
937+
/// the mesh as up to date by recording the current tick.
938+
///
939+
/// 3. [`sweep_entities_needing_specialization`] runs for material A and checks
940+
/// to ensure it's safe to remove the [`EntitySpecializationTickPair`] for the mesh
941+
/// from the [`EntitySpecializationTicks`]. To do this, it needs to know
942+
/// whether [`extract_entities_needs_specialization`] for some *different*
943+
/// material (in this case, material B) ran earlier in the frame and updated the
944+
/// change tick, and to skip removing the [`EntitySpecializationTickPair`] if so.
945+
/// It can't reliably use the [`Self::system_tick`] to determine this because
946+
/// the [`SystemChangeTick`] can be updated multiple times in the same frame.
947+
/// Instead, it needs a type of tick that's updated only once per frame, after
948+
/// all materials' versions of [`sweep_entities_needing_specialization`] have
949+
/// run. The [`RenderMaterialInstances`] tick satisfies this criterion, and so
950+
/// that's what [`sweep_entities_needing_specialization`] uses.
951+
#[derive(Clone, Copy, Debug)]
952+
pub struct EntitySpecializationTickPair {
953+
/// The standard Bevy system tick.
954+
pub system_tick: Tick,
955+
/// The tick in [`RenderMaterialInstances`], which is updated in
956+
/// `late_sweep_material_instances`.
957+
pub material_instances_tick: Tick,
864958
}
865959

866960
/// Stores the [`SpecializedMaterialViewPipelineCache`] for each view.
@@ -970,7 +1064,10 @@ pub fn specialize_material_meshes(
9701064
else {
9711065
continue;
9721066
};
973-
let entity_tick = entity_specialization_ticks.get(visible_entity).unwrap();
1067+
let entity_tick = entity_specialization_ticks
1068+
.get(visible_entity)
1069+
.unwrap()
1070+
.system_tick;
9741071
let last_specialized_tick = view_specialized_material_pipeline_cache
9751072
.get(visible_entity)
9761073
.map(|(tick, _)| *tick);

crates/bevy_pbr/src/prepass/mod.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -888,7 +888,10 @@ pub fn specialize_prepass_material_meshes(
888888
else {
889889
continue;
890890
};
891-
let entity_tick = entity_specialization_ticks.get(visible_entity).unwrap();
891+
let entity_tick = entity_specialization_ticks
892+
.get(visible_entity)
893+
.unwrap()
894+
.system_tick;
892895
let last_specialized_tick = view_specialized_material_pipeline_cache
893896
.get(visible_entity)
894897
.map(|(tick, _)| *tick);

crates/bevy_pbr/src/render/light.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1844,7 +1844,9 @@ pub fn specialize_shadows(
18441844
.map(|(tick, _)| *tick);
18451845
let needs_specialization = last_specialized_tick.is_none_or(|tick| {
18461846
view_tick.is_newer_than(tick, ticks.this_run())
1847-
|| entity_tick.is_newer_than(tick, ticks.this_run())
1847+
|| entity_tick
1848+
.system_tick
1849+
.is_newer_than(tick, ticks.this_run())
18481850
});
18491851
if !needs_specialization {
18501852
continue;

crates/bevy_sprite_render/src/mesh2d/material.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ where
283283

284284
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
285285
render_app
286-
.init_resource::<EntitySpecializationTicks<M>>()
286+
.init_resource::<EntitySpecializationTickPair<M>>()
287287
.init_resource::<SpecializedMaterial2dPipelineCache<M>>()
288288
.add_render_command::<Opaque2d, DrawMaterial2d<M>>()
289289
.add_render_command::<AlphaMask2d, DrawMaterial2d<M>>()
@@ -566,7 +566,7 @@ pub const fn tonemapping_pipeline_key(tonemapping: Tonemapping) -> Mesh2dPipelin
566566

567567
pub fn extract_entities_needs_specialization<M>(
568568
entities_needing_specialization: Extract<Res<EntitiesNeedingSpecialization<M>>>,
569-
mut entity_specialization_ticks: ResMut<EntitySpecializationTicks<M>>,
569+
mut entity_specialization_ticks: ResMut<EntitySpecializationTickPair<M>>,
570570
mut removed_mesh_material_components: Extract<RemovedComponents<MeshMaterial2d<M>>>,
571571
mut specialized_material2d_pipeline_cache: ResMut<SpecializedMaterial2dPipelineCache<M>>,
572572
views: Query<&MainEntity, With<ExtractedView>>,
@@ -608,13 +608,13 @@ impl<M> Default for EntitiesNeedingSpecialization<M> {
608608
}
609609

610610
#[derive(Clone, Resource, Deref, DerefMut, Debug)]
611-
pub struct EntitySpecializationTicks<M> {
611+
pub struct EntitySpecializationTickPair<M> {
612612
#[deref]
613613
pub entities: MainEntityHashMap<Tick>,
614614
_marker: PhantomData<M>,
615615
}
616616

617-
impl<M> Default for EntitySpecializationTicks<M> {
617+
impl<M> Default for EntitySpecializationTickPair<M> {
618618
fn default() -> Self {
619619
Self {
620620
entities: MainEntityHashMap::default(),
@@ -702,7 +702,7 @@ pub fn specialize_material2d_meshes<M: Material2d>(
702702
alpha_mask_render_phases: Res<ViewBinnedRenderPhases<AlphaMask2d>>,
703703
views: Query<(&MainEntity, &ExtractedView, &RenderVisibleEntities)>,
704704
view_key_cache: Res<ViewKeyCache>,
705-
entity_specialization_ticks: Res<EntitySpecializationTicks<M>>,
705+
entity_specialization_ticks: Res<EntitySpecializationTickPair<M>>,
706706
view_specialization_ticks: Res<ViewSpecializationTicks>,
707707
ticks: SystemChangeTick,
708708
mut specialized_material_pipeline_cache: ResMut<SpecializedMaterial2dPipelineCache<M>>,

examples/3d/manual_material.rs

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ use bevy::{
88
SystemChangeTick, SystemParamItem,
99
},
1010
pbr::{
11-
DrawMaterial, EntitiesNeedingSpecialization, EntitySpecializationTicks,
12-
MaterialBindGroupAllocator, MaterialBindGroupAllocators, MaterialDrawFunction,
11+
late_sweep_material_instances, DrawMaterial, EntitiesNeedingSpecialization,
12+
EntitySpecializationTickPair, EntitySpecializationTicks, MaterialBindGroupAllocator,
13+
MaterialBindGroupAllocators, MaterialDrawFunction,
14+
MaterialExtractEntitiesNeedingSpecializationSystems, MaterialExtractionSystems,
1315
MaterialFragmentShader, MaterialProperties, PreparedMaterial, RenderMaterialBindings,
1416
RenderMaterialInstance, RenderMaterialInstances, SpecializedMaterialPipelineCache,
1517
},
@@ -66,7 +68,12 @@ impl Plugin for ImageMaterialPlugin {
6668
ExtractSchedule,
6769
(
6870
extract_image_materials,
69-
extract_image_materials_needing_specialization,
71+
extract_image_materials_needing_specialization
72+
.in_set(MaterialExtractEntitiesNeedingSpecializationSystems),
73+
sweep_image_materials_needing_specialization
74+
.after(MaterialExtractEntitiesNeedingSpecializationSystems)
75+
.after(MaterialExtractionSystems)
76+
.before(late_sweep_material_instances),
7077
),
7178
);
7279
}
@@ -285,6 +292,7 @@ fn extract_image_materials_needing_specialization(
285292
mut entity_specialization_ticks: ResMut<EntitySpecializationTicks>,
286293
mut removed_mesh_material_components: Extract<RemovedComponents<ImageMaterial3d>>,
287294
mut specialized_material_pipeline_cache: ResMut<SpecializedMaterialPipelineCache>,
295+
render_material_instances: Res<RenderMaterialInstances>,
288296
views: Query<&ExtractedView>,
289297
ticks: SystemChangeTick,
290298
) {
@@ -304,6 +312,44 @@ fn extract_image_materials_needing_specialization(
304312

305313
for entity in entities_needing_specialization.iter() {
306314
// Update the entity's specialization tick with this run's tick
307-
entity_specialization_ticks.insert((*entity).into(), ticks.this_run());
315+
entity_specialization_ticks.insert(
316+
(*entity).into(),
317+
EntitySpecializationTickPair {
318+
system_tick: ticks.this_run(),
319+
material_instances_tick: render_material_instances.current_change_tick,
320+
},
321+
);
322+
}
323+
}
324+
325+
fn sweep_image_materials_needing_specialization(
326+
mut entity_specialization_ticks: ResMut<EntitySpecializationTicks>,
327+
mut removed_mesh_material_components: Extract<RemovedComponents<ImageMaterial3d>>,
328+
mut specialized_material_pipeline_cache: ResMut<SpecializedMaterialPipelineCache>,
329+
render_material_instances: Res<RenderMaterialInstances>,
330+
views: Query<&ExtractedView>,
331+
) {
332+
// Clean up any despawned entities, we do this first in case the removed material was re-added
333+
// the same frame, thus will appear both in the removed components list and have been added to
334+
// the `EntitiesNeedingSpecialization` collection by triggering the `Changed` filter
335+
for entity in removed_mesh_material_components.read() {
336+
if entity_specialization_ticks
337+
.get(&MainEntity::from(entity))
338+
.is_some_and(|ticks| {
339+
ticks.material_instances_tick == render_material_instances.current_change_tick
340+
})
341+
{
342+
continue;
343+
}
344+
345+
entity_specialization_ticks.remove(&MainEntity::from(entity));
346+
347+
for view in views {
348+
if let Some(cache) =
349+
specialized_material_pipeline_cache.get_mut(&view.retained_view_entity)
350+
{
351+
cache.remove(&MainEntity::from(entity));
352+
}
353+
}
308354
}
309355
}

0 commit comments

Comments
 (0)