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