Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
topological_sort.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
19#include "common/darktable.h"
21#include <stdio.h>
22#include <string.h>
23
24/* ------------------------- flatten_nodes() ------------------------- */
25
26static dt_digraph_node_t *_get_or_create_node(GHashTable *by_id, const char *id)
27{
28 dt_digraph_node_t *n = g_hash_table_lookup(by_id, id);
29 if(n) return n;
30
31 n = g_new0(dt_digraph_node_t, 1);
32 n->id = g_strdup(id);
33 n->tag = NULL;
34 n->previous = NULL;
35 n->user_data = NULL;
36
37 g_hash_table_insert(by_id, (gpointer)n->id, n);
38 return n;
39}
40
42{
44 if(IS_NULL_PTR(n)) return NULL;
45 n->id = g_strdup(id);
46 n->tag = NULL;
47 n->previous = NULL;
48 n->user_data = NULL;
49 return n;
50}
51
52int flatten_nodes(GList *input_nodes, GList **out_nodes)
53{
54 if(IS_NULL_PTR(out_nodes)) return 1;
55 *out_nodes = NULL;
56
57 GHashTable *by_id = g_hash_table_new(g_str_hash, g_str_equal);
58 if(IS_NULL_PTR(by_id)) return 1;
59
60 // 1) Create canonical nodes for every id we see (and keep first non-NULL user_data)
61 for(GList *it = g_list_first(input_nodes); it; it = g_list_next(it))
62 {
63 dt_digraph_node_t *in = (dt_digraph_node_t *)it->data;
64 if(IS_NULL_PTR(in) || !in->id) continue;
65
66 dt_digraph_node_t *canon = _get_or_create_node(by_id, in->id);
67 if(!canon->user_data && in->user_data) canon->user_data = in->user_data;
68 if(!canon->tag && in->tag) canon->tag = g_strdup(in->tag);
69 if(canon->tag && in->tag && strcmp(canon->tag, in->tag))
70 {
71 // Minimal provenance merging: keep both when we see a conflict.
72 if(((!strcmp(canon->tag, "dst") && !strcmp(in->tag, "src"))
73 || (!strcmp(canon->tag, "src") && !strcmp(in->tag, "dst"))))
74 {
75 dt_free(canon->tag);
76 canon->tag = g_strdup("dst+src");
77 }
78 else if(strcmp(canon->tag, "dst+src"))
79 {
80 dt_free(canon->tag);
81 canon->tag = g_strdup("mixed");
82 }
83 }
84 }
85
86 // 2) Merge previous lists onto canonical nodes, remapping to canonical nodes by id and deduplicating
87 for(GList *it = g_list_first(input_nodes); it; it = g_list_next(it))
88 {
89 dt_digraph_node_t *in = (dt_digraph_node_t *)it->data;
90 if(IS_NULL_PTR(in) || !in->id) continue;
91
92 dt_digraph_node_t *self = _get_or_create_node(by_id, in->id);
93
94 for(GList *p = g_list_first(in->previous); p; p = g_list_next(p))
95 {
96 dt_digraph_node_t *pred = (dt_digraph_node_t *)p->data;
97 if(IS_NULL_PTR(pred) || !pred->id) continue;
98
99 dt_digraph_node_t *cpred = _get_or_create_node(by_id, pred->id);
100
101 // add cpred to self->previous if not already present
102 gboolean found = FALSE;
103 for(GList *q = g_list_first(self->previous); q; q = g_list_next(q))
104 {
105 if(q->data == cpred) { found = TRUE; break; }
106 }
107 if(!found) self->previous = g_list_prepend(self->previous, cpred);
108 }
109 }
110
111 // 3) Build output list with unique nodes, preserving first-seen order from input_nodes
112 GHashTable *added = g_hash_table_new(g_str_hash, g_str_equal);
113 if(IS_NULL_PTR(added))
114 {
115 g_hash_table_destroy(by_id);
116 return 1;
117 }
118
119 for(GList *it = g_list_first(input_nodes); it; it = g_list_next(it))
120 {
121 dt_digraph_node_t *in = (dt_digraph_node_t *)it->data;
122 if(IS_NULL_PTR(in) || !in->id) continue;
123
124 if(g_hash_table_contains(added, in->id)) continue;
125
126 dt_digraph_node_t *canon = g_hash_table_lookup(by_id, in->id);
127 if(canon)
128 {
129 *out_nodes = g_list_append(*out_nodes, canon);
130 g_hash_table_add(added, (gpointer)canon->id);
131 }
132 }
133
134 // Also include nodes that exist only because they were referenced by previous
135 GHashTableIter iter;
136 gpointer key = NULL, val = NULL;
137 g_hash_table_iter_init(&iter, by_id);
138 while(g_hash_table_iter_next(&iter, &key, &val))
139 {
140 const char *id = (const char *)key;
141 dt_digraph_node_t *canon = (dt_digraph_node_t *)val;
142 if(!g_hash_table_contains(added, id)) *out_nodes = g_list_append(*out_nodes, canon);
143 }
144
145 g_hash_table_destroy(added);
146 g_hash_table_destroy(by_id);
147
148 return 0;
149}
150
151
152/* ---------------------- topological_sort() (DFS) ---------------------- */
153
154/*
155 Constraint semantics used here (common with this structure):
156 - Each dt_digraph_node_constraints_t is stored in node->constraints, so "self" is the owner.
157 - If !IS_NULL_PTR(c->previous): edge (c->previous -> self)
158 - If c->next != NULL: edge (self -> c->next)
159
160 Returns 0 on success, 1 on cycle / unsatisfiable.
161*/
162
169
170static gboolean _add_edge(GHashTable *outgoing, dt_digraph_node_t *from, dt_digraph_node_t *to)
171{
172 if(IS_NULL_PTR(from) || IS_NULL_PTR(to)) return FALSE;
173
174 GList *lst = (GList *)g_hash_table_lookup(outgoing, from);
175 lst = g_list_prepend(lst, to);
176 g_hash_table_replace(outgoing, from, lst);
177 return TRUE;
178}
179
180static GList *_toposort_extract_cycle_from_stack(const GList *dfs_stack, const dt_digraph_node_t *gray)
181{
182 /* Build a cycle list from the current DFS recursion stack.
183 *
184 * We keep `dfs_stack` as a LIFO list whose head is the currently explored node.
185 * When DFS reaches a GRAY node again, that node is an ancestor present somewhere in the stack.
186 *
187 * The cycle nodes are the sub-path:
188 * gray -> ... -> current
189 * in traversal order. The returned list contains each node once (no closing duplicate).
190 *
191 * Ownership:
192 * - The returned list container is newly allocated and must be freed with g_list_free() by the caller.
193 * - The node pointers are not owned (they belong to the canonical node graph).
194 */
195 if(IS_NULL_PTR(dfs_stack) || IS_NULL_PTR(gray)) return NULL;
196
197 GList *cycle = NULL;
198 for(const GList *it = dfs_stack; it; it = g_list_next(it))
199 {
201 if(IS_NULL_PTR(n)) continue;
202 cycle = g_list_prepend(cycle, n);
203 if(n == gray) break;
204 }
205 return g_list_reverse(cycle);
206}
207
209 GHashTable *outgoing,
210 GHashTable *color,
211 GList **result,
212 int *dfs_count,
213 GList **dfs_stack,
214 GList **cycle_out)
215{
216 dt_visit_color_t c = (dt_visit_color_t)GPOINTER_TO_INT(g_hash_table_lookup(color, n));
217
218 if(c == DT_VISIT_GRAY)
219 {
220 fprintf(stderr, "[toposort] Cycle detected visiting node '%s'\n", n->id);
221 if(cycle_out && !*cycle_out) *cycle_out = _toposort_extract_cycle_from_stack(*dfs_stack, n);
222 return TRUE; // cycle
223 }
224 if(c == DT_VISIT_BLACK)
225 {
226 fprintf(stderr, "[toposort] Already finished node '%s'\n", n->id);
227 return FALSE; // already done
228 }
229
230 *dfs_count = *dfs_count + 1;
231
232 // Push on the recursion stack so we can reconstruct a cycle path if needed.
233 if(dfs_stack) *dfs_stack = g_list_prepend(*dfs_stack, n);
234
235 g_hash_table_replace(color, n, GINT_TO_POINTER(DT_VISIT_GRAY));
236
237 GList *nbrs = (GList *)g_hash_table_lookup(outgoing, n);
238 for(GList *it = nbrs; it; it = g_list_next(it))
239 {
241 if(_dfs_visit(m, outgoing, color, result, dfs_count, dfs_stack, cycle_out))
242 {
243 // Pop before propagating the cycle error.
244 if(dfs_stack && *dfs_stack) *dfs_stack = g_list_delete_link(*dfs_stack, *dfs_stack);
245 return TRUE;
246 }
247 }
248
249 g_hash_table_replace(color, n, GINT_TO_POINTER(DT_VISIT_BLACK));
250 *result = g_list_prepend(*result, n);
251
252 // Pop on normal exit.
253 if(dfs_stack && *dfs_stack) *dfs_stack = g_list_delete_link(*dfs_stack, *dfs_stack);
254 return FALSE;
255}
256
257int topological_sort(GList *nodes, GList **sorted, GList **cycle_out)
258{
259 if(IS_NULL_PTR(sorted)) return 1;
260 *sorted = NULL;
261 if(cycle_out) *cycle_out = NULL;
262
263 // Debug: print initial nodes and their predecessors
264 fprintf(stderr, "[toposort] Initial node list and constraints:\n");
265 for(GList *it = g_list_first(nodes); it; it = g_list_next(it))
266 {
268 fprintf(stderr, "[toposort] node '%s'", n->id);
269 if(n->tag) fprintf(stderr, " [%s]", n->tag);
270 fprintf(stderr, " (predecessors:");
271 for(GList *p = g_list_first(n->previous); p; p = g_list_next(p))
272 {
273 dt_digraph_node_t *pred = (dt_digraph_node_t *)p->data;
274 fprintf(stderr, " '%s'", pred->id);
275 }
276 fprintf(stderr, ")\n");
277 }
278
279 // Debug: print all edges (previous -> node)
280 fprintf(stderr, "[toposort] Edges:\n");
281 for(GList *it = g_list_first(nodes); it; it = g_list_next(it))
282 {
284 for(GList *p = g_list_first(n->previous); p; p = g_list_next(p))
285 {
286 dt_digraph_node_t *pred = (dt_digraph_node_t *)p->data;
287 fprintf(stderr, "[toposort] '%s' -> '%s'\n", pred->id, n->id);
288 }
289 }
290
291 GHashTable *outgoing = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, NULL);
292 GHashTable *color = g_hash_table_new(g_direct_hash, g_direct_equal);
293 if(IS_NULL_PTR(outgoing) || IS_NULL_PTR(color))
294 {
295 if(outgoing) g_hash_table_destroy(outgoing);
296 if(color) g_hash_table_destroy(color);
297 return 1;
298 }
299
300 // Ensure every node appears as a key (even if it has no outgoing edges)
301 for(GList *it = g_list_first(nodes); it; it = g_list_next(it))
302 {
304 if(IS_NULL_PTR(n)) continue;
305
306 if(!g_hash_table_contains(outgoing, n)) g_hash_table_insert(outgoing, n, NULL);
307 if(!g_hash_table_contains(color, n)) g_hash_table_insert(color, n, GINT_TO_POINTER(DT_VISIT_WHITE));
308 }
309
310 // Build edges from previous lists: for each node, for each pred in node->previous, add edge pred -> node
311 for(GList *it = g_list_first(nodes); it; it = g_list_next(it))
312 {
313 dt_digraph_node_t *self = (dt_digraph_node_t *)it->data;
314 if(IS_NULL_PTR(self)) continue;
315
316 for(GList *p = g_list_first(self->previous); p; p = g_list_next(p))
317 {
318 dt_digraph_node_t *pred = (dt_digraph_node_t *)p->data;
319 if(IS_NULL_PTR(pred)) continue;
320
321 if(!g_hash_table_contains(outgoing, pred)) g_hash_table_insert(outgoing, pred, NULL);
322 if(!g_hash_table_contains(color, pred))
323 g_hash_table_insert(color, pred, GINT_TO_POINTER(DT_VISIT_WHITE));
324
325 _add_edge(outgoing, pred, self);
326 }
327 }
328
329 // DFS over all nodes (including any that appeared only in previous)
330 GList *result = NULL;
331 GList *cycle = NULL;
332 GList *dfs_stack = NULL;
333
334 int dfs_count = 0;
335
336 GHashTableIter iter;
337 gpointer key = NULL, val = NULL;
338 g_hash_table_iter_init(&iter, outgoing);
339 while(g_hash_table_iter_next(&iter, &key, &val))
340 {
342 dt_visit_color_t c = (dt_visit_color_t)GPOINTER_TO_INT(g_hash_table_lookup(color, n));
343 if(c == DT_VISIT_WHITE)
344 {
345 if(_dfs_visit(n, outgoing, color, &result, &dfs_count, &dfs_stack, &cycle))
346 {
347 g_list_free(result);
348 result = NULL;
349 if(dfs_stack)
350 {
351 g_list_free(dfs_stack);
352 dfs_stack = NULL;
353 }
354 g_hash_table_destroy(outgoing);
355 g_hash_table_destroy(color);
356 *sorted = NULL;
357 if(cycle_out) *cycle_out = cycle;
358 else if(cycle) g_list_free(cycle);
359 return 1;
360 }
361 dfs_count++;
362 }
363 }
364
365 if(dfs_stack)
366 {
367 g_list_free(dfs_stack);
368 dfs_stack = NULL;
369 }
370 if(cycle)
371 {
372 g_list_free(cycle); // should never happen on success, but keep cleanup symmetric
373 cycle = NULL;
374 }
375
376 g_hash_table_destroy(outgoing);
377 g_hash_table_destroy(color);
378
379 *sorted = result;
380
381 // Debug: print the sorted solution
382 fprintf(stderr, "[toposort] Solution order:\n");
383 int idx = 0;
384 for(GList *it = g_list_first(result); it; it = g_list_next(it), idx++)
385 {
387 fprintf(stderr, "[toposort] %d: '%s'", idx, n->id);
388 if(n->tag) fprintf(stderr, " [%s]", n->tag);
389 fprintf(stderr, "\n");
390 }
391
392 fprintf(stderr, "[toposort] DFS visits performed: %d\n", dfs_count);
393
394 return 0;
395}
396
397/*
398 Frees one node and everything owned by it.
399 - constraints
400 - constraint objects
401 - node
402 Optional:
403 - id
404 - user_data
405*/
406
408{
409 if(IS_NULL_PTR(node)) return;
410
411 if(node->previous)
412 {
413 g_list_free(node->previous);
414 node->previous = NULL;
415 }
416
417 if(user_destroy && node->user_data) user_destroy(node->user_data);
418
419 if(node->tag)
420 {
421 dt_free(node->tag);
422 }
423 dt_free(node->id);
424
425 dt_free(node);
426}
427
428static void _dt_digraph_nodes_free_full(GList *nodes, dt_node_user_data_destroy_t user_destroy)
429{
430 for(GList *it = g_list_first(nodes); it; it = g_list_next(it))
431 dt_digraph_node_free_full(it->data, user_destroy);
432
433 g_list_free(nodes);
434 nodes = NULL;
435}
436
438{
439 if(IS_NULL_PTR(ht)) return;
440
441 GHashTableIter iter;
442 gpointer key, val;
443
444 g_hash_table_iter_init(&iter, ht);
445 while(g_hash_table_iter_next(&iter, &key, &val))
446 {
447 dt_digraph_node_t *node = val;
448 dt_digraph_node_free_full(node, user_destroy);
449 }
450
451 g_hash_table_destroy(ht);
452}
453
454void dt_digraph_cleanup_full(GList *nodes, GHashTable *node_ht, dt_node_user_data_destroy_t user_destroy)
455{
456 if(node_ht)
457 _dt_digraph_nodes_hashtable_free_full(node_ht, user_destroy);
458 else
459 _dt_digraph_nodes_free_full(nodes, user_destroy);
460}
461
462// clang-format off
463// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py
464// vim: shiftwidth=2 expandtab tabstop=2 cindent
465// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
466// clang-format on
#define TRUE
Definition ashift_lsd.c:162
#define FALSE
Definition ashift_lsd.c:158
#define m
Definition basecurve.c:278
static const dt_aligned_pixel_simd_t const dt_adaptation_t const float p
char * key
#define dt_free(ptr)
Definition darktable.h:456
#define IS_NULL_PTR(p)
C is way too permissive with !=, == and if(var) checks, which can mean too many things depending on w...
Definition darktable.h:281
Directed graph node.
static dt_digraph_node_t * _get_or_create_node(GHashTable *by_id, const char *id)
static void _dt_digraph_nodes_hashtable_free_full(GHashTable *ht, dt_node_user_data_destroy_t user_destroy)
dt_visit_color_t
@ DT_VISIT_BLACK
@ DT_VISIT_GRAY
@ DT_VISIT_WHITE
static GList * _toposort_extract_cycle_from_stack(const GList *dfs_stack, const dt_digraph_node_t *gray)
static void dt_digraph_node_free_full(dt_digraph_node_t *node, dt_node_user_data_destroy_t user_destroy)
static void _dt_digraph_nodes_free_full(GList *nodes, dt_node_user_data_destroy_t user_destroy)
static gboolean _add_edge(GHashTable *outgoing, dt_digraph_node_t *from, dt_digraph_node_t *to)
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.
static gboolean _dfs_visit(dt_digraph_node_t *n, GHashTable *outgoing, GHashTable *color, GList **result, int *dfs_count, GList **dfs_stack, GList **cycle_out)
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.
void(* dt_node_user_data_destroy_t)(void *data)
Optional destructor for node payloads.