Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
history_merge.c
Go to the documentation of this file.
1/*
2 This file is part of Ansel,
3 Copyright (C) 2026 Aurélien PIERRE.
4
5 Ansel is free software: you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation, either version 3 of the License, or
8 (at your option) any later version.
9
10 Ansel is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 GNU General Public License for more details.
14
15 You should have received a copy of the GNU General Public License
16 along with Ansel. If not, see <http://www.gnu.org/licenses/>.
17*/
18
83
84#include "common/darktable.h"
85#include "common/debug.h"
86#include "common/iop_order.h"
88#include "develop/blend.h"
89#include "develop/dev_history.h"
90#include "develop/develop.h"
91#include "develop/imageop.h"
92#include <glib.h>
93#include <stdlib.h>
94#include <string.h>
95
96char *_hm_make_node_id(const char *op, const char *multi_name)
97{
98 /* Build the unique node identifier used everywhere in this file.
99 *
100 * Convention:
101 * node_id := "<module->op>|<module->multi_name>"
102 *
103 * Rationale:
104 * - `op` identifies the module kind.
105 * - `multi_name` is the stable user-visible identifier for multi-instances.
106 * - Base instances usually have an empty multi_name, giving "<op>|".
107 *
108 * Assumptions:
109 * - `multi_name` is stable across reloads (unlike `instance`, which is a runtime counter).
110 * - Neither `op` nor `multi_name` contains the '|' separator.
111 *
112 * Ownership:
113 * - Returns a newly allocated string that must be freed with g_free().
114 */
115 return g_strdup_printf("%s|%s", op, multi_name);
116}
117
119{
120 if(IS_NULL_PTR(n)) return;
121 g_list_free(n->previous);
122 n->previous = NULL;
123 if(n->tag)
124 {
125 dt_free(n->tag);
126 }
127 dt_free(n->id);
128 dt_free(n);
129}
130
131static void _hm_free_input_nodes(GList *input_nodes)
132{
133 /* Free the temporary graph nodes created during constraint construction.
134 *
135 * Context:
136 * - `_hm_build_input_nodes_from_ids()` and `_iop_rules()` build a list of heap-allocated
137 * `dt_digraph_node_t` nodes used as INPUT to `flatten_nodes()`.
138 * - `flatten_nodes()` allocates its OWN canonical node objects and does not take ownership
139 * of the input nodes, so we must always free the input side ourselves.
140 *
141 * Important detail:
142 * - `node->previous` is a GList that stores non-owning pointers to other input nodes.
143 * We free only the list container, not the pointed-to nodes here.
144 */
145 g_list_free_full(input_nodes, (GDestroyNotify)_hm_free_input_node);
146 input_nodes = NULL;
147}
148
149void _hm_id_to_op_name(const char *id, char *op, char *name)
150{
151 /* Parse a node id ("op|multi_name") into two fixed-size buffers.
152 *
153 * Why this exists:
154 * - Many APIs in develop/ and history/ use fixed-size arrays for op/multi_name.
155 * - This helper ensures consistent splitting and clamping to those sizes.
156 *
157 * Assumptions:
158 * - `id` uses the `_hm_make_node_id()` convention.
159 * - If no separator is found (defensive), the whole string is treated as `op`.
160 */
161 op[0] = '\0';
162 name[0] = '\0';
163 const char *sep = strchr(id, '|');
164 if(IS_NULL_PTR(sep))
165 {
166 g_strlcpy(op, id, sizeof(((dt_dev_history_item_t *)0)->op_name));
167 return;
168 }
169
170 const size_t op_len = MIN((size_t)(sep - id), sizeof(((dt_dev_history_item_t *)0)->op_name) - 1);
171 memcpy(op, id, op_len);
172 op[op_len] = '\0';
173
174 g_strlcpy(name, sep + 1, sizeof(((dt_dev_history_item_t *)0)->multi_name));
175}
176
177static int _hm_build_prev_map_from_ids(const GList *ids, GHashTable **out_prev)
178{
179 /* Build an adjacency map from a list of node ids that is already in pipeline order.
180 *
181 * Output:
182 * prev[id_i] = id_{i-1}
183 *
184 * This is not a general graph analysis: it is a representation of the *local* "previous"
185 * relationship implied by the linear list.
186 *
187 * Ownership:
188 * - Returns a hashtable owning both keys and values (g_hash_table_destroy()).
189 */
190 GHashTable *prev = g_hash_table_new_full(g_str_hash, g_str_equal, dt_free_gpointer, dt_free_gpointer);
191 if(IS_NULL_PTR(prev)) return 1;
192
193 const char *prev_id = NULL;
194 // Walk the list in order to record each element's immediate predecessor.
195 for(const GList *l = ids; l; l = g_list_next(l))
196 {
197 const char *id = (const char *)l->data;
198
199 if(prev_id) g_hash_table_replace(prev, g_strdup(id), g_strdup(prev_id));
200
201 prev_id = id;
202 }
203
204 *out_prev = prev;
205 return 0;
206}
207
208static int _hm_build_next_map_from_ids(const GList *ids, GHashTable **out_next)
209{
210 /* Symmetric to `_hm_build_prev_map_from_ids()`.
211 *
212 * Output:
213 * next[id_{i-1}] = id_i
214 *
215 * Ownership:
216 * - Returns a hashtable owning both keys and values (g_hash_table_destroy()).
217 */
218 GHashTable *next = g_hash_table_new_full(g_str_hash, g_str_equal, dt_free_gpointer, dt_free_gpointer);
219 if(IS_NULL_PTR(next)) return 1;
220
221 const char *prev_id = NULL;
222 // Walk the list in order to record each element's immediate successor.
223 for(const GList *l = ids; l; l = g_list_next(l))
224 {
225 const char *id = (const char *)l->data;
226
227 if(prev_id) g_hash_table_replace(next, g_strdup(prev_id), g_strdup(id));
228
229 prev_id = id;
230 }
231
232 *out_next = next;
233 return 0;
234}
235
237{
238 /* Check whether the flattened canonical node `pred` is already registered as a predecessor of `n`.
239 *
240 * We use this to detect a direct 2-cycle:
241 * - `a` has predecessor `b`
242 * - `b` has predecessor `a`
243 */
244
245 // Linear scan: predecessor lists are small (pipeline-sized), so this is fine.
246 for(const GList *p = g_list_first(n->previous); p; p = g_list_next(p))
247 if(p->data == pred) return TRUE;
248 return FALSE;
249}
250
252{
253 /* Remove a single predecessor edge `pred -> n` from the flattened graph.
254 *
255 * This is a *local* conflict resolver used to break direct 2-cycles before running the
256 * full topological sort.
257 */
258
259 GList *link = g_list_find(n->previous, pred);
260 if(link) n->previous = g_list_delete_link(n->previous, link);
261}
262
263typedef enum
264{
265 // Id was present in the pasted module list (`mod_list`).
267 // Id was present in the source pipeline (`dev_src->iop`).
269 // Id was present in the destination pipeline (`dev_dest->iop`).
271 // Id was introduced by a global fence rule (base instance only, multi_name="").
272 HM_ID_FROM_RULE = 1 << 3
274
275typedef struct
276{
277 // Bitmask of `_hm_id_origin_t` describing where the id was seen.
278 guint flags;
279 // Non-owning pointer to the module instance from the pasted set (if any).
281 // Non-owning pointer to the module instance in the source pipeline (if any).
283 // Non-owning pointer to the module instance in the destination pipeline (if any).
286
287typedef struct
288{
289 GList *history;
292 GPtrArray *orig_labels;
293 GPtrArray *orig_styles;
294 GHashTable *orig_ids;
296
297static int _hm_build_last_history_by_id_from_history(GList *history, const int history_end, GHashTable **out_map)
298{
299 GHashTable *map = g_hash_table_new_full(g_str_hash, g_str_equal, dt_free_gpointer, NULL);
300 if(IS_NULL_PTR(map)) return 1;
301
302 int idx = 0;
303 for(GList *l = g_list_first(history); l && idx < history_end; l = g_list_next(l), idx++)
304 {
306 g_hash_table_replace(map, _hm_make_node_id(hist->op_name, hist->multi_name), hist);
307 }
308
309 *out_map = map;
311 "[_hm_build_last_history_by_id_from_history] history_end=%d scanned=%d entries=%d\n",
312 history_end, idx, g_hash_table_size(map));
313 return 0;
314}
315
316static int _hm_backup_dest(const dt_develop_t *dev_dest, const GHashTable *mod_list_ids, _hm_dest_backup_t *backup)
317{
318 *backup = (_hm_dest_backup_t){ 0 };
319 backup->history = dt_history_duplicate(dev_dest->history);
322 if(IS_NULL_PTR(backup->iop_order_list)) return 1;
323
324 GHashTable *last_by_id = NULL;
325 if(_hm_build_last_history_by_id_from_history(backup->history, backup->history_end, &last_by_id)) return 1;
326 backup->orig_labels = _hm_collect_labels_from_history_map(last_by_id, mod_list_ids, &backup->orig_styles);
327 if(IS_NULL_PTR(backup->orig_labels) || IS_NULL_PTR(backup->orig_styles))
328 {
329 if(backup->orig_labels) g_ptr_array_free(backup->orig_labels, TRUE);
330 if(backup->orig_styles) g_ptr_array_free(backup->orig_styles, TRUE);
331 g_hash_table_destroy(last_by_id);
332 return 1;
333 }
334 backup->orig_ids = last_by_id;
336 "[_hm_backup_dest] imgid=%d history_end=%d history_len=%d iop_order=%d labels=%u selected=%d\n",
337 dev_dest->image_storage.id, backup->history_end, g_list_length(backup->history),
338 g_list_length(backup->iop_order_list), backup->orig_labels->len,
339 mod_list_ids ? g_hash_table_size((GHashTable *)mod_list_ids) : 0);
340 return 0;
341}
342
344{
346 "[_hm_restore_dest_from_backup] imgid=%d history_end=%d history_len=%d iop_order=%d\n",
347 dev_dest->image_storage.id, backup->history_end, g_list_length(backup->history),
348 g_list_length(backup->iop_order_list));
349
351 dev_dest->history = backup->history;
352 backup->history = NULL;
353 dt_dev_set_history_end_ext(dev_dest, backup->history_end);
354
355 g_list_free_full(dev_dest->iop_order_list, dt_free_gpointer);
356 dev_dest->iop_order_list = backup->iop_order_list;
357 backup->iop_order_list = NULL;
358
360 dt_dev_write_history_ext(dev_dest, dev_dest->image_storage.id);
361}
362
364{
365 if(backup->history)
366 {
367 g_list_free_full(backup->history, dt_dev_free_history_item);
368 backup->history = NULL;
369 }
370 if(backup->iop_order_list)
371 {
372 g_list_free_full(backup->iop_order_list, dt_free_gpointer);
373 backup->iop_order_list = NULL;
374 }
375 if(backup->orig_labels) g_ptr_array_free(backup->orig_labels, TRUE);
376 if(backup->orig_styles) g_ptr_array_free(backup->orig_styles, TRUE);
377 if(backup->orig_ids) g_hash_table_destroy(backup->orig_ids);
378 backup->history = NULL;
379 backup->iop_order_list = NULL;
380 backup->orig_labels = NULL;
381 backup->orig_styles = NULL;
382 backup->orig_ids = NULL;
383}
384
385int _hm_build_last_history_by_id(const dt_develop_t *dev, GHashTable **out_map)
386{
387 /* Build a map of last history item per module instance in the given develop stack.
388 *
389 * Key: "<op>|<multi_name>"
390 * Value: dt_dev_history_item_t* (non-owning)
391 *
392 * This is used to decide whether a post-merge history item matches the source or destination history.
393 */
394 GHashTable *map = g_hash_table_new_full(g_str_hash, g_str_equal, dt_free_gpointer, NULL);
395 if(IS_NULL_PTR(map)) return 1;
396
397 const int history_end = dt_dev_get_history_end_ext((dt_develop_t *)dev);
398 for(GList *modules = g_list_first(dev->iop); modules; modules = g_list_next(modules))
399 {
400 dt_iop_module_t *mod = (dt_iop_module_t *)modules->data;
402 g_hash_table_replace(map, _hm_make_node_id(mod->op, mod->multi_name), hist);
403 }
404
405 *out_map = map;
407 "[_hm_build_last_history_by_id] imgid=%d history_end=%d iop=%d entries=%d\n",
408 dev->image_storage.id, history_end, g_list_length(dev->iop), g_hash_table_size(map));
409 return 0;
410}
411
412static int _hm_build_id_set_from_mod_list(const GList *mod_list, GHashTable **out_ids)
413{
414 /* Build a set of node ids from the pasted module list. */
415 GHashTable *ids = g_hash_table_new_full(g_str_hash, g_str_equal, dt_free_gpointer, NULL);
416 if(IS_NULL_PTR(ids)) return 1;
417 for(const GList *l = g_list_first((GList *)mod_list); l; l = g_list_next(l))
418 {
419 const dt_iop_module_t *mod = (const dt_iop_module_t *)l->data;
420 g_hash_table_add(ids, _hm_make_node_id(mod->op, mod->multi_name));
421 }
422 *out_ids = ids;
423 dt_print(DT_DEBUG_HISTORY | DT_DEBUG_VERBOSE, "[_hm_build_id_set_from_mod_list] modules=%d ids=%d\n",
424 g_list_length((GList *)mod_list), g_hash_table_size(ids));
425 return 0;
426}
427
428static _hm_id_info_t *_hm_id_info_upsert(GHashTable *id_ht, const char *op, const char *multi_name,
429 const _hm_id_origin_t origin, const dt_iop_module_t *mod_list,
430 const dt_iop_module_t *src_iop, dt_iop_module_t *dst_iop)
431{
432 /* Insert or update an entry in the id->info table.
433 *
434 * This table is the "join" structure for the whole merge:
435 * - keys are node ids ("op|multi_name"),
436 * - values store:
437 * - where the id was seen (bitmask),
438 * - pointers to the corresponding module instance when available.
439 *
440 * We populate it in a deliberate order (mod_list -> src_iop -> dst_iop) so later stages can:
441 * - decide whether a node should be copied (present in mod_list),
442 * - reuse an existing destination instance when possible,
443 * - create missing destination instances for nodes that appear in the solved ordering.
444 *
445 * Ownership:
446 * - The hashtable owns the key string (allocated here via `_hm_make_node_id()`).
447 * - The `_hm_id_info_t` is heap-allocated and owned by the hashtable, but the stored module pointers
448 * are non-owning references (modules are owned by the develop contexts).
449 */
450
451 char *id = _hm_make_node_id(op, multi_name);
452 _hm_id_info_t *info = (_hm_id_info_t *)g_hash_table_lookup(id_ht, id);
453 if(IS_NULL_PTR(info))
454 {
455 if(origin & HM_ID_FROM_MOD_LIST)
457 "[_hm_id_info_upsert] %s input node \n",
458 id);
459 info = g_new0(_hm_id_info_t, 1);
460 g_hash_table_insert(id_ht, id, info);
461 }
462 else
463 {
464 dt_free(id);
465 }
466
467 info->flags |= origin;
468 if(mod_list) info->mod_list = mod_list;
469 if(src_iop && IS_NULL_PTR(info->src_iop)) info->src_iop = src_iop;
470 if(dst_iop && IS_NULL_PTR(info->dst_iop)) info->dst_iop = dst_iop;
471
472 return info;
473}
474
475/* Build a list of node ids in pipeline order, restricted by an origin bitmask.
476 *
477 * This transforms a `dev->iop` list into a list of freshly allocated ids.
478 * We filter using `id_ht` so later steps can operate on a consistent kept set.
479 *
480 * Ownership:
481 * - Returned ids are owned by the caller (free with g_list_free_full(list, dt_free_gpointer)).
482 */
483static int _hm_ids_from_iop_list(GList *iop, GHashTable *id_ht, const guint keep_mask, GList **out_ids)
484{
485 GList *ids = NULL;
486 // Walk the iop list in order so the returned ids encode adjacency constraints.
487 for(const GList *l = iop; l; l = g_list_next(l))
488 {
489 const dt_iop_module_t *const mod = (const dt_iop_module_t *)l->data;
490
491 char *id = _hm_make_node_id(mod->op, mod->multi_name);
492 const _hm_id_info_t *info = (_hm_id_info_t *)g_hash_table_lookup(id_ht, id);
493 if(info && (info->flags & keep_mask))
494 ids = g_list_append(ids, id);
495 else
496 dt_free(id);
497 }
498 *out_ids = ids;
499 return 0;
500}
501
502static int _hm_build_input_nodes_from_ids(const GList *ids, const char *tag, GList **out_nodes)
503{
504 /* Build a list of digraph nodes encoding linear "previous" constraints.
505 *
506 * For ids [a,b,c], we build nodes with:
507 * b.previous = [a]
508 * c.previous = [b]
509 *
510 * This is enough for topological sorting: it constrains only immediate adjacency, not all pairs.
511 *
512 * Tag:
513 * - Used as provenance metadata ("src"/"dst"/"rule") that is propagated during flattening for debug.
514 */
515 GList *nodes = NULL;
516 dt_digraph_node_t *prev = NULL;
517
518 // Iterate in order; link each node to the previously created node.
519 for(const GList *l = ids; l; l = g_list_next(l))
520 {
521 const char *id = (const char *)l->data;
522
524 if(IS_NULL_PTR(n))
525 {
527 return 1;
528 }
529 if(tag)
530 {
531 n->tag = g_strdup(tag);
532 if(IS_NULL_PTR(n->tag))
533 {
536 return 1;
537 }
538 }
539
540 if(prev) n->previous = g_list_append(n->previous, prev);
541
542 nodes = g_list_append(nodes, n);
543 prev = n;
544 }
545 *out_nodes = nodes;
546 return 0;
547}
548
549static int _hm_build_input_nodes_from_ids_filtered(const GList *ids, const char *tag, const GHashTable *focus,
550 GList **out_nodes)
551{
552 /* Build input constraint nodes from an ordered id list, optionally filtering which adjacency edges are kept.
553 *
554 * When `focus` is NULL, this is equivalent to `_hm_build_input_nodes_from_ids()` (all consecutive edges).
555 *
556 * When `focus` is non-NULL, we keep an edge prev->cur only if:
557 * - prev is in focus, OR
558 * - cur is in focus.
559 *
560 * This allows importing ordering constraints from a full source pipeline while restricting them to
561 * the neighborhood of the modules we actually want to position.
562 */
563 GList *nodes = NULL;
564 dt_digraph_node_t *prev = NULL;
565
566 for(const GList *l = ids; l; l = g_list_next(l))
567 {
568 const char *id = (const char *)l->data;
569
571 if(IS_NULL_PTR(n))
572 {
574 return 1;
575 }
576 if(tag)
577 {
578 n->tag = g_strdup(tag);
579 if(IS_NULL_PTR(n->tag))
580 {
583 return 1;
584 }
585 }
586
587 if(prev)
588 {
589 const gboolean keep_edge = !focus || g_hash_table_contains((GHashTable *)focus, prev->id)
590 || g_hash_table_contains((GHashTable *)focus, n->id);
591 if(keep_edge) n->previous = g_list_append(n->previous, prev);
592 }
593
594 nodes = g_list_append(nodes, n);
595 prev = n;
596 }
597 *out_nodes = nodes;
598 return 0;
599}
600
601static int _hm_build_isolated_nodes_from_modules(const GList *modules, const char *tag, GList **out_nodes)
602{
603 /* Build nodes without edges, so modules are present in the graph even if no adjacency constraints apply. */
604 GList *nodes = NULL;
605 for(const GList *l = g_list_first((GList *)modules); l; l = g_list_next(l))
606 {
607 const dt_iop_module_t *mod = (const dt_iop_module_t *)l->data;
608
609 char *id = _hm_make_node_id(mod->op, mod->multi_name);
611 if(IS_NULL_PTR(n))
612 {
613 dt_free(id);
615 return 1;
616 }
617 if(tag)
618 {
619 n->tag = g_strdup(tag);
620 if(IS_NULL_PTR(n->tag))
621 {
622 dt_free(id);
625 return 1;
626 }
627 }
628 nodes = g_list_append(nodes, n);
629 dt_free(id);
630 }
631 *out_nodes = nodes;
632 return 0;
633}
634
635/* Build constraint nodes enforcing raster-mask producer -> user ordering. */
636static int _hm_build_raster_mask_nodes_from_modules(const GList *modules, GHashTable *id_ht, const guint keep_mask,
637 const char *tag, GList **out_nodes)
638{
639 GList *nodes = NULL;
640 // For each module using a raster mask, add a producer->user edge if both are kept in the graph.
641 for(const GList *l = g_list_first((GList *)modules); l; l = g_list_next(l))
642 {
643 const dt_iop_module_t *mod = (const dt_iop_module_t *)l->data;
644 const dt_iop_module_t *producer = mod->raster_mask.sink.source;
645 if(IS_NULL_PTR(producer)) continue;
646
647 char *user_id = _hm_make_node_id(mod->op, mod->multi_name);
648 char *prod_id = _hm_make_node_id(producer->op, producer->multi_name);
649
650 const _hm_id_info_t *user_info = (_hm_id_info_t *)g_hash_table_lookup(id_ht, user_id);
651 const _hm_id_info_t *prod_info = (_hm_id_info_t *)g_hash_table_lookup(id_ht, prod_id);
652 if(!(user_info && (user_info->flags & keep_mask) && prod_info && (prod_info->flags & keep_mask)))
653 {
654 dt_free(user_id);
655 dt_free(prod_id);
656 continue;
657 }
658
659 dt_digraph_node_t *prod = dt_digraph_node_new(prod_id);
660 dt_digraph_node_t *user = dt_digraph_node_new(user_id);
661 if(IS_NULL_PTR(prod) || IS_NULL_PTR(user))
662 {
666 return 1;
667 }
668 if(tag)
669 {
670 prod->tag = g_strdup(tag);
671 user->tag = g_strdup(tag);
672 if(IS_NULL_PTR(prod->tag) || IS_NULL_PTR(user->tag))
673 {
677 return 1;
678 }
679 }
680 user->previous = g_list_append(user->previous, prod);
681
682 nodes = g_list_append(nodes, prod);
683 nodes = g_list_append(nodes, user);
684
685 dt_free(user_id);
686 dt_free(prod_id);
687 }
688 *out_nodes = nodes;
689 return 0;
690}
691
692// Extract rules from IOP global fences
693static int _iop_rules(GHashTable *keep, GList **out_nodes)
694{
695 /* Convert global iop-order fence rules into digraph constraints.
696 *
697 * Each rule (op_prev -> op_next) becomes an edge in the constraint graph.
698 * We always use node ids "op|" (empty multi_name) to target base instances.
699 *
700 * Note: `keep` is currently unused (hook for potential future filtering).
701 */
702 GList *iop_rules = NULL;
703 // Walk the global rule list; each entry yields two nodes (prev,next) and one predecessor edge.
704 for(const GList *rules = g_list_first(darktable.iop_order_rules); rules; rules = g_list_next(rules))
705 {
706 const dt_iop_order_rule_t *const restrict rule = (dt_iop_order_rule_t *)rules->data;
707
708 // Always use "op|" as the node id for rules, to match dev->iop instance names
709 char next_id[256], prev_id[256];
710 snprintf(next_id, sizeof(next_id), "%s|", rule->op_next);
711 snprintf(prev_id, sizeof(prev_id), "%s|", rule->op_prev);
712
713 dt_digraph_node_t *next = dt_digraph_node_new(next_id);
714 dt_digraph_node_t *prev = dt_digraph_node_new(prev_id);
715 if(IS_NULL_PTR(next) || IS_NULL_PTR(prev))
716 {
719 _hm_free_input_nodes(iop_rules);
720 return 1;
721 }
722 next->tag = g_strdup("rule");
723 prev->tag = g_strdup("rule");
724 if(IS_NULL_PTR(next->tag) || IS_NULL_PTR(prev->tag))
725 {
728 _hm_free_input_nodes(iop_rules);
729 return 1;
730 }
731 next->previous = g_list_append(next->previous, prev);
732 iop_rules = g_list_append(iop_rules, next);
733 iop_rules = g_list_append(iop_rules, prev);
734 }
735 *out_nodes = iop_rules;
736 return 0;
737}
738
740{
741 /* Transient state for a single topological merge attempt.
742 *
743 * Keeping this in a struct makes the main function `_hm_try_merge_iop_order_topologically()`
744 * easier to read and reduces the risk of leaking allocations on error paths.
745 */
746 // id string ("op|multi_name") -> `_hm_id_info_t` (ownership: ctx owns keys + values).
747 GHashTable *id_ht;
748 // Bitmask of `_hm_id_origin_t` selecting the kept node set for this merge attempt.
750 // Ordered list of kept ids representing destination adjacency constraints (allocated ids).
751 GList *dest_ids;
752 // Ordered list of kept ids representing source pipeline adjacency constraints (allocated ids).
753 GList *src_ids;
754 // Raw constraint nodes built from dest/src/rules before flattening (ownership: ctx).
756 // Canonical flattened nodes (ownership: ctx via dt_digraph_cleanup_full()).
757 GList *flat;
758 // Topologically sorted solution order (list container only; nodes are owned by `flat`).
759 GList *sorted;
760 // When FALSE, topo merge only updates ordering/instances; it does not copy module content.
762 // When FALSE, the source iop list cannot impose successors around newly-created instances.
764 // Set of ids (strings) selecting modules that source-order or destination-slot constraints should position.
765 // In source-order mode, edges touching this set are imported; otherwise this set holds missing instances.
766 GHashTable *src_focus_ids;
767 // Modules selected for pasting (source instances).
768 const GList *mod_list;
769 // Destination develop context for raster-mask constraints.
772
774{
775 /* Free everything allocated in the topo-merge context.
776 *
777 * This must be safe to call multiple times and after partial initialization, hence the NULL checks.
778 */
779
780 if(ctx->sorted)
781 {
782 g_list_free(ctx->sorted);
783 ctx->sorted = NULL;
784 }
785 if(ctx->flat) dt_digraph_cleanup_full(ctx->flat, NULL, NULL);
787 if(ctx->dest_ids)
788 {
789 g_list_free_full(ctx->dest_ids, dt_free_gpointer);
790 ctx->dest_ids = NULL;
791 }
792 if(ctx->src_ids)
793 {
794 g_list_free_full(ctx->src_ids, dt_free_gpointer);
795 ctx->src_ids = NULL;
796 }
797 if(ctx->src_focus_ids) g_hash_table_destroy(ctx->src_focus_ids);
798 if(ctx->id_ht) g_hash_table_destroy(ctx->id_ht);
799
800 ctx->sorted = NULL;
801 ctx->flat = NULL;
802 ctx->input_nodes = NULL;
803 ctx->dest_ids = NULL;
804 ctx->src_ids = NULL;
805 ctx->src_focus_ids = NULL;
806 ctx->id_ht = NULL;
807}
808
810 const GList *mod_list)
811{
812 /* Build the global id->info table used throughout the topo merge.
813 *
814 * We record membership ("seen in dst", "seen in mod_list", ...) and pointers to the relevant module
815 * instances so that later steps can:
816 * - create missing destination instances,
817 * - copy module contents for the pasted set.
818 */
819
820 // Build a single ID->info table, filled in the requested order:
821 // 1) mod_list, 2) dev_src->iop, 3) dev_dest->iop.
822 ctx->id_ht = g_hash_table_new_full(g_str_hash, g_str_equal, dt_free_gpointer, dt_free_gpointer);
823 if(IS_NULL_PTR(ctx->id_ht)) return 1;
824
825 // Register ids for modules we intend to paste.
826 for(const GList *l = g_list_first((GList *)mod_list); l; l = g_list_next(l))
827 {
828 const dt_iop_module_t *mod = (const dt_iop_module_t *)l->data;
829 _hm_id_info_upsert(ctx->id_ht, mod->op, mod->multi_name, HM_ID_FROM_MOD_LIST, mod, NULL, NULL);
830 }
831
832 // Register ids for the source pipeline (useful for debugging/incompatibility resolution).
833 for(const GList *l = g_list_first(dev_src->iop); l; l = g_list_next(l))
834 {
835 const dt_iop_module_t *mod = (const dt_iop_module_t *)l->data;
836 _hm_id_info_upsert(ctx->id_ht, mod->op, mod->multi_name, HM_ID_FROM_SRC_IOP, NULL, mod, NULL);
837 }
838
839 // Register ids for the destination pipeline so we can reuse existing instances when applying the solution.
840 for(const GList *l = g_list_first(dev_dest->iop); l; l = g_list_next(l))
841 {
842 dt_iop_module_t *mod = (dt_iop_module_t *)l->data;
843 _hm_id_info_upsert(ctx->id_ht, mod->op, mod->multi_name, HM_ID_FROM_DST_IOP, NULL, NULL, mod);
844 }
845
846 // Also register fence rules (base instances only, multi_name="") so they can participate in constraints.
847 // These don't have module pointers; they participate only as ordering constraints.
848 for(const GList *rules = g_list_first(darktable.iop_order_rules); rules; rules = g_list_next(rules))
849 {
850 const dt_iop_order_rule_t *rule = (dt_iop_order_rule_t *)rules->data;
851 _hm_id_info_upsert(ctx->id_ht, rule->op_next, "", HM_ID_FROM_RULE, NULL, NULL, NULL);
852 _hm_id_info_upsert(ctx->id_ht, rule->op_prev, "", HM_ID_FROM_RULE, NULL, NULL, NULL);
853 }
854
855 return 0;
856}
857
859 const GList *mod_list, const gboolean merge_iop_order)
860{
861 /* Build the ordered id lists that represent adjacency constraints for the merge.
862 *
863 * We merge constraints from:
864 * - destination pipeline order (to keep the current image stable),
865 * - the source pipeline order (to constrain where pasted modules are placed),
866 * - global fence rules (added later when building input nodes).
867 *
868 * We filter both lists to the kept set (dst ∪ mod_list ∪ rules) to avoid importing unrelated
869 * source-only modules into the ordering problem.
870 */
871
872 // Build a focus set selecting which pasted modules need source-order or insertion-slot constraints.
873 // - merge_iop_order=TRUE: import source ordering constraints around all pasted modules.
874 // - merge_iop_order=FALSE: slot only modules missing in destination into the existing destination pipeline.
875 ctx->src_focus_ids = g_hash_table_new_full(g_str_hash, g_str_equal, dt_free_gpointer, NULL);
876 if(IS_NULL_PTR(ctx->src_focus_ids)) return 1;
877 for(const GList *l = g_list_first((GList *)mod_list); l; l = g_list_next(l))
878 {
879 const dt_iop_module_t *mod = (const dt_iop_module_t *)l->data;
880
881 char *id = _hm_make_node_id(mod->op, mod->multi_name);
882 const _hm_id_info_t *info = (_hm_id_info_t *)g_hash_table_lookup(ctx->id_ht, id);
883 const gboolean exists_in_dest = (info->flags & HM_ID_FROM_DST_IOP);
884
885 if(merge_iop_order || !exists_in_dest) g_hash_table_add(ctx->src_focus_ids, g_strdup(id));
886
887 dt_free(id);
888 }
889
890 // Restrict sorting to everything in destination plus what we need to paste, plus rule nodes.
892
893 // Destination constraints: current destination pipeline order, filtered to the kept ids.
894 if(_hm_ids_from_iop_list(g_list_first(dev_dest->iop), ctx->id_ht, ctx->keep_mask, &ctx->dest_ids)) return 1;
895 // Source constraints: the source pipeline order, filtered to the kept ids.
896 // We will later import only the edges that touch the focus set (`ctx->src_focus_ids`).
897 if(_hm_ids_from_iop_list(g_list_first(dev_src->iop), ctx->id_ht, ctx->keep_mask, &ctx->src_ids)) return 1;
898
900 "[dt_history_merge_module_list_into_image_topological] iop-order solve: merge_iop_order=%d mod_list=%d "
901 "src_iop=%d "
902 "dst_iop=%d keep(dst+mod+rules) dest_constraints=%d src_constraints=%d focus=%d\n",
903 merge_iop_order, g_list_length((GList *)mod_list), g_list_length(dev_src->iop),
904 g_list_length(dev_dest->iop), g_list_length(ctx->dest_ids), g_list_length(ctx->src_ids),
905 g_hash_table_size(ctx->src_focus_ids));
906
907 return 0;
908}
909
910// Resolve direct 2-cycles after flattening (declared here because `_hm_topo_flatten_constraints()` calls it).
911static int _hm_topo_resolve_incompatible_constraints(GList *flat, GHashTable *id_ht, const GList *src_ids,
912 const GList *dest_ids);
913
915{
916 /* Build and flatten constraint nodes into canonical digraph nodes.
917 *
918 * - `_hm_build_input_nodes_from_ids()` turns a linear id list into predecessor edges.
919 * - `_iop_rules()` adds global fence constraints (base-instance only).
920 * - `flatten_nodes()` merges nodes with identical ids and deduplicates predecessor edges.
921 *
922 * The result `ctx->flat` is the canonical constraint graph given to `topological_sort()`.
923 */
924
925
926
927 GList *dest_nodes = NULL;
928 GList *src_nodes = NULL;
929 GList *mod_nodes = NULL;
930 GList *dst_raster_nodes = NULL;
931 GList *src_raster_nodes = NULL;
932 GList *rule_nodes = NULL;
933 GHashTable *dest_rank = NULL;
934
935 if(_hm_build_input_nodes_from_ids(ctx->dest_ids, "dst", &dest_nodes)) goto error;
936 if(ctx->source_iop_order)
937 {
938 // Source-order merge imports the local source neighborhood around pasted modules.
939 if(_hm_build_input_nodes_from_ids_filtered(ctx->src_ids, "src", ctx->src_focus_ids, &src_nodes)) goto error;
940 }
941 else
942 {
943 /* Destination-order merge must not let a style's full iop_list move existing destination modules.
944 *
945 * For each missing source instance, we use the style order only to find a destination slot:
946 * - the latest destination module that appears before it in the style,
947 * - the earliest destination module that appears after it in the style and still follows that lower bound.
948 *
949 * This keeps the destination pipe stable while allowing style instances to land in their intended
950 * pipeline region. It also drops stale successor constraints that would move existing modules or create
951 * cycles, such as "Exposure (AAA) before Mask manager" on current RAW orders.
952 */
953 dest_rank = g_hash_table_new(g_str_hash, g_str_equal);
954 if(IS_NULL_PTR(dest_rank)) goto error;
955
956 int rank = 1;
957 for(const GList *d = g_list_first(ctx->dest_ids); d; d = g_list_next(d), rank++)
958 g_hash_table_insert(dest_rank, d->data, GINT_TO_POINTER(rank));
959
960 for(const GList *l = g_list_first(ctx->src_ids); l; l = g_list_next(l))
961 {
962 const char *id = (const char *)l->data;
963 if(!g_hash_table_contains(ctx->src_focus_ids, id)) continue;
964
965 int lower_rank = 0;
966 int upper_rank = 0;
967 const char *lower_id = NULL;
968 const char *upper_id = NULL;
969 const char *prev_focus_id = NULL;
970 const char *next_focus_id = NULL;
971
972 const GList *prev_link = g_list_previous((GList *)l);
973 if(prev_link && g_hash_table_contains(ctx->src_focus_ids, prev_link->data))
974 prev_focus_id = (const char *)prev_link->data;
975 const GList *next_link = g_list_next(l);
976 if(next_link && g_hash_table_contains(ctx->src_focus_ids, next_link->data))
977 next_focus_id = (const char *)next_link->data;
978
979 // Scan source predecessors and keep the latest one in destination order as lower slot boundary.
980 for(const GList *p = g_list_first(ctx->src_ids); p && p != l; p = g_list_next(p))
981 {
982 const char *candidate_id = (const char *)p->data;
983 const int candidate_rank = GPOINTER_TO_INT(g_hash_table_lookup(dest_rank, candidate_id));
984 if(candidate_rank > lower_rank)
985 {
986 lower_rank = candidate_rank;
987 lower_id = candidate_id;
988 }
989 }
990
991 // Scan source successors and keep the earliest destination node that still follows the lower boundary.
992 for(const GList *n = g_list_next(l); n; n = g_list_next(n))
993 {
994 const char *candidate_id = (const char *)n->data;
995 const int candidate_rank = GPOINTER_TO_INT(g_hash_table_lookup(dest_rank, candidate_id));
996 if(candidate_rank <= lower_rank) continue;
997 if(upper_rank == 0 || candidate_rank < upper_rank)
998 {
999 upper_rank = candidate_rank;
1000 upper_id = candidate_id;
1001 }
1002 }
1003
1005 if(IS_NULL_PTR(cur)) goto error;
1006 src_nodes = g_list_append(src_nodes, cur);
1007
1008 if(lower_id)
1009 {
1010 dt_digraph_node_t *lower = dt_digraph_node_new(lower_id);
1011 if(IS_NULL_PTR(lower)) goto error;
1012 cur->previous = g_list_append(cur->previous, lower);
1013 src_nodes = g_list_append(src_nodes, lower);
1014 }
1015 if(prev_focus_id)
1016 {
1017 dt_digraph_node_t *prev_focus = dt_digraph_node_new(prev_focus_id);
1018 if(IS_NULL_PTR(prev_focus)) goto error;
1019 cur->previous = g_list_append(cur->previous, prev_focus);
1020 src_nodes = g_list_append(src_nodes, prev_focus);
1021 }
1022
1023 if(next_focus_id)
1024 {
1025 dt_digraph_node_t *next_focus = dt_digraph_node_new(next_focus_id);
1026 dt_digraph_node_t *next_focus_prev = dt_digraph_node_new(id);
1027 if(IS_NULL_PTR(next_focus) || IS_NULL_PTR(next_focus_prev))
1028 {
1029 _hm_free_input_node(next_focus);
1030 _hm_free_input_node(next_focus_prev);
1031 goto error;
1032 }
1033 next_focus->previous = g_list_append(next_focus->previous, next_focus_prev);
1034 src_nodes = g_list_append(src_nodes, next_focus_prev);
1035 src_nodes = g_list_append(src_nodes, next_focus);
1036 }
1037 if(upper_id)
1038 {
1039 dt_digraph_node_t *upper = dt_digraph_node_new(upper_id);
1040 dt_digraph_node_t *upper_prev = dt_digraph_node_new(id);
1041 if(IS_NULL_PTR(upper) || IS_NULL_PTR(upper_prev))
1042 {
1043 _hm_free_input_node(upper);
1044 _hm_free_input_node(upper_prev);
1045 goto error;
1046 }
1047 upper->previous = g_list_append(upper->previous, upper_prev);
1048 src_nodes = g_list_append(src_nodes, upper_prev);
1049 src_nodes = g_list_append(src_nodes, upper);
1050 }
1051
1053 "[_hm_topo_flatten_constraints] destination slot: %s after %s%s%s before %s%s%s\n",
1054 id, lower_id ? lower_id : "(none)", (lower_id && prev_focus_id) ? ", " : "",
1055 prev_focus_id ? prev_focus_id : "", upper_id ? upper_id : "(end)",
1056 (upper_id && next_focus_id) ? ", " : "", next_focus_id ? next_focus_id : "");
1057 }
1058 g_hash_table_destroy(dest_rank);
1059 dest_rank = NULL;
1060 }
1061 // Ensure all pasted modules are present in the graph, even if they don't appear in src/dst adjacency lists.
1062 if(_hm_build_isolated_nodes_from_modules(ctx->mod_list, "mod", &mod_nodes)) goto error;
1063 // Raster mask constraints: producer must come before user.
1064 if(_hm_build_raster_mask_nodes_from_modules(ctx->dev_dest->iop, ctx->id_ht, ctx->keep_mask, "dst-raster",
1065 &dst_raster_nodes))
1066 goto error;
1067 if(_hm_build_raster_mask_nodes_from_modules(ctx->mod_list, ctx->id_ht, ctx->keep_mask, "src-raster",
1068 &src_raster_nodes))
1069 goto error;
1070 if(_iop_rules(NULL, &rule_nodes)) goto error;
1071
1072 const int dest_nodes_len = g_list_length(dest_nodes);
1073 const int src_nodes_len = g_list_length(src_nodes);
1074 const int mod_nodes_len = g_list_length(mod_nodes);
1075 const int dst_raster_nodes_len = g_list_length(dst_raster_nodes);
1076 const int src_raster_nodes_len = g_list_length(src_raster_nodes);
1077 const int rule_nodes_len = g_list_length(rule_nodes);
1078
1079 ctx->input_nodes = g_list_concat(
1080 g_list_concat(g_list_concat(dest_nodes, src_nodes),
1081 g_list_concat(mod_nodes, g_list_concat(dst_raster_nodes, src_raster_nodes))),
1082 rule_nodes);
1084 "[_hm_topo_flatten_constraints] input nodes: dst=%d src=%d mod=%d dst-raster=%d src-raster=%d "
1085 "rules=%d total=%d\n",
1086 dest_nodes_len, src_nodes_len, mod_nodes_len, dst_raster_nodes_len, src_raster_nodes_len,
1087 rule_nodes_len, g_list_length(ctx->input_nodes));
1088
1089 if(flatten_nodes(ctx->input_nodes, &ctx->flat))
1090 {
1092 "[dt_history_merge_module_list_into_image_topological] iop-order merge: flatten failed\n");
1093 return 1;
1094 }
1095
1096 if(_hm_topo_resolve_incompatible_constraints(ctx->flat, ctx->id_ht, ctx->src_ids, ctx->dest_ids)) return 1;
1097
1098 dt_print(DT_DEBUG_HISTORY | DT_DEBUG_VERBOSE, "[_hm_topo_flatten_constraints] flat nodes=%d\n", g_list_length(ctx->flat));
1099 return 0;
1100
1101error:
1102 if(dest_rank) g_hash_table_destroy(dest_rank);
1104 "[_hm_topo_flatten_constraints] failed while building input nodes: dst=%d src=%d mod=%d "
1105 "dst-raster=%d src-raster=%d rules=%d\n",
1106 g_list_length(dest_nodes), g_list_length(src_nodes), g_list_length(mod_nodes),
1107 g_list_length(dst_raster_nodes), g_list_length(src_raster_nodes), g_list_length(rule_nodes));
1108 _hm_free_input_nodes(dest_nodes);
1109 _hm_free_input_nodes(src_nodes);
1110 _hm_free_input_nodes(mod_nodes);
1111 _hm_free_input_nodes(dst_raster_nodes);
1112 _hm_free_input_nodes(src_raster_nodes);
1113 _hm_free_input_nodes(rule_nodes);
1114 return 1;
1115}
1116
1117static int _hm_topo_resolve_incompatible_constraints(GList *flat, GHashTable *id_ht, const GList *src_ids,
1118 const GList *dest_ids)
1119{
1120 /* Break direct 2-cycles (A<->B) by removing one of the two conflicting edges.
1121 *
1122 * The topo-sort implementation cannot succeed on cyclic graphs. Some cycles are unavoidable,
1123 * but direct 2-cycles are frequently caused by the destination and source imposing contradictory
1124 * immediate-predecessor constraints. We handle those specifically because:
1125 * - they are easy to detect,
1126 * - they are easy to resolve by choosing one of the two orderings.
1127 *
1128 * When a GUI is available, we ask the user whether to preserve source or destination ordering.
1129 * Otherwise we default to preserving destination ordering.
1130 */
1131
1132 GList *_hm_cycles = NULL;
1133 GHashTable *seen_cycles = NULL;
1134 // Build adjacency lookup tables from the original linear constraints.
1135 GHashTable *src_prev = NULL;
1136 GHashTable *src_next = NULL;
1137 GHashTable *dst_prev = NULL;
1138 GHashTable *dst_next = NULL;
1139 const char *cleanup_reason = NULL;
1140 int cleanup_line = 0;
1141 if(_hm_build_prev_map_from_ids(src_ids, &src_prev))
1142 {
1143 cleanup_reason = "_hm_build_prev_map_from_ids(src_ids)";
1144 cleanup_line = __LINE__;
1145 goto cleanup;
1146 }
1147 if(_hm_build_next_map_from_ids(src_ids, &src_next))
1148 {
1149 cleanup_reason = "_hm_build_next_map_from_ids(src_ids)";
1150 cleanup_line = __LINE__;
1151 goto cleanup;
1152 }
1153 if(_hm_build_prev_map_from_ids(dest_ids, &dst_prev))
1154 {
1155 cleanup_reason = "_hm_build_prev_map_from_ids(dest_ids)";
1156 cleanup_line = __LINE__;
1157 goto cleanup;
1158 }
1159 if(_hm_build_next_map_from_ids(dest_ids, &dst_next))
1160 {
1161 cleanup_reason = "_hm_build_next_map_from_ids(dest_ids)";
1162 cleanup_line = __LINE__;
1163 goto cleanup;
1164 }
1165
1166 typedef struct
1167 {
1168 // One node participating in the 2-cycle (edge b->a and a->b).
1170 // The other node participating in the 2-cycle.
1172 // Node we report to the user as "faulty" (prefer one belonging to the pasted set).
1173 dt_digraph_node_t *faulty;
1174 } _hm_cycle_t;
1175
1176 seen_cycles = g_hash_table_new_full(g_str_hash, g_str_equal, dt_free_gpointer, NULL);
1177 if(IS_NULL_PTR(seen_cycles))
1178 {
1179 cleanup_reason = "g_hash_table_new_full(seen_cycles)";
1180 cleanup_line = __LINE__;
1181 goto cleanup;
1182 }
1183
1184 // Scan all edges a<-b and record those where b also has a as predecessor (2-cycle).
1185 for(GList *it = g_list_first(flat); it; it = g_list_next(it))
1186 {
1187 dt_digraph_node_t *a = (dt_digraph_node_t *)it->data;
1188
1189 // Each `a->previous` entry means an edge (pred -> a).
1190 for(GList *p = g_list_first(a->previous); p; p = g_list_next(p))
1191 {
1193
1194 if(!_hm_node_has_predecessor(b, a)) continue;
1195
1196 // Deduplicate: we'll discover (a,b) and (b,a), so normalize the key ordering.
1197 const char *id1 = a->id;
1198 const char *id2 = b->id;
1199 if(strcmp(id1, id2) > 0)
1200 {
1201 id1 = b->id;
1202 id2 = a->id;
1203 }
1204
1205 gchar *key = g_strdup_printf("%s<->%s", id1, id2);
1206 if(IS_NULL_PTR(key))
1207 {
1208 cleanup_reason = "g_strdup_printf(cycle key)";
1209 cleanup_line = __LINE__;
1210 goto cleanup;
1211 }
1212 if(g_hash_table_contains(seen_cycles, key))
1213 {
1214 dt_free(key);
1215 continue;
1216 }
1217 g_hash_table_add(seen_cycles, key);
1218
1219 _hm_cycle_t *c = g_new0(_hm_cycle_t, 1);
1220 if(IS_NULL_PTR(c))
1221 {
1222 cleanup_reason = "g_new0(_hm_cycle_t)";
1223 cleanup_line = __LINE__;
1224 goto cleanup;
1225 }
1226 c->a = a;
1227 c->b = b;
1228
1229 // Prefer blaming a pasted module when possible, because that's usually what users care about.
1230 const _hm_id_info_t *ai = (_hm_id_info_t *)g_hash_table_lookup(id_ht, a->id);
1231 const _hm_id_info_t *bi = (_hm_id_info_t *)g_hash_table_lookup(id_ht, b->id);
1232 c->faulty = (bi->flags & HM_ID_FROM_MOD_LIST) ? b : a;
1233 if(ai->flags & HM_ID_FROM_MOD_LIST) c->faulty = a;
1234
1235 _hm_cycles = g_list_append(_hm_cycles, c);
1236 }
1237 }
1238
1239 if(_hm_cycles)
1240 {
1241 // Ask once (for the first faulty module) and apply the chosen policy to all found 2-cycles.
1242 const _hm_cycle_t *first = (const _hm_cycle_t *)_hm_cycles->data;
1243 const dt_digraph_node_t *faulty = first ? first->faulty : NULL;
1244
1245 const char *sp = (faulty && src_prev) ? (const char *)g_hash_table_lookup(src_prev, faulty->id) : NULL;
1246 const char *sn = (faulty && src_next) ? (const char *)g_hash_table_lookup(src_next, faulty->id) : NULL;
1247 const char *dp = (faulty && dst_prev) ? (const char *)g_hash_table_lookup(dst_prev, faulty->id) : NULL;
1248 const char *dn = (faulty && dst_next) ? (const char *)g_hash_table_lookup(dst_next, faulty->id) : NULL;
1249
1250 dt_print(
1252 "[dt_history_merge_module_list_into_image_topological] incompatible constraints: found %d 2-cycle(s)\n",
1253 g_list_length(_hm_cycles));
1254
1255 const dt_hm_constraint_choice_t choice
1256 = _hm_ask_user_constraints_choice(id_ht, faulty ? faulty->id : NULL, sp, sn, dp, dn);
1257
1259 "[dt_history_merge_module_list_into_image_topological] incompatible constraints choice: %s\n",
1260 (choice == DT_HM_CONSTRAINTS_PREFER_SRC) ? "src" : "dst");
1261
1262 for(GList *l = _hm_cycles; l; l = g_list_next(l))
1263 {
1264 _hm_cycle_t *c = (_hm_cycle_t *)l->data;
1265
1266 dt_digraph_node_t *a = c->a;
1267 dt_digraph_node_t *b = c->b;
1268
1269 // Resolve this 2-cycle based on the chosen topology by removing the opposite edge.
1270 const char *want_prev_a = NULL;
1271 const char *want_prev_b = NULL;
1272 if(choice == DT_HM_CONSTRAINTS_PREFER_SRC)
1273 {
1274 // Preserve the predecessor relationship implied by the source/paste list.
1275 want_prev_a = src_prev ? (const char *)g_hash_table_lookup(src_prev, a->id) : NULL;
1276 want_prev_b = src_prev ? (const char *)g_hash_table_lookup(src_prev, b->id) : NULL;
1277 }
1278 else
1279 {
1280 // Preserve the predecessor relationship implied by the destination list.
1281 want_prev_a = dst_prev ? (const char *)g_hash_table_lookup(dst_prev, a->id) : NULL;
1282 want_prev_b = dst_prev ? (const char *)g_hash_table_lookup(dst_prev, b->id) : NULL;
1283 }
1284
1285 if(want_prev_a && !strcmp(want_prev_a, b->id))
1286 {
1287 // Keep b -> a, remove a -> b
1289 }
1290 else if(want_prev_b && !strcmp(want_prev_b, a->id))
1291 {
1292 // Keep a -> b, remove b -> a
1294 }
1295 else
1296 {
1297 // Fallback: keep destination ordering (least surprising for the current image).
1298 const char *dpa = dst_prev ? (const char *)g_hash_table_lookup(dst_prev, a->id) : NULL;
1299 const char *dpb = dst_prev ? (const char *)g_hash_table_lookup(dst_prev, b->id) : NULL;
1300 if(dpa && !strcmp(dpa, b->id))
1302 else if(dpb && !strcmp(dpb, a->id))
1304 else
1306 }
1307 }
1308 }
1309
1310 g_list_free_full(_hm_cycles, dt_free_gpointer);
1311 _hm_cycles = NULL;
1312 g_hash_table_destroy(seen_cycles);
1313 g_hash_table_destroy(src_prev);
1314 g_hash_table_destroy(src_next);
1315 g_hash_table_destroy(dst_prev);
1316 g_hash_table_destroy(dst_next);
1317 return 0;
1318
1319cleanup:
1321 "[_hm_topo_resolve_incompatible_constraints] cleanup from line %d: %s\n",
1322 cleanup_line, cleanup_reason ? cleanup_reason : "unknown");
1323 if(seen_cycles) g_hash_table_destroy(seen_cycles);
1324 if(src_prev) g_hash_table_destroy(src_prev);
1325 if(src_next) g_hash_table_destroy(src_next);
1326 if(dst_prev) g_hash_table_destroy(dst_prev);
1327 if(dst_next) g_hash_table_destroy(dst_next);
1328 g_list_free_full(_hm_cycles, dt_free_gpointer);
1329 _hm_cycles = NULL;
1330 return 1;
1331}
1332
1334{
1335 /* Run a topological sort on the flattened constraint graph.
1336 *
1337 * Returns:
1338 * - 0 on success (ctx->sorted is a linear ordering of nodes),
1339 * - 1 when constraints are unsatisfiable (cycle).
1340 *
1341 * Note:
1342 * - We may have already removed some direct 2-cycles, but longer cycles can still remain.
1343 */
1344
1345 GList *cycle_nodes = NULL;
1346 const int topo_err = topological_sort(ctx->flat, &ctx->sorted, &cycle_nodes);
1347 if(topo_err != 0)
1348 {
1349 dt_print(DT_DEBUG_HISTORY, "[dt_history_merge_module_list_into_image_topological] iop-order merge: "
1350 "unsatisfiable constraints (cycle)\n");
1351 _hm_show_toposort_cycle_popup(cycle_nodes, ctx->id_ht);
1352 if(cycle_nodes)
1353 {
1354 g_list_free(cycle_nodes);
1355 cycle_nodes = NULL;
1356 }
1357 return 1;
1358 }
1359
1360 if(cycle_nodes)
1361 {
1362 g_list_free(cycle_nodes);
1363 cycle_nodes = NULL;
1364 }
1365 dt_print(DT_DEBUG_HISTORY | DT_DEBUG_VERBOSE, "[_hm_topo_sort_constraints] sorted nodes=%d\n", g_list_length(ctx->sorted));
1366 return 0;
1367}
1368
1370{
1371 /* Apply the topological solution to the destination develop context.
1372 *
1373 * For each node id in `ctx->sorted` (solution order), we:
1374 * 1) ensure the corresponding destination module instance exists (create if missing),
1375 * 2) copy module contents when the id belongs to the pasted module list,
1376 * 3) rebuild `dev_dest->iop_order_list` from scratch to match that order.
1377 *
1378 * We rebuild the iop_order_list in one shot to avoid leaving partially updated state on error paths.
1379 */
1380
1381 // Apply solution:
1382 // - ensure every solved node exists in dev_dest->iop (create missing as new instances),
1383 // - copy module content for items that are in mod_list,
1384 // - rebuild dev_dest->iop_order_list from scratch.
1385 GList *ordered_modules = NULL;
1386 int created = 0;
1387 int copied = 0;
1388
1389 // Iterate in the final solved order; this is the new pipeline order.
1390 for(const GList *l = g_list_first(ctx->sorted); l; l = g_list_next(l))
1391 {
1392 const dt_digraph_node_t *n = (const dt_digraph_node_t *)l->data;
1393
1394 _hm_id_info_t *info = (_hm_id_info_t *)g_hash_table_lookup(ctx->id_ht, n->id);
1395 // Skip nodes that are not part of the kept set (dst/mod/rule).
1396 if(!(info->flags & ctx->keep_mask)) continue;
1397
1398 char op[sizeof(((dt_dev_history_item_t *)0)->op_name)];
1399 char name[sizeof(((dt_dev_history_item_t *)0)->multi_name)];
1400 _hm_id_to_op_name(n->id, op, name);
1401
1402 // Resolve (or create) the destination instance for this id.
1403 dt_iop_module_t *mod_dest = info->dst_iop ? info->dst_iop : dt_dev_get_module_instance(dev_dest, op, name, 0);
1404 if(IS_NULL_PTR(mod_dest))
1405 {
1406 mod_dest = dt_dev_create_module_instance(dev_dest, op, name, 0, TRUE);
1407 if(IS_NULL_PTR(mod_dest)) return 1;
1408 created++;
1409 info->dst_iop = mod_dest;
1410 info->flags |= HM_ID_FROM_DST_IOP;
1411 }
1412
1413 // Only nodes originating from mod_list trigger content overwrite, and only when requested by caller.
1414 if(ctx->copy_module_contents && info->mod_list)
1415 {
1416 if(dt_dev_copy_module_contents(dev_dest, dev_src, mod_dest, info->mod_list)) return 1;
1417 copied++;
1418 }
1419
1420 ordered_modules = g_list_append(ordered_modules, mod_dest);
1421 }
1422
1423 // Replace iop_order_list in one shot.
1424 if(ordered_modules)
1425 {
1426 dt_ioppr_rebuild_iop_order_from_modules(dev_dest, ordered_modules);
1427 g_list_free(ordered_modules);
1428 ordered_modules = NULL;
1429 }
1430
1431 dt_print(
1433 "[dt_history_merge_module_list_into_image_topological] iop-order solve: created=%d copied=%d\n",
1434 created, copied);
1435 return 0;
1436}
1437
1439 const GList *mod_list, const gboolean merge_iop_order)
1440{
1441 /* Topologically merge ordering constraints and apply the result to the destination pipeline.
1442 *
1443 * This is intentionally structured as a sequence of small steps operating on `_hm_topo_merge_ctx_t`
1444 * so that intermediate artifacts (id tables, constraint nodes, flattened graph, sorted list) can be
1445 * reused and cleaned up reliably.
1446 */
1447
1448 _hm_topo_merge_ctx_t ctx = { 0 };
1449 ctx.copy_module_contents = merge_iop_order;
1450 ctx.source_iop_order = merge_iop_order;
1451 ctx.mod_list = mod_list;
1452 ctx.dev_dest = dev_dest;
1453
1455 "[_hm_try_merge_iop_order_topologically] start merge_iop_order=%d modules=%d dst_iop=%d src_iop=%d\n",
1456 merge_iop_order, g_list_length((GList *)mod_list), g_list_length(dev_dest->iop),
1457 g_list_length(dev_src->iop));
1458
1459 if(_hm_topo_build_id_info_table(&ctx, dev_dest, dev_src, mod_list))
1460 {
1461 dt_print(DT_DEBUG_HISTORY | DT_DEBUG_VERBOSE, "[_hm_try_merge_iop_order_topologically] failed: id info table\n");
1463 return 1;
1464 }
1465
1466 if(_hm_topo_build_constraint_ids(&ctx, dev_dest, dev_src, mod_list, merge_iop_order))
1467 {
1468 dt_print(DT_DEBUG_HISTORY | DT_DEBUG_VERBOSE, "[_hm_try_merge_iop_order_topologically] failed: constraint ids\n");
1470 return 1;
1471 }
1472
1474 {
1475 dt_print(DT_DEBUG_HISTORY | DT_DEBUG_VERBOSE, "[_hm_try_merge_iop_order_topologically] failed: flatten constraints\n");
1477 return 1;
1478 }
1479
1481 {
1482 dt_print(DT_DEBUG_HISTORY | DT_DEBUG_VERBOSE, "[_hm_try_merge_iop_order_topologically] failed: topological sort\n");
1484 return 1;
1485 }
1486
1487 if(_hm_topo_apply_solution(&ctx, dev_dest, dev_src))
1488 {
1489 dt_print(DT_DEBUG_HISTORY | DT_DEBUG_VERBOSE, "[_hm_try_merge_iop_order_topologically] failed: apply solution\n");
1491 return 1;
1492 }
1493
1495 "[_hm_try_merge_iop_order_topologically] success merge_iop_order=%d dst_iop=%d order=%d\n",
1496 merge_iop_order, g_list_length(dev_dest->iop), g_list_length(dev_dest->iop_order_list));
1498 return 0;
1499}
1500
1501static void _hm_renumber_history(GList *history)
1502{
1503 /* Ensure each history item's `num` matches its position in the list.
1504 *
1505 * After concatenation (append/prepend), list indices change. We renumber so that:
1506 * - debug output is consistent,
1507 * - DB write/read paths that assume `num` is sequential do not get confused.
1508 */
1509 int idx = 0;
1510 // Assign sequential numbers in list order.
1511 for(GList *it = g_list_first(history); it; it = g_list_next(it), idx++)
1512 {
1514 h->num = idx;
1515 }
1516 dt_print(DT_DEBUG_HISTORY | DT_DEBUG_VERBOSE, "[_hm_renumber_history] history_len=%d\n", idx);
1517}
1518
1520{
1521 /* Drop the redo tail from destination history if destination is not at the tip.
1522 *
1523 * If a user has undone some steps (history_end < len), the items after history_end are redoable.
1524 * Any new change (including history merge) invalidates redo, so we remove those items now.
1525 *
1526 * This mirrors standard undo/redo behavior: after a new edit you can no longer redo the old tail.
1527 */
1528
1529 const int history_end = dt_dev_get_history_end_ext(dev_dest);
1530 const int history_len = g_list_length(dev_dest->history);
1531
1532 if(history_end >= history_len)
1533 {
1535 "[_hm_truncate_dest_redo_tail] no redo tail: imgid=%d end=%d len=%d\n",
1536 dev_dest->image_storage.id, history_end, history_len);
1537 return;
1538 }
1539
1541 "[dt_history_merge_module_list_into_image_advanced] truncating destination redo tail: end=%d len=%d\n",
1542 history_end, history_len);
1543
1544 // history_end is a cursor expressed in "number of applied items" terms:
1545 // - keep items [0..history_end-1]
1546 // - remove items [history_end..]
1547 GList *link = g_list_nth(dev_dest->history, history_end);
1548 // Walk from the first redo item to the end, freeing and unlinking each node.
1549 while(link)
1550 {
1551 GList *next = g_list_next(link);
1552 dt_dev_free_history_item(link->data);
1553 dev_dest->history = g_list_delete_link(dev_dest->history, link);
1554 link = next;
1555 }
1556}
1557
1558int dt_history_merge(dt_develop_t *dev_dest, dt_develop_t *dev_src, const int32_t dest_imgid,
1559 const GList *mod_list, const gboolean merge_iop_order,
1560 const dt_history_merge_strategy_t strategy, const gboolean force_new_modules,
1561 const char *source_label, dt_hm_batch_state_t *batch)
1562{
1563 /* Merge module edits from `dev_src` into `dev_dest` and write the resulting history to DB.
1564 *
1565 * Inputs:
1566 * - `mod_list` is the set of source module instances we want to paste.
1567 * - `merge_iop_order` decides whether we try to also merge pipeline ordering constraints.
1568 * - `strategy` chooses whether the pasted history is appended or prepended (prepend) to destination history.
1569 *
1570 * High-level algorithm:
1571 * 1) Invalidate destination redo tail (new edit semantics).
1572 * 2) Solve and apply a (possibly constrained) iop order topologically.
1573 * 3) Ensure destination has all module instances required by `mod_list`.
1574 * 4) Build a temporary history: one history item per module (the last relevant source history item),
1575 * but bound to the destination module ordering (which may have changed in step 2).
1576 * 5) Concatenate temporary history with destination history (append/prepend), renumber, set history_end,
1577 * pop into modules, and write to DB.
1578 *
1579 * Assumptions:
1580 * - `dev_dest` is initialized for `dest_imgid` (pipeline loaded, defaults applied).
1581 * - `dev_src` is non-NULL when `merge_iop_order` is requested.
1582 */
1583 if(dest_imgid <= 0) return 1;
1584 if(IS_NULL_PTR(mod_list)) return 0;
1585
1586 if(!_hm_warn_missing_raster_producers(mod_list)) return 1;
1587
1588 int rc = 1;
1589 gboolean used_source_order = merge_iop_order;
1590 gboolean revert = FALSE;
1591 GHashTable *mod_list_ids = NULL;
1592 GHashTable *src_last_by_id = NULL;
1593 GHashTable *dst_last_before_by_id = NULL;
1594 _hm_dest_backup_t backup = { 0 };
1595 const char *cleanup_reason = NULL;
1596 int cleanup_line = 0;
1597
1598 // Snapshot the original destination pipeline and last history items before we modify the destination history.
1599 if(_hm_build_id_set_from_mod_list(mod_list, &mod_list_ids))
1600 {
1601 cleanup_reason = "_hm_build_id_set_from_mod_list(mod_list)";
1602 cleanup_line = __LINE__;
1603 goto cleanup;
1604 }
1605 if(_hm_backup_dest(dev_dest, mod_list_ids, &backup))
1606 {
1607 cleanup_reason = "_hm_backup_dest(dev_dest)";
1608 cleanup_line = __LINE__;
1609 goto cleanup;
1610 }
1611 if(_hm_build_last_history_by_id(dev_src, &src_last_by_id))
1612 {
1613 cleanup_reason = "_hm_build_last_history_by_id(dev_src)";
1614 cleanup_line = __LINE__;
1615 goto cleanup;
1616 }
1617 if(_hm_build_last_history_by_id(dev_dest, &dst_last_before_by_id))
1618 {
1619 cleanup_reason = "_hm_build_last_history_by_id(dev_dest)";
1620 cleanup_line = __LINE__;
1621 goto cleanup;
1622 }
1623
1624 if(force_new_modules)
1625 dt_print(DT_DEBUG_HISTORY, "[dt_history_merge] force_new_modules is "
1626 "temporarily unsupported, ignoring\n");
1627
1629 "[dt_history_merge] imgid=%d merge_iop_order=%d strategy=%d "
1630 "force_new=%d modules=%d\n",
1631 dest_imgid, merge_iop_order, strategy, force_new_modules, g_list_length((GList *)mod_list));
1632
1633 // If the destination history has an undo/redo tail (history_end < length), any new merge must invalidate
1634 // the redo part, like a regular edit does.
1636
1637 // Always run a topological solve so we can insert missing source instances into the destination pipeline.
1638 // The difference between merge_iop_order modes is which source edges we import (see
1639 // `_hm_topo_build_constraint_ids()`).
1640 if(_hm_try_merge_iop_order_topologically(dev_dest, dev_src, mod_list, merge_iop_order))
1641 {
1642 // If it failed with source IOP order, retry with destination order
1643 if(merge_iop_order)
1644 {
1645 used_source_order = FALSE;
1646 if(_hm_try_merge_iop_order_topologically(dev_dest, dev_src, mod_list, FALSE))
1647 {
1648 // It's unlikely that it fail again, but if it does, there is nothing we can do.
1649 // The only mathematically valid way to insert new instances is through topology.
1650 // Abort then.
1651 cleanup_reason = "_hm_try_merge_iop_order_topologically() retry";
1652 cleanup_line = __LINE__;
1653 goto cleanup;
1654 }
1655 }
1656 }
1657
1658 // Sanitize and flatten module order
1659 dt_ioppr_resync_pipeline(dev_dest, dest_imgid, "_history_copy_and_paste_on_image_merge", FALSE);
1660
1661 GList *temp_history = NULL;
1662 // Build the temporary history list from the source history stack, module-by-module.
1663 for(const GList *l = g_list_first((GList *)mod_list); l; l = g_list_next(l))
1664 {
1665 const dt_iop_module_t *mod_src = (const dt_iop_module_t *)l->data;
1666
1667 // Last history item for this module in the source history stack.
1668 const int src_end = dt_dev_get_history_end_ext(dev_src);
1669 const dt_dev_history_item_t *hist_src
1670 = dt_dev_history_get_last_item_by_module(dev_src->history, (dt_iop_module_t *)mod_src, src_end);
1671
1672 // Destination module instance and its current pipeline ordering info. Single-instance
1673 // modules are keyed by operation only in the destination pipe, so ignore source
1674 // multi-instance metadata when binding the history item.
1675 dt_iop_module_t *mod_dest = NULL;
1676 if((mod_src->flags() & IOP_FLAGS_ONE_INSTANCE) == IOP_FLAGS_ONE_INSTANCE)
1677 mod_dest = dt_iop_get_module_by_op_priority(dev_dest->iop, mod_src->op, -1);
1678 else
1679 mod_dest = dt_dev_get_module_instance(dev_dest, mod_src->op, mod_src->multi_name, mod_src->multi_priority);
1680 dt_dev_history_item_t *hist = NULL;
1682 "[dt_history_merge] build temp history: src=%s multi='%s' priority=%d hist=%s dest=%s dest_priority=%d\n",
1683 mod_src->op, mod_src->multi_name, mod_src->multi_priority,
1684 hist_src ? "yes" : "no", mod_dest ? "yes" : "no",
1685 mod_dest ? mod_dest->multi_priority : -1);
1686 if(dt_dev_history_item_from_source_history_item(dev_dest, dev_src, hist_src, mod_dest, &hist))
1687 {
1688 cleanup_reason = "dt_dev_history_item_from_source_history_item()";
1689 cleanup_line = __LINE__;
1690 goto cleanup;
1691 }
1692
1693 temp_history = g_list_append(temp_history, hist);
1694 }
1695
1696 // Concatenate temporary history with destination history in the requested order.
1697 if(strategy == DT_HISTORY_MERGE_APPEND)
1698 dev_dest->history = g_list_concat(dev_dest->history, temp_history);
1699 else // DT_HISTORY_MERGE_PREPEND
1700 dev_dest->history = g_list_concat(temp_history, dev_dest->history);
1701
1702 // Don't g_list_free(temp_history), it belongs to dev_dst->history now
1703
1704 _hm_renumber_history(dev_dest->history);
1705 dt_dev_set_history_end_ext(dev_dest, g_list_length(dev_dest->history));
1706
1707 dt_print(DT_DEBUG_HISTORY, "[dt_history_merge] merged history: end=%d len=%d\n",
1708 dt_dev_get_history_end_ext(dev_dest), g_list_length(dev_dest->history));
1709
1710 if(batch && batch->decision != DT_HM_BATCH_UNDECIDED)
1711 revert = (batch->decision == DT_HM_BATCH_REVERT);
1712 else
1713 revert = _hm_show_merge_report_popup(dev_dest, dev_src, merge_iop_order, used_source_order, strategy,
1714 src_last_by_id, dst_last_before_by_id, backup.orig_labels,
1715 backup.orig_styles, backup.orig_ids, mod_list_ids, source_label,
1716 batch);
1717
1718 if(revert)
1719 {
1720 _hm_restore_dest_from_backup(dev_dest, &backup);
1721 cleanup_reason = "_hm_show_merge_report_popup() revert";
1722 cleanup_line = __LINE__;
1723 goto cleanup;
1724 }
1725
1726 // Sanitize and flatten module order
1727 dt_ioppr_resync_pipeline(dev_dest, dest_imgid, "_history_copy_and_paste_on_image_merge 2", FALSE);
1728
1729 rc = 0;
1730
1731cleanup:
1732 if(cleanup_reason)
1733 dt_print(DT_DEBUG_HISTORY | DT_DEBUG_VERBOSE, "[dt_history_merge] cleanup from line %d: %s\n",
1734 cleanup_line, cleanup_reason);
1735 if(src_last_by_id) g_hash_table_destroy(src_last_by_id);
1736 if(dst_last_before_by_id) g_hash_table_destroy(dst_last_before_by_id);
1737 if(mod_list_ids) g_hash_table_destroy(mod_list_ids);
1738 _hm_backup_cleanup(&backup);
1739 return rc;
1740}
1741
1742// clang-format off
1743// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
1744// vim: shiftwidth=2 expandtab tabstop=2 cindent
1745// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
1746// clang-format on
static void error(char *msg)
Definition ashift_lsd.c:202
#define TRUE
Definition ashift_lsd.c:162
#define FALSE
Definition ashift_lsd.c:158
void cleanup(dt_imageio_module_format_t *self)
Definition avif.c:164
static const dt_aligned_pixel_simd_t const dt_adaptation_t const float p
char * key
char * name
darktable_t darktable
Definition darktable.c:181
void dt_print(dt_debug_thread_t thread, const char *msg,...)
Definition darktable.c:1542
@ DT_DEBUG_HISTORY
Definition darktable.h:740
@ DT_DEBUG_VERBOSE
Definition darktable.h:743
static void dt_free_gpointer(gpointer ptr)
Definition darktable.h:463
#define dt_free(ptr)
Definition darktable.h:456
#define IS_NULL_PTR(p)
C is way too permissive with !=, == and if(var) checks, which can mean too many things depending on w...
Definition darktable.h:281
dt_iop_module_t * dt_dev_get_module_instance(dt_develop_t *dev, const char *op, const char *multi_name, const int multi_priority)
Find a module instance by op name and instance metadata.
void dt_dev_free_history_item(gpointer data)
Free a single history item (used as GList free callback).
dt_iop_module_t * dt_dev_create_module_instance(dt_develop_t *dev, const char *op, const char *multi_name, const int multi_priority, gboolean use_next_priority)
Create a new module instance from an existing base .so.
int dt_dev_copy_module_contents(dt_develop_t *dev_dest, dt_develop_t *dev_src, dt_iop_module_t *mod_dest, const dt_iop_module_t *mod_src)
void dt_dev_history_free_history(dt_develop_t *dev)
Free the whole history list attached to dev->history.
void dt_dev_pop_history_items_ext(dt_develop_t *dev)
Apply history items to module params up to dev->history_end.
int dt_dev_history_item_from_source_history_item(dt_develop_t *dev_dest, dt_develop_t *dev_src, const dt_dev_history_item_t *hist_src, dt_iop_module_t *mod_dest, dt_dev_history_item_t **out_hist)
dt_dev_history_item_t * dt_dev_history_get_last_item_by_module(GList *history_list, dt_iop_module_t *module, int history_end)
Find the last history item referencing a module up to history_end.
void dt_dev_write_history_ext(dt_develop_t *dev, const int32_t imgid)
Write dev->history to DB and XMP for a given image id.
void dt_dev_set_history_end_ext(struct dt_develop_t *dev, const uint32_t index)
Set the history end index (GUI perspective).
Definition develop.c:1665
int32_t dt_dev_get_history_end_ext(struct dt_develop_t *dev)
Get the current history end index (GUI perspective).
Definition develop.c:1659
GList * dt_history_duplicate(GList *hist)
Deep-copy a history list.
static int _hm_build_last_history_by_id_from_history(GList *history, const int history_end, GHashTable **out_map)
static void _hm_free_input_nodes(GList *input_nodes)
static int _hm_topo_resolve_incompatible_constraints(GList *flat, GHashTable *id_ht, const GList *src_ids, const GList *dest_ids)
static void _hm_truncate_dest_redo_tail(dt_develop_t *dev_dest)
void _hm_id_to_op_name(const char *id, char *op, char *name)
static void _hm_backup_cleanup(_hm_dest_backup_t *backup)
int dt_history_merge(dt_develop_t *dev_dest, dt_develop_t *dev_src, const int32_t dest_imgid, const GList *mod_list, const gboolean merge_iop_order, const dt_history_merge_strategy_t strategy, const gboolean force_new_modules, const char *source_label, dt_hm_batch_state_t *batch)
Merge a list of modules into a destination image, solving pipeline topologies for proper insertion of...
int _hm_build_last_history_by_id(const dt_develop_t *dev, GHashTable **out_map)
static int _hm_topo_build_id_info_table(_hm_topo_merge_ctx_t *ctx, dt_develop_t *dev_dest, dt_develop_t *dev_src, const GList *mod_list)
char * _hm_make_node_id(const char *op, const char *multi_name)
static int _hm_topo_build_constraint_ids(_hm_topo_merge_ctx_t *ctx, dt_develop_t *dev_dest, dt_develop_t *dev_src, const GList *mod_list, const gboolean merge_iop_order)
static gboolean _hm_node_has_predecessor(const dt_digraph_node_t *n, const dt_digraph_node_t *pred)
static int _hm_build_input_nodes_from_ids(const GList *ids, const char *tag, GList **out_nodes)
static void _hm_remove_predecessor(dt_digraph_node_t *n, const dt_digraph_node_t *pred)
static _hm_id_info_t * _hm_id_info_upsert(GHashTable *id_ht, const char *op, const char *multi_name, const _hm_id_origin_t origin, const dt_iop_module_t *mod_list, const dt_iop_module_t *src_iop, dt_iop_module_t *dst_iop)
static void _hm_topo_merge_cleanup(_hm_topo_merge_ctx_t *ctx)
static int _hm_build_raster_mask_nodes_from_modules(const GList *modules, GHashTable *id_ht, const guint keep_mask, const char *tag, GList **out_nodes)
static int _hm_topo_apply_solution(_hm_topo_merge_ctx_t *ctx, dt_develop_t *dev_dest, dt_develop_t *dev_src)
static void _hm_free_input_node(dt_digraph_node_t *n)
static int _hm_topo_flatten_constraints(_hm_topo_merge_ctx_t *ctx)
static int _hm_try_merge_iop_order_topologically(dt_develop_t *dev_dest, dt_develop_t *dev_src, const GList *mod_list, const gboolean merge_iop_order)
static int _hm_build_input_nodes_from_ids_filtered(const GList *ids, const char *tag, const GHashTable *focus, GList **out_nodes)
_hm_id_origin_t
@ HM_ID_FROM_MOD_LIST
@ HM_ID_FROM_SRC_IOP
@ HM_ID_FROM_RULE
@ HM_ID_FROM_DST_IOP
static int _hm_ids_from_iop_list(GList *iop, GHashTable *id_ht, const guint keep_mask, GList **out_ids)
static int _hm_backup_dest(const dt_develop_t *dev_dest, const GHashTable *mod_list_ids, _hm_dest_backup_t *backup)
static int _hm_build_prev_map_from_ids(const GList *ids, GHashTable **out_prev)
static int _hm_build_next_map_from_ids(const GList *ids, GHashTable **out_next)
static int _hm_build_isolated_nodes_from_modules(const GList *modules, const char *tag, GList **out_nodes)
static int _hm_topo_sort_constraints(_hm_topo_merge_ctx_t *ctx)
static int _hm_build_id_set_from_mod_list(const GList *mod_list, GHashTable **out_ids)
static void _hm_renumber_history(GList *history)
static int _iop_rules(GHashTable *keep, GList **out_nodes)
static void _hm_restore_dest_from_backup(dt_develop_t *dev_dest, _hm_dest_backup_t *backup)
@ DT_HM_BATCH_UNDECIDED
@ DT_HM_BATCH_REVERT
dt_history_merge_strategy_t
@ DT_HISTORY_MERGE_APPEND
gboolean _hm_show_merge_report_popup(dt_develop_t *dev_dest, dt_develop_t *dev_src, const gboolean merge_iop_order, const gboolean used_source_order, const dt_history_merge_strategy_t strategy, GHashTable *src_last_by_id, GHashTable *dst_last_before_by_id, const GPtrArray *orig_labels, const GPtrArray *orig_styles, const GHashTable *orig_ids, const GHashTable *mod_list_ids, const char *source_label, dt_hm_batch_state_t *batch)
gboolean _hm_warn_missing_raster_producers(const GList *mod_list)
void _hm_show_toposort_cycle_popup(GList *cycle_nodes, GHashTable *id_ht)
dt_hm_constraint_choice_t _hm_ask_user_constraints_choice(GHashTable *id_ht, const char *faulty_id, const char *src_prev, const char *src_next, const char *dst_prev, const char *dst_next)
GPtrArray * _hm_collect_labels_from_history_map(GHashTable *last_by_id, const GHashTable *mod_list_ids, GPtrArray **out_styles)
dt_hm_constraint_choice_t
@ DT_HM_CONSTRAINTS_PREFER_SRC
dt_iop_module_t * dt_iop_get_module_by_op_priority(GList *modules, const char *operation, const int multi_priority)
Definition imageop.c:3081
@ IOP_FLAGS_ONE_INSTANCE
Definition imageop.h:172
void dt_ioppr_rebuild_iop_order_from_modules(struct dt_develop_t *dev, GList *ordered_modules)
Rebuild dev->iop_order_list from a list of ordered modules.
Definition iop_order.c:1294
GList * dt_ioppr_iop_order_copy_deep(GList *iop_order_list)
Deep-copy an order list.
Definition iop_order.c:1996
void dt_ioppr_resync_pipeline(dt_develop_t *dev, const int32_t imgid, const char *msg, gboolean check_duplicates)
Resynchronize pipeline order and related structures.
Definition iop_order.c:1263
GHashTable * orig_ids
GPtrArray * orig_styles
GPtrArray * orig_labels
const dt_iop_module_t * src_iop
dt_iop_module_t * dst_iop
const dt_iop_module_t * mod_list
const GList * mod_list
dt_develop_t * dev_dest
GHashTable * src_focus_ids
GList * iop_order_rules
Definition darktable.h:763
GList * iop_order_list
Definition develop.h:285
dt_image_t image_storage
Definition develop.h:259
GList * iop
Definition develop.h:279
GList * history
Definition develop.h:275
Directed graph node.
dt_hm_batch_decision_t decision
int32_t id
Definition image.h:319
struct dt_iop_module_t::@31 raster_mask
char multi_name[128]
Definition imageop.h:363
struct dt_iop_module_t::@31::@32 source
GModule *dt_dev_operation_t op
Definition imageop.h:256
struct dt_iop_module_t::@31::@33 sink
#define MIN(a, b)
Definition thinplate.c:32
void dt_digraph_cleanup_full(GList *nodes, GHashTable *node_ht, dt_node_user_data_destroy_t user_destroy)
Free a canonical graph (nodes, constraints, ids) in one call.
int flatten_nodes(GList *input_nodes, GList **out_nodes)
Canonicalize / merge duplicated nodes by id.
dt_digraph_node_t * dt_digraph_node_new(const char *id)
Allocate and initialize a new digraph node with the given id.
int topological_sort(GList *nodes, GList **sorted, GList **cycle_out)
Perform a topological sort using depth-first search (DFS).
Small directed-graph helper for constraint aggregation and topological sorting.