@@ -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 ) ]
609616pub 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`." ) ]
613625pub 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
782794pub 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 ) ]
861912pub 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) ;
0 commit comments