diff --git a/src/develop/pixelpipe_hb.c b/src/develop/pixelpipe_hb.c index a6dc98a59b75..60493e541754 100644 --- a/src/develop/pixelpipe_hb.c +++ b/src/develop/pixelpipe_hb.c @@ -640,6 +640,81 @@ static void _dev_pixelpipe_synch(dt_dev_pixelpipe_t *pipe, } } +/** remove stale entries (deleted, disabled or de-synced consumers) from a + raster mask source's users table, so it doesn't keep + publishing/invalidating forever */ +static void _iop_prune_stale_raster_users(dt_dev_pixelpipe_t *pipe, dt_iop_module_t *module) +{ + GHashTable *users = module->raster_mask.source.users; + if(!module->dev || !users || g_hash_table_size(users) == 0) + return; + + /* Phantom users (deleted or de-synced consumers) keep the source republishing + -- and invalidating downstream -- every run. Judge each consumer from its + node in THIS pipe, never from module->enabled/blend_params: those track the + darkroom UI and are stale in the export pipe, where the piece is authoritative + (dropped a live export consumer's mask, regression 0167-raster-mask). We only + compare the consumer pointer, so a freed module is never dereferenced. */ + GHashTableIter iter; + gpointer key, value; + g_hash_table_iter_init(&iter, users); + while(g_hash_table_iter_next(&iter, &key, &value)) + { + dt_iop_module_t *sink = key; + + // locate the consumer's node in this pipe (pointer-only match) + dt_dev_pixelpipe_iop_t *sink_piece = NULL; + for(GList *nodes = pipe->nodes; nodes; nodes = g_list_next(nodes)) + { + dt_dev_pixelpipe_iop_t *p = nodes->data; + if(p->module == sink) + { + sink_piece = p; + break; + } + } + if(!sink_piece) + { + g_hash_table_iter_remove(&iter); + dt_print_pipe(DT_DEBUG_PIPE | DT_DEBUG_MASKS, + "prune stale raster user", + pipe, + module, + DT_DEVICE_NONE, + NULL, + NULL, + "dropped deleted consumer"); + continue; + } + + // a real consumer must still point back at us, have its node enabled in + // this pipe, and actually have its blending in raster-mask mode. A module + // that named us as raster source but is then disabled (or switched its mask + // to drawn/parametric) leaves a phantom entry that would otherwise keep us + // publishing -- and invalidating every downstream cacheline -- on every run. + const dt_develop_blend_params_t *bp = sink_piece->blendop_data; + const gboolean consumes = sink->raster_mask.sink.source == module && sink_piece->enabled && + bp && (bp->mask_mode & DEVELOP_MASK_RASTER); + if(!consumes) + { + g_hash_table_iter_remove(&iter); + dt_print_pipe(DT_DEBUG_PIPE | DT_DEBUG_MASKS, + "prune stale raster user", + pipe, + module, + DT_DEVICE_NONE, + NULL, + NULL, + "dropped '%s%s' (%s)", + sink->op, + dt_iop_get_instance_id(sink), + sink->raster_mask.sink.source != module ? "de-synced" + : !sink_piece->enabled ? "disabled" + : "not in raster mode"); + } + } +} + void dt_dev_pixelpipe_synch_all(dt_dev_pixelpipe_t *pipe, dt_develop_t *dev) { 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) _dev_pixelpipe_synch(pipe, dev, history); history = g_list_next(history); } + + // history has been (re)applied, so real raster consumers have re-registered; + // drop any phantom users left behind by deleted or de-synced consumers + for(GList *nodes = pipe->nodes; nodes; nodes = g_list_next(nodes)) + _iop_prune_stale_raster_users(pipe, ((dt_dev_pixelpipe_iop_t *)nodes->data)->module); + dt_print_pipe(DT_DEBUG_PARAMS, "synch all modules done", 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) dt_print_pipe(DT_DEBUG_PARAMS, "synch top history module missing!", pipe, NULL, DT_DEVICE_NONE, NULL, NULL); } + + // clear any phantom raster users (deleted/de-synced consumers) so a source + // doesn't keep republishing its mask and invalidating downstream every run + for(GList *nodes = pipe->nodes; nodes; nodes = g_list_next(nodes)) + _iop_prune_stale_raster_users(pipe, ((dt_dev_pixelpipe_iop_t *)nodes->data)->module); + dt_pthread_mutex_unlock(&pipe->busy_mutex); }