@@ -387,9 +387,16 @@ where
387387 early_sweep_material_instances :: < M >
388388 . after ( MaterialExtractionSystems )
389389 . before ( late_sweep_material_instances) ,
390+ // See the comments in
391+ // `sweep_entities_needing_specialization` for an
392+ // explanation of why the systems are ordered this way.
390393 extract_entities_needs_specialization :: < M >
394+ . in_set ( MaterialExtractEntitiesNeedingSpecializationSystems ) ,
395+ sweep_entities_needing_specialization :: < M >
396+ . after ( MaterialExtractEntitiesNeedingSpecializationSystems )
397+ . after ( MaterialExtractionSystems )
391398 . after ( extract_cameras)
392- . after ( MaterialExtractionSystems ) ,
399+ . before ( late_sweep_material_instances ) ,
393400 ) ,
394401 ) ;
395402 }
@@ -604,6 +611,11 @@ pub struct RenderMaterialInstance {
604611#[ derive( SystemSet , Clone , PartialEq , Eq , Debug , Hash ) ]
605612pub struct MaterialExtractionSystems ;
606613
614+ /// A [`SystemSet`] that contains all `extract_entities_needs_specialization`
615+ /// systems.
616+ #[ derive( SystemSet , Clone , PartialEq , Eq , Debug , Hash ) ]
617+ pub struct MaterialExtractEntitiesNeedingSpecializationSystems ;
618+
607619/// Deprecated alias for [`MaterialExtractionSystems`].
608620#[ deprecated( since = "0.17.0" , note = "Renamed to `MaterialExtractionSystems`." ) ]
609621pub type ExtractMaterialsSet = MaterialExtractionSystems ;
@@ -750,10 +762,10 @@ fn early_sweep_material_instances<M>(
750762/// Removes mesh materials from [`RenderMaterialInstances`] when their
751763/// [`ViewVisibility`] components are removed.
752764///
753- /// This runs after all invocations of [ `early_sweep_material_instances`] and is
765+ /// This runs after all invocations of `early_sweep_material_instances` and is
754766/// responsible for bumping [`RenderMaterialInstances::current_change_tick`] in
755767/// preparation for a new frame.
756- pub ( crate ) fn late_sweep_material_instances (
768+ pub fn late_sweep_material_instances (
757769 mut material_instances : ResMut < RenderMaterialInstances > ,
758770 mut removed_meshes_query : Extract < RemovedComponents < Mesh3d > > ,
759771) {
@@ -777,7 +789,39 @@ pub(crate) fn late_sweep_material_instances(
777789
778790pub fn extract_entities_needs_specialization < M > (
779791 entities_needing_specialization : Extract < Res < EntitiesNeedingSpecialization < M > > > ,
780- material_instances : Res < RenderMaterialInstances > ,
792+ mut entity_specialization_ticks : ResMut < EntitySpecializationTicks > ,
793+ render_material_instances : Res < RenderMaterialInstances > ,
794+ ticks : SystemChangeTick ,
795+ ) where
796+ M : Material ,
797+ {
798+ for entity in entities_needing_specialization. iter ( ) {
799+ // Update the entity's specialization tick with this run's tick
800+ entity_specialization_ticks. insert (
801+ ( * entity) . into ( ) ,
802+ EntitySpecializationTickPair {
803+ system_tick : ticks. this_run ( ) ,
804+ material_instances_tick : render_material_instances. current_change_tick ,
805+ } ,
806+ ) ;
807+ }
808+ }
809+
810+ /// A system that runs after all instances of
811+ /// [`extract_entities_needs_specialization`] in order to delete specialization
812+ /// ticks for entities that are no longer renderable.
813+ ///
814+ /// We delete entities from the [`EntitySpecializationTicks`] table *after*
815+ /// updating it with newly-discovered renderable entities in order to handle the
816+ /// case in which a single entity changes material types. If we naïvely removed
817+ /// entities from that table when their [`MeshMaterial3d<M>`] components were
818+ /// removed, and an entity changed material types, we might end up adding a new
819+ /// set of [`EntitySpecializationTickPair`] for the new material and then
820+ /// deleting it upon detecting the removed component for the old material.
821+ /// Deferring [`sweep_entities_needing_specialization`] to the end allows us to
822+ /// detect the case in which another material type updated the entity
823+ /// specialization ticks this frame and avoid deleting it if so.
824+ pub fn sweep_entities_needing_specialization < M > (
781825 mut entity_specialization_ticks : ResMut < EntitySpecializationTicks > ,
782826 mut removed_mesh_material_components : Extract < RemovedComponents < MeshMaterial3d < M > > > ,
783827 mut specialized_material_pipeline_cache : ResMut < SpecializedMaterialPipelineCache > ,
@@ -787,24 +831,31 @@ pub fn extract_entities_needs_specialization<M>(
787831 mut specialized_shadow_material_pipeline_cache : Option <
788832 ResMut < SpecializedShadowMaterialPipelineCache > ,
789833 > ,
834+ render_material_instances : Res < RenderMaterialInstances > ,
790835 views : Query < & ExtractedView > ,
791- ticks : SystemChangeTick ,
792836) where
793837 M : Material ,
794838{
795839 // Clean up any despawned entities, we do this first in case the removed material was re-added
796840 // the same frame, thus will appear both in the removed components list and have been added to
797841 // the `EntitiesNeedingSpecialization` collection by triggering the `Changed` filter
798842 //
799- // Additionally, we need to make sure that we are careful about materials that could have changed
800- // type, e.g. from a `StandardMaterial` to a `CustomMaterial`, as this will also appear in the
801- // removed components list. As such, we make sure that this system runs after `MaterialExtractionSystems`
802- // so that the `RenderMaterialInstances` bookkeeping has already been done, and we can check if the entity
803- // still has a valid material instance.
843+ // Additionally, we need to make sure that we are careful about materials
844+ // that could have changed type, e.g. from a `StandardMaterial` to a
845+ // `CustomMaterial`, as this will also appear in the removed components
846+ // list. As such, we make sure that this system runs after
847+ // `extract_entities_needs_specialization` so that the entity specialization
848+ // tick bookkeeping has already been done, and we can check if the entity's
849+ // tick was updated this frame.
804850 for entity in removed_mesh_material_components. read ( ) {
805- if material_instances
806- . instances
807- . contains_key ( & MainEntity :: from ( entity) )
851+ // If the entity's specialization tick was updated this frame, that
852+ // means that that entity changed materials this frame. Don't remove the
853+ // entity from the table in that case.
854+ if entity_specialization_ticks
855+ . get ( & MainEntity :: from ( entity) )
856+ . is_some_and ( |ticks| {
857+ ticks. material_instances_tick == render_material_instances. current_change_tick
858+ } )
808859 {
809860 continue ;
810861 }
@@ -830,11 +881,6 @@ pub fn extract_entities_needs_specialization<M>(
830881 }
831882 }
832883 }
833-
834- for entity in entities_needing_specialization. iter ( ) {
835- // Update the entity's specialization tick with this run's tick
836- entity_specialization_ticks. insert ( ( * entity) . into ( ) , ticks. this_run ( ) ) ;
837- }
838884}
839885
840886#[ derive( Resource , Deref , DerefMut , Clone , Debug ) ]
@@ -853,10 +899,58 @@ impl<M> Default for EntitiesNeedingSpecialization<M> {
853899 }
854900}
855901
902+ /// Stores ticks specifying the last time Bevy specialized the pipelines of each
903+ /// entity.
904+ ///
905+ /// Every entity that has a mesh and material must be present in this table,
906+ /// even if that mesh isn't visible.
856907#[ derive( Resource , Deref , DerefMut , Default , Clone , Debug ) ]
857908pub struct EntitySpecializationTicks {
909+ /// A mapping from each main entity to ticks that specify the last time this
910+ /// entity's pipeline was specialized.
911+ ///
912+ /// Every entity that has a mesh and material must be present in this table,
913+ /// even if that mesh isn't visible.
858914 #[ deref]
859- pub entities : MainEntityHashMap < Tick > ,
915+ pub entities : MainEntityHashMap < EntitySpecializationTickPair > ,
916+ }
917+
918+ /// Ticks that specify the last time an entity's pipeline was specialized.
919+ ///
920+ /// We need two different types of ticks here for a subtle reason. First, we
921+ /// need the [`Self::system_tick`], which maps to Bevy's [`SystemChangeTick`],
922+ /// because that's what we use in [`specialize_material_meshes`] to check
923+ /// whether pipelines need specialization. But we also need
924+ /// [`Self::material_instances_tick`], which maps to the
925+ /// [`RenderMaterialInstances::current_change_tick`]. That's because the latter
926+ /// only changes once per frame, which is a guarantee we need to handle the
927+ /// following case:
928+ ///
929+ /// 1. The app removes material A from a mesh and replaces it with material B.
930+ /// Both A and B are of different [`Material`] types entirely.
931+ ///
932+ /// 2. [`extract_entities_needs_specialization`] runs for material B and marks
933+ /// the mesh as up to date by recording the current tick.
934+ ///
935+ /// 3. [`sweep_entities_needing_specialization`] runs for material A and checks
936+ /// to ensure it's safe to remove the [`EntitySpecializationTickPair`] for the mesh
937+ /// from the [`EntitySpecializationTicks`]. To do this, it needs to know
938+ /// whether [`extract_entities_needs_specialization`] for some *different*
939+ /// material (in this case, material B) ran earlier in the frame and updated the
940+ /// change tick, and to skip removing the [`EntitySpecializationTickPair`] if so.
941+ /// It can't reliably use the [`Self::system_tick`] to determine this because
942+ /// the [`SystemChangeTick`] can be updated multiple times in the same frame.
943+ /// Instead, it needs a type of tick that's updated only once per frame, after
944+ /// all materials' versions of [`sweep_entities_needing_specialization`] have
945+ /// run. The [`RenderMaterialInstances`] tick satisfies this criterion, and so
946+ /// that's what [`sweep_entities_needing_specialization`] uses.
947+ #[ derive( Clone , Copy , Debug ) ]
948+ pub struct EntitySpecializationTickPair {
949+ /// The standard Bevy system tick.
950+ pub system_tick : Tick ,
951+ /// The tick in [`RenderMaterialInstances`], which is updated in
952+ /// `late_sweep_material_instances`.
953+ pub material_instances_tick : Tick ,
860954}
861955
862956/// Stores the [`SpecializedMaterialViewPipelineCache`] for each view.
@@ -966,7 +1060,10 @@ pub fn specialize_material_meshes(
9661060 else {
9671061 continue ;
9681062 } ;
969- let entity_tick = entity_specialization_ticks. get ( visible_entity) . unwrap ( ) ;
1063+ let entity_tick = entity_specialization_ticks
1064+ . get ( visible_entity)
1065+ . unwrap ( )
1066+ . system_tick ;
9701067 let last_specialized_tick = view_specialized_material_pipeline_cache
9711068 . get ( visible_entity)
9721069 . map ( |( tick, _) | * tick) ;
0 commit comments