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