Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
supervisor_window.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
20#include "common/darktable.h"
21#include "common/image_cache.h"
22#include "common/mipmap_cache.h"
23#include "common/opencl.h"
25#include "develop/supervisor.h"
26#include "gui/gtk.h"
27
28#include <gtk/gtk.h>
29#include <json-glib/json-glib.h>
30
31// Cap on how many timeline rows we keep live (older ones are trimmed). The full
32// log lives in the supervisor; this only bounds GTK widget pressure.
33#define TIMELINE_MAX_ROWS 4000
34#define POLL_INTERVAL_MS 300
35#define MEMORY_MAX_ROWS 500 // cap items shown per cache in the memory view
36#define MEMORY_REFRESH_TICKS 4 // refresh the memory view every Nth poll tick when visible
37
38static struct
39{
45 GtkWidget *mem_box; // vbox holding the memory view content
46 GtkWidget *search_entry; // global search entry (in the toolbar)
47 GtkWidget *search_list; // search results page
49 GtkWidget *groupby; // GtkComboBoxText: domain / thread / op
50 GHashTable *decl_map; // hex string -> GtkListBoxRow* (the create/declaration row)
51 GHashTable *group_map; // group key -> _group_t* (incremental grouped buckets)
52 uint64_t last_seq; // highest captured seq already displayed in the timeline
53 uint64_t grouped_last_seq; // highest seq already folded into the grouped view
54 gchar *scroll_target; // hash of a timeline row to scroll into view (deferred)
55 guint timer_id;
56 guint tick; // poll tick counter (for throttling the memory view)
57 int page_timeline, page_grouped, page_memory, page_search; // notebook page indices
58} _g = { 0 };
59
60// One grouped bucket: its detail body (where event rows are appended) and the
61// header label (whose count we keep in sync).
69
70static void _group_free(gpointer p)
71{
72 _group_t *g = (_group_t *)p;
73 if(!g) return;
74 g_free(g->key);
75 g_free(g); // widgets are owned by the list box
76}
77
78static gchar *_hashhex(const uint64_t h)
79{
80 return g_strdup_printf("0x%016" G_GINT64_MODIFIER "x", h);
81}
82
83static const char *_domain_color(const char *d)
84{
85 if(!g_strcmp0(d, "history")) return "#7fbfff";
86 if(!g_strcmp0(d, "node")) return "#9fe0a0";
87 if(!g_strcmp0(d, "cacheline")) return "#ffd080";
88 if(!g_strcmp0(d, "backbuf")) return "#ff9a9a";
89 if(!g_strcmp0(d, "widget")) return "#d0a8ff";
90 if(!g_strcmp0(d, "thumbnail")) return "#bcbcbc";
91 if(!g_strcmp0(d, "mipmap")) return "#b0c4de";
92 if(!g_strcmp0(d, "image")) return "#e8c8a0";
93 if(!g_strcmp0(d, "form")) return "#f0a0c0";
94 return "#dddddd";
95}
96
97static gchar *_pretty_json(const char *compact)
98{
99 JsonParser *p = json_parser_new();
100 gchar *out = NULL;
101 if(json_parser_load_from_data(p, compact, -1, NULL))
102 {
103 JsonGenerator *g = json_generator_new();
104 json_generator_set_root(g, json_parser_get_root(p));
105 json_generator_set_pretty(g, TRUE);
106 out = json_generator_to_data(g, NULL);
107 g_object_unref(g);
108 }
109 g_object_unref(p);
110 return out ? out : g_strdup(compact);
111}
112
113static void _run_search(const char *query);
114
115// Scroll a timeline row (centred) into the visible area of its scrolled window.
117{
118 if(!row || !_g.timeline_scroll) return;
119 GtkAdjustment *vadj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(_g.timeline_scroll));
120 if(!vadj) return;
121 gint y = 0;
122 if(!gtk_widget_translate_coordinates(row, _g.timeline_list, 0, 0, NULL, &y)) return;
123 GtkAllocation alloc;
124 gtk_widget_get_allocation(row, &alloc);
125 const double page = gtk_adjustment_get_page_size(vadj);
126 const double upper = gtk_adjustment_get_upper(vadj);
127 double target = (double)y + alloc.height / 2.0 - page / 2.0; // centre the row
128 target = CLAMP(target, 0.0, MAX(0.0, upper - page));
129 gtk_adjustment_set_value(vadj, target);
130}
131
132// Deferred so the scroll runs after the page switch / row expansion are laid out.
133static gboolean _scroll_target_idle(gpointer u)
134{
135 if(_g.window && _g.scroll_target)
136 {
137 GtkWidget *row = (GtkWidget *)g_hash_table_lookup(_g.decl_map, _g.scroll_target);
139 }
140 g_clear_pointer(&_g.scroll_target, g_free);
141 return G_SOURCE_REMOVE;
142}
143
144// Clicking a hash jumps to the declaration (create event) of that object.
145static gboolean _on_link(GtkLabel *label, gchar *uri, gpointer user_data)
146{
147 if(!_g.decl_map) return TRUE;
148 GtkWidget *row = (GtkWidget *)g_hash_table_lookup(_g.decl_map, uri);
149 if(row)
150 {
151 gtk_notebook_set_current_page(GTK_NOTEBOOK(_g.notebook), _g.page_timeline);
152 GtkWidget *toggle = (GtkWidget *)g_object_get_data(G_OBJECT(row), "toggle");
153 if(toggle) gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(toggle), TRUE); // expand
154 gtk_list_box_select_row(GTK_LIST_BOX(_g.timeline_list), GTK_LIST_BOX_ROW(row));
155 gtk_widget_grab_focus(row);
156 // Defer the explicit scroll: the page just switched and the row may not be
157 // laid out yet, so translate_coordinates would be stale right now.
158 g_free(_g.scroll_target);
159 _g.scroll_target = g_strdup(uri);
160 g_idle_add(_scroll_target_idle, NULL);
161 }
162 return TRUE; // handled: do not try to open as an URL
163}
164
165static void _on_search_this_hash(GtkMenuItem *item, gpointer u)
166{
167 const char *uri = (const char *)g_object_get_data(G_OBJECT(item), "uri");
168 if(uri && _g.search_entry)
169 {
170 gtk_entry_set_text(GTK_ENTRY(_g.search_entry), uri); // triggers search-changed → _run_search
171 gtk_notebook_set_current_page(GTK_NOTEBOOK(_g.notebook), _g.page_search);
172 }
173}
174
175// Right-click over a hash link offers to search for that hash.
176static gboolean _on_label_button(GtkWidget *label, GdkEventButton *e, gpointer u)
177{
178 if(e->type != GDK_BUTTON_PRESS || e->button != GDK_BUTTON_SECONDARY) return FALSE;
179 const char *uri = gtk_label_get_current_uri(GTK_LABEL(label)); // link under the pointer
180 // Inside a list box (timeline/grouped/search) the pointer-tracking that sets
181 // the "current link" is unreliable, so fall back to the label's single own hash.
182 if(!uri || !*uri) uri = (const char *)g_object_get_data(G_OBJECT(label), "own-hash");
183 if(!uri || !*uri) return FALSE;
184
185 GtkWidget *menu = gtk_menu_new();
186 GtkWidget *mi = gtk_menu_item_new_with_label(_("Search for this hash"));
187 g_object_set_data_full(G_OBJECT(mi), "uri", g_strdup(uri), g_free);
188 g_signal_connect(mi, "activate", G_CALLBACK(_on_search_this_hash), NULL);
189 gtk_menu_shell_append(GTK_MENU_SHELL(menu), mi);
190 gtk_widget_show_all(menu);
191 gtk_menu_popup_at_pointer(GTK_MENU(menu), (GdkEvent *)e);
192 return TRUE;
193}
194
195// Wire a hash-bearing label: left-click navigates, right-click offers search.
196static void _wire_hash_label(GtkWidget *label)
197{
198 gtk_label_set_track_visited_links(GTK_LABEL(label), FALSE);
199 g_signal_connect(label, "activate-link", G_CALLBACK(_on_link), NULL);
200 gtk_widget_add_events(label, GDK_BUTTON_PRESS_MASK);
201 g_signal_connect(label, "button-press-event", G_CALLBACK(_on_label_button), NULL);
202}
203
204static GtkWidget *_event_body_from(const char *json, GArray *links);
205
206static void _on_toggle(GtkToggleButton *t, gpointer u)
207{
208 const gboolean a = gtk_toggle_button_get_active(t);
209 GtkWidget *rev = (GtkWidget *)g_object_get_data(G_OBJECT(t), "revealer");
210 GtkWidget *arrow = (GtkWidget *)g_object_get_data(G_OBJECT(t), "arrow");
211
212 // Lazily build an event's detail body the first time it is expanded, so the
213 // JSON is not parsed for rows the user never opens (important under streaming).
214 if(a && rev && !gtk_bin_get_child(GTK_BIN(rev)))
215 {
216 const char *json = (const char *)g_object_get_data(G_OBJECT(t), "lazy_json");
217 if(json)
218 {
219 GtkWidget *body = _event_body_from(json, (GArray *)g_object_get_data(G_OBJECT(t), "lazy_links"));
220 gtk_container_add(GTK_CONTAINER(rev), body);
221 gtk_widget_show_all(body);
222 }
223 }
224 if(rev) gtk_revealer_set_reveal_child(GTK_REVEALER(rev), a);
225 if(arrow)
226 gtk_image_set_from_icon_name(GTK_IMAGE(arrow), a ? "pan-down-symbolic" : "pan-end-symbolic",
227 GTK_ICON_SIZE_BUTTON);
228}
229
230// A collapsible block: [arrow toggle][header label] + a revealer holding `body`.
231// The header is a separate label (NOT the toggle), so its hash links stay
232// clickable. Returns a vbox; the toggle button is stored as data "toggle".
233static GtkWidget *_collapsible(const char *header_markup, GtkWidget *body, const gboolean expanded)
234{
235 GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
236 GtkWidget *hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
237
238 GtkWidget *toggle = gtk_toggle_button_new();
239 gtk_button_set_relief(GTK_BUTTON(toggle), GTK_RELIEF_NONE);
240 gtk_widget_set_focus_on_click(toggle, FALSE);
241 GtkWidget *arrow = gtk_image_new_from_icon_name(expanded ? "pan-down-symbolic" : "pan-end-symbolic",
242 GTK_ICON_SIZE_BUTTON);
243 gtk_container_add(GTK_CONTAINER(toggle), arrow);
244
245 GtkWidget *header = gtk_label_new(NULL);
246 gtk_label_set_markup(GTK_LABEL(header), header_markup);
247 gtk_label_set_xalign(GTK_LABEL(header), 0.0);
248 _wire_hash_label(header);
249
250 GtkWidget *revealer = gtk_revealer_new();
251 gtk_revealer_set_reveal_child(GTK_REVEALER(revealer), expanded);
252 if(body) gtk_container_add(GTK_CONTAINER(revealer), body);
253
254 g_object_set_data(G_OBJECT(toggle), "revealer", revealer);
255 g_object_set_data(G_OBJECT(toggle), "arrow", arrow);
256 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(toggle), expanded);
257 g_signal_connect(toggle, "toggled", G_CALLBACK(_on_toggle), NULL);
258
259 gtk_box_pack_start(GTK_BOX(hbox), toggle, FALSE, FALSE, 0);
260 gtk_box_pack_start(GTK_BOX(hbox), header, TRUE, TRUE, 0);
261 gtk_box_pack_start(GTK_BOX(vbox), hbox, FALSE, FALSE, 0);
262 gtk_box_pack_start(GTK_BOX(vbox), revealer, FALSE, FALSE, 0);
263
264 g_object_set_data(G_OBJECT(vbox), "toggle", toggle);
265 g_object_set_data(G_OBJECT(vbox), "header", header); // so callers can update the label
266 return vbox;
267}
268
269static gchar *_header_markup(const dt_sv_logged_event_t *ev)
270{
271 gchar *hx = _hashhex(ev->hash);
272 gchar *e_op = g_markup_escape_text(ev->op, -1);
273 gchar *e_dom = g_markup_escape_text(ev->domain, -1);
274 gchar *e_thr = g_markup_escape_text(ev->thread, -1);
275 gchar *e_mn_raw = g_markup_escape_text(ev->mnemonic[0] ? ev->mnemonic : "-", -1);
276 // Raster masks are side-band cachelines, not module outputs: dim the domain
277 // badge and tag the title so they read as secondary entries among plain
278 // image cachelines of the same module.
279 const gboolean is_raster_mask
280 = !g_strcmp0(ev->domain, "cacheline") && !g_strcmp0(ev->cache_name, "raster mask");
281 gchar *e_mn = is_raster_mask ? g_strdup_printf("%s - raster mask", e_mn_raw) : g_strdup(e_mn_raw);
282 g_free(e_mn_raw);
283 gchar *out = g_strdup_printf(
284 "<tt>%9.3f</tt> <b>%-7s</b> <span foreground=\"%s\">%-10s</span> <b>%-16s</b> "
285 "<a href=\"%s\"><tt>%s</tt></a> <span size=\"small\"><i>%s</i></span>",
286 ev->ts, e_op, is_raster_mask ? "#b78d45" : _domain_color(ev->domain), e_dom, e_mn, hx, hx, e_thr);
287 g_free(hx);
288 g_free(e_op);
289 g_free(e_dom);
290 g_free(e_thr);
291 g_free(e_mn);
292 return out;
293}
294
295// Build the detail body for an event: clickable linked hashes + full record.
296static GtkWidget *_event_body_from(const char *json, GArray *links)
297{
298 GtkWidget *detail = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
299 gtk_widget_set_margin_start(detail, 28);
300 gtk_widget_set_margin_bottom(detail, 6);
301
302 if(links && links->len)
303 {
304 GString *ls = g_string_new(NULL);
305 for(guint i = 0; i < links->len; i++)
306 {
307 const dt_sv_link_t *lk = &g_array_index(links, dt_sv_link_t, i);
308 gchar *lhx = _hashhex(lk->hash);
309 g_string_append_printf(ls, "%s%s → <a href=\"%s\"><tt>%s</tt></a>", i ? " " : "", lk->label,
310 lhx, lhx);
311 g_free(lhx);
312 }
313 GtkWidget *lw = gtk_label_new(NULL);
314 gtk_label_set_markup(GTK_LABEL(lw), ls->str);
315 gtk_label_set_xalign(GTK_LABEL(lw), 0.0);
317 gtk_box_pack_start(GTK_BOX(detail), lw, FALSE, FALSE, 0);
318 g_string_free(ls, TRUE);
319 }
320
321 gchar *pretty = _pretty_json(json);
322 gchar *escaped = g_markup_escape_text(pretty, -1);
323 gchar *mono = g_strdup_printf("<tt>%s</tt>", escaped);
324 GtkWidget *body = gtk_label_new(NULL);
325 gtk_label_set_markup(GTK_LABEL(body), mono);
326 gtk_label_set_xalign(GTK_LABEL(body), 0.0);
327 gtk_label_set_selectable(GTK_LABEL(body), TRUE);
328 gtk_box_pack_start(GTK_BOX(detail), body, FALSE, FALSE, 0);
329 g_free(pretty);
330 g_free(escaped);
331 g_free(mono);
332 return detail;
333}
334
335static void _links_free(gpointer p)
336{
337 if(p) g_array_free((GArray *)p, TRUE);
338}
339
340// Collapsed event row; the detail body is built lazily on first expand.
342{
343 gchar *hm = _header_markup(ev);
344 GtkWidget *w = _collapsible(hm, NULL, FALSE);
345 g_free(hm);
346
347 GtkWidget *toggle = (GtkWidget *)g_object_get_data(G_OBJECT(w), "toggle");
348 g_object_set_data_full(G_OBJECT(toggle), "lazy_json", g_strdup(ev->json), g_free);
349 if(ev->links && ev->links->len)
350 {
351 GArray *copy = g_array_new(FALSE, FALSE, sizeof(dt_sv_link_t));
352 g_array_append_vals(copy, ev->links->data, ev->links->len);
353 g_object_set_data_full(G_OBJECT(toggle), "lazy_links", copy, _links_free);
354 }
355
356 // The header carries exactly one hash (the event's own): stash it so right-click
357 // search works even when the list box defeats the label's link tracking.
358 GtkWidget *header = (GtkWidget *)g_object_get_data(G_OBJECT(w), "header");
359 if(header) g_object_set_data_full(G_OBJECT(header), "own-hash", _hashhex(ev->hash), g_free);
360 return w;
361}
362
363// Append one event to the timeline list and register it as a declaration when
364// it is a create event.
366{
367 GtkWidget *w = _event_widget(ev);
368 gtk_widget_show_all(w);
369 gtk_container_add(GTK_CONTAINER(_g.timeline_list), w);
370 GtkWidget *row = gtk_widget_get_parent(w); // the auto-created GtkListBoxRow
371 g_object_set_data(G_OBJECT(row), "toggle", g_object_get_data(G_OBJECT(w), "toggle"));
372 if(!g_strcmp0(ev->op, "create"))
373 {
374 gchar *hx = _hashhex(ev->hash);
375 g_object_set_data_full(G_OBJECT(row), "svhash", g_strdup(hx), g_free);
376 g_hash_table_replace(_g.decl_map, hx, row); // map owns hx
377 }
378}
379
380static void _timeline_trim(void)
381{
382 GList *children = gtk_container_get_children(GTK_CONTAINER(_g.timeline_list));
383 int excess = (int)g_list_length(children) - TIMELINE_MAX_ROWS;
384 for(GList *l = children; l && excess > 0; l = l->next, excess--)
385 {
386 GtkWidget *row = GTK_WIDGET(l->data);
387 const char *hx = (const char *)g_object_get_data(G_OBJECT(row), "svhash");
388 if(hx && g_hash_table_lookup(_g.decl_map, hx) == row) g_hash_table_remove(_g.decl_map, hx);
389 gtk_widget_destroy(row);
390 }
391 g_list_free(children);
392}
393
394static void _clear_list(GtkWidget *list)
395{
396 GList *children = gtk_container_get_children(GTK_CONTAINER(list));
397 for(GList *l = children; l; l = l->next) gtk_widget_destroy(GTK_WIDGET(l->data));
398 g_list_free(children);
399}
400
401static void _update_count(void)
402{
403 gchar *s = g_strdup_printf(_("%u events"), dt_supervisor_events_count());
404 gtk_label_set_text(GTK_LABEL(_g.count_label), s);
405 g_free(s);
406}
407
408static const char *_group_key(const dt_sv_logged_event_t *ev, const char *by)
409{
410 if(!g_strcmp0(by, "thread")) return ev->thread;
411 if(!g_strcmp0(by, "op")) return ev->op;
412 return ev->domain;
413}
414
416{
417 gchar *e = g_markup_escape_text(g->key && *g->key ? g->key : "(none)", -1);
418 gchar *hm = g_strdup_printf("<b>%s</b> <span size=\"small\">(%d)</span>", e, g->count);
419 gtk_label_set_markup(GTK_LABEL(g->header), hm);
420 g_free(e);
421 g_free(hm);
422}
423
424// Append one event into its grouped bucket (creating the bucket if needed),
425// preserving existing groups/rows and their expansion state.
426static void _grouped_append(const dt_sv_logged_event_t *ev, const char *by)
427{
428 const char *k = _group_key(ev, by);
429 _group_t *g = (_group_t *)g_hash_table_lookup(_g.group_map, k);
430 if(!g)
431 {
432 g = g_new0(_group_t, 1);
433 g->key = g_strdup(k);
434 g->body = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
435 gtk_widget_set_margin_start(g->body, 12);
436 GtkWidget *coll = _collapsible("", g->body, FALSE);
437 g->header = (GtkWidget *)g_object_get_data(G_OBJECT(coll), "header");
438 gtk_container_add(GTK_CONTAINER(_g.grouped_list), coll);
439 gtk_widget_show_all(coll);
440 g_hash_table_insert(_g.group_map, g_strdup(k), g);
441 }
443 gtk_box_pack_start(GTK_BOX(g->body), row, FALSE, FALSE, 0);
444 gtk_widget_show_all(row);
445 g->count++;
447}
448
449// Fold all events newer than the grouped cursor into the buckets (live append).
450static void _grouped_catch_up(void)
451{
452 uint64_t newlast = _g.grouped_last_seq;
453 GPtrArray *evs = dt_supervisor_events_snapshot_since(_g.grouped_last_seq, &newlast);
454 if(evs->len)
455 {
456 gchar *by = gtk_combo_box_text_get_active_text(GTK_COMBO_BOX_TEXT(_g.groupby));
457 for(guint i = 0; i < evs->len; i++) _grouped_append(g_ptr_array_index(evs, i), by ? by : "domain");
458 g_free(by);
459 _g.grouped_last_seq = newlast;
460 }
462}
463
464// Full rebuild (group-by change / Clear): wipe buckets and re-fold everything.
465static void _rebuild_grouped_now(void)
466{
467 _clear_list(_g.grouped_list);
468 g_hash_table_remove_all(_g.group_map);
469 _g.grouped_last_seq = 0;
471}
472
473// ---- Memory view -------------------------------------------------------------
474
475static void _add_usage_bar(GtkWidget *box, const char *title, const size_t cur, const size_t max)
476{
477 GtkWidget *hdr = gtk_label_new(NULL);
478 gchar *t = g_markup_printf_escaped("<b>%s</b>", title);
479 gtk_label_set_markup(GTK_LABEL(hdr), t);
480 gtk_label_set_xalign(GTK_LABEL(hdr), 0.0);
481 gtk_widget_set_margin_top(hdr, 8);
482 g_free(t);
483 gtk_box_pack_start(GTK_BOX(box), hdr, FALSE, FALSE, 0);
484
485 const double frac = max ? CLAMP((double)cur / (double)max, 0.0, 1.0) : 0.0;
486 GtkWidget *bar = gtk_progress_bar_new();
487 gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(bar), frac);
488 gtk_progress_bar_set_show_text(GTK_PROGRESS_BAR(bar), TRUE);
489 gchar *txt = g_strdup_printf("%.1f / %.1f MiB (%.0f%%)", cur / 1048576.0, max / 1048576.0, frac * 100.0);
490 gtk_progress_bar_set_text(GTK_PROGRESS_BAR(bar), txt);
491 g_free(txt);
492 gtk_box_pack_start(GTK_BOX(box), bar, FALSE, FALSE, 0);
493}
494
495// A clickable memory item (markup carries the navigation link). When `trailing`
496// is non-NULL it is packed at the right edge as a separate label, so the trailing
497// column (e.g. vRAM) lines up vertically across rows.
498static void _add_mem_item(GtkWidget *box, const char *markup, const char *trailing)
499{
500 GtkWidget *lbl = gtk_label_new(NULL);
501 gtk_label_set_markup(GTK_LABEL(lbl), markup);
502 gtk_label_set_xalign(GTK_LABEL(lbl), 0.0);
503 _wire_hash_label(lbl);
504
505 if(trailing && *trailing)
506 {
507 GtkWidget *row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
508 gtk_widget_set_margin_start(row, 12);
509 gtk_box_pack_start(GTK_BOX(row), lbl, TRUE, TRUE, 0); // expands → pushes trailing to the right
510 GtkWidget *tl = gtk_label_new(NULL);
511 gtk_label_set_markup(GTK_LABEL(tl), trailing);
512 gtk_label_set_xalign(GTK_LABEL(tl), 1.0);
513 gtk_box_pack_end(GTK_BOX(row), tl, FALSE, FALSE, 0);
514 gtk_box_pack_start(GTK_BOX(box), row, FALSE, FALSE, 0);
515 }
516 else
517 {
518 gtk_widget_set_margin_start(lbl, 12);
519 gtk_box_pack_start(GTK_BOX(box), lbl, FALSE, FALSE, 0);
520 }
521}
522
523static gint _cmp_pixel_size(gconstpointer a, gconstpointer b)
524{
525 const dt_pixel_cache_stats_entry_t *x = a, *y = b;
526 return (y->size > x->size) - (y->size < x->size); // descending
527}
528
529static gint _cmp_mip_size(gconstpointer a, gconstpointer b)
530{
531 const dt_mipmap_cache_stats_entry_t *x = a, *y = b;
532 return (y->size > x->size) - (y->size < x->size); // descending
533}
534
535static gint _cmp_image_imgid(gconstpointer a, gconstpointer b)
536{
537 const dt_image_cache_stats_entry_t *x = a, *y = b;
538 return x->imgid - y->imgid; // ascending (entries are all the same size)
539}
540
541static void _rebuild_memory(void)
542{
543 if(!_g.mem_box) return;
544 _clear_list(_g.mem_box);
545
546 // Pipeline cache
547 size_t cur = 0, max = 0;
550
551 const gboolean gpu = dt_opencl_is_enabled();
552 size_t vram_used = 0;
553 for(guint i = 0; i < pe->len; i++) vram_used += g_array_index(pe, dt_pixel_cache_stats_entry_t, i).cl_bytes;
554
555 gchar *ptitle = g_strdup_printf(gpu ? _("Pipeline cache (RAM) — %u items") : _("Pipeline cache — %u items"),
556 pe->len);
557 _add_usage_bar(_g.mem_box, ptitle, cur, max);
558 g_free(ptitle);
559 if(gpu)
560 _add_usage_bar(_g.mem_box, _("Pipeline cache (vRAM)"), vram_used,
562
563 g_array_sort(pe, _cmp_pixel_size);
564 for(guint i = 0; i < pe->len && i < MEMORY_MAX_ROWS; i++)
565 {
566 const dt_pixel_cache_stats_entry_t *e = &g_array_index(pe, dt_pixel_cache_stats_entry_t, i);
567 gchar *hx = _hashhex(e->hash);
568 gchar *name = g_markup_escape_text(e->name[0] ? e->name : "-", -1);
569 // fixed-width monospace trailing column so the vRAM figures line up vertically
570 gchar *vram = (gpu && e->cl_count > 0)
571 ? g_strdup_printf("<tt>+%8.2f MiB vRAM (%2d buf)</tt>", e->cl_bytes / 1048576.0,
572 e->cl_count)
573 : NULL;
574 gchar *m = g_strdup_printf("<a href=\"%s\"><tt>%s</tt></a> %.2f MiB refs=%d hits=%d <i>%s</i>", hx, hx,
575 e->size / 1048576.0, e->refcount, e->hits, name);
576 _add_mem_item(_g.mem_box, m, vram);
577 g_free(hx);
578 g_free(name);
579 g_free(vram);
580 g_free(m);
581 }
582 g_array_free(pe, TRUE);
583
584 // Mipmap cache
587 gchar *mtitle = g_strdup_printf(_("Mipmap cache — %u items"), me->len);
588 _add_usage_bar(_g.mem_box, mtitle, cur, max);
589 g_free(mtitle);
590 g_array_sort(me, _cmp_mip_size);
591 for(guint i = 0; i < me->len && i < MEMORY_MAX_ROWS; i++)
592 {
593 const dt_mipmap_cache_stats_entry_t *e = &g_array_index(me, dt_mipmap_cache_stats_entry_t, i);
594 gchar *hx = _hashhex(dt_supervisor_mipmap_key(e->imgid, e->mip));
595 gchar *m = g_strdup_printf("<a href=\"%s\">image #%d · mip %d</a> %.2f MiB", hx, e->imgid, e->mip,
596 e->size / 1048576.0);
597 _add_mem_item(_g.mem_box, m, NULL);
598 g_free(hx);
599 g_free(m);
600 }
601 g_array_free(me, TRUE);
602
603 // Image cache
606 gchar *ititle = g_strdup_printf(_("Image cache — %u items"), ie->len);
607 _add_usage_bar(_g.mem_box, ititle, cur, max);
608 g_free(ititle);
609 g_array_sort(ie, _cmp_image_imgid);
610 for(guint i = 0; i < ie->len && i < MEMORY_MAX_ROWS; i++)
611 {
612 const dt_image_cache_stats_entry_t *e = &g_array_index(ie, dt_image_cache_stats_entry_t, i);
613 gchar *hx = _hashhex(dt_supervisor_image_key(e->imgid));
614 gchar *fn = g_markup_escape_text(e->filename[0] ? e->filename : "-", -1);
615 gchar *m = g_strdup_printf("<a href=\"%s\">image #%d</a> <i>%s</i>", hx, e->imgid, fn);
616 _add_mem_item(_g.mem_box, m, NULL);
617 g_free(hx);
618 g_free(fn);
619 g_free(m);
620 }
621 g_array_free(ie, TRUE);
622
623 gtk_widget_show_all(_g.mem_box);
624}
625
626// ---- Search ------------------------------------------------------------------
627
628// TRUE if the hash's hex (lowercased, with 0x) contains the needle.
629static gboolean _hex_contains(const uint64_t h, const char *needle)
630{
631 gchar *hx = _hashhex(h);
632 gchar *lh = g_ascii_strdown(hx, -1);
633 const gboolean m = strstr(lh, needle) != NULL;
634 g_free(hx);
635 g_free(lh);
636 return m;
637}
638
639// An event matches when:
640// - its own hash or any linked hash contains `hash_needle` (hex, 0x stripped), or
641// - its full record (JSON: module/op/domain/widget/filename/parameters/…)
642// contains `text_needle` as a case-insensitive substring.
643static gboolean _event_matches(const dt_sv_logged_event_t *ev, const char *hash_needle,
644 const char *text_needle)
645{
646 if(*hash_needle)
647 {
648 if(_hex_contains(ev->hash, hash_needle)) return TRUE;
649 if(ev->links)
650 for(guint i = 0; i < ev->links->len; i++)
651 if(_hex_contains(g_array_index(ev->links, dt_sv_link_t, i).hash, hash_needle)) return TRUE;
652 }
653 if(*text_needle && ev->json)
654 {
655 gchar *jl = g_ascii_strdown(ev->json, -1);
656 const gboolean m = strstr(jl, text_needle) != NULL;
657 g_free(jl);
658 if(m) return TRUE;
659 }
660 return FALSE;
661}
662
663static void _run_search(const char *query)
664{
665 if(!_g.search_list) return;
666 _clear_list(_g.search_list);
667 if(!query || !*query)
668 {
669 gtk_widget_show_all(_g.search_list);
670 return;
671 }
672
673 // text_needle matches any field of the record; hash_needle (0x stripped) matches hashes.
674 gchar *text_needle = g_ascii_strdown(query, -1);
675 const char *hash_needle = g_str_has_prefix(text_needle, "0x") ? text_needle + 2 : text_needle;
676
677 GPtrArray *evs = dt_supervisor_events_snapshot();
678 for(guint i = 0; i < evs->len; i++)
679 {
680 const dt_sv_logged_event_t *ev = g_ptr_array_index(evs, i);
681 if(_event_matches(ev, hash_needle, text_needle))
682 gtk_container_add(GTK_CONTAINER(_g.search_list), _event_widget(ev));
683 }
685 g_free(text_needle);
686 gtk_widget_show_all(_g.search_list);
687}
688
689static void _on_search_changed(GtkSearchEntry *e, gpointer u)
690{
691 const char *txt = gtk_entry_get_text(GTK_ENTRY(e));
692 // Typing a query jumps to the Search page automatically.
693 if(txt && *txt) gtk_notebook_set_current_page(GTK_NOTEBOOK(_g.notebook), _g.page_search);
694 _run_search(txt);
695}
696
697static gboolean _scroll_bottom_idle(gpointer u)
698{
699 if(!_g.timeline_scroll) return G_SOURCE_REMOVE;
700 GtkAdjustment *adj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(_g.timeline_scroll));
701 if(adj) gtk_adjustment_set_value(adj, gtk_adjustment_get_upper(adj));
702 return G_SOURCE_REMOVE;
703}
704
705static gboolean _poll(gpointer u)
706{
707 if(!_g.window) return G_SOURCE_REMOVE;
708
709 _g.tick++;
710 const int page = gtk_notebook_get_current_page(GTK_NOTEBOOK(_g.notebook));
711 // The grouped view appends new events live (incrementally, preserving folds)
712 // while it is the visible page.
713 if(page == _g.page_grouped) _grouped_catch_up();
714 // The memory view reflects live cache state; refresh it on a throttled cadence.
715 // The search view is intentionally NOT auto-refreshed here: rebuilding it would
716 // collapse any rows the user expanded. It updates on input / Refresh instead.
717 if((_g.tick % MEMORY_REFRESH_TICKS) == 0 && page == _g.page_memory) _rebuild_memory();
718
719 uint64_t newlast = _g.last_seq;
720 GPtrArray *evs = dt_supervisor_events_snapshot_since(_g.last_seq, &newlast);
721 if(evs->len)
722 {
723 GtkAdjustment *adj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(_g.timeline_scroll));
724 const gboolean at_bottom = !adj
725 || gtk_adjustment_get_value(adj)
726 >= gtk_adjustment_get_upper(adj) - gtk_adjustment_get_page_size(adj) - 4.0;
727
728 for(guint i = 0; i < evs->len; i++) _timeline_append(g_ptr_array_index(evs, i));
729 _g.last_seq = newlast;
732 if(at_bottom) g_idle_add(_scroll_bottom_idle, NULL);
733 }
735 return G_SOURCE_CONTINUE;
736}
737
738static void _full_reload(void)
739{
740 _clear_list(_g.timeline_list);
741 g_hash_table_remove_all(_g.decl_map);
742 _g.last_seq = 0;
743 _poll(NULL); // appends everything since the start
746 _run_search(gtk_entry_get_text(GTK_ENTRY(_g.search_entry)));
747}
748
749static void _on_refresh(GtkButton *b, gpointer u) { _full_reload(); }
750
751static void _on_clear(GtkButton *b, gpointer u)
752{
754 _full_reload();
755}
756
757static void _on_groupby_changed(GtkComboBox *c, gpointer u)
758{
759 _rebuild_grouped_now(); // regroup everything under the new key
760}
761
762static void _on_record_toggled(GtkToggleButton *t, gpointer u)
763{
764 dt_supervisor_set_recording(gtk_toggle_button_get_active(t));
765}
766
767static void _on_page_changed(GtkNotebook *nb, GtkWidget *page, guint page_num, gpointer u)
768{
769 if((int)page_num == _g.page_grouped)
770 _grouped_catch_up(); // fold in whatever arrived while the page was hidden
771 else if((int)page_num == _g.page_memory)
773 else if((int)page_num == _g.page_search)
774 _run_search(gtk_entry_get_text(GTK_ENTRY(_g.search_entry)));
775}
776
777static void _on_destroy(GtkWidget *w, gpointer u)
778{
779 if(_g.timer_id) g_source_remove(_g.timer_id);
780 dt_supervisor_set_recording(FALSE); // stop capturing once the viewer is gone
781 if(_g.decl_map) g_hash_table_destroy(_g.decl_map);
782 if(_g.group_map) g_hash_table_destroy(_g.group_map);
783 g_free(_g.scroll_target);
784 memset(&_g, 0, sizeof(_g));
785}
786
788{
789 if(_g.window)
790 {
791 gtk_window_present(GTK_WINDOW(_g.window));
792 return;
793 }
794
795 _g.decl_map = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
796 _g.group_map = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, _group_free);
797
798 _g.window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
799 gtk_window_set_title(GTK_WINDOW(_g.window), _("Event supervisor"));
800 gtk_window_set_default_size(GTK_WINDOW(_g.window), 1000, 640);
801 gtk_window_set_transient_for(GTK_WINDOW(_g.window), GTK_WINDOW(dt_ui_main_window(darktable.gui->ui)));
802 g_signal_connect(_g.window, "destroy", G_CALLBACK(_on_destroy), NULL);
803
804 GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
805 gtk_container_set_border_width(GTK_CONTAINER(vbox), 8);
806 gtk_container_add(GTK_CONTAINER(_g.window), vbox);
807
808 // toolbar
809 GtkWidget *bar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
810 gtk_box_pack_start(GTK_BOX(vbox), bar, FALSE, FALSE, 0);
811
812 GtkWidget *record = gtk_check_button_new_with_label(_("Record"));
813 gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(record), TRUE);
814 dt_supervisor_set_recording(TRUE); // start capturing on open
815 g_signal_connect(record, "toggled", G_CALLBACK(_on_record_toggled), NULL);
816 gtk_box_pack_start(GTK_BOX(bar), record, FALSE, FALSE, 0);
817
818 GtkWidget *refresh = gtk_button_new_with_label(_("Refresh"));
819 g_signal_connect(refresh, "clicked", G_CALLBACK(_on_refresh), NULL);
820 gtk_box_pack_start(GTK_BOX(bar), refresh, FALSE, FALSE, 0);
821
822 GtkWidget *clear = gtk_button_new_with_label(_("Clear"));
823 g_signal_connect(clear, "clicked", G_CALLBACK(_on_clear), NULL);
824 gtk_box_pack_start(GTK_BOX(bar), clear, FALSE, FALSE, 0);
825
826 gtk_box_pack_start(GTK_BOX(bar), gtk_label_new(_("Group by")), FALSE, FALSE, 0);
827 _g.groupby = gtk_combo_box_text_new();
828 gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(_g.groupby), "domain");
829 gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(_g.groupby), "thread");
830 gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(_g.groupby), "op");
831 gtk_combo_box_set_active(GTK_COMBO_BOX(_g.groupby), 0);
832 g_signal_connect(_g.groupby, "changed", G_CALLBACK(_on_groupby_changed), NULL);
833 gtk_box_pack_start(GTK_BOX(bar), _g.groupby, FALSE, FALSE, 0);
834
835 // global search entry: query all events by hash (own or linked)
836 _g.search_entry = gtk_search_entry_new();
837 gtk_entry_set_placeholder_text(GTK_ENTRY(_g.search_entry), _("search hash or text…"));
838 gtk_widget_set_size_request(_g.search_entry, 220, -1);
839 g_signal_connect(_g.search_entry, "search-changed", G_CALLBACK(_on_search_changed), NULL);
840 gtk_box_pack_end(GTK_BOX(bar), _g.search_entry, FALSE, FALSE, 0);
841
842 _g.count_label = gtk_label_new("");
843 gtk_box_pack_end(GTK_BOX(bar), _g.count_label, FALSE, FALSE, 0);
844
845 // notebook with the four pages, tabs spread across the full width
846 _g.notebook = gtk_notebook_new();
847 g_signal_connect(_g.notebook, "switch-page", G_CALLBACK(_on_page_changed), NULL);
848 gtk_box_pack_start(GTK_BOX(vbox), _g.notebook, TRUE, TRUE, 0);
849
850 _g.timeline_list = gtk_list_box_new();
851 gtk_list_box_set_selection_mode(GTK_LIST_BOX(_g.timeline_list), GTK_SELECTION_SINGLE);
852 _g.timeline_scroll = gtk_scrolled_window_new(NULL, NULL);
853 gtk_container_add(GTK_CONTAINER(_g.timeline_scroll), _g.timeline_list);
854 _g.page_timeline = gtk_notebook_append_page(GTK_NOTEBOOK(_g.notebook), _g.timeline_scroll,
855 gtk_label_new(_("Timeline")));
856
857 _g.grouped_list = gtk_list_box_new();
858 gtk_list_box_set_selection_mode(GTK_LIST_BOX(_g.grouped_list), GTK_SELECTION_NONE);
859 GtkWidget *sw2 = gtk_scrolled_window_new(NULL, NULL);
860 gtk_container_add(GTK_CONTAINER(sw2), _g.grouped_list);
861 _g.page_grouped = gtk_notebook_append_page(GTK_NOTEBOOK(_g.notebook), sw2, gtk_label_new(_("Grouped")));
862
863 _g.mem_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2);
864 gtk_container_set_border_width(GTK_CONTAINER(_g.mem_box), 6);
865 GtkWidget *sw3 = gtk_scrolled_window_new(NULL, NULL);
866 gtk_container_add(GTK_CONTAINER(sw3), _g.mem_box);
867 _g.page_memory = gtk_notebook_append_page(GTK_NOTEBOOK(_g.notebook), sw3, gtk_label_new(_("Memory")));
868
869 _g.search_list = gtk_list_box_new();
870 gtk_list_box_set_selection_mode(GTK_LIST_BOX(_g.search_list), GTK_SELECTION_NONE);
871 GtkWidget *sw4 = gtk_scrolled_window_new(NULL, NULL);
872 gtk_container_add(GTK_CONTAINER(sw4), _g.search_list);
873 _g.page_search = gtk_notebook_append_page(GTK_NOTEBOOK(_g.notebook), sw4, gtk_label_new(_("Search")));
874
875 // make each tab expand to fill the notebook width
876 GList *pages = gtk_container_get_children(GTK_CONTAINER(_g.notebook));
877 for(GList *l = pages; l; l = l->next)
878 gtk_container_child_set(GTK_CONTAINER(_g.notebook), GTK_WIDGET(l->data), "tab-expand", TRUE,
879 "tab-fill", TRUE, NULL);
880 g_list_free(pages);
881
882 gtk_widget_show_all(_g.window);
883 _full_reload();
884 _g.timer_id = g_timeout_add(POLL_INTERVAL_MS, _poll, NULL);
885}
#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
const float max
const dt_colormatrix_t dt_aligned_pixel_t out
static const int row
char * name
darktable_t darktable
Definition darktable.c:183
GtkWidget * dt_ui_main_window(dt_ui_t *ui)
get the main window widget
void dt_image_cache_get_usage(dt_image_cache_t *cache, size_t *current, size_t *max)
GArray * dt_image_cache_get_entries_stats(dt_image_cache_t *cache)
static const float x
const int t
float *const restrict const size_t k
GArray * dt_mipmap_cache_get_entries_stats(dt_mipmap_cache_t *cache)
void dt_mipmap_cache_get_usage(dt_mipmap_cache_t *cache, size_t *current, size_t *max)
int dt_opencl_is_enabled(void)
Definition opencl.c:2835
size_t dt_dev_pixelpipe_cache_get_vram_total(void)
GArray * dt_dev_pixelpipe_cache_get_entries_stats(dt_dev_pixelpipe_cache_t *cache)
void dt_dev_pixelpipe_cache_get_usage(dt_dev_pixelpipe_cache_t *cache, size_t *current, size_t *max)
Pixelpipe cache for storing intermediate results in the pixelpipe.
void copy(double *dest, double *source, size_t num_el)
Copy a flat buffer.
#define lw
Definition retouch.c:1055
struct _GtkWidget GtkWidget
Definition splash.h:29
unsigned __int64 uint64_t
Definition strptime.c:75
GtkWidget * body
GtkWidget * header
struct dt_dev_pixelpipe_cache_t * pixelpipe_cache
Definition darktable.h:808
struct dt_gui_gtk_t * gui
Definition darktable.h:793
struct dt_mipmap_cache_t * mipmap_cache
Definition darktable.h:794
struct dt_image_cache_t * image_cache
Definition darktable.h:795
dt_ui_t * ui
Definition gtk.h:165
Definition image_cache.h:64
char filename[128]
Definition image_cache.h:67
int32_t imgid
Definition image_cache.h:65
int mip
size_t size
int32_t imgid
int refcount
char name[64]
int hits
size_t size
size_t cl_bytes
uint64_t hash
int cl_count
void dt_supervisor_events_free(GPtrArray *events)
void dt_supervisor_set_recording(const gboolean on)
Definition supervisor.c:305
void dt_supervisor_events_clear(void)
Definition supervisor.c:310
GPtrArray * dt_supervisor_events_snapshot(void)
GPtrArray * dt_supervisor_events_snapshot_since(const uint64_t after_seq, uint64_t *out_last_seq)
uint64_t dt_supervisor_image_key(const int32_t imgid)
Definition supervisor.c:191
guint dt_supervisor_events_count(void)
uint64_t dt_supervisor_mipmap_key(const int32_t imgid, const int mip)
Definition supervisor.c:181
GtkWidget * groupby
GtkWidget * mem_box
int page_memory
static GtkWidget * _event_widget(const dt_sv_logged_event_t *ev)
GtkWidget * timeline_list
guint timer_id
uint64_t last_seq
static gboolean _poll(gpointer u)
static void _wire_hash_label(GtkWidget *label)
static void _group_set_header(_group_t *g)
GtkWidget * timeline_scroll
GtkWidget * window
guint tick
static void _run_search(const char *query)
GtkWidget * search_list
gchar * scroll_target
GtkWidget * count_label
static void _on_groupby_changed(GtkComboBox *c, gpointer u)
GtkWidget * grouped_list
uint64_t grouped_last_seq
#define TIMELINE_MAX_ROWS
static void _clear_list(GtkWidget *list)
static void _timeline_append(const dt_sv_logged_event_t *ev)
GtkWidget * notebook
#define MEMORY_REFRESH_TICKS
static gint _cmp_image_imgid(gconstpointer a, gconstpointer b)
static void _on_search_changed(GtkSearchEntry *e, gpointer u)
static void _links_free(gpointer p)
static void _group_free(gpointer p)
static const char * _domain_color(const char *d)
static void _on_toggle(GtkToggleButton *t, gpointer u)
void dt_gui_supervisor_window_show(void)
static gboolean _on_label_button(GtkWidget *label, GdkEventButton *e, gpointer u)
GHashTable * decl_map
static gboolean _on_link(GtkLabel *label, gchar *uri, gpointer user_data)
static void _grouped_append(const dt_sv_logged_event_t *ev, const char *by)
static void _update_count(void)
static void _add_usage_bar(GtkWidget *box, const char *title, const size_t cur, const size_t max)
static gint _cmp_mip_size(gconstpointer a, gconstpointer b)
int page_timeline
static gboolean _event_matches(const dt_sv_logged_event_t *ev, const char *hash_needle, const char *text_needle)
GtkWidget * search_entry
static gchar * _hashhex(const uint64_t h)
int page_search
static void _on_search_this_hash(GtkMenuItem *item, gpointer u)
static void _rebuild_grouped_now(void)
static void _on_destroy(GtkWidget *w, gpointer u)
static void _on_record_toggled(GtkToggleButton *t, gpointer u)
static void _on_page_changed(GtkNotebook *nb, GtkWidget *page, guint page_num, gpointer u)
static GtkWidget * _collapsible(const char *header_markup, GtkWidget *body, const gboolean expanded)
static gint _cmp_pixel_size(gconstpointer a, gconstpointer b)
static void _rebuild_memory(void)
static gboolean _hex_contains(const uint64_t h, const char *needle)
static const char * _group_key(const dt_sv_logged_event_t *ev, const char *by)
static void _full_reload(void)
#define MEMORY_MAX_ROWS
static struct @45 _g
static void _scroll_row_into_view(GtkWidget *row)
static gchar * _header_markup(const dt_sv_logged_event_t *ev)
static GtkWidget * _event_body_from(const char *json, GArray *links)
static gchar * _pretty_json(const char *compact)
static void _on_refresh(GtkButton *b, gpointer u)
int page_grouped
static gboolean _scroll_target_idle(gpointer u)
#define POLL_INTERVAL_MS
static void _timeline_trim(void)
static void _add_mem_item(GtkWidget *box, const char *markup, const char *trailing)
static void _on_clear(GtkButton *b, gpointer u)
static gboolean _scroll_bottom_idle(gpointer u)
GHashTable * group_map
static void _grouped_catch_up(void)
typedef double((*spd)(unsigned long int wavelength, double TempK))
#define MAX(a, b)
Definition thinplate.c:29