@@ -640,6 +640,81 @@ static void _dev_pixelpipe_synch(dt_dev_pixelpipe_t *pipe,
640640 }
641641}
642642
643+ /** remove stale entries (deleted, disabled or de-synced consumers) from a
644+ raster mask source's users table, so it doesn't keep
645+ publishing/invalidating forever */
646+ static void _iop_prune_stale_raster_users (dt_dev_pixelpipe_t * pipe , dt_iop_module_t * module )
647+ {
648+ GHashTable * users = module -> raster_mask .source .users ;
649+ if (!module -> dev || !users || g_hash_table_size (users ) == 0 )
650+ return ;
651+
652+ /* Phantom users (deleted or de-synced consumers) keep the source republishing
653+ -- and invalidating downstream -- every run. Judge each consumer from its
654+ node in THIS pipe, never from module->enabled/blend_params: those track the
655+ darkroom UI and are stale in the export pipe, where the piece is authoritative
656+ (dropped a live export consumer's mask, regression 0167-raster-mask). We only
657+ compare the consumer pointer, so a freed module is never dereferenced. */
658+ GHashTableIter iter ;
659+ gpointer key , value ;
660+ g_hash_table_iter_init (& iter , users );
661+ while (g_hash_table_iter_next (& iter , & key , & value ))
662+ {
663+ dt_iop_module_t * sink = key ;
664+
665+ // locate the consumer's node in this pipe (pointer-only match)
666+ dt_dev_pixelpipe_iop_t * sink_piece = NULL ;
667+ for (GList * nodes = pipe -> nodes ; nodes ; nodes = g_list_next (nodes ))
668+ {
669+ dt_dev_pixelpipe_iop_t * p = nodes -> data ;
670+ if (p -> module == sink )
671+ {
672+ sink_piece = p ;
673+ break ;
674+ }
675+ }
676+ if (!sink_piece )
677+ {
678+ g_hash_table_iter_remove (& iter );
679+ dt_print_pipe (DT_DEBUG_PIPE | DT_DEBUG_MASKS ,
680+ "prune stale raster user" ,
681+ pipe ,
682+ module ,
683+ DT_DEVICE_NONE ,
684+ NULL ,
685+ NULL ,
686+ "dropped deleted consumer" );
687+ continue ;
688+ }
689+
690+ // a real consumer must still point back at us, have its node enabled in
691+ // this pipe, and actually have its blending in raster-mask mode. A module
692+ // that named us as raster source but is then disabled (or switched its mask
693+ // to drawn/parametric) leaves a phantom entry that would otherwise keep us
694+ // publishing -- and invalidating every downstream cacheline -- on every run.
695+ const dt_develop_blend_params_t * bp = sink_piece -> blendop_data ;
696+ const gboolean consumes = sink -> raster_mask .sink .source == module && sink_piece -> enabled &&
697+ bp && (bp -> mask_mode & DEVELOP_MASK_RASTER );
698+ if (!consumes )
699+ {
700+ g_hash_table_iter_remove (& iter );
701+ dt_print_pipe (DT_DEBUG_PIPE | DT_DEBUG_MASKS ,
702+ "prune stale raster user" ,
703+ pipe ,
704+ module ,
705+ DT_DEVICE_NONE ,
706+ NULL ,
707+ NULL ,
708+ "dropped '%s%s' (%s)" ,
709+ sink -> op ,
710+ dt_iop_get_instance_id (sink ),
711+ sink -> raster_mask .sink .source != module ? "de-synced"
712+ : !sink_piece -> enabled ? "disabled"
713+ : "not in raster mode" );
714+ }
715+ }
716+ }
717+
643718void dt_dev_pixelpipe_synch_all (dt_dev_pixelpipe_t * pipe , dt_develop_t * dev )
644719{
645720 dt_pthread_mutex_lock (& pipe -> busy_mutex );
@@ -675,6 +750,12 @@ void dt_dev_pixelpipe_synch_all(dt_dev_pixelpipe_t *pipe, dt_develop_t *dev)
675750 _dev_pixelpipe_synch (pipe , dev , history );
676751 history = g_list_next (history );
677752 }
753+
754+ // history has been (re)applied, so real raster consumers have re-registered;
755+ // drop any phantom users left behind by deleted or de-synced consumers
756+ for (GList * nodes = pipe -> nodes ; nodes ; nodes = g_list_next (nodes ))
757+ _iop_prune_stale_raster_users (pipe , ((dt_dev_pixelpipe_iop_t * )nodes -> data )-> module );
758+
678759 dt_print_pipe (DT_DEBUG_PARAMS ,
679760 "synch all modules done" ,
680761 pipe , NULL , DT_DEVICE_NONE , NULL , NULL ,
@@ -699,6 +780,12 @@ void dt_dev_pixelpipe_synch_top(dt_dev_pixelpipe_t *pipe, dt_develop_t *dev)
699780 dt_print_pipe (DT_DEBUG_PARAMS , "synch top history module missing!" ,
700781 pipe , NULL , DT_DEVICE_NONE , NULL , NULL );
701782 }
783+
784+ // clear any phantom raster users (deleted/de-synced consumers) so a source
785+ // doesn't keep republishing its mask and invalidating downstream every run
786+ for (GList * nodes = pipe -> nodes ; nodes ; nodes = g_list_next (nodes ))
787+ _iop_prune_stale_raster_users (pipe , ((dt_dev_pixelpipe_iop_t * )nodes -> data )-> module );
788+
702789 dt_pthread_mutex_unlock (& pipe -> busy_mutex );
703790}
704791
0 commit comments