1 /*
2    This file is part of darktable,
3    Copyright (C) 2019-2021 darktable developers.
4 
5    darktable 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    darktable 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 darktable.  If not, see <http://www.gnu.org/licenses/>.
17 */
18 #ifdef HAVE_CONFIG_H
19 #include "config.h"
20 #endif
21 #include "bauhaus/bauhaus.h"
22 #include "common/colorspaces_inline_conversions.h"
23 #include "common/darktable.h"
24 #include "common/dwt.h"
25 #include "common/image.h"
26 #include "common/iop_profile.h"
27 #include "common/opencl.h"
28 #include "control/control.h"
29 #include "develop/develop.h"
30 #include "develop/imageop_gui.h"
31 #include "develop/imageop_math.h"
32 #include "develop/noise_generator.h"
33 #include "develop/openmp_maths.h"
34 #include "dtgtk/button.h"
35 #include "dtgtk/drawingarea.h"
36 #include "dtgtk/expander.h"
37 #include "dtgtk/paint.h"
38 #include "gui/accelerators.h"
39 #include "gui/color_picker_proxy.h"
40 #include "gui/gtk.h"
41 #include "gui/presets.h"
42 #include "iop/gaussian_elimination.h"
43 #include "iop/iop_api.h"
44 
45 
46 #include "develop/imageop.h"
47 #include "gui/draw.h"
48 
49 #include <assert.h>
50 #include <math.h>
51 #include <stdlib.h>
52 #include <string.h>
53 #include <time.h>
54 
55 
56 #define NORM_MIN 1.52587890625e-05f // norm can't be < to 2^(-16)
57 #define INVERSE_SQRT_3 0.5773502691896258f
58 
59 #define DT_GUI_CURVE_EDITOR_INSET DT_PIXEL_APPLY_DPI(1)
60 
61 
62 DT_MODULE_INTROSPECTION(4, dt_iop_filmicrgb_params_t)
63 
64 /**
65  * DOCUMENTATION
66  *
67  * This code ports :
68  * 1. Troy Sobotka's filmic curves for Blender (and other softs)
69  *      https://github.com/sobotka/OpenAgX/blob/master/lib/agx_colour.py
70  * 2. ACES camera logarithmic encoding
71  *        https://github.com/ampas/aces-dev/blob/master/transforms/ctl/utilities/ACESutil.Lin_to_Log2_param.ctl
72  *
73  * The ACES log implementation is taken from the profile_gamma.c IOP
74  * where it works in camera RGB space. Here, it works on an arbitrary RGB
75  * space. ProPhotoRGB has been chosen for its wide gamut coverage and
76  * for conveniency because it's already in darktable's libs. Any other
77  * RGB working space could work. This chouice could (should) also be
78  * exposed to the user.
79  *
80  * The filmic curves are tonecurves intended to simulate the luminance
81  * transfer function of film with "S" curves. These could be reproduced in
82  * the tonecurve.c IOP, however what we offer here is a parametric
83  * interface useful to remap accurately and promptly the middle grey
84  * to any arbitrary value chosen accordingly to the destination space.
85  *
86  * The combined use of both define a modern way to deal with large
87  * dynamic range photographs by remapping the values with a comprehensive
88  * interface avoiding many of the back and forth adjustments darktable
89  * is prone to enforce.
90  *
91  * */
92 
93 
94 /** Note :
95  * we use finite-math-only and fast-math because divisions by zero are manually avoided in the code
96  * fp-contract=fast enables hardware-accelerated Fused Multiply-Add
97  * the rest is loop reorganization and vectorization optimization
98  **/
99 #if defined(__GNUC__)
100 #pragma GCC optimize("unroll-loops", "tree-loop-if-convert", "tree-loop-distribution", "no-strict-aliasing",      \
101                      "loop-interchange",  "tree-loop-im", "unswitch-loops",                  \
102                      "tree-loop-ivcanon", "ira-loop-pressure", "split-ivs-in-unroller",                           \
103                      "variable-expansion-in-unroller", "split-loops", "ivopts", "predictive-commoning",           \
104                         "finite-math-only", "fp-contract=fast", \
105                      "fast-math", "no-math-errno")
106 #endif
107 
108 
109 typedef enum dt_iop_filmicrgb_methods_type_t
110 {
111   DT_FILMIC_METHOD_NONE = 0,              // $DESCRIPTION: "no"
112   DT_FILMIC_METHOD_MAX_RGB = 1,           // $DESCRIPTION: "max RGB"
113   DT_FILMIC_METHOD_LUMINANCE = 2,         // $DESCRIPTION: "luminance Y"
114   DT_FILMIC_METHOD_POWER_NORM = 3,        // $DESCRIPTION: "RGB power norm"
115   DT_FILMIC_METHOD_EUCLIDEAN_NORM_V2 = 5, // $DESCRIPTION: "RGB euclidean norm"
116   DT_FILMIC_METHOD_EUCLIDEAN_NORM_V1 = 4, // $DESCRIPTION: "RGB euclidean norm (legacy)"
117 } dt_iop_filmicrgb_methods_type_t;
118 
119 
120 typedef enum dt_iop_filmicrgb_curve_type_t
121 {
122   DT_FILMIC_CURVE_POLY_4 = 0, // $DESCRIPTION: "hard"
123   DT_FILMIC_CURVE_POLY_3 = 1,  // $DESCRIPTION: "soft"
124   DT_FILMIC_CURVE_RATIONAL = 2, // $DESCRIPTION: "safe"
125 } dt_iop_filmicrgb_curve_type_t;
126 
127 
128 typedef enum dt_iop_filmicrgb_colorscience_type_t
129 {
130   DT_FILMIC_COLORSCIENCE_V1 = 0, // $DESCRIPTION: "v3 (2019)"
131   DT_FILMIC_COLORSCIENCE_V2 = 1, // $DESCRIPTION: "v4 (2020)"
132   DT_FILMIC_COLORSCIENCE_V3 = 2, // $DESCRIPTION: "v5 (2021)"
133 } dt_iop_filmicrgb_colorscience_type_t;
134 
135 
136 typedef enum dt_iop_filmicrgb_reconstruction_type_t
137 {
138   DT_FILMIC_RECONSTRUCT_RGB = 0,
139   DT_FILMIC_RECONSTRUCT_RATIOS = 1,
140 } dt_iop_filmicrgb_reconstruction_type_t;
141 
142 
143 typedef struct dt_iop_filmic_rgb_spline_t
144 {
145   float DT_ALIGNED_PIXEL M1[4], M2[4], M3[4], M4[4], M5[4]; // factors for the interpolation polynom
146   float latitude_min, latitude_max;                         // bounds of the latitude == linear part by design
147   float y[5];                                               // controls nodes
148   float x[5];                                               // controls nodes
149   dt_iop_filmicrgb_curve_type_t type[2];
150 } dt_iop_filmic_rgb_spline_t;
151 
152 
153 typedef enum dt_iop_filmic_rgb_gui_mode_t
154 {
155   DT_FILMIC_GUI_LOOK = 0,      // default GUI, showing only the contrast curve in a log/gamma space
156   DT_FILMIC_GUI_BASECURVE = 1, // basecurve-like GUI, showing the contrast and brightness curves, in lin/lin space
157   DT_FILMIC_GUI_BASECURVE_LOG = 2, // same as previous, but log-scaled
158   DT_FILMIC_GUI_RANGES = 3,        // zone-system-like GUI, showing the range to range mapping
159   DT_FILMIC_GUI_LAST
160 } dt_iop_filmic_rgb_gui_mode_t;
161 
162 // clang-format off
163 typedef struct dt_iop_filmicrgb_params_t
164 {
165   float grey_point_source;     // $MIN: 0 $MAX: 100 $DEFAULT: 18.45 $DESCRIPTION: "middle gray luminance"
166   float black_point_source;    // $MIN: -16 $MAX: -0.1 $DEFAULT: -8.0 $DESCRIPTION: "black relative exposure"
167   float white_point_source;    // $MIN: 0 $MAX: 16 $DEFAULT: 4.0 $DESCRIPTION: "white relative exposure"
168   float reconstruct_threshold; // $MIN: -6.0 $MAX: 6.0 $DEFAULT: +3.0 $DESCRIPTION: "threshold"
169   float reconstruct_feather;   // $MIN: 0.25 $MAX: 6.0 $DEFAULT: 3.0 $DESCRIPTION: "transition"
170   float reconstruct_bloom_vs_details; // $MIN: -100.0 $MAX: 100.0 $DEFAULT: 100.0 $DESCRIPTION: "bloom/reconstruct"
171   float reconstruct_grey_vs_color; // $MIN: -100.0 $MAX: 100.0 $DEFAULT: 100.0 $DESCRIPTION: "gray/colorful details"
172   float reconstruct_structure_vs_texture; // $MIN: -100.0 $MAX: 100.0 $DEFAULT: 0.0 $DESCRIPTION: "structure/texture"
173   float security_factor;                  // $MIN: -50 $MAX: 200 $DEFAULT: 0 $DESCRIPTION: "dynamic range scaling"
174   float grey_point_target;                // $MIN: 1 $MAX: 50 $DEFAULT: 18.45 $DESCRIPTION: "target middle gray"
175   float black_point_target; // $MIN: 0.000 $MAX: 20.000 $DEFAULT: 0.01517634 $DESCRIPTION: "target black luminance"
176   float white_point_target; // $MIN: 0 $MAX: 1600 $DEFAULT: 100 $DESCRIPTION: "target white luminance"
177   float output_power;       // $MIN: 1 $MAX: 10 $DEFAULT: 4.0 $DESCRIPTION: "hardness"
178   float latitude;           // $MIN: 0.01 $MAX: 100 $DEFAULT: 33.0
179   float contrast;           // $MIN: 0 $MAX: 5 $DEFAULT: 1.35
180   float saturation;         // $MIN: -50 $MAX: 200 $DEFAULT: 0 $DESCRIPTION: "extreme luminance saturation"
181   float balance;            // $MIN: -50 $MAX: 50 $DEFAULT: 0.0 $DESCRIPTION: "shadows/highlights balance"
182   float noise_level;        // $MIN: 0.0 $MAX: 6.0 $DEFAULT: 0.2f $DESCRIPTION: "add noise in highlights"
183   dt_iop_filmicrgb_methods_type_t preserve_color; // $DEFAULT: DT_FILMIC_METHOD_POWER_NORM $DESCRIPTION: "preserve chrominance"
184   dt_iop_filmicrgb_colorscience_type_t version; // $DEFAULT: DT_FILMIC_COLORSCIENCE_V3 $DESCRIPTION: "color science"
185   gboolean auto_hardness;                       // $DEFAULT: TRUE $DESCRIPTION: "auto adjust hardness"
186   gboolean custom_grey;                         // $DEFAULT: FALSE $DESCRIPTION: "use custom middle-gray values"
187   int high_quality_reconstruction;       // $MIN: 0 $MAX: 10 $DEFAULT: 1 $DESCRIPTION: "iterations of high-quality reconstruction"
188   int noise_distribution;                // $DEFAULT: DT_NOISE_GAUSSIAN $DESCRIPTION: "type of noise"
189   dt_iop_filmicrgb_curve_type_t shadows; // $DEFAULT: DT_FILMIC_CURVE_RATIONAL $DESCRIPTION: "contrast in shadows"
190   dt_iop_filmicrgb_curve_type_t highlights; // $DEFAULT: DT_FILMIC_CURVE_RATIONAL $DESCRIPTION: "contrast in highlights"
191   gboolean compensate_icc_black; // $DEFAULT: FALSE $DESCRIPTION: "compensate output ICC profile black point"
192   gint internal_version;         // $DEFAULT: 2020 $DESCRIPTION: "version of the spline generator"
193 } dt_iop_filmicrgb_params_t;
194 // clang-format on
195 
196 
197 // custom buttons in graph views
198 typedef enum dt_iop_filmicrgb_gui_button_t
199 {
200   DT_FILMIC_GUI_BUTTON_TYPE = 0,
201   DT_FILMIC_GUI_BUTTON_LABELS = 1,
202   DT_FILMIC_GUI_BUTTON_LAST
203 } dt_iop_filmicrgb_gui_button_t;
204 
205 // custom buttons in graph views - data
206 typedef struct dt_iop_filmicrgb_gui_button_data_t
207 {
208   // coordinates in GUI - compute them only in the drawing function
209   float left;
210   float right;
211   float top;
212   float bottom;
213   float w;
214   float h;
215 
216   // properties
217   gint mouse_hover; // whether it should be acted on / mouse is over it
218   GtkStateFlags state;
219 
220   // icon drawing, function as set in dtgtk/paint.h
221   DTGTKCairoPaintIconFunc icon;
222 
223 } dt_iop_filmicrgb_gui_button_data_t;
224 
225 
226 typedef struct dt_iop_filmicrgb_gui_data_t
227 {
228   GtkWidget *white_point_source;
229   GtkWidget *grey_point_source;
230   GtkWidget *black_point_source;
231   GtkWidget *reconstruct_threshold, *reconstruct_bloom_vs_details, *reconstruct_grey_vs_color,
232       *reconstruct_structure_vs_texture, *reconstruct_feather;
233   GtkWidget *show_highlight_mask;
234   GtkWidget *security_factor;
235   GtkWidget *auto_button;
236   GtkWidget *grey_point_target;
237   GtkWidget *white_point_target;
238   GtkWidget *black_point_target;
239   GtkWidget *output_power;
240   GtkWidget *latitude;
241   GtkWidget *contrast;
242   GtkWidget *saturation;
243   GtkWidget *balance;
244   GtkWidget *preserve_color;
245   GtkWidget *autoset_display_gamma;
246   GtkWidget *shadows, *highlights;
247   GtkWidget *version;
248   GtkWidget *auto_hardness;
249   GtkWidget *custom_grey;
250   GtkWidget *high_quality_reconstruction;
251   GtkWidget *noise_level, *noise_distribution;
252   GtkWidget *compensate_icc_black;
253   GtkNotebook *notebook;
254   GtkDrawingArea *area;
255   struct dt_iop_filmic_rgb_spline_t spline DT_ALIGNED_ARRAY;
256   gint show_mask;
257   dt_iop_filmic_rgb_gui_mode_t gui_mode; // graph display mode
258   gint gui_show_labels;
259   gint gui_hover;
260   gint gui_sizes_inited;
261   dt_iop_filmicrgb_gui_button_t active_button; // ID of the button under cursor
262   dt_iop_filmicrgb_gui_button_data_t buttons[DT_FILMIC_GUI_BUTTON_LAST];
263 
264   // Cache Pango and Cairo stuff for the equalizer drawing
265   float line_height;
266   float sign_width;
267   float zero_width;
268   float graph_width;
269   float graph_height;
270   int inset;
271   int inner_padding;
272 
273   GtkAllocation allocation;
274   PangoRectangle ink;
275   GtkStyleContext *context;
276 } dt_iop_filmicrgb_gui_data_t;
277 
278 typedef struct dt_iop_filmicrgb_data_t
279 {
280   float max_grad;
281   float white_source;
282   float grey_source;
283   float black_source;
284   float reconstruct_threshold;
285   float reconstruct_feather;
286   float reconstruct_bloom_vs_details;
287   float reconstruct_grey_vs_color;
288   float reconstruct_structure_vs_texture;
289   float normalize;
290   float dynamic_range;
291   float saturation;
292   float output_power;
293   float contrast;
294   float sigma_toe, sigma_shoulder;
295   float noise_level;
296   int preserve_color;
297   int version;
298   int high_quality_reconstruction;
299   struct dt_iop_filmic_rgb_spline_t spline DT_ALIGNED_ARRAY;
300   dt_noise_distribution_t noise_distribution;
301 } dt_iop_filmicrgb_data_t;
302 
303 
304 typedef struct dt_iop_filmicrgb_global_data_t
305 {
306   int kernel_filmic_rgb_split;
307   int kernel_filmic_rgb_chroma;
308   int kernel_filmic_mask;
309   int kernel_filmic_show_mask;
310   int kernel_filmic_inpaint_noise;
311   int kernel_filmic_bspline_vertical;
312   int kernel_filmic_bspline_horizontal;
313   int kernel_filmic_init_reconstruct;
314   int kernel_filmic_wavelets_detail;
315   int kernel_filmic_wavelets_reconstruct;
316   int kernel_filmic_compute_ratios;
317   int kernel_filmic_restore_ratios;
318 } dt_iop_filmicrgb_global_data_t;
319 
320 
name()321 const char *name()
322 {
323   return _("filmic rgb");
324 }
325 
aliases()326 const char *aliases()
327 {
328   return _("tone mapping|curve|view transform|contrast|saturation|highlights");
329 }
330 
description(struct dt_iop_module_t * self)331 const char *description(struct dt_iop_module_t *self)
332 {
333   return dt_iop_set_description(self, _("apply a view transform to prepare the scene-referred pipeline\n"
334                                         "for display on SDR screens and paper prints\n"
335                                         "while preventing clipping in non-destructive ways"),
336                                       _("corrective and creative"),
337                                       _("linear or non-linear, RGB, scene-referred"),
338                                       _("non-linear, RGB"),
339                                       _("non-linear, RGB, display-referred"));
340 }
341 
default_group()342 int default_group()
343 {
344   return IOP_GROUP_TONE | IOP_GROUP_TECHNICAL;
345 }
346 
flags()347 int flags()
348 {
349   return IOP_FLAGS_INCLUDE_IN_STYLES | IOP_FLAGS_SUPPORTS_BLENDING;
350 }
351 
default_colorspace(dt_iop_module_t * self,dt_dev_pixelpipe_t * pipe,dt_dev_pixelpipe_iop_t * piece)352 int default_colorspace(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece)
353 {
354   return iop_cs_rgb;
355 }
356 
legacy_params(dt_iop_module_t * self,const void * const old_params,const int old_version,void * new_params,const int new_version)357 int legacy_params(dt_iop_module_t *self, const void *const old_params, const int old_version, void *new_params,
358                   const int new_version)
359 {
360   if(old_version == 1 && new_version == 4)
361   {
362     typedef struct dt_iop_filmicrgb_params_v1_t
363     {
364       float grey_point_source;
365       float black_point_source;
366       float white_point_source;
367       float security_factor;
368       float grey_point_target;
369       float black_point_target;
370       float white_point_target;
371       float output_power;
372       float latitude;
373       float contrast;
374       float saturation;
375       float balance;
376       int preserve_color;
377     } dt_iop_filmicrgb_params_v1_t;
378 
379     dt_iop_filmicrgb_params_v1_t *o = (dt_iop_filmicrgb_params_v1_t *)old_params;
380     dt_iop_filmicrgb_params_t *n = (dt_iop_filmicrgb_params_t *)new_params;
381     dt_iop_filmicrgb_params_t *d = (dt_iop_filmicrgb_params_t *)self->default_params;
382 
383     *n = *d; // start with a fresh copy of default parameters
384 
385     n->grey_point_source = o->grey_point_source;
386     n->white_point_source = o->white_point_source;
387     n->black_point_source = o->black_point_source;
388     n->security_factor = o->security_factor;
389     n->grey_point_target = o->grey_point_target;
390     n->black_point_target = o->black_point_target;
391     n->white_point_target = o->white_point_target;
392     n->output_power = o->output_power;
393     n->latitude = o->latitude;
394     n->contrast = o->contrast;
395     n->saturation = o->saturation;
396     n->balance = o->balance;
397     n->preserve_color = o->preserve_color;
398     n->shadows = DT_FILMIC_CURVE_POLY_4;
399     n->highlights = DT_FILMIC_CURVE_POLY_3;
400     n->reconstruct_threshold
401         = 6.0f; // for old edits, this ensures clipping threshold >> white level, so it's a no-op
402     n->reconstruct_bloom_vs_details = d->reconstruct_bloom_vs_details;
403     n->reconstruct_grey_vs_color = d->reconstruct_grey_vs_color;
404     n->reconstruct_structure_vs_texture = d->reconstruct_structure_vs_texture;
405     n->reconstruct_feather = 3.0f;
406     n->version = DT_FILMIC_COLORSCIENCE_V1;
407     n->auto_hardness = TRUE;
408     n->custom_grey = TRUE;
409     n->high_quality_reconstruction = 0;
410     n->noise_distribution = d->noise_distribution;
411     n->noise_level = 0.f;
412     n->internal_version = 2019;
413     n->compensate_icc_black = FALSE;
414     return 0;
415   }
416   if(old_version == 2 && new_version == 4)
417   {
418     typedef struct dt_iop_filmicrgb_params_v2_t
419     {
420       float grey_point_source;
421       float black_point_source;
422       float white_point_source;
423       float reconstruct_threshold;
424       float reconstruct_feather;
425       float reconstruct_bloom_vs_details;
426       float reconstruct_grey_vs_color;
427       float reconstruct_structure_vs_texture;
428       float security_factor;
429       float grey_point_target;
430       float black_point_target;
431       float white_point_target;
432       float output_power;
433       float latitude;
434       float contrast;
435       float saturation;
436       float balance;
437       int preserve_color;
438       int version;
439       int auto_hardness;
440       int custom_grey;
441       int high_quality_reconstruction;
442       dt_iop_filmicrgb_curve_type_t shadows;
443       dt_iop_filmicrgb_curve_type_t highlights;
444     } dt_iop_filmicrgb_params_v2_t;
445 
446     dt_iop_filmicrgb_params_v2_t *o = (dt_iop_filmicrgb_params_v2_t *)old_params;
447     dt_iop_filmicrgb_params_t *n = (dt_iop_filmicrgb_params_t *)new_params;
448     dt_iop_filmicrgb_params_t *d = (dt_iop_filmicrgb_params_t *)self->default_params;
449 
450     *n = *d; // start with a fresh copy of default parameters
451 
452     n->grey_point_source = o->grey_point_source;
453     n->white_point_source = o->white_point_source;
454     n->black_point_source = o->black_point_source;
455     n->security_factor = o->security_factor;
456     n->grey_point_target = o->grey_point_target;
457     n->black_point_target = o->black_point_target;
458     n->white_point_target = o->white_point_target;
459     n->output_power = o->output_power;
460     n->latitude = o->latitude;
461     n->contrast = o->contrast;
462     n->saturation = o->saturation;
463     n->balance = o->balance;
464     n->preserve_color = o->preserve_color;
465     n->shadows = o->shadows;
466     n->highlights = o->highlights;
467     n->reconstruct_threshold = o->reconstruct_threshold;
468     n->reconstruct_bloom_vs_details = o->reconstruct_bloom_vs_details;
469     n->reconstruct_grey_vs_color = o->reconstruct_grey_vs_color;
470     n->reconstruct_structure_vs_texture = o->reconstruct_structure_vs_texture;
471     n->reconstruct_feather = o->reconstruct_feather;
472     n->version = o->version;
473     n->auto_hardness = o->auto_hardness;
474     n->custom_grey = o->custom_grey;
475     n->high_quality_reconstruction = o->high_quality_reconstruction;
476     n->noise_level = d->noise_level;
477     n->noise_distribution = d->noise_distribution;
478     n->noise_level = 0.f;
479     n->internal_version = 2019;
480     n->compensate_icc_black = FALSE;
481     return 0;
482   }
483   if(old_version == 3 && new_version == 4)
484   {
485     typedef struct dt_iop_filmicrgb_params_v3_t
486     {
487       float grey_point_source;     // $MIN: 0 $MAX: 100 $DEFAULT: 18.45 $DESCRIPTION: "middle gray luminance"
488       float black_point_source;    // $MIN: -16 $MAX: -0.1 $DEFAULT: -8.0 $DESCRIPTION: "black relative exposure"
489       float white_point_source;    // $MIN: 0 $MAX: 16 $DEFAULT: 4.0 $DESCRIPTION: "white relative exposure"
490       float reconstruct_threshold; // $MIN: -6.0 $MAX: 6.0 $DEFAULT: +3.0 $DESCRIPTION: "threshold"
491       float reconstruct_feather;   // $MIN: 0.25 $MAX: 6.0 $DEFAULT: 3.0 $DESCRIPTION: "transition"
492       float reconstruct_bloom_vs_details; // $MIN: -100.0 $MAX: 100.0 $DEFAULT: 100.0 $DESCRIPTION:
493                                           // "bloom/reconstruct"
494       float reconstruct_grey_vs_color;    // $MIN: -100.0 $MAX: 100.0 $DEFAULT: 100.0 $DESCRIPTION: "gray/colorful
495                                           // details"
496       float reconstruct_structure_vs_texture; // $MIN: -100.0 $MAX: 100.0 $DEFAULT: 0.0 $DESCRIPTION:
497                                               // "structure/texture"
498       float security_factor;    // $MIN: -50 $MAX: 200 $DEFAULT: 0 $DESCRIPTION: "dynamic range scaling"
499       float grey_point_target;  // $MIN: 1 $MAX: 50 $DEFAULT: 18.45 $DESCRIPTION: "target middle gray"
500       float black_point_target; // $MIN: 0 $MAX: 20 $DEFAULT: 0 $DESCRIPTION: "target black luminance"
501       float white_point_target; // $MIN: 0 $MAX: 1600 $DEFAULT: 100 $DESCRIPTION: "target white luminance"
502       float output_power;       // $MIN: 1 $MAX: 10 $DEFAULT: 4.0 $DESCRIPTION: "hardness"
503       float latitude;           // $MIN: 0.01 $MAX: 100 $DEFAULT: 33.0
504       float contrast;           // $MIN: 0 $MAX: 5 $DEFAULT: 1.50
505       float saturation;         // $MIN: -50 $MAX: 200 $DEFAULT: 0 $DESCRIPTION: "extreme luminance saturation"
506       float balance;            // $MIN: -50 $MAX: 50 $DEFAULT: 0.0 $DESCRIPTION: "shadows/highlights balance"
507       float noise_level;        // $MIN: 0.0 $MAX: 6.0 $DEFAULT: 0.1f $DESCRIPTION: "add noise in highlights"
508       dt_iop_filmicrgb_methods_type_t preserve_color; // $DEFAULT: DT_FILMIC_METHOD_POWER_NORM $DESCRIPTION:
509                                                       // "preserve chrominance"
510       dt_iop_filmicrgb_colorscience_type_t version;   // $DEFAULT: DT_FILMIC_COLORSCIENCE_V3 $DESCRIPTION: "color
511                                                       // science"
512       gboolean auto_hardness;                         // $DEFAULT: TRUE $DESCRIPTION: "auto adjust hardness"
513       gboolean custom_grey;            // $DEFAULT: FALSE $DESCRIPTION: "use custom middle-gray values"
514       int high_quality_reconstruction; // $MIN: 0 $MAX: 10 $DEFAULT: 1 $DESCRIPTION: "iterations of high-quality
515                                        // reconstruction"
516       int noise_distribution;          // $DEFAULT: DT_NOISE_POISSONIAN $DESCRIPTION: "type of noise"
517       dt_iop_filmicrgb_curve_type_t shadows; // $DEFAULT: DT_FILMIC_CURVE_POLY_4 $DESCRIPTION: "contrast in shadows"
518       dt_iop_filmicrgb_curve_type_t highlights; // $DEFAULT: DT_FILMIC_CURVE_POLY_4 $DESCRIPTION: "contrast in
519                                                 // highlights"
520     } dt_iop_filmicrgb_params_v3_t;
521 
522     dt_iop_filmicrgb_params_v3_t *o = (dt_iop_filmicrgb_params_v3_t *)old_params;
523     dt_iop_filmicrgb_params_t *n = (dt_iop_filmicrgb_params_t *)new_params;
524     dt_iop_filmicrgb_params_t *d = (dt_iop_filmicrgb_params_t *)self->default_params;
525 
526     *n = *d; // start with a fresh copy of default parameters
527 
528     n->grey_point_source = o->grey_point_source;
529     n->white_point_source = o->white_point_source;
530     n->black_point_source = o->black_point_source;
531     n->security_factor = o->security_factor;
532     n->grey_point_target = o->grey_point_target;
533     n->black_point_target = o->black_point_target;
534     n->white_point_target = o->white_point_target;
535     n->output_power = o->output_power;
536     n->latitude = o->latitude;
537     n->contrast = o->contrast;
538     n->saturation = o->saturation;
539     n->balance = o->balance;
540     n->preserve_color = o->preserve_color;
541     n->shadows = o->shadows;
542     n->highlights = o->highlights;
543     n->reconstruct_threshold = o->reconstruct_threshold;
544     n->reconstruct_bloom_vs_details = o->reconstruct_bloom_vs_details;
545     n->reconstruct_grey_vs_color = o->reconstruct_grey_vs_color;
546     n->reconstruct_structure_vs_texture = o->reconstruct_structure_vs_texture;
547     n->reconstruct_feather = o->reconstruct_feather;
548     n->version = o->version;
549     n->auto_hardness = o->auto_hardness;
550     n->custom_grey = o->custom_grey;
551     n->high_quality_reconstruction = o->high_quality_reconstruction;
552     n->noise_level = d->noise_level;
553     n->noise_distribution = d->noise_distribution;
554     n->noise_level = d->noise_level;
555     n->internal_version = 2019;
556     n->compensate_icc_black = FALSE;
557     return 0;
558   }
559   return 1;
560 }
561 
562 
563 #ifdef _OPENMP
564 #pragma omp declare simd aligned(pixel:16)
565 #endif
pixel_rgb_norm_power(const float pixel[4])566 static inline float pixel_rgb_norm_power(const float pixel[4])
567 {
568   // weird norm sort of perceptual. This is black magic really, but it looks good.
569   // the full norm is (R^3 + G^3 + B^3) / (R^2 + G^2 + B^2) and it should be in ]0; +infinity[
570 
571   float numerator = 0.0f;
572   float denominator = 0.0f;
573 
574   for(int c = 0; c < 3; c++)
575   {
576     const float value = fabsf(pixel[c]);
577     const float RGB_square = value * value;
578     const float RGB_cubic = RGB_square * value;
579     numerator += RGB_cubic;
580     denominator += RGB_square;
581   }
582 
583   return numerator / fmaxf(denominator, 1e-12f); // prevent from division-by-0 (note: (1e-6)^2 = 1e-12
584 }
585 
586 
587 #ifdef _OPENMP
588 #pragma omp declare simd aligned(pixel : 16) uniform(variant, work_profile)
589 #endif
get_pixel_norm(const float pixel[4],const dt_iop_filmicrgb_methods_type_t variant,const dt_iop_order_iccprofile_info_t * const work_profile)590 static inline float get_pixel_norm(const float pixel[4], const dt_iop_filmicrgb_methods_type_t variant,
591                                    const dt_iop_order_iccprofile_info_t *const work_profile)
592 {
593   // a newly added norm should satisfy the condition that it is linear with respect to grey pixels:
594   // norm(R, G, B) = norm(x, x, x) = x
595   // the desaturation code in chroma preservation mode relies on this assumption.
596   // DT_FILMIC_METHOD_EUCLIDEAN_NORM_V1 is an exception to this and is marked as legacy.
597   // DT_FILMIC_METHOD_EUCLIDEAN_NORM_V2 takes the Euclidean norm and scales it such that
598   // norm(1, 1, 1) = 1.
599   switch(variant)
600   {
601     case(DT_FILMIC_METHOD_MAX_RGB):
602       return fmaxf(fmaxf(pixel[0], pixel[1]), pixel[2]);
603 
604     case(DT_FILMIC_METHOD_LUMINANCE):
605       return (work_profile)
606                  ? dt_ioppr_get_rgb_matrix_luminance(pixel, work_profile->matrix_in, work_profile->lut_in,
607                                                      work_profile->unbounded_coeffs_in, work_profile->lutsize,
608                                                      work_profile->nonlinearlut)
609                  : dt_camera_rgb_luminance(pixel);
610 
611     case(DT_FILMIC_METHOD_POWER_NORM):
612       return pixel_rgb_norm_power(pixel);
613 
614     case(DT_FILMIC_METHOD_EUCLIDEAN_NORM_V1):
615       return sqrtf(sqf(pixel[0]) + sqf(pixel[1]) + sqf(pixel[2]));
616 
617     case(DT_FILMIC_METHOD_EUCLIDEAN_NORM_V2):
618       return sqrtf(sqf(pixel[0]) + sqf(pixel[1]) + sqf(pixel[2])) * INVERSE_SQRT_3;
619 
620     default:
621       return (work_profile)
622                  ? dt_ioppr_get_rgb_matrix_luminance(pixel, work_profile->matrix_in, work_profile->lut_in,
623                                                      work_profile->unbounded_coeffs_in, work_profile->lutsize,
624                                                      work_profile->nonlinearlut)
625                  : dt_camera_rgb_luminance(pixel);
626   }
627 }
628 
629 
630 #ifdef _OPENMP
631 #pragma omp declare simd uniform(grey, black, dynamic_range)
632 #endif
log_tonemapping_v1(const float x,const float grey,const float black,const float dynamic_range)633 static inline float log_tonemapping_v1(const float x, const float grey, const float black,
634                                        const float dynamic_range)
635 {
636   const float temp = (log2f(x / grey) - black) / dynamic_range;
637   return fmaxf(fminf(temp, 1.0f), NORM_MIN);
638 }
639 
640 
641 #ifdef _OPENMP
642 #pragma omp declare simd uniform(grey, black, dynamic_range)
643 #endif
log_tonemapping_v2(const float x,const float grey,const float black,const float dynamic_range)644 static inline float log_tonemapping_v2(const float x, const float grey, const float black,
645                                        const float dynamic_range)
646 {
647   return clamp_simd((log2f(x / grey) - black) / dynamic_range);
648 }
649 
650 #ifdef _OPENMP
651 #pragma omp declare simd uniform(grey, black, dynamic_range)
652 #endif
exp_tonemapping_v2(const float x,const float grey,const float black,const float dynamic_range)653 static inline float exp_tonemapping_v2(const float x, const float grey, const float black,
654                                        const float dynamic_range)
655 {
656   // inverse of log_tonemapping
657   return grey * exp2f(dynamic_range * x + black);
658 }
659 
660 
661 #ifdef _OPENMP
662 #pragma omp declare simd aligned(M1, M2, M3, M4 : 16) uniform(M1, M2, M3, M4, M5, latitude_min, latitude_max)
663 #endif
filmic_spline(const float x,const float M1[4],const float M2[4],const float M3[4],const float M4[4],const float M5[4],const float latitude_min,const float latitude_max,const dt_iop_filmicrgb_curve_type_t type[2])664 static inline float filmic_spline(const float x, const float M1[4], const float M2[4], const float M3[4],
665                                   const float M4[4], const float M5[4], const float latitude_min,
666                                   const float latitude_max, const dt_iop_filmicrgb_curve_type_t type[2])
667 {
668   // if type polynomial :
669   // y = M5 * x⁴ + M4 * x³ + M3 * x² + M2 * x¹ + M1 * x⁰
670   // but we rewrite it using Horner factorisation, to spare ops and enable FMA in available
671   // else if type rational :
672   // y = M1 * (M2 * (x - x_0)² + (x - x_0)) / (M2 * (x - x_0)² + (x - x_0) + M3)
673 
674   float result;
675 
676   if(x < latitude_min)
677   {
678     // toe
679     if(type[0] == DT_FILMIC_CURVE_POLY_4)
680     {
681       // polynomial toe, 4th order
682       result = M1[0] + x * (M2[0] + x * (M3[0] + x * (M4[0] + x * M5[0])));
683     }
684     else if(type[0] == DT_FILMIC_CURVE_POLY_3)
685     {
686       // polynomial toe, 3rd order
687       result = M1[0] + x * (M2[0] + x * (M3[0] + x * M4[0]));
688     }
689     else
690     {
691       // rational toe
692       const float xi = latitude_min - x;
693       const float rat = xi * (xi * M2[0] + 1.f);
694       result = M4[0] - M1[0] * rat / (rat + M3[0]);
695     }
696   }
697   else if(x > latitude_max)
698   {
699     // shoulder
700     if(type[1] == DT_FILMIC_CURVE_POLY_4)
701     {
702       // polynomial shoulder, 4th order
703       result = M1[1] + x * (M2[1] + x * (M3[1] + x * (M4[1] + x * M5[1])));
704     }
705     else if(type[1] == DT_FILMIC_CURVE_POLY_3)
706     {
707       // polynomial shoulder, 3rd order
708       result = M1[1] + x * (M2[1] + x * (M3[1] + x * M4[1]));
709     }
710     else
711     {
712       // rational toe
713       const float xi = x - latitude_max;
714       const float rat = xi * (xi * M2[1] + 1.f);
715       result = M4[1] + M1[1] * rat / (rat + M3[1]);
716     }
717   }
718   else
719   {
720     // latitude
721     result = M1[2] + x * M2[2];
722   }
723 
724   return result;
725 }
726 
727 
728 #ifdef _OPENMP
729 #pragma omp declare simd uniform(sigma_toe, sigma_shoulder)
730 #endif
filmic_desaturate_v1(const float x,const float sigma_toe,const float sigma_shoulder,const float saturation)731 static inline float filmic_desaturate_v1(const float x, const float sigma_toe, const float sigma_shoulder,
732                                          const float saturation)
733 {
734   const float radius_toe = x;
735   const float radius_shoulder = 1.0f - x;
736 
737   const float key_toe = expf(-0.5f * radius_toe * radius_toe / sigma_toe);
738   const float key_shoulder = expf(-0.5f * radius_shoulder * radius_shoulder / sigma_shoulder);
739 
740   return 1.0f - clamp_simd((key_toe + key_shoulder) / saturation);
741 }
742 
743 
744 #ifdef _OPENMP
745 #pragma omp declare simd uniform(sigma_toe, sigma_shoulder)
746 #endif
filmic_desaturate_v2(const float x,const float sigma_toe,const float sigma_shoulder,const float saturation)747 static inline float filmic_desaturate_v2(const float x, const float sigma_toe, const float sigma_shoulder,
748                                          const float saturation)
749 {
750   const float radius_toe = x;
751   const float radius_shoulder = 1.0f - x;
752   const float sat2 = 0.5f / sqrtf(saturation);
753   const float key_toe = expf(-radius_toe * radius_toe / sigma_toe * sat2);
754   const float key_shoulder = expf(-radius_shoulder * radius_shoulder / sigma_shoulder * sat2);
755 
756   return (saturation - (key_toe + key_shoulder) * (saturation));
757 }
758 
759 
760 #ifdef _OPENMP
761 #pragma omp declare simd
762 #endif
linear_saturation(const float x,const float luminance,const float saturation)763 static inline float linear_saturation(const float x, const float luminance, const float saturation)
764 {
765   return luminance + saturation * (x - luminance);
766 }
767 
768 
769 #define MAX_NUM_SCALES 12
770 
771 
772 #ifdef _OPENMP
773 #pragma omp declare simd aligned(in, mask : 64) uniform(feathering, normalize, width, height, ch)
774 #endif
mask_clipped_pixels(const float * const restrict in,float * const restrict mask,const float normalize,const float feathering,const size_t width,const size_t height,const size_t ch)775 static inline gint mask_clipped_pixels(const float *const restrict in, float *const restrict mask,
776                                        const float normalize, const float feathering, const size_t width,
777                                        const size_t height, const size_t ch)
778 {
779   /* 1. Detect if pixels are clipped and count them,
780    * 2. assign them a weight in [0. ; 1.] depending on how close from clipping they are. The weights are defined
781    *    by a sigmoid centered in `reconstruct_threshold` so the transition is soft and symmetrical
782    */
783 
784   int clipped = 0;
785 
786   #ifdef __SSE2__
787     // flush denormals to zero for masking to avoid performance penalty
788     // if there are a lot of zero values in the mask
789     const unsigned int oldMode = _MM_GET_FLUSH_ZERO_MODE();
790     _MM_SET_FLUSH_ZERO_MODE(_MM_FLUSH_ZERO_ON);
791   #endif
792 
793 #ifdef _OPENMP
794 #pragma omp parallel for simd default(none) \
795   dt_omp_firstprivate(in, mask, normalize, feathering, width, height, ch) \
796   schedule(simd:static) aligned(mask, in:64) reduction(+:clipped)
797 #endif
798   for(size_t k = 0; k < height * width * ch; k += ch)
799   {
800     const float pix_max = fmaxf(sqrtf(sqf(in[k]) + sqf(in[k + 1]) + sqf(in[k + 2])), 0.f);
801     const float argument = -pix_max * normalize + feathering;
802     const float weight = clamp_simd(1.0f / (1.0f + exp2f(argument)));
803     mask[k / ch] = weight;
804 
805     // at x = 4, the sigmoid produces opacity = 5.882 %.
806     // any x > 4 will produce negligible changes over the image,
807     // especially since we have reduced visual sensitivity in highlights.
808     // so we discard pixels for argument > 4. for they are not worth computing.
809     clipped += (4.f > argument);
810   }
811 
812   #ifdef __SSE2__
813     _MM_SET_FLUSH_ZERO_MODE(oldMode);
814   #endif
815 
816   // If clipped area is < 9 pixels, recovery is not worth the computational cost, so skip it.
817   return (clipped > 9);
818 }
819 
820 
821 #ifdef _OPENMP
822 #pragma omp declare simd aligned(in, mask, inpainted:64) uniform(width, height, ch, noise_level, noise_distribution, threshold)
823 #endif
inpaint_noise(const float * const in,const float * const mask,float * const inpainted,const float noise_level,const float threshold,const dt_noise_distribution_t noise_distribution,const size_t width,const size_t height,const size_t ch)824 inline static void inpaint_noise(const float *const in, const float *const mask,
825                                  float *const inpainted, const float noise_level, const float threshold,
826                                  const dt_noise_distribution_t noise_distribution,
827                                  const size_t width, const size_t height, const size_t ch)
828 {
829   // add statistical noise in highlights to fill-in texture
830   // this creates "particules" in highlights, that will help the implicit partial derivative equation
831   // solver used in wavelets reconstruction to generate texture
832 
833 #ifdef _OPENMP
834 #pragma omp parallel for simd default(none) \
835   dt_omp_firstprivate(in, mask, inpainted, width, height, ch, noise_level, noise_distribution, threshold) \
836   schedule(simd:static) aligned(mask, in, inpainted:64) collapse(2)
837 #endif
838   for(size_t i = 0; i < height; i++)
839     for(size_t j = 0; j < width; j++)
840     {
841       // Init random number generator
842       uint32_t DT_ALIGNED_ARRAY state[4] = { splitmix32(j + 1), splitmix32((j + 1) * (i + 3)), splitmix32(1337), splitmix32(666) };
843       xoshiro128plus(state);
844       xoshiro128plus(state);
845       xoshiro128plus(state);
846       xoshiro128plus(state);
847 
848       // get the mask value in [0 ; 1]
849       const size_t idx = i * width + j;
850       const size_t index = idx * ch;
851       const float weight = mask[idx];
852       const float *const restrict pix_in = __builtin_assume_aligned(in + index, 16);
853       float DT_ALIGNED_ARRAY noise[3] = { 0.f };
854       float DT_ALIGNED_ARRAY sigma[3] = { 0.f };
855       const int DT_ALIGNED_ARRAY flip[3] = { TRUE, FALSE, TRUE };
856 
857       for(size_t c = 0; c < 3; c++) sigma[c] = pix_in[c] * noise_level / threshold;
858 
859       // create statistical noise
860       dt_noise_generator_simd(noise_distribution, pix_in, sigma, flip, state, noise);
861 
862       // add noise to input
863       float *const restrict pix_out = __builtin_assume_aligned(inpainted + index, 16);
864       for(size_t c = 0; c < 3; c++) pix_out[c] = fmaxf(pix_in[c] * (1.0f - weight) + weight * noise[c], 0.f);
865     }
866 }
867 
868 
869 // B spline filter
870 #define FSIZE 5
871 
872 
873 #ifdef _OPENMP
874 #pragma omp declare simd aligned(buf, indices, result:64)
875 #endif
sparse_scalar_product(const float * const buf,const size_t indices[FSIZE],float result[4])876 inline static void sparse_scalar_product(const float *const buf, const size_t indices[FSIZE], float result[4])
877 {
878   // scalar product of 2 3×5 vectors stored as RGB planes and B-spline filter,
879   // e.g. RRRRR - GGGGG - BBBBB
880 
881   const float DT_ALIGNED_ARRAY filter[FSIZE] =
882                         { 1.0f / 16.0f, 4.0f / 16.0f, 6.0f / 16.0f, 4.0f / 16.0f, 1.0f / 16.0f };
883 
884   #ifdef _OPENMP
885   #pragma omp simd
886   #endif
887   for(size_t c = 0; c < 4; ++c)
888   {
889     float acc = 0.0f;
890     for(size_t k = 0; k < FSIZE; ++k)
891       acc += filter[k] * buf[indices[k] + c];
892     result[c] = fmaxf(acc, 0.f);
893   }
894 }
895 
896 #ifdef _OPENMP
897 #pragma omp declare simd aligned(in, out:64) aligned(tempbuf:16)
898 #endif
blur_2D_Bspline(const float * const restrict in,float * const restrict out,float * const restrict tempbuf,const size_t width,const size_t height,const int mult)899 inline static void blur_2D_Bspline(const float *const restrict in, float *const restrict out,
900                                    float *const restrict tempbuf,
901                                    const size_t width, const size_t height, const int mult)
902 {
903   // À-trous B-spline interpolation/blur shifted by mult
904   #ifdef _OPENMP
905   #pragma omp parallel for default(none) \
906     dt_omp_firstprivate(width, height, mult)  \
907     dt_omp_sharedconst(out, in, tempbuf) \
908     schedule(simd:static)
909   #endif
910   for(size_t row = 0; row < height; row++)
911   {
912     // get a thread-private one-row temporary buffer
913     float *const temp = tempbuf + 4 * width * dt_get_thread_num();
914     // interleave the order in which we process the rows so that we minimize cache misses
915     const size_t i = dwt_interleave_rows(row, height, mult);
916     // Convolve B-spline filter over columns: for each pixel in the current row, compute vertical blur
917     size_t DT_ALIGNED_ARRAY indices[FSIZE] = { 0 };
918     // Start by computing the array indices of the pixels of interest; the offsets from the current pixel stay
919     // unchanged over the entire row, so we can compute once and just offset the base address while iterating
920     // over the row
921     for(size_t ii = 0; ii < FSIZE; ++ii)
922     {
923       const size_t r = CLAMP(mult * (int)(ii - (FSIZE - 1) / 2) + (int)i, (int)0, (int)height - 1);
924       indices[ii] = 4 * r * width;
925     }
926     for(size_t j = 0; j < width; j++)
927     {
928       // Compute the vertical blur of the current pixel and store it in the temp buffer for the row
929       sparse_scalar_product(in + j * 4, indices, temp + j * 4);
930     }
931     // Convolve B-spline filter horizontally over current row
932     for(size_t j = 0; j < width; j++)
933     {
934       // Compute the array indices of the pixels of interest; since the offsets will change near the ends of
935       // the row, we need to recompute for each pixel
936       for(size_t jj = 0; jj < FSIZE; ++jj)
937       {
938         const size_t col = CLAMP(mult * (int)(jj - (FSIZE - 1) / 2) + (int)j, (int)0, (int)width - 1);
939         indices[jj] = 4 * col;
940       }
941       // Compute the horizontal blur of the already vertically-blurred pixel and store the result at the proper
942       //  row/column location in the output buffer
943       sparse_scalar_product(temp, indices, out + (i * width + j) * 4);
944     }
945   }
946 }
947 
948 
wavelets_reconstruct_RGB(const float * const restrict HF,const float * const restrict LF,const float * const restrict texture,const float * const restrict mask,float * const restrict reconstructed,const size_t width,const size_t height,const size_t ch,const float gamma,const float gamma_comp,const float beta,const float beta_comp,const float delta,const size_t s,const size_t scales)949 inline static void wavelets_reconstruct_RGB(const float *const restrict HF, const float *const restrict LF,
950                                             const float *const restrict texture, const float *const restrict mask,
951                                             float *const restrict reconstructed, const size_t width,
952                                             const size_t height, const size_t ch, const float gamma,
953                                             const float gamma_comp, const float beta, const float beta_comp,
954                                             const float delta, const size_t s, const size_t scales)
955 {
956 #ifdef _OPENMP
957 #pragma omp parallel for default(none)                                                                       \
958     dt_omp_firstprivate(width, height, ch, HF, LF, texture, mask, reconstructed, gamma, gamma_comp, beta,         \
959                         beta_comp, delta, s, scales) schedule(simd : static)
960 #endif
961   for(size_t k = 0; k < height * width * ch; k += 4)
962   {
963     const float alpha = mask[k / ch];
964 
965     // cache RGB wavelets scales just to be sure the compiler doesn't reload them
966     const float *const restrict HF_c = __builtin_assume_aligned(HF + k, 16);
967     const float *const restrict LF_c = __builtin_assume_aligned(LF + k, 16);
968     const float *const restrict TT_c = __builtin_assume_aligned(texture + k, 16);
969 
970     // synthesize the max of all RGB channels texture as a flat texture term for the whole pixel
971     // this is useful if only 1 or 2 channels are clipped, so we transfer the valid/sharpest texture on the other
972     // channels
973     const float grey_texture = fmaxabsf(fmaxabsf(TT_c[0], TT_c[1]), TT_c[2]);
974 
975     // synthesize the max of all interpolated/inpainted RGB channels as a flat details term for the whole pixel
976     // this is smoother than grey_texture and will fill holes smoothly in details layers if grey_texture ~= 0.f
977     const float grey_details = (HF_c[0] + HF_c[1] + HF_c[2]) / 3.f;
978 
979     // synthesize both terms with weighting
980     // when beta_comp ~= 1.0, we force the reconstruction to be achromatic, which may help with gamut issues or
981     // magenta highlights.
982     const float grey_HF = beta_comp * (gamma_comp * grey_details + gamma * grey_texture);
983 
984     // synthesize the min of all low-frequency RGB channels as a flat structure term for the whole pixel
985     // when beta_comp ~= 1.0, we force the reconstruction to be achromatic, which may help with gamut issues or magenta highlights.
986     const float grey_residual = beta_comp * (LF_c[0] + LF_c[1] + LF_c[2]) / 3.f;
987 
988     #ifdef _OPENMP
989     #pragma omp simd aligned(reconstructed:64) aligned(HF_c, LF_c, TT_c:16)
990     #endif
991     for(size_t c = 0; c < 4; c++)
992     {
993       // synthesize interpolated/inpainted RGB channels color details residuals and weigh them
994       // this brings back some color on top of the grey_residual
995 
996       // synthesize interpolated/inpainted RGB channels color details and weigh them
997       // this brings back some color on top of the grey_details
998       const float details = (gamma_comp * HF_c[c] + gamma * TT_c[c]) * beta + grey_HF;
999 
1000       // reconstruction
1001       const float residual = (s == scales - 1) ? (grey_residual + LF_c[c] * beta) : 0.f;
1002       reconstructed[k + c] += alpha * (delta * details + residual);
1003     }
1004   }
1005 }
1006 
wavelets_reconstruct_ratios(const float * const restrict HF,const float * const restrict LF,const float * const restrict texture,const float * const restrict mask,float * const restrict reconstructed,const size_t width,const size_t height,const size_t ch,const float gamma,const float gamma_comp,const float beta,const float beta_comp,const float delta,const size_t s,const size_t scales)1007 inline static void wavelets_reconstruct_ratios(const float *const restrict HF, const float *const restrict LF,
1008                                                const float *const restrict texture,
1009                                                const float *const restrict mask,
1010                                                float *const restrict reconstructed, const size_t width,
1011                                                const size_t height, const size_t ch, const float gamma,
1012                                                const float gamma_comp, const float beta, const float beta_comp,
1013                                                const float delta, const size_t s, const size_t scales)
1014 {
1015 /*
1016  * This is the adapted version of the RGB reconstruction
1017  * RGB contain high frequencies that we try to recover, so we favor them in the reconstruction.
1018  * The ratios represent the chromaticity in image and contain low frequencies in the absence of noise or
1019  * aberrations, so, here, we favor them instead.
1020  *
1021  * Consequences : 
1022  *  1. use min of interpolated channels details instead of max, to get smoother details
1023  *  4. use the max of low frequency channels instead of min, to favor achromatic solution.
1024  *
1025  * Note : ratios close to 1 mean higher spectral purity (more white). Ratios close to 0 mean lower spectral purity
1026  * (more colorful)
1027  */
1028 #ifdef _OPENMP
1029 #pragma omp parallel for default(none)                                                                       \
1030     dt_omp_firstprivate(width, height, ch, HF, LF, texture, mask, reconstructed, gamma, gamma_comp, beta,         \
1031                         beta_comp, delta, s, scales) schedule(simd                                                \
1032                                                               : static)
1033 #endif
1034   for(size_t k = 0; k < height * width * ch; k += 4)
1035   {
1036     const float alpha = mask[k / ch];
1037 
1038     // cache RGB wavelets scales just to be sure the compiler doesn't reload them
1039     const float *const restrict HF_c = __builtin_assume_aligned(HF + k, 16);
1040     const float *const restrict LF_c = __builtin_assume_aligned(LF + k, 16);
1041     const float *const restrict TT_c = __builtin_assume_aligned(texture + k, 16);
1042 
1043     // synthesize the max of all RGB channels texture as a flat texture term for the whole pixel
1044     // this is useful if only 1 or 2 channels are clipped, so we transfer the valid/sharpest texture on the other
1045     // channels
1046     const float grey_texture = fmaxabsf(fmaxabsf(TT_c[0], TT_c[1]), TT_c[2]);
1047 
1048     // synthesize the max of all interpolated/inpainted RGB channels as a flat details term for the whole pixel
1049     // this is smoother than grey_texture and will fill holes smoothly in details layers if grey_texture ~= 0.f
1050     const float grey_details = (HF_c[0] + HF_c[1] + HF_c[2]) / 3.f;
1051 
1052     // synthesize both terms with weighting
1053     // when beta_comp ~= 1.0, we force the reconstruction to be achromatic, which may help with gamut issues or
1054     // magenta highlights.
1055     const float grey_HF = (gamma_comp * grey_details + gamma * grey_texture);
1056 
1057     #ifdef _OPENMP
1058     #pragma omp simd aligned(reconstructed:64) aligned(HF_c, TT_c, LF_c:16) linear(k:4)
1059     #endif
1060     for(size_t c = 0; c < 4; c++)
1061     {
1062       // synthesize interpolated/inpainted RGB channels color details residuals and weigh them
1063       // this brings back some color on top of the grey_residual
1064       const float details = 0.5f * ((gamma_comp * HF_c[c] + gamma * TT_c[c]) + grey_HF);
1065 
1066       // reconstruction
1067       const float residual = (s == scales - 1) ? LF_c[c] : 0.f;
1068       reconstructed[k + c] += alpha * (delta * details + residual);
1069     }
1070   }
1071 }
1072 
1073 
init_reconstruct(const float * const restrict in,const float * const restrict mask,float * const restrict reconstructed,const size_t width,const size_t height,const size_t ch)1074 static inline void init_reconstruct(const float *const restrict in, const float *const restrict mask,
1075                                     float *const restrict reconstructed, const size_t width, const size_t height,
1076                                     const size_t ch)
1077 {
1078 // init the reconstructed buffer with non-clipped and partially clipped pixels
1079 // Note : it's a simple multiplied alpha blending where mask = alpha weight
1080 #ifdef _OPENMP
1081 #pragma omp parallel for simd default(none) dt_omp_firstprivate(in, mask, reconstructed, width, height, ch)       \
1082     schedule(simd                                                                                                 \
1083              : static) aligned(in, mask, reconstructed : 64)
1084 #endif
1085   for(size_t k = 0; k < height * width * ch; k++)
1086   {
1087     reconstructed[k] = fmaxf(in[k] * (1.f - mask[k / ch]), 0.f);
1088   }
1089 }
1090 
1091 
wavelets_detail_level(const float * const restrict detail,const float * const restrict LF,float * const restrict HF,float * const restrict texture,const size_t width,const size_t height,const size_t ch)1092 static inline void wavelets_detail_level(const float *const restrict detail, const float *const restrict LF,
1093                                              float *const restrict HF, float *const restrict texture,
1094                                              const size_t width, const size_t height, const size_t ch)
1095 {
1096 #ifdef _OPENMP
1097 #pragma omp parallel for simd default(none) dt_omp_firstprivate(width, height, HF, LF, detail, texture)           \
1098     schedule(simd                                                                                                 \
1099              : static) aligned(HF, LF, detail, texture : 64)
1100 #endif
1101   for(size_t k = 0; k < height * width; k++)
1102   {
1103     for(size_t c = 0; c < 4; ++c) HF[4*k + c] = texture[4*k + c] = detail[4*k + c] - LF[4*k + c];
1104   }
1105 }
1106 
get_scales(const dt_iop_roi_t * roi_in,const dt_dev_pixelpipe_iop_t * const piece)1107 static int get_scales(const dt_iop_roi_t *roi_in, const dt_dev_pixelpipe_iop_t *const piece)
1108 {
1109   /* How many wavelets scales do we need to compute at current zoom level ?
1110    * 0. To get the same preview no matter the zoom scale, the relative image coverage ratio of the filter at
1111    * the coarsest wavelet level should always stay constant.
1112    * 1. The image coverage of each B spline filter of size `FSIZE` is `2^(level) * (FSIZE - 1) / 2 + 1` pixels
1113    * 2. The coarsest level filter at full resolution should cover `1/FSIZE` of the largest image dimension.
1114    * 3. The coarsest level filter at current zoom level should cover `scale/FSIZE` of the largest image dimension.
1115    *
1116    * So we compute the level that solves 1. subject to 3. Of course, integer rounding doesn't make that 1:1
1117    * accurate.
1118    */
1119   const float scale = roi_in->scale / piece->iscale;
1120   const size_t size = MAX(piece->buf_in.height * piece->iscale, piece->buf_in.width * piece->iscale);
1121   const int scales = floorf(log2f((2.0f * size * scale / ((FSIZE - 1) * FSIZE)) - 1.0f));
1122   return CLAMP(scales, 1, MAX_NUM_SCALES);
1123 }
1124 
1125 
reconstruct_highlights(const float * const restrict in,const float * const restrict mask,float * const restrict reconstructed,const dt_iop_filmicrgb_reconstruction_type_t variant,const size_t ch,const dt_iop_filmicrgb_data_t * const data,dt_dev_pixelpipe_iop_t * piece,const dt_iop_roi_t * const roi_in,const dt_iop_roi_t * const roi_out)1126 static inline gint reconstruct_highlights(const float *const restrict in, const float *const restrict mask,
1127                                           float *const restrict reconstructed,
1128                                           const dt_iop_filmicrgb_reconstruction_type_t variant, const size_t ch,
1129                                           const dt_iop_filmicrgb_data_t *const data, dt_dev_pixelpipe_iop_t *piece,
1130                                           const dt_iop_roi_t *const roi_in, const dt_iop_roi_t *const roi_out)
1131 {
1132   gint success = TRUE;
1133 
1134   // wavelets scales
1135   const int scales = get_scales(roi_in, piece);
1136 
1137   // wavelets scales buffers
1138   float *const restrict LF_even = dt_alloc_sse_ps(ch * roi_out->width * roi_out->height); // low-frequencies RGB
1139   float *const restrict LF_odd = dt_alloc_sse_ps(ch * roi_out->width * roi_out->height);  // low-frequencies RGB
1140   float *const restrict HF_RGB = dt_alloc_sse_ps(ch * roi_out->width * roi_out->height);  // high-frequencies RGB
1141   float *const restrict HF_grey = dt_alloc_sse_ps(ch * roi_out->width * roi_out->height); // high-frequencies RGB backup
1142 
1143   // alloc a permanent reusable buffer for intermediate computations - avoid multiple alloc/free
1144   float *const restrict temp = dt_alloc_sse_ps(dt_get_num_threads() * ch * roi_out->width);
1145 
1146   if(!LF_even || !LF_odd || !HF_RGB || !HF_grey || !temp)
1147   {
1148     dt_control_log(_("filmic highlights reconstruction failed to allocate memory, check your RAM settings"));
1149     success = FALSE;
1150     goto error;
1151   }
1152 
1153   // Init reconstructed with valid parts of image
1154   init_reconstruct(in, mask, reconstructed, roi_out->width, roi_out->height, ch);
1155 
1156   // structure inpainting vs. texture duplicating weight
1157   const float gamma = (data->reconstruct_structure_vs_texture);
1158   const float gamma_comp = 1.0f - data->reconstruct_structure_vs_texture;
1159 
1160   // colorful vs. grey weight
1161   const float beta = data->reconstruct_grey_vs_color;
1162   const float beta_comp = 1.f - data->reconstruct_grey_vs_color;
1163 
1164   // bloom vs reconstruct weight
1165   const float delta = data->reconstruct_bloom_vs_details;
1166 
1167   // À trous wavelet decompose
1168   // there is a paper from a guy we know that explains it : https://jo.dreggn.org/home/2010_atrous.pdf
1169   // the wavelets decomposition here is the same as the equalizer/atrous module,
1170   // but simplified because we don't need the edge-aware term, so we can separate the convolution kernel
1171   // with a vertical and horizontal blur, which is 10 multiply-add instead of 25 by pixel.
1172   for(int s = 0; s < scales; ++s)
1173   {
1174     const float *restrict detail;       // buffer containing this scale's input
1175     float *restrict LF;                 // output buffer for the current scale
1176     float *restrict HF_RGB_temp;        // temp buffer for HF_RBG terms before blurring
1177 
1178     // swap buffers so we only need 2 LF buffers : the LF at scale (s-1) and the one at current scale (s)
1179     if(s == 0)
1180     {
1181       detail = in;
1182       LF = LF_odd;
1183       HF_RGB_temp = LF_even;
1184     }
1185     else if(s % 2 != 0)
1186     {
1187       detail = LF_odd;
1188       LF = LF_even;
1189       HF_RGB_temp = LF_odd;
1190     }
1191     else
1192     {
1193       detail = LF_even;
1194       LF = LF_odd;
1195       HF_RGB_temp = LF_even;
1196     }
1197 
1198     const int mult = 1 << s; // fancy-pants C notation for 2^s with integer type, don't be afraid
1199 
1200     // Compute wavelets low-frequency scales
1201     blur_2D_Bspline(detail, LF, temp, roi_out->width, roi_out->height, mult);
1202 
1203     // Compute wavelets high-frequency scales and save the minimum of texture over the RGB channels
1204     // Note : HF_RGB = detail - LF, HF_grey = max(HF_RGB)
1205     wavelets_detail_level(detail, LF, HF_RGB_temp, HF_grey, roi_out->width, roi_out->height, ch);
1206 
1207     // interpolate/blur/inpaint (same thing) the RGB high-frequency to fill holes
1208     blur_2D_Bspline(HF_RGB_temp, HF_RGB, temp, roi_out->width, roi_out->height, 1);
1209 
1210     // Reconstruct clipped parts
1211     if(variant == DT_FILMIC_RECONSTRUCT_RGB)
1212       wavelets_reconstruct_RGB(HF_RGB, LF, HF_grey, mask, reconstructed, roi_out->width, roi_out->height, ch,
1213                                gamma, gamma_comp, beta, beta_comp, delta, s, scales);
1214     else if(variant == DT_FILMIC_RECONSTRUCT_RATIOS)
1215       wavelets_reconstruct_ratios(HF_RGB, LF, HF_grey, mask, reconstructed, roi_out->width, roi_out->height, ch,
1216                                gamma, gamma_comp, beta, beta_comp, delta, s, scales);
1217   }
1218 
1219 error:
1220   if(temp) dt_free_align(temp);
1221   if(LF_even) dt_free_align(LF_even);
1222   if(LF_odd) dt_free_align(LF_odd);
1223   if(HF_RGB) dt_free_align(HF_RGB);
1224   if(HF_grey) dt_free_align(HF_grey);
1225   return success;
1226 }
1227 
1228 
filmic_split_v1(const float * const restrict in,float * const restrict out,const dt_iop_order_iccprofile_info_t * const work_profile,const dt_iop_filmicrgb_data_t * const data,const dt_iop_filmic_rgb_spline_t spline,const size_t width,const size_t height,const size_t ch)1229 static inline void filmic_split_v1(const float *const restrict in, float *const restrict out,
1230                                    const dt_iop_order_iccprofile_info_t *const work_profile,
1231                                    const dt_iop_filmicrgb_data_t *const data,
1232                                    const dt_iop_filmic_rgb_spline_t spline, const size_t width,
1233                                    const size_t height, const size_t ch)
1234 {
1235 #ifdef _OPENMP
1236 #pragma omp parallel for simd default(none) dt_omp_firstprivate(width, height, ch, data, in, out, work_profile,   \
1237                                                                 spline) schedule(simd                             \
1238                                                                                  : static) aligned(in, out : 64)
1239 #endif
1240   for(size_t k = 0; k < height * width * ch; k += ch)
1241   {
1242     const float *const restrict pix_in = in + k;
1243     float *const restrict pix_out = out + k;
1244     float DT_ALIGNED_ARRAY temp[4];
1245 
1246     // Log tone-mapping
1247     for(int c = 0; c < 3; c++)
1248       temp[c] = log_tonemapping_v1(fmaxf(pix_in[c], NORM_MIN), data->grey_source, data->black_source,
1249                                    data->dynamic_range);
1250 
1251     // Get the desaturation coeff based on the log value
1252     const float lum = (work_profile)
1253                           ? dt_ioppr_get_rgb_matrix_luminance(temp, work_profile->matrix_in, work_profile->lut_in,
1254                                                               work_profile->unbounded_coeffs_in,
1255                                                               work_profile->lutsize, work_profile->nonlinearlut)
1256                           : dt_camera_rgb_luminance(temp);
1257     const float desaturation = filmic_desaturate_v1(lum, data->sigma_toe, data->sigma_shoulder, data->saturation);
1258 
1259     // Desaturate on the non-linear parts of the curve
1260     // Filmic S curve on the max RGB
1261     // Apply the transfer function of the display
1262     for(int c = 0; c < 3; c++)
1263       pix_out[c] = powf(
1264           clamp_simd(filmic_spline(linear_saturation(temp[c], lum, desaturation), spline.M1, spline.M2, spline.M3,
1265                                    spline.M4, spline.M5, spline.latitude_min, spline.latitude_max, spline.type)),
1266           data->output_power);
1267   }
1268 }
1269 
1270 
filmic_split_v2_v3(const float * const restrict in,float * const restrict out,const dt_iop_order_iccprofile_info_t * const work_profile,const dt_iop_filmicrgb_data_t * const data,const dt_iop_filmic_rgb_spline_t spline,const size_t width,const size_t height,const size_t ch)1271 static inline void filmic_split_v2_v3(const float *const restrict in, float *const restrict out,
1272                                       const dt_iop_order_iccprofile_info_t *const work_profile,
1273                                       const dt_iop_filmicrgb_data_t *const data,
1274                                       const dt_iop_filmic_rgb_spline_t spline, const size_t width,
1275                                       const size_t height, const size_t ch)
1276 {
1277 #ifdef _OPENMP
1278 #pragma omp parallel for simd default(none) dt_omp_firstprivate(width, height, ch, data, in, out, work_profile,   \
1279                                                                 spline) schedule(simd                             \
1280                                                                                  : static) aligned(in, out : 64)
1281 #endif
1282   for(size_t k = 0; k < height * width * ch; k += ch)
1283   {
1284     const float *const restrict pix_in = in + k;
1285     float *const restrict pix_out = out + k;
1286     float DT_ALIGNED_ARRAY temp[4];
1287 
1288     // Log tone-mapping
1289     for(int c = 0; c < 3; c++)
1290       temp[c] = log_tonemapping_v2(fmaxf(pix_in[c], NORM_MIN), data->grey_source, data->black_source,
1291                                    data->dynamic_range);
1292 
1293     // Get the desaturation coeff based on the log value
1294     const float lum = (work_profile)
1295                           ? dt_ioppr_get_rgb_matrix_luminance(temp, work_profile->matrix_in, work_profile->lut_in,
1296                                                               work_profile->unbounded_coeffs_in,
1297                                                               work_profile->lutsize, work_profile->nonlinearlut)
1298                           : dt_camera_rgb_luminance(temp);
1299     const float desaturation = filmic_desaturate_v2(lum, data->sigma_toe, data->sigma_shoulder, data->saturation);
1300 
1301     // Desaturate on the non-linear parts of the curve
1302     // Filmic S curve on the max RGB
1303     // Apply the transfer function of the display
1304     for(int c = 0; c < 3; c++)
1305       pix_out[c] = powf(
1306           clamp_simd(filmic_spline(linear_saturation(temp[c], lum, desaturation), spline.M1, spline.M2, spline.M3,
1307                                    spline.M4, spline.M5, spline.latitude_min, spline.latitude_max, spline.type)),
1308           data->output_power);
1309   }
1310 }
1311 
1312 
filmic_chroma_v1(const float * const restrict in,float * const restrict out,const dt_iop_order_iccprofile_info_t * const work_profile,const dt_iop_filmicrgb_data_t * const data,const dt_iop_filmic_rgb_spline_t spline,const int variant,const size_t width,const size_t height,const size_t ch)1313 static inline void filmic_chroma_v1(const float *const restrict in, float *const restrict out,
1314                                     const dt_iop_order_iccprofile_info_t *const work_profile,
1315                                     const dt_iop_filmicrgb_data_t *const data,
1316                                     const dt_iop_filmic_rgb_spline_t spline, const int variant, const size_t width,
1317                                     const size_t height, const size_t ch)
1318 {
1319 #ifdef _OPENMP
1320 #pragma omp parallel for simd default(none)                                                                       \
1321     dt_omp_firstprivate(width, height, ch, data, in, out, work_profile, variant, spline) schedule(simd            \
1322                                                                                                   : static)       \
1323         aligned(in, out : 64)
1324 #endif
1325   for(size_t k = 0; k < height * width * ch; k += ch)
1326   {
1327     const float *const restrict pix_in = in + k;
1328     float *const restrict pix_out = out + k;
1329 
1330     float DT_ALIGNED_ARRAY ratios[4] = { 0.0f };
1331     float norm = fmaxf(get_pixel_norm(pix_in, variant, work_profile), NORM_MIN);
1332 
1333     // Save the ratios
1334     for(int c = 0; c < 3; c++) ratios[c] = pix_in[c] / norm;
1335 
1336     // Sanitize the ratios
1337     const float min_ratios = fminf(fminf(ratios[0], ratios[1]), ratios[2]);
1338     if(min_ratios < 0.0f)
1339       for(int c = 0; c < 3; c++) ratios[c] -= min_ratios;
1340 
1341     // Log tone-mapping
1342     norm = log_tonemapping_v1(norm, data->grey_source, data->black_source, data->dynamic_range);
1343 
1344     // Get the desaturation value based on the log value
1345     const float desaturation = filmic_desaturate_v1(norm, data->sigma_toe, data->sigma_shoulder, data->saturation);
1346 
1347     for(int c = 0; c < 3; c++) ratios[c] *= norm;
1348 
1349     const float lum = (work_profile) ? dt_ioppr_get_rgb_matrix_luminance(
1350                           ratios, work_profile->matrix_in, work_profile->lut_in, work_profile->unbounded_coeffs_in,
1351                           work_profile->lutsize, work_profile->nonlinearlut)
1352                                      : dt_camera_rgb_luminance(ratios);
1353 
1354     // Desaturate on the non-linear parts of the curve and save ratios
1355     for(int c = 0; c < 3; c++) ratios[c] = linear_saturation(ratios[c], lum, desaturation) / norm;
1356 
1357     // Filmic S curve on the max RGB
1358     // Apply the transfer function of the display
1359     norm = powf(clamp_simd(filmic_spline(norm, spline.M1, spline.M2, spline.M3, spline.M4, spline.M5,
1360                                          spline.latitude_min, spline.latitude_max, spline.type)),
1361                 data->output_power);
1362 
1363     // Re-apply ratios
1364     for(int c = 0; c < 3; c++) pix_out[c] = ratios[c] * norm;
1365   }
1366 }
1367 
1368 
filmic_chroma_v2_v3(const float * const restrict in,float * const restrict out,const dt_iop_order_iccprofile_info_t * const work_profile,const dt_iop_filmicrgb_data_t * const data,const dt_iop_filmic_rgb_spline_t spline,const int variant,const size_t width,const size_t height,const size_t ch,const dt_iop_filmicrgb_colorscience_type_t colorscience_version)1369 static inline void filmic_chroma_v2_v3(const float *const restrict in, float *const restrict out,
1370                                        const dt_iop_order_iccprofile_info_t *const work_profile,
1371                                        const dt_iop_filmicrgb_data_t *const data,
1372                                        const dt_iop_filmic_rgb_spline_t spline, const int variant,
1373                                        const size_t width, const size_t height, const size_t ch,
1374                                        const dt_iop_filmicrgb_colorscience_type_t colorscience_version)
1375 {
1376 
1377 #ifdef _OPENMP
1378 #pragma omp parallel for simd default(none)                                                                       \
1379     dt_omp_firstprivate(width, height, ch, data, in, out, work_profile, variant, spline, colorscience_version)    \
1380         schedule(simd                                                                                             \
1381                  : static) aligned(in, out : 64)
1382 #endif
1383   for(size_t k = 0; k < height * width * ch; k += ch)
1384   {
1385     const float *const restrict pix_in = in + k;
1386     float *const restrict pix_out = out + k;
1387 
1388     float norm = fmaxf(get_pixel_norm(pix_in, variant, work_profile), NORM_MIN);
1389 
1390     // Save the ratios
1391     float DT_ALIGNED_ARRAY ratios[4] = { 0.0f };
1392 
1393     for(int c = 0; c < 3; c++) ratios[c] = pix_in[c] / norm;
1394 
1395     // Sanitize the ratios
1396     const float min_ratios = fminf(fminf(ratios[0], ratios[1]), ratios[2]);
1397     const int sanitize = (min_ratios < 0.0f);
1398 
1399     if(sanitize)
1400       for(int c = 0; c < 3; c++)
1401         ratios[c] -= min_ratios;
1402 
1403     // Log tone-mapping
1404     norm = log_tonemapping_v2(norm, data->grey_source, data->black_source, data->dynamic_range);
1405 
1406     // Get the desaturation value based on the log value
1407     const float desaturation = filmic_desaturate_v2(norm, data->sigma_toe, data->sigma_shoulder, data->saturation);
1408 
1409     // Filmic S curve on the max RGB
1410     // Apply the transfer function of the display
1411     norm = powf(clamp_simd(filmic_spline(norm, spline.M1, spline.M2, spline.M3, spline.M4, spline.M5,
1412                                          spline.latitude_min, spline.latitude_max, spline.type)),
1413                 data->output_power);
1414 
1415     // Re-apply ratios with saturation change
1416     for(int c = 0; c < 3; c++) ratios[c] = fmaxf(ratios[c] + (1.0f - ratios[c]) * (1.0f - desaturation), 0.0f);
1417 
1418     // color science v3: normalize again after desaturation - the norm might have changed by the desaturation
1419     // operation.
1420     if(colorscience_version == DT_FILMIC_COLORSCIENCE_V3)
1421       norm /= fmaxf(get_pixel_norm(ratios, variant, work_profile), NORM_MIN);
1422 
1423     for(int c = 0; c < 3; c++) pix_out[c] = ratios[c] * norm;
1424 
1425     // Gamut mapping
1426     const float max_pix = fmaxf(fmaxf(pix_out[0], pix_out[1]), pix_out[2]);
1427     const int penalize = (max_pix > 1.0f);
1428 
1429     // Penalize the ratios by the amount of clipping
1430     if(penalize)
1431     {
1432       for(int c = 0; c < 3; c++)
1433       {
1434         ratios[c] = fmaxf(ratios[c] + (1.0f - max_pix), 0.0f);
1435         pix_out[c] = clamp_simd(ratios[c] * norm);
1436       }
1437     }
1438   }
1439 }
1440 
1441 
display_mask(const float * const restrict mask,float * const restrict out,const size_t width,const size_t height,const size_t ch)1442 static inline void display_mask(const float *const restrict mask, float *const restrict out, const size_t width,
1443                                 const size_t height, const size_t ch)
1444 {
1445 #ifdef _OPENMP
1446 #pragma omp parallel for simd default(none) dt_omp_firstprivate(width, height, ch, out, mask) schedule(simd       \
1447                                                                                                        : static)  \
1448     aligned(mask, out : 64)
1449 #endif
1450   for(size_t k = 0; k < height * width * ch; k++) out[k] = mask[k / ch];
1451 }
1452 
1453 
compute_ratios(const float * const restrict in,float * const restrict norms,float * const restrict ratios,const dt_iop_order_iccprofile_info_t * const work_profile,const int variant,const size_t width,const size_t height,const size_t ch)1454 static inline void compute_ratios(const float *const restrict in, float *const restrict norms,
1455                                   float *const restrict ratios,
1456                                   const dt_iop_order_iccprofile_info_t *const work_profile, const int variant,
1457                                   const size_t width, const size_t height, const size_t ch)
1458 {
1459 #ifdef _OPENMP
1460 #pragma omp parallel for simd default(none)                                                                       \
1461     dt_omp_firstprivate(ch, width, height, norms, ratios, in, work_profile, variant) schedule(simd                \
1462                                                                                               : static)           \
1463         aligned(norms, ratios, in : 64)
1464 #endif
1465   for(size_t k = 0; k < height * width * ch; k += ch)
1466   {
1467     const float norm = fmaxf(get_pixel_norm(in + k, variant, work_profile), NORM_MIN);
1468     norms[k / ch] = norm;
1469     for(size_t c = 0; c < 3; c++) ratios[k + c] = in[k + c] / norm;
1470   }
1471 }
1472 
1473 
restore_ratios(float * const restrict ratios,const float * const restrict norms,const size_t width,const size_t height,const size_t ch)1474 static inline void restore_ratios(float *const restrict ratios, const float *const restrict norms,
1475                                   const size_t width, const size_t height, const size_t ch)
1476 {
1477   #ifdef _OPENMP
1478   #pragma omp parallel for simd default(none) \
1479     dt_omp_firstprivate(width, height, ch, norms, ratios) \
1480     schedule(simd:static) aligned(norms, ratios:64) collapse(2)
1481   #endif
1482   for(size_t k = 0; k < height * width * ch; k += ch)
1483     for(size_t c = 0; c < 3; c++) ratios[k + c] = clamp_simd(ratios[k + c]) * norms[k / ch];
1484 }
1485 
1486 
process(dt_iop_module_t * self,dt_dev_pixelpipe_iop_t * piece,const void * const restrict ivoid,void * const restrict ovoid,const dt_iop_roi_t * const roi_in,const dt_iop_roi_t * const roi_out)1487 void process(dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece, const void *const restrict ivoid,
1488              void *const restrict ovoid, const dt_iop_roi_t *const roi_in, const dt_iop_roi_t *const roi_out)
1489 {
1490   const dt_iop_filmicrgb_data_t *const data = (dt_iop_filmicrgb_data_t *)piece->data;
1491   const dt_iop_order_iccprofile_info_t *const work_profile = dt_ioppr_get_pipe_work_profile_info(piece->pipe);
1492 
1493   if(piece->colors != 4)
1494   {
1495     dt_control_log(_("filmic works only on RGB input"));
1496     return;
1497   }
1498 
1499   const size_t ch = 4;
1500 
1501   /** The log2(x) -> -INF when x -> 0
1502    * thus very low values (noise) will get even lower, resulting in noise negative amplification,
1503    * which leads to pepper noise in shadows. To avoid that, we need to clip values that are noise for sure.
1504    * Using 16 bits RAW data, the black value (known by rawspeed for every manufacturer) could be used as a
1505    * threshold. However, at this point of the pixelpipe, the RAW levels have already been corrected and everything
1506    * can happen with black levels in the exposure module. So we define the threshold as the first non-null 16 bit
1507    * integer
1508    */
1509 
1510   float *restrict in = (float *)ivoid;
1511   float *const restrict out = (float *)ovoid;
1512   float *const restrict mask = dt_alloc_sse_ps((size_t)roi_out->width * roi_out->height);
1513 
1514   // used to adjuste noise level depending on size. Don't amplify noise if magnified > 100%
1515   const float scale = fmaxf(piece->iscale / roi_in->scale, 1.f);
1516 
1517   // build a mask of clipped pixels
1518   const int recover_highlights = mask_clipped_pixels(in, mask, data->normalize, data->reconstruct_feather, roi_out->width, roi_out->height, 4);
1519 
1520   // display mask and exit
1521   if(self->dev->gui_attached && (piece->pipe->type & DT_DEV_PIXELPIPE_FULL) == DT_DEV_PIXELPIPE_FULL && mask)
1522   {
1523     dt_iop_filmicrgb_gui_data_t *g = (dt_iop_filmicrgb_gui_data_t *)self->gui_data;
1524 
1525     if(g->show_mask)
1526     {
1527       display_mask(mask, out, roi_out->width, roi_out->height, ch);
1528       dt_free_align(mask);
1529       return;
1530     }
1531   }
1532 
1533   float *const restrict reconstructed = dt_alloc_sse_ps(ch * roi_out->width * roi_out->height);
1534   const gboolean run_fast = (piece->pipe->type & DT_DEV_PIXELPIPE_FAST) == DT_DEV_PIXELPIPE_FAST;
1535 
1536   // if fast mode is not in use
1537   if(!run_fast && recover_highlights && mask && reconstructed)
1538   {
1539     // init the blown areas with noise to create particles
1540     float *const restrict inpainted =  dt_alloc_sse_ps(ch * roi_out->width * roi_out->height);
1541     inpaint_noise(in, mask, inpainted, data->noise_level / scale, data->reconstruct_threshold, data->noise_distribution,
1542                   roi_out->width, roi_out->height, ch);
1543 
1544     // diffuse particles with wavelets reconstruction
1545     // PASS 1 on RGB channels
1546     const gint success_1 = reconstruct_highlights(inpainted, mask, reconstructed, DT_FILMIC_RECONSTRUCT_RGB, ch, data, piece, roi_in, roi_out);
1547     gint success_2 = TRUE;
1548 
1549     dt_free_align(inpainted);
1550 
1551     if(data->high_quality_reconstruction > 0 && success_1)
1552     {
1553       float *const restrict norms = dt_alloc_sse_ps((size_t)roi_out->width * roi_out->height);
1554       float *const restrict ratios = dt_alloc_sse_ps(ch * roi_out->width * roi_out->height);
1555 
1556       // reconstruct highlights PASS 2 on ratios
1557       if(norms && ratios)
1558       {
1559         for(int i = 0; i < data->high_quality_reconstruction; i++)
1560         {
1561           compute_ratios(reconstructed, norms, ratios, work_profile, DT_FILMIC_METHOD_EUCLIDEAN_NORM_V1,
1562                          roi_out->width, roi_out->height, ch);
1563           success_2 = success_2
1564                       && reconstruct_highlights(ratios, mask, reconstructed, DT_FILMIC_RECONSTRUCT_RATIOS, ch,
1565                                                 data, piece, roi_in, roi_out);
1566           restore_ratios(reconstructed, norms, roi_out->width, roi_out->height, ch);
1567         }
1568       }
1569 
1570       if(norms) dt_free_align(norms);
1571       if(ratios) dt_free_align(ratios);
1572     }
1573 
1574     if(success_1 && success_2) in = reconstructed; // use reconstructed buffer as tonemapping input
1575   }
1576 
1577   if(mask) dt_free_align(mask);
1578 
1579   if(data->preserve_color == DT_FILMIC_METHOD_NONE)
1580   {
1581     // no chroma preservation
1582     if(data->version == DT_FILMIC_COLORSCIENCE_V1)
1583       filmic_split_v1(in, out, work_profile, data, data->spline, roi_out->width, roi_in->height, ch);
1584     else if(data->version == DT_FILMIC_COLORSCIENCE_V2 || data->version == DT_FILMIC_COLORSCIENCE_V3)
1585       filmic_split_v2_v3(in, out, work_profile, data, data->spline, roi_out->width, roi_in->height, ch);
1586   }
1587   else
1588   {
1589     // chroma preservation
1590     if(data->version == DT_FILMIC_COLORSCIENCE_V1)
1591       filmic_chroma_v1(in, out, work_profile, data, data->spline, data->preserve_color, roi_out->width,
1592                        roi_out->height, ch);
1593     else if(data->version == DT_FILMIC_COLORSCIENCE_V2 || data->version == DT_FILMIC_COLORSCIENCE_V3)
1594       filmic_chroma_v2_v3(in, out, work_profile, data, data->spline, data->preserve_color, roi_out->width,
1595                           roi_out->height, ch, data->version);
1596   }
1597 
1598   if(reconstructed) dt_free_align(reconstructed);
1599 
1600   if(piece->pipe->mask_display & DT_DEV_PIXELPIPE_DISPLAY_MASK)
1601     dt_iop_alpha_copy(ivoid, ovoid, roi_out->width, roi_out->height);
1602 }
1603 
1604 #ifdef HAVE_OPENCL
reconstruct_highlights_cl(cl_mem in,cl_mem mask,cl_mem reconstructed,const dt_iop_filmicrgb_reconstruction_type_t variant,dt_iop_filmicrgb_global_data_t * const gd,const dt_iop_filmicrgb_data_t * const data,dt_dev_pixelpipe_iop_t * piece,const dt_iop_roi_t * const roi_in)1605 static inline cl_int reconstruct_highlights_cl(cl_mem in, cl_mem mask, cl_mem reconstructed,
1606                                           const dt_iop_filmicrgb_reconstruction_type_t variant, dt_iop_filmicrgb_global_data_t *const gd,
1607                                           const dt_iop_filmicrgb_data_t *const data, dt_dev_pixelpipe_iop_t *piece,
1608                                           const dt_iop_roi_t *const roi_in)
1609 {
1610   cl_int err = -999;
1611   const int devid = piece->pipe->devid;
1612   const int width = roi_in->width;
1613   const int height = roi_in->height;
1614   size_t sizes[] = { ROUNDUPWD(width), ROUNDUPHT(height), 1 };
1615 
1616   // wavelets scales
1617   const int scales = get_scales(roi_in, piece);
1618 
1619   // wavelets scales buffers
1620   cl_mem LF_even = dt_opencl_alloc_device(devid, sizes[0], sizes[1], sizeof(float) * 4); // low-frequencies RGB
1621   cl_mem LF_odd = dt_opencl_alloc_device(devid, sizes[0], sizes[1], sizeof(float) * 4);  // low-frequencies RGB
1622   cl_mem HF_RGB = dt_opencl_alloc_device(devid, sizes[0], sizes[1], sizeof(float) * 4);  // high-frequencies RGB
1623   cl_mem HF_grey = dt_opencl_alloc_device(devid, sizes[0], sizes[1], sizeof(float) * 4); // high-frequencies RGB backup
1624 
1625   // alloc a permanent reusable buffer for intermediate computations - avoid multiple alloc/free
1626   cl_mem temp = dt_opencl_alloc_device(devid, sizes[0], sizes[1], sizeof(float) * 4);;
1627 
1628   if(!LF_even || !LF_odd || !HF_RGB || !HF_grey || !temp)
1629   {
1630     dt_control_log(_("filmic highlights reconstruction failed to allocate memory on GPU"));
1631     err = CL_MEM_OBJECT_ALLOCATION_FAILURE;
1632     goto error;
1633   }
1634 
1635   // Init reconstructed with valid parts of image
1636   dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_init_reconstruct, 0, sizeof(cl_mem), (void *)&in);
1637   dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_init_reconstruct, 1, sizeof(cl_mem), (void *)&mask);
1638   dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_init_reconstruct, 2, sizeof(cl_mem), (void *)&reconstructed);
1639   dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_init_reconstruct, 3, sizeof(int), (void *)&width);
1640   dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_init_reconstruct, 4, sizeof(int), (void *)&height);
1641   err = dt_opencl_enqueue_kernel_2d(devid, gd->kernel_filmic_init_reconstruct, sizes);
1642   if(err != CL_SUCCESS) goto error;
1643 
1644   // structure inpainting vs. texture duplicating weight
1645   const float gamma = (data->reconstruct_structure_vs_texture);
1646   const float gamma_comp = 1.0f - data->reconstruct_structure_vs_texture;
1647 
1648   // colorful vs. grey weight
1649   const float beta = data->reconstruct_grey_vs_color;
1650   const float beta_comp = 1.f - data->reconstruct_grey_vs_color;
1651 
1652   // bloom vs reconstruct weight
1653   const float delta = data->reconstruct_bloom_vs_details;
1654 
1655   // À trous wavelet decompose
1656   // there is a paper from a guy we know that explains it : https://jo.dreggn.org/home/2010_atrous.pdf
1657   // the wavelets decomposition here is the same as the equalizer/atrous module,
1658   // but simplified because we don't need the edge-aware term, so we can separate the convolution kernel
1659   // with a vertical and horizontal blur, which is 10 multiply-add instead of 25 by pixel.
1660   for(int s = 0; s < scales; ++s)
1661   {
1662     cl_mem detail;
1663     cl_mem LF;
1664 
1665     // swap buffers so we only need 2 LF buffers : the LF at scale (s-1) and the one at current scale (s)
1666     if(s == 0)
1667     {
1668       detail = in;
1669       LF = LF_odd;
1670     }
1671     else if(s % 2 != 0)
1672     {
1673       detail = LF_odd;
1674       LF = LF_even;
1675     }
1676     else
1677     {
1678       detail = LF_even;
1679       LF = LF_odd;
1680     }
1681 
1682     const int mult = 1 << s; // fancy-pants C notation for 2^s with integer type, don't be afraid
1683 
1684     // Compute wavelets low-frequency scales
1685     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_bspline_horizontal, 0, sizeof(cl_mem), (void *)&detail);
1686     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_bspline_horizontal, 1, sizeof(cl_mem), (void *)&temp);
1687     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_bspline_horizontal, 2, sizeof(int), (void *)&width);
1688     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_bspline_horizontal, 3, sizeof(int), (void *)&height);
1689     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_bspline_horizontal, 4, sizeof(int), (void *)&mult);
1690     err = dt_opencl_enqueue_kernel_2d(devid, gd->kernel_filmic_bspline_horizontal, sizes);
1691     if(err != CL_SUCCESS) goto error;
1692 
1693     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_bspline_vertical, 0, sizeof(cl_mem), (void *)&temp);
1694     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_bspline_vertical, 1, sizeof(cl_mem), (void *)&LF);
1695     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_bspline_vertical, 2, sizeof(int), (void *)&width);
1696     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_bspline_vertical, 3, sizeof(int), (void *)&height);
1697     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_bspline_vertical, 4, sizeof(int), (void *)&mult);
1698     err = dt_opencl_enqueue_kernel_2d(devid, gd->kernel_filmic_bspline_vertical, sizes);
1699     if(err != CL_SUCCESS) goto error;
1700 
1701     // Compute wavelets high-frequency scales and backup the maximum of texture over the RGB channels
1702     // Note : HF_RGB = detail - LF, HF_grey = HF_RGB
1703     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_wavelets_detail, 0, sizeof(cl_mem), (void *)&detail);
1704     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_wavelets_detail, 1, sizeof(cl_mem), (void *)&LF);
1705     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_wavelets_detail, 2, sizeof(cl_mem), (void *)&HF_RGB);
1706     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_wavelets_detail, 3, sizeof(cl_mem), (void *)&HF_grey);
1707     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_wavelets_detail, 4, sizeof(int), (void *)&width);
1708     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_wavelets_detail, 5, sizeof(int), (void *)&height);
1709     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_wavelets_detail, 6, sizeof(int), (void *)&variant);
1710     err = dt_opencl_enqueue_kernel_2d(devid, gd->kernel_filmic_wavelets_detail, sizes);
1711     if(err != CL_SUCCESS) goto error;
1712 
1713     // interpolate/blur/inpaint (same thing) the RGB high-frequency to fill holes
1714     const int blur_size = 1;
1715     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_bspline_vertical, 0, sizeof(cl_mem), (void *)&HF_RGB);
1716     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_bspline_vertical, 1, sizeof(cl_mem), (void *)&temp);
1717     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_bspline_vertical, 2, sizeof(int), (void *)&width);
1718     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_bspline_vertical, 3, sizeof(int), (void *)&height);
1719     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_bspline_vertical, 4, sizeof(int), (void *)&blur_size);
1720     err = dt_opencl_enqueue_kernel_2d(devid, gd->kernel_filmic_bspline_vertical, sizes);
1721     if(err != CL_SUCCESS) goto error;
1722 
1723     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_bspline_horizontal, 0, sizeof(cl_mem), (void *)&temp);
1724     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_bspline_horizontal, 1, sizeof(cl_mem), (void *)&HF_RGB);
1725     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_bspline_horizontal, 2, sizeof(int), (void *)&width);
1726     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_bspline_horizontal, 3, sizeof(int), (void *)&height);
1727     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_bspline_horizontal, 4, sizeof(int), (void *)&blur_size);
1728     err = dt_opencl_enqueue_kernel_2d(devid, gd->kernel_filmic_bspline_horizontal, sizes);
1729     if(err != CL_SUCCESS) goto error;
1730 
1731     // Reconstruct clipped parts
1732     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_wavelets_reconstruct, 0, sizeof(cl_mem), (void *)&HF_RGB);
1733     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_wavelets_reconstruct, 1, sizeof(cl_mem), (void *)&LF);
1734     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_wavelets_reconstruct, 2, sizeof(cl_mem), (void *)&HF_grey);
1735     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_wavelets_reconstruct, 3, sizeof(cl_mem), (void *)&mask);
1736     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_wavelets_reconstruct, 4, sizeof(cl_mem), (void *)&reconstructed);
1737     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_wavelets_reconstruct, 5, sizeof(cl_mem), (void *)&reconstructed);
1738     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_wavelets_reconstruct, 6, sizeof(int), (void *)&width);
1739     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_wavelets_reconstruct, 7, sizeof(int), (void *)&height);
1740     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_wavelets_reconstruct, 8, sizeof(float), (void *)&gamma);
1741     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_wavelets_reconstruct, 9, sizeof(float), (void *)&gamma_comp);
1742     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_wavelets_reconstruct, 10, sizeof(float), (void *)&beta);
1743     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_wavelets_reconstruct, 11, sizeof(float), (void *)&beta_comp);
1744     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_wavelets_reconstruct, 12, sizeof(float), (void *)&delta);
1745     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_wavelets_reconstruct, 13, sizeof(int), (void *)&s);
1746     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_wavelets_reconstruct, 14, sizeof(int), (void *)&scales);
1747     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_wavelets_reconstruct, 15, sizeof(int), (void *)&variant);
1748     err = dt_opencl_enqueue_kernel_2d(devid, gd->kernel_filmic_wavelets_reconstruct, sizes);
1749     if(err != CL_SUCCESS) goto error;
1750   }
1751 
1752 error:
1753   dt_opencl_release_mem_object(temp);
1754   dt_opencl_release_mem_object(LF_even);
1755   dt_opencl_release_mem_object(LF_odd);
1756   dt_opencl_release_mem_object(HF_RGB);
1757   dt_opencl_release_mem_object(HF_grey);
1758   return err;
1759 }
1760 
1761 
process_cl(struct dt_iop_module_t * self,dt_dev_pixelpipe_iop_t * piece,cl_mem dev_in,cl_mem dev_out,const dt_iop_roi_t * const roi_in,const dt_iop_roi_t * const roi_out)1762 int process_cl(struct dt_iop_module_t *self, dt_dev_pixelpipe_iop_t *piece, cl_mem dev_in, cl_mem dev_out,
1763                const dt_iop_roi_t *const roi_in, const dt_iop_roi_t *const roi_out)
1764 {
1765   const dt_iop_filmicrgb_data_t *const d = (dt_iop_filmicrgb_data_t *)piece->data;
1766   dt_iop_filmicrgb_global_data_t *const gd = (dt_iop_filmicrgb_global_data_t *)self->global_data;
1767 
1768   cl_int err = -999;
1769 
1770   if(piece->colors != 4)
1771   {
1772     dt_control_log(_("filmic works only on RGB input"));
1773     return err;
1774   }
1775 
1776   const int devid = piece->pipe->devid;
1777   const int width = roi_in->width;
1778   const int height = roi_in->height;
1779 
1780   size_t sizes[] = { ROUNDUPWD(width), ROUNDUPHT(height), 1 };
1781 
1782   cl_mem in = dev_in;
1783   cl_mem inpainted = NULL;
1784   cl_mem reconstructed = NULL;
1785   cl_mem mask = NULL;
1786   cl_mem ratios = NULL;
1787   cl_mem norms = NULL;
1788 
1789   // fetch working color profile
1790   const dt_iop_order_iccprofile_info_t *const work_profile = dt_ioppr_get_pipe_work_profile_info(piece->pipe);
1791   const int use_work_profile = (work_profile == NULL) ? 0 : 1;
1792   cl_mem dev_profile_info = NULL;
1793   cl_mem dev_profile_lut = NULL;
1794   dt_colorspaces_iccprofile_info_cl_t *profile_info_cl;
1795   cl_float *profile_lut_cl = NULL;
1796 
1797   err = dt_ioppr_build_iccprofile_params_cl(work_profile, devid, &profile_info_cl, &profile_lut_cl,
1798                                             &dev_profile_info, &dev_profile_lut);
1799   if(err != CL_SUCCESS) goto error;
1800 
1801   // used to adjust noise level depending on size. Don't amplify noise if magnified > 100%
1802   const float scale = fmaxf(piece->iscale / roi_in->scale, 1.f);
1803 
1804   // get the number of OpenCL threads
1805   uint16_t is_clipped = 0;
1806   cl_mem clipped = dt_opencl_alloc_device(devid, 1, 1, sizeof(uint16_t));
1807 
1808   // build a mask of clipped pixels
1809   mask = dt_opencl_alloc_device(devid, sizes[0], sizes[1], sizeof(float));
1810   dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_mask, 0, sizeof(cl_mem), (void *)&in);
1811   dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_mask, 1, sizeof(cl_mem), (void *)&mask);
1812   dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_mask, 2, sizeof(int), (void *)&width);
1813   dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_mask, 3, sizeof(int), (void *)&height);
1814   dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_mask, 4, sizeof(float), (void *)&d->normalize);
1815   dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_mask, 5, sizeof(float), (void *)&d->reconstruct_feather);
1816   dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_mask, 6, sizeof(cl_mem), (void *)&clipped);
1817   err = dt_opencl_enqueue_kernel_2d(devid, gd->kernel_filmic_mask, sizes);
1818   if(err != CL_SUCCESS) goto error;
1819 
1820   // read the number of clipped pixels
1821   dt_opencl_copy_device_to_host(devid, &is_clipped, clipped, 1, 1, sizeof(uint16_t));
1822   dt_opencl_release_mem_object(clipped);
1823   clipped = NULL;
1824 
1825   // display mask and exit
1826   if(self->dev->gui_attached && (piece->pipe->type & DT_DEV_PIXELPIPE_FULL) == DT_DEV_PIXELPIPE_FULL)
1827   {
1828     dt_iop_filmicrgb_gui_data_t *g = (dt_iop_filmicrgb_gui_data_t *)self->gui_data;
1829 
1830     if(g->show_mask)
1831     {
1832       dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_show_mask, 0, sizeof(cl_mem), (void *)&mask);
1833       dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_show_mask, 1, sizeof(cl_mem), (void *)&dev_out);
1834       dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_show_mask, 2, sizeof(int), (void *)&width);
1835       dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_show_mask, 3, sizeof(int), (void *)&height);
1836       err = dt_opencl_enqueue_kernel_2d(devid, gd->kernel_filmic_show_mask, sizes);
1837       dt_opencl_release_mem_object(mask);
1838       dt_ioppr_free_iccprofile_params_cl(&profile_info_cl, &profile_lut_cl, &dev_profile_info, &dev_profile_lut);
1839       return TRUE;
1840     }
1841   }
1842 
1843   const gboolean run_fast = (piece->pipe->type & DT_DEV_PIXELPIPE_FAST) == DT_DEV_PIXELPIPE_FAST;
1844 
1845   if(!run_fast && is_clipped > 0)
1846   {
1847     // Inpaint noise
1848     const float noise_level = d->noise_level / scale;
1849     inpainted = dt_opencl_alloc_device(devid, sizes[0], sizes[1], sizeof(float) * 4);
1850     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_inpaint_noise, 0, sizeof(cl_mem), (void *)&in);
1851     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_inpaint_noise, 1, sizeof(cl_mem), (void *)&mask);
1852     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_inpaint_noise, 2, sizeof(cl_mem), (void *)&inpainted);
1853     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_inpaint_noise, 3, sizeof(int), (void *)&width);
1854     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_inpaint_noise, 4, sizeof(int), (void *)&height);
1855     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_inpaint_noise, 5, sizeof(float), (void *)&noise_level);
1856     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_inpaint_noise, 6, sizeof(float), (void *)&d->reconstruct_threshold);
1857     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_inpaint_noise, 7, sizeof(float), (void *)&d->noise_distribution);
1858     err = dt_opencl_enqueue_kernel_2d(devid, gd->kernel_filmic_inpaint_noise, sizes);
1859     if(err != CL_SUCCESS) goto error;
1860 
1861     // first step of highlight reconstruction in RGB
1862     reconstructed = dt_opencl_alloc_device(devid, sizes[0], sizes[1], sizeof(float) * 4);
1863     err = reconstruct_highlights_cl(inpainted, mask, reconstructed, DT_FILMIC_RECONSTRUCT_RGB, gd, d, piece, roi_in);
1864     if(err != CL_SUCCESS) goto error;
1865     dt_opencl_release_mem_object(inpainted);
1866     inpainted = NULL;
1867 
1868     if(d->high_quality_reconstruction > 0)
1869     {
1870       ratios = dt_opencl_alloc_device(devid, sizes[0], sizes[1], sizeof(float) * 4);
1871       norms = dt_opencl_alloc_device(devid, sizes[0], sizes[1], sizeof(float));
1872 
1873       if(norms && ratios)
1874       {
1875         for(int i = 0; i < d->high_quality_reconstruction; i++)
1876         {
1877           // break ratios and norms
1878           dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_compute_ratios, 0, sizeof(cl_mem), (void *)&reconstructed);
1879           dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_compute_ratios, 1, sizeof(cl_mem), (void *)&norms);
1880           dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_compute_ratios, 2, sizeof(cl_mem), (void *)&ratios);
1881           dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_compute_ratios, 3, sizeof(int), (void *)&d->preserve_color);
1882           dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_compute_ratios, 4, sizeof(int), (void *)&width);
1883           dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_compute_ratios, 5, sizeof(int), (void *)&height);
1884           err = dt_opencl_enqueue_kernel_2d(devid, gd->kernel_filmic_compute_ratios, sizes);
1885           if(err != CL_SUCCESS) goto error;
1886 
1887           // second step of reconstruction over ratios
1888           err = reconstruct_highlights_cl(ratios, mask, reconstructed, DT_FILMIC_RECONSTRUCT_RATIOS, gd, d, piece, roi_in);
1889           if(err != CL_SUCCESS) goto error;
1890 
1891           // restore ratios to RGB
1892           dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_restore_ratios, 0, sizeof(cl_mem), (void *)&reconstructed);
1893           dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_restore_ratios, 1, sizeof(cl_mem), (void *)&norms);
1894           dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_restore_ratios, 2, sizeof(cl_mem), (void *)&reconstructed);
1895           dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_restore_ratios, 3, sizeof(int), (void *)&width);
1896           dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_restore_ratios, 4, sizeof(int), (void *)&height);
1897           err = dt_opencl_enqueue_kernel_2d(devid, gd->kernel_filmic_restore_ratios, sizes);
1898           if(err != CL_SUCCESS) goto error;
1899         }
1900       }
1901 
1902       dt_opencl_release_mem_object(ratios);
1903       dt_opencl_release_mem_object(norms);
1904       ratios = NULL;
1905       norms = NULL;
1906     }
1907 
1908     in = reconstructed;
1909   }
1910 
1911   dt_opencl_release_mem_object(mask); // mask is only used for highlights reconstruction.
1912   mask = NULL;
1913 
1914   const dt_iop_filmic_rgb_spline_t spline = (dt_iop_filmic_rgb_spline_t)d->spline;
1915 
1916   if(d->preserve_color == DT_FILMIC_METHOD_NONE)
1917   {
1918     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_rgb_split, 0, sizeof(cl_mem), (void *)&in);
1919     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_rgb_split, 1, sizeof(cl_mem), (void *)&dev_out);
1920     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_rgb_split, 2, sizeof(int), (void *)&width);
1921     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_rgb_split, 3, sizeof(int), (void *)&height);
1922     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_rgb_split, 4, sizeof(float), (void *)&d->dynamic_range);
1923     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_rgb_split, 5, sizeof(float), (void *)&d->black_source);
1924     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_rgb_split, 6, sizeof(float), (void *)&d->grey_source);
1925     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_rgb_split, 7, sizeof(cl_mem), (void *)&dev_profile_info);
1926     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_rgb_split, 8, sizeof(cl_mem), (void *)&dev_profile_lut);
1927     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_rgb_split, 9, sizeof(int), (void *)&use_work_profile);
1928     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_rgb_split, 10, sizeof(float), (void *)&d->sigma_toe);
1929     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_rgb_split, 11, sizeof(float), (void *)&d->sigma_shoulder);
1930     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_rgb_split, 12, sizeof(float), (void *)&d->saturation);
1931     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_rgb_split, 13, 4 * sizeof(float), (void *)&spline.M1);
1932     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_rgb_split, 14, 4 * sizeof(float), (void *)&spline.M2);
1933     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_rgb_split, 15, 4 * sizeof(float), (void *)&spline.M3);
1934     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_rgb_split, 16, 4 * sizeof(float), (void *)&spline.M4);
1935     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_rgb_split, 17, 4 * sizeof(float), (void *)&spline.M5);
1936     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_rgb_split, 18, sizeof(float), (void *)&spline.latitude_min);
1937     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_rgb_split, 19, sizeof(float), (void *)&spline.latitude_max);
1938     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_rgb_split, 20, sizeof(float), (void *)&d->output_power);
1939     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_rgb_split, 21, sizeof(int), (void *)&d->version);
1940     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_rgb_split, 22, sizeof(int), (void *)&spline.type[0]);
1941     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_rgb_split, 23, sizeof(int), (void *)&spline.type[1]);
1942 
1943     err = dt_opencl_enqueue_kernel_2d(devid, gd->kernel_filmic_rgb_split, sizes);
1944     if(err != CL_SUCCESS) goto error;
1945   }
1946   else
1947   {
1948     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_rgb_chroma, 0, sizeof(cl_mem), (void *)&in);
1949     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_rgb_chroma, 1, sizeof(cl_mem), (void *)&dev_out);
1950     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_rgb_chroma, 2, sizeof(int), (void *)&width);
1951     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_rgb_chroma, 3, sizeof(int), (void *)&height);
1952     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_rgb_chroma, 4, sizeof(float), (void *)&d->dynamic_range);
1953     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_rgb_chroma, 5, sizeof(float), (void *)&d->black_source);
1954     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_rgb_chroma, 6, sizeof(float), (void *)&d->grey_source);
1955     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_rgb_chroma, 7, sizeof(cl_mem), (void *)&dev_profile_info);
1956     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_rgb_chroma, 8, sizeof(cl_mem), (void *)&dev_profile_lut);
1957     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_rgb_chroma, 9, sizeof(int), (void *)&use_work_profile);
1958     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_rgb_chroma, 10, sizeof(float), (void *)&d->sigma_toe);
1959     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_rgb_chroma, 11, sizeof(float), (void *)&d->sigma_shoulder);
1960     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_rgb_chroma, 12, sizeof(float), (void *)&d->saturation);
1961     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_rgb_chroma, 13, 4 * sizeof(float), (void *)&spline.M1);
1962     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_rgb_chroma, 14, 4 * sizeof(float), (void *)&spline.M2);
1963     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_rgb_chroma, 15, 4 * sizeof(float), (void *)&spline.M3);
1964     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_rgb_chroma, 16, 4 * sizeof(float), (void *)&spline.M4);
1965     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_rgb_chroma, 17, 4 * sizeof(float), (void *)&spline.M5);
1966     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_rgb_chroma, 18, sizeof(float), (void *)&spline.latitude_min);
1967     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_rgb_chroma, 19, sizeof(float), (void *)&spline.latitude_max);
1968     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_rgb_chroma, 20, sizeof(float), (void *)&d->output_power);
1969     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_rgb_chroma, 21, sizeof(int), (void *)&d->preserve_color);
1970     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_rgb_chroma, 22, sizeof(int), (void *)&d->version);
1971     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_rgb_chroma, 23, sizeof(int), (void *)&spline.type[0]);
1972     dt_opencl_set_kernel_arg(devid, gd->kernel_filmic_rgb_chroma, 24, sizeof(int), (void *)&spline.type[1]);
1973 
1974     err = dt_opencl_enqueue_kernel_2d(devid, gd->kernel_filmic_rgb_chroma, sizes);
1975     if(err != CL_SUCCESS) goto error;
1976   }
1977 
1978   dt_opencl_release_mem_object(reconstructed);
1979   dt_ioppr_free_iccprofile_params_cl(&profile_info_cl, &profile_lut_cl, &dev_profile_info, &dev_profile_lut);
1980   return TRUE;
1981 
1982 error:
1983   dt_ioppr_free_iccprofile_params_cl(&profile_info_cl, &profile_lut_cl, &dev_profile_info, &dev_profile_lut);
1984   dt_opencl_release_mem_object(reconstructed);
1985   dt_opencl_release_mem_object(inpainted);
1986   dt_opencl_release_mem_object(mask);
1987   dt_opencl_release_mem_object(ratios);
1988   dt_opencl_release_mem_object(norms);
1989   dt_print(DT_DEBUG_OPENCL, "[opencl_filmicrgb] couldn't enqueue kernel! %d\n", err);
1990   return FALSE;
1991 }
1992 #endif
1993 
1994 
apply_auto_grey(dt_iop_module_t * self)1995 static void apply_auto_grey(dt_iop_module_t *self)
1996 {
1997   if(darktable.gui->reset) return;
1998   dt_iop_filmicrgb_params_t *p = (dt_iop_filmicrgb_params_t *)self->params;
1999   dt_iop_filmicrgb_gui_data_t *g = (dt_iop_filmicrgb_gui_data_t *)self->gui_data;
2000 
2001   const dt_iop_order_iccprofile_info_t *const work_profile
2002       = dt_ioppr_get_iop_work_profile_info(self, self->dev->iop);
2003   const float grey = get_pixel_norm(self->picked_color, p->preserve_color, work_profile) / 2.0f;
2004 
2005   const float prev_grey = p->grey_point_source;
2006   p->grey_point_source = CLAMP(100.f * grey, 0.001f, 100.0f);
2007   const float grey_var = log2f(prev_grey / p->grey_point_source);
2008   p->black_point_source = p->black_point_source - grey_var;
2009   p->white_point_source = p->white_point_source + grey_var;
2010   p->output_power = logf(p->grey_point_target / 100.0f)
2011                     / logf(-p->black_point_source / (p->white_point_source - p->black_point_source));
2012 
2013   ++darktable.gui->reset;
2014   dt_bauhaus_slider_set_soft(g->grey_point_source, p->grey_point_source);
2015   dt_bauhaus_slider_set_soft(g->black_point_source, p->black_point_source);
2016   dt_bauhaus_slider_set_soft(g->white_point_source, p->white_point_source);
2017   dt_bauhaus_slider_set_soft(g->output_power, p->output_power);
2018   --darktable.gui->reset;
2019 
2020   gtk_widget_queue_draw(self->widget);
2021   dt_dev_add_history_item(darktable.develop, self, TRUE);
2022 }
2023 
apply_auto_black(dt_iop_module_t * self)2024 static void apply_auto_black(dt_iop_module_t *self)
2025 {
2026   if(darktable.gui->reset) return;
2027   dt_iop_filmicrgb_params_t *p = (dt_iop_filmicrgb_params_t *)self->params;
2028   dt_iop_filmicrgb_gui_data_t *g = (dt_iop_filmicrgb_gui_data_t *)self->gui_data;
2029 
2030   // Black
2031   const dt_iop_order_iccprofile_info_t *const work_profile
2032       = dt_ioppr_get_iop_work_profile_info(self, self->dev->iop);
2033   const float black = get_pixel_norm(self->picked_color_min, DT_FILMIC_METHOD_MAX_RGB, work_profile);
2034 
2035   float EVmin = CLAMP(log2f(black / (p->grey_point_source / 100.0f)), -16.0f, -1.0f);
2036   EVmin *= (1.0f + p->security_factor / 100.0f);
2037 
2038   p->black_point_source = fmaxf(EVmin, -16.0f);
2039   p->output_power = logf(p->grey_point_target / 100.0f)
2040                     / logf(-p->black_point_source / (p->white_point_source - p->black_point_source));
2041 
2042   ++darktable.gui->reset;
2043   dt_bauhaus_slider_set_soft(g->black_point_source, p->black_point_source);
2044   dt_bauhaus_slider_set_soft(g->output_power, p->output_power);
2045   --darktable.gui->reset;
2046 
2047   gtk_widget_queue_draw(self->widget);
2048   dt_dev_add_history_item(darktable.develop, self, TRUE);
2049 }
2050 
2051 
apply_auto_white_point_source(dt_iop_module_t * self)2052 static void apply_auto_white_point_source(dt_iop_module_t *self)
2053 {
2054   if(darktable.gui->reset) return;
2055   dt_iop_filmicrgb_params_t *p = (dt_iop_filmicrgb_params_t *)self->params;
2056   dt_iop_filmicrgb_gui_data_t *g = (dt_iop_filmicrgb_gui_data_t *)self->gui_data;
2057 
2058   // White
2059   const dt_iop_order_iccprofile_info_t *const work_profile
2060       = dt_ioppr_get_iop_work_profile_info(self, self->dev->iop);
2061   const float white = get_pixel_norm(self->picked_color_max, DT_FILMIC_METHOD_MAX_RGB, work_profile);
2062 
2063   float EVmax = CLAMP(log2f(white / (p->grey_point_source / 100.0f)), 1.0f, 16.0f);
2064   EVmax *= (1.0f + p->security_factor / 100.0f);
2065 
2066   p->white_point_source = EVmax;
2067   p->output_power = logf(p->grey_point_target / 100.0f)
2068                     / logf(-p->black_point_source / (p->white_point_source - p->black_point_source));
2069 
2070   ++darktable.gui->reset;
2071   dt_bauhaus_slider_set_soft(g->white_point_source, p->white_point_source);
2072   dt_bauhaus_slider_set_soft(g->output_power, p->output_power);
2073   --darktable.gui->reset;
2074 
2075   gtk_widget_queue_draw(self->widget);
2076   dt_dev_add_history_item(darktable.develop, self, TRUE);
2077 }
2078 
apply_autotune(dt_iop_module_t * self)2079 static void apply_autotune(dt_iop_module_t *self)
2080 {
2081   dt_iop_filmicrgb_gui_data_t *g = (dt_iop_filmicrgb_gui_data_t *)self->gui_data;
2082   dt_iop_filmicrgb_params_t *p = (dt_iop_filmicrgb_params_t *)self->params;
2083   const dt_iop_order_iccprofile_info_t *const work_profile
2084       = dt_ioppr_get_iop_work_profile_info(self, self->dev->iop);
2085 
2086   // Grey
2087   if(p->custom_grey)
2088   {
2089     const float grey = get_pixel_norm(self->picked_color, p->preserve_color, work_profile) / 2.0f;
2090     p->grey_point_source = CLAMP(100.f * grey, 0.001f, 100.0f);
2091   }
2092 
2093   // White
2094   const float white = get_pixel_norm(self->picked_color_max, DT_FILMIC_METHOD_MAX_RGB, work_profile);
2095   float EVmax = CLAMP(log2f(white / (p->grey_point_source / 100.0f)), 1.0f, 16.0f);
2096   EVmax *= (1.0f + p->security_factor / 100.0f);
2097 
2098   // Black
2099   const float black = get_pixel_norm(self->picked_color_min, DT_FILMIC_METHOD_MAX_RGB, work_profile);
2100   float EVmin = CLAMP(log2f(black / (p->grey_point_source / 100.0f)), -16.0f, -1.0f);
2101   EVmin *= (1.0f + p->security_factor / 100.0f);
2102 
2103   p->black_point_source = fmaxf(EVmin, -16.0f);
2104   p->white_point_source = EVmax;
2105   p->output_power = logf(p->grey_point_target / 100.0f)
2106                     / logf(-p->black_point_source / (p->white_point_source - p->black_point_source));
2107 
2108   ++darktable.gui->reset;
2109   dt_bauhaus_slider_set_soft(g->grey_point_source, p->grey_point_source);
2110   dt_bauhaus_slider_set_soft(g->black_point_source, p->black_point_source);
2111   dt_bauhaus_slider_set_soft(g->white_point_source, p->white_point_source);
2112   dt_bauhaus_slider_set_soft(g->output_power, p->output_power);
2113   --darktable.gui->reset;
2114 
2115   gtk_widget_queue_draw(self->widget);
2116   dt_dev_add_history_item(darktable.develop, self, TRUE);
2117 }
2118 
color_picker_apply(dt_iop_module_t * self,GtkWidget * picker,dt_dev_pixelpipe_iop_t * piece)2119 void color_picker_apply(dt_iop_module_t *self, GtkWidget *picker, dt_dev_pixelpipe_iop_t *piece)
2120 {
2121   dt_iop_filmicrgb_gui_data_t *g = (dt_iop_filmicrgb_gui_data_t *)self->gui_data;
2122 
2123   if(picker == g->grey_point_source)
2124     apply_auto_grey(self);
2125   else if(picker == g->black_point_source)
2126     apply_auto_black(self);
2127   else if(picker == g->white_point_source)
2128     apply_auto_white_point_source(self);
2129   else if(picker == g->auto_button)
2130     apply_autotune(self);
2131 }
2132 
show_mask_callback(GtkWidget * slider,gpointer user_data)2133 static void show_mask_callback(GtkWidget *slider, gpointer user_data)
2134 {
2135   dt_iop_module_t *self = (dt_iop_module_t *)user_data;
2136   if(darktable.gui->reset) return;
2137   gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(self->off), TRUE);
2138   dt_iop_filmicrgb_gui_data_t *g = (dt_iop_filmicrgb_gui_data_t *)self->gui_data;
2139   g->show_mask = !(g->show_mask);
2140   dt_bauhaus_widget_set_quad_active(g->show_highlight_mask, g->show_mask);
2141   dt_bauhaus_widget_set_quad_toggle(g->show_highlight_mask, g->show_mask);
2142   dt_dev_reprocess_center(self->dev);
2143 }
2144 
2145 
2146 #define ORDER_4 5
2147 #define ORDER_3 4
2148 
2149 
dt_iop_filmic_rgb_compute_spline(const dt_iop_filmicrgb_params_t * const p,struct dt_iop_filmic_rgb_spline_t * const spline)2150 inline static void dt_iop_filmic_rgb_compute_spline(const dt_iop_filmicrgb_params_t *const p,
2151                                                     struct dt_iop_filmic_rgb_spline_t *const spline)
2152 {
2153   float grey_display = 0.4638f;
2154 
2155   if(p->custom_grey)
2156   {
2157     // user set a custom value
2158     grey_display = powf(CLAMP(p->grey_point_target, p->black_point_target, p->white_point_target) / 100.0f,
2159                         1.0f / (p->output_power));
2160   }
2161   else
2162   {
2163     // use 18.45% grey and don't bother
2164     grey_display = powf(0.1845f, 1.0f / (p->output_power));
2165   }
2166 
2167   const float white_source = p->white_point_source;
2168   const float black_source = p->black_point_source;
2169   const float dynamic_range = white_source - black_source;
2170 
2171   // luminance after log encoding
2172   const float black_log = 0.0f; // assumes user set log as in the autotuner
2173   const float grey_log = fabsf(p->black_point_source) / dynamic_range;
2174   const float white_log = 1.0f; // assumes user set log as in the autotuner
2175 
2176   // target luminance desired after filmic curve
2177   float black_display, white_display;
2178 
2179   if(p->internal_version == 2019)
2180   {
2181     // this is a buggy version that doesn't take the output power function into account
2182     // it was silent because black and white display were set to 0 and 1 and users were advised to not touch them.
2183     // (since 0^x = 0 and 1^x = 1). It's not silent anymore if black display > 0,
2184     // for example if compensating ICC black level for target medium
2185     black_display = CLAMP(p->black_point_target, 0.0f, p->grey_point_target) / 100.0f; // in %
2186     white_display = fmaxf(p->white_point_target, p->grey_point_target) / 100.0f;       // in %
2187   }
2188   else //(p->internal_version == 2020)
2189   {
2190     // this is the fixed version
2191     black_display = powf(CLAMP(p->black_point_target, 0.0f, p->grey_point_target) / 100.0f,
2192                          1.0f / (p->output_power)); // in %
2193     white_display
2194         = powf(fmaxf(p->white_point_target, p->grey_point_target) / 100.0f, 1.0f / (p->output_power)); // in %
2195   }
2196 
2197   float latitude = CLAMP(p->latitude, 0.0f, 100.0f) / 100.0f * dynamic_range; // in % of dynamic range
2198   float balance = CLAMP(p->balance, -50.0f, 50.0f) / 100.0f;                  // in %
2199   float contrast = CLAMP(p->contrast, 1.00001f, 6.0f);
2200 
2201   // nodes for mapping from log encoding to desired target luminance
2202   // X coordinates
2203   float toe_log = grey_log - latitude / dynamic_range * fabsf(black_source / dynamic_range);
2204   float shoulder_log = grey_log + latitude / dynamic_range * fabsf(white_source / dynamic_range);
2205 
2206   // interception
2207   float linear_intercept = grey_display - (contrast * grey_log);
2208 
2209   // y coordinates
2210   float toe_display = (toe_log * contrast + linear_intercept);
2211   float shoulder_display = (shoulder_log * contrast + linear_intercept);
2212 
2213   // Apply the highlights/shadows balance as a shift along the contrast slope
2214   const float norm = sqrtf(contrast * contrast + 1.0f);
2215 
2216   // negative values drag to the left and compress the shadows, on the UI negative is the inverse
2217   const float coeff = -((2.0f * latitude) / dynamic_range) * balance;
2218 
2219   toe_display += coeff * contrast / norm;
2220   shoulder_display += coeff * contrast / norm;
2221   toe_log += coeff / norm;
2222   shoulder_log += coeff / norm;
2223 
2224   /**
2225    * Now we have 3 segments :
2226    *  - x = [0.0 ; toe_log], curved part
2227    *  - x = [toe_log ; grey_log ; shoulder_log], linear part
2228    *  - x = [shoulder_log ; 1.0] curved part
2229    *
2230    * BUT : in case some nodes overlap, we need to remove them to avoid
2231    * degenerating of the curve
2232    **/
2233 
2234   // Build the curve from the nodes
2235   spline->x[0] = black_log;
2236   spline->x[1] = toe_log;
2237   spline->x[2] = grey_log;
2238   spline->x[3] = shoulder_log;
2239   spline->x[4] = white_log;
2240 
2241   spline->y[0] = black_display;
2242   spline->y[1] = toe_display;
2243   spline->y[2] = grey_display;
2244   spline->y[3] = shoulder_display;
2245   spline->y[4] = white_display;
2246 
2247   spline->latitude_min = spline->x[1];
2248   spline->latitude_max = spline->x[3];
2249 
2250   spline->type[0] = p->shadows;
2251   spline->type[1] = p->highlights;
2252 
2253   /**
2254    * For background and details, see :
2255    * https://eng.aurelienpierre.com/2018/11/30/filmic-darktable-and-the-quest-of-the-hdr-tone-mapping/#filmic_s_curve
2256    *
2257    **/
2258   const double Tl = spline->x[1];
2259   const double Tl2 = Tl * Tl;
2260   const double Tl3 = Tl2 * Tl;
2261   const double Tl4 = Tl3 * Tl;
2262 
2263   const double Sl = spline->x[3];
2264   const double Sl2 = Sl * Sl;
2265   const double Sl3 = Sl2 * Sl;
2266   const double Sl4 = Sl3 * Sl;
2267 
2268   // if type polynomial :
2269   // y = M5 * x⁴ + M4 * x³ + M3 * x² + M2 * x¹ + M1 * x⁰
2270   // else if type rational :
2271   // y = M1 * (M2 * (x - x_0)² + (x - x_0)) / (M2 * (x - x_0)² + (x - x_0) + M3)
2272   // We then compute M1 to M5 coeffs using the imposed conditions over the curve.
2273   // M1 to M5 are 3×1 vectors, where each element belongs to a part of the curve.
2274 
2275   // solve the linear central part - affine function
2276   spline->M2[2] = contrast;                                    // * x¹ (slope)
2277   spline->M1[2] = spline->y[1] - spline->M2[2] * spline->x[1]; // * x⁰ (offset)
2278   spline->M3[2] = 0.f;                                         // * x²
2279   spline->M4[2] = 0.f;                                         // * x³
2280   spline->M5[2] = 0.f;                                         // * x⁴
2281 
2282   // solve the toe part
2283   if(p->shadows == DT_FILMIC_CURVE_POLY_4)
2284   {
2285     // fourth order polynom - only mode in darktable 3.0.0
2286     double A0[ORDER_4 * ORDER_4] = { 0.,        0.,       0.,      0., 1.,   // position in 0
2287                                      0.,        0.,       0.,      1., 0.,   // first derivative in 0
2288                                      Tl4,       Tl3,      Tl2,     Tl, 1.,   // position at toe node
2289                                      4. * Tl3,  3. * Tl2, 2. * Tl, 1., 0.,   // first derivative at toe node
2290                                      12. * Tl2, 6. * Tl,  2.,      0., 0. }; // second derivative at toe node
2291 
2292     double b0[ORDER_4] = { spline->y[0], 0., spline->y[1], spline->M2[2], 0. };
2293 
2294     gauss_solve(A0, b0, ORDER_4);
2295 
2296     spline->M5[0] = b0[0]; // * x⁴
2297     spline->M4[0] = b0[1]; // * x³
2298     spline->M3[0] = b0[2]; // * x²
2299     spline->M2[0] = b0[3]; // * x¹
2300     spline->M1[0] = b0[4]; // * x⁰
2301   }
2302   else if(p->shadows == DT_FILMIC_CURVE_POLY_3)
2303   {
2304     // third order polynom
2305     double A0[ORDER_3 * ORDER_3] = { 0.,       0.,      0., 1.,   // position in 0
2306                                      Tl3,      Tl2,     Tl, 1.,   // position at toe node
2307                                      3. * Tl2, 2. * Tl, 1., 0.,   // first derivative at toe node
2308                                      6. * Tl,  2.,      0., 0. }; // second derivative at toe node
2309 
2310     double b0[ORDER_3] = { spline->y[0], spline->y[1], spline->M2[2], 0. };
2311 
2312     gauss_solve(A0, b0, ORDER_3);
2313 
2314     spline->M5[0] = 0.0f;  // * x⁴
2315     spline->M4[0] = b0[0]; // * x³
2316     spline->M3[0] = b0[1]; // * x²
2317     spline->M2[0] = b0[2]; // * x¹
2318     spline->M1[0] = b0[3]; // * x⁰
2319   }
2320   else
2321   {
2322     const float P1[2] = { black_log, black_display };
2323     const float P0[2] = { toe_log, toe_display };
2324     const float x = P0[0] - P1[0];
2325     const float y = P0[1] - P1[1];
2326     const float g = contrast;
2327     const float b = g / (2.f * y) + (sqrtf(sqf(x * g / y + 1.f) - 4.f) - 1.f) / (2.f * x);
2328     const float c = y / g * (b * sqf(x) + x) / (b * sqf(x) + x - (y / g));
2329     const float a = c * g;
2330     spline->M1[0] = a;
2331     spline->M2[0] = b;
2332     spline->M3[0] = c;
2333     spline->M4[0] = toe_display;
2334   }
2335 
2336   // solve the shoulder part
2337   if(p->highlights == DT_FILMIC_CURVE_POLY_3)
2338   {
2339     // 3rd order polynom - only mode in darktable 3.0.0
2340     double A1[ORDER_3 * ORDER_3] = { 1.,       1.,      1., 1.,   // position in 1
2341                                      Sl3,      Sl2,     Sl, 1.,   // position at shoulder node
2342                                      3. * Sl2, 2. * Sl, 1., 0.,   // first derivative at shoulder node
2343                                      6. * Sl,  2.,      0., 0. }; // second derivative at shoulder node
2344 
2345     double b1[ORDER_3] = { spline->y[4], spline->y[3], spline->M2[2], 0. };
2346 
2347     gauss_solve(A1, b1, ORDER_3);
2348 
2349     spline->M5[1] = 0.0f;  // * x⁴
2350     spline->M4[1] = b1[0]; // * x³
2351     spline->M3[1] = b1[1]; // * x²
2352     spline->M2[1] = b1[2]; // * x¹
2353     spline->M1[1] = b1[3]; // * x⁰
2354   }
2355   else if(p->highlights == DT_FILMIC_CURVE_POLY_4)
2356   {
2357     // 4th order polynom
2358     double A1[ORDER_4 * ORDER_4] = { 1.,        1.,       1.,      1., 1.,   // position in 1
2359                                      4.,        3.,       2.,      1., 0.,   // first derivative in 1
2360                                      Sl4,       Sl3,      Sl2,     Sl, 1.,   // position at shoulder node
2361                                      4. * Sl3,  3. * Sl2, 2. * Sl, 1., 0.,   // first derivative at shoulder node
2362                                      12. * Sl2, 6. * Sl,  2.,      0., 0. }; // second derivative at shoulder node
2363 
2364     double b1[ORDER_4] = { spline->y[4], 0., spline->y[3], spline->M2[2], 0. };
2365 
2366     gauss_solve(A1, b1, ORDER_4);
2367 
2368     spline->M5[1] = b1[0]; // * x⁴
2369     spline->M4[1] = b1[1]; // * x³
2370     spline->M3[1] = b1[2]; // * x²
2371     spline->M2[1] = b1[3]; // * x¹
2372     spline->M1[1] = b1[4]; // * x⁰
2373   }
2374   else
2375   {
2376     const float P1[2] = { white_log, white_display };
2377     const float P0[2] = { shoulder_log, shoulder_display };
2378     const float x = P1[0] - P0[0];
2379     const float y = P1[1] - P0[1];
2380     const float g = contrast;
2381     const float b = g / (2.f * y) + (sqrtf(sqf(x * g / y + 1.f) - 4.f) - 1.f) / (2.f * x);
2382     const float c = y / g * (b * sqf(x) + x) / (b * sqf(x) + x - (y / g));
2383     const float a = c * g;
2384     spline->M1[1] = a;
2385     spline->M2[1] = b;
2386     spline->M3[1] = c;
2387     spline->M4[1] = shoulder_display;
2388   }
2389 }
2390 
commit_params(dt_iop_module_t * self,dt_iop_params_t * p1,dt_dev_pixelpipe_t * pipe,dt_dev_pixelpipe_iop_t * piece)2391 void commit_params(dt_iop_module_t *self, dt_iop_params_t *p1, dt_dev_pixelpipe_t *pipe,
2392                    dt_dev_pixelpipe_iop_t *piece)
2393 {
2394   dt_iop_filmicrgb_params_t *p = (dt_iop_filmicrgb_params_t *)p1;
2395   dt_iop_filmicrgb_data_t *d = (dt_iop_filmicrgb_data_t *)piece->data;
2396 
2397   // source and display greys
2398   float grey_source = 0.1845f, grey_display = 0.4638f;
2399   if(p->custom_grey)
2400   {
2401     // user set a custom value
2402     grey_source = p->grey_point_source / 100.0f; // in %
2403     grey_display = powf(p->grey_point_target / 100.0f, 1.0f / (p->output_power));
2404   }
2405   else
2406   {
2407     // use 18.45% grey and don't bother
2408     grey_source = 0.1845f; // in %
2409     grey_display = powf(0.1845f, 1.0f / (p->output_power));
2410   }
2411 
2412   // source luminance - Used only in the log encoding
2413   const float white_source = p->white_point_source;
2414   const float black_source = p->black_point_source;
2415   const float dynamic_range = white_source - black_source;
2416 
2417   // luminance after log encoding
2418   const float grey_log = fabsf(p->black_point_source) / dynamic_range;
2419 
2420 
2421   float contrast = p->contrast;
2422   if(contrast < grey_display / grey_log)
2423   {
2424     // We need grey_display - (contrast * grey_log) <= 0.0
2425     contrast = 1.0001f * grey_display / grey_log;
2426   }
2427 
2428   // commit
2429   d->dynamic_range = dynamic_range;
2430   d->black_source = black_source;
2431   d->grey_source = grey_source;
2432   d->output_power = p->output_power;
2433   d->contrast = contrast;
2434   d->version = p->version;
2435   d->preserve_color = p->preserve_color;
2436   d->high_quality_reconstruction = p->high_quality_reconstruction;
2437   d->noise_level = p->noise_level;
2438   d->noise_distribution = p->noise_distribution;
2439 
2440   // compute the curves and their LUT
2441   dt_iop_filmic_rgb_compute_spline(p, &d->spline);
2442 
2443   d->saturation = (2.0f * p->saturation / 100.0f + 1.0f);
2444   d->sigma_toe = powf(d->spline.latitude_min / 3.0f, 2.0f);
2445   d->sigma_shoulder = powf((1.0f - d->spline.latitude_max) / 3.0f, 2.0f);
2446 
2447   d->reconstruct_threshold = powf(2.0f, white_source + p->reconstruct_threshold) * grey_source;
2448   d->reconstruct_feather = exp2f(12.f / p->reconstruct_feather);
2449 
2450   // offset and rescale user param to alpha blending so 0 -> 50% and 1 -> 100%
2451   d->normalize = d->reconstruct_feather / d->reconstruct_threshold;
2452   d->reconstruct_structure_vs_texture = (p->reconstruct_structure_vs_texture / 100.0f + 1.f) / 2.f;
2453   d->reconstruct_bloom_vs_details = (p->reconstruct_bloom_vs_details / 100.0f + 1.f) / 2.f;
2454   d->reconstruct_grey_vs_color = (p->reconstruct_grey_vs_color / 100.0f + 1.f) / 2.f;
2455 }
2456 
gui_focus(struct dt_iop_module_t * self,gboolean in)2457 void gui_focus(struct dt_iop_module_t *self, gboolean in)
2458 {
2459   dt_iop_filmicrgb_gui_data_t *g = (dt_iop_filmicrgb_gui_data_t *)self->gui_data;
2460 
2461   if(!in)
2462   {
2463     // lost focus - hide the mask
2464     gint mask_was_shown = g->show_mask;
2465     g->show_mask = FALSE;
2466     dt_bauhaus_widget_set_quad_toggle(g->show_highlight_mask, FALSE);
2467     dt_bauhaus_widget_set_quad_active(g->show_highlight_mask, FALSE);
2468     if(mask_was_shown) dt_dev_reprocess_center(self->dev);
2469   }
2470 }
2471 
init_pipe(dt_iop_module_t * self,dt_dev_pixelpipe_t * pipe,dt_dev_pixelpipe_iop_t * piece)2472 void init_pipe(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece)
2473 {
2474   piece->data = calloc(1, sizeof(dt_iop_filmicrgb_data_t));
2475 }
2476 
cleanup_pipe(dt_iop_module_t * self,dt_dev_pixelpipe_t * pipe,dt_dev_pixelpipe_iop_t * piece)2477 void cleanup_pipe(dt_iop_module_t *self, dt_dev_pixelpipe_t *pipe, dt_dev_pixelpipe_iop_t *piece)
2478 {
2479   free(piece->data);
2480   piece->data = NULL;
2481 }
2482 
gui_update(dt_iop_module_t * self)2483 void gui_update(dt_iop_module_t *self)
2484 {
2485   dt_iop_filmicrgb_gui_data_t *g = (dt_iop_filmicrgb_gui_data_t *)self->gui_data;
2486   dt_iop_filmicrgb_params_t *p = (dt_iop_filmicrgb_params_t *)self->params;
2487 
2488   dt_iop_color_picker_reset(self, TRUE);
2489 
2490   g->show_mask = FALSE;
2491   g->gui_mode = dt_conf_get_int("plugins/darkroom/filmicrgb/graph_view");
2492   g->gui_show_labels = dt_conf_get_int("plugins/darkroom/filmicrgb/graph_show_labels");
2493   g->gui_hover = FALSE;
2494   g->gui_sizes_inited = FALSE;
2495 
2496   // fetch last view in dartablerc
2497 
2498 
2499   self->color_picker_box[0] = self->color_picker_box[1] = .25f;
2500   self->color_picker_box[2] = self->color_picker_box[3] = .50f;
2501   self->color_picker_point[0] = self->color_picker_point[1] = 0.5f;
2502 
2503   dt_bauhaus_slider_set_soft(g->white_point_source, p->white_point_source);
2504   dt_bauhaus_slider_set_soft(g->grey_point_source, p->grey_point_source);
2505   dt_bauhaus_slider_set_soft(g->black_point_source, p->black_point_source);
2506   dt_bauhaus_slider_set_soft(g->security_factor, p->security_factor);
2507   dt_bauhaus_slider_set_soft(g->reconstruct_threshold, p->reconstruct_threshold);
2508   dt_bauhaus_slider_set_soft(g->reconstruct_feather, p->reconstruct_feather);
2509   dt_bauhaus_slider_set_soft(g->reconstruct_bloom_vs_details, p->reconstruct_bloom_vs_details);
2510   dt_bauhaus_slider_set_soft(g->reconstruct_grey_vs_color, p->reconstruct_grey_vs_color);
2511   dt_bauhaus_slider_set_soft(g->reconstruct_structure_vs_texture, p->reconstruct_structure_vs_texture);
2512   dt_bauhaus_slider_set_soft(g->white_point_target, p->white_point_target);
2513   dt_bauhaus_slider_set_soft(g->grey_point_target, p->grey_point_target);
2514   dt_bauhaus_slider_set_soft(g->black_point_target, p->black_point_target);
2515   dt_bauhaus_slider_set_soft(g->output_power, p->output_power);
2516   dt_bauhaus_slider_set_soft(g->latitude, p->latitude);
2517   dt_bauhaus_slider_set_soft(g->contrast, p->contrast);
2518   dt_bauhaus_slider_set_soft(g->saturation, p->saturation);
2519   dt_bauhaus_slider_set_soft(g->balance, p->balance);
2520 
2521   dt_bauhaus_combobox_set_from_value(g->version, p->version);
2522   dt_bauhaus_combobox_set_from_value(g->preserve_color, p->preserve_color);
2523   dt_bauhaus_combobox_set_from_value(g->shadows, p->shadows);
2524   dt_bauhaus_combobox_set_from_value(g->highlights, p->highlights);
2525   gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->auto_hardness), p->auto_hardness);
2526   gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(g->custom_grey), p->custom_grey);
2527 
2528   dt_bauhaus_slider_set_soft(g->high_quality_reconstruction, p->high_quality_reconstruction);
2529   dt_bauhaus_slider_set_soft(g->noise_level, p->noise_level);
2530   dt_bauhaus_combobox_set(g->noise_distribution, p->noise_distribution);
2531 
2532   gui_changed(self, NULL, NULL);
2533 
2534   gtk_widget_queue_draw(self->widget);
2535 }
2536 
reload_defaults(dt_iop_module_t * module)2537 void reload_defaults(dt_iop_module_t *module)
2538 {
2539   dt_iop_filmicrgb_params_t *d = module->default_params;
2540 
2541   d->black_point_source = module->so->get_f("black_point_source")->Float.Default;
2542   d->white_point_source = module->so->get_f("white_point_source")->Float.Default;
2543   d->output_power = module->so->get_f("output_power")->Float.Default;
2544 
2545   module->default_enabled = FALSE;
2546 
2547   gchar *workflow = dt_conf_get_string("plugins/darkroom/workflow");
2548   const gboolean is_scene_referred = strcmp(workflow, "scene-referred") == 0;
2549   g_free(workflow);
2550 
2551   if(dt_image_is_matrix_correction_supported(&module->dev->image_storage) && is_scene_referred)
2552   {
2553     // For scene-referred workflow, auto-enable and adjust based on exposure
2554     // TODO: fetch actual exposure in module, don't assume 1.
2555     const float exposure = 0.5f - dt_image_get_exposure_bias(&module->dev->image_storage);
2556 
2557     // As global exposure increases, white exposure increases faster than black
2558     // this is probably because raw black/white points offsets the lower bound of the dynamic range to 0
2559     // so exposure compensation actually increases the dynamic range too (stretches only white).
2560     d->black_point_source += 0.5f * exposure;
2561     d->white_point_source += 0.8f * exposure;
2562     d->output_power = logf(d->grey_point_target / 100.0f)
2563                       / logf(-d->black_point_source / (d->white_point_source - d->black_point_source));
2564   }
2565 }
2566 
2567 
init_global(dt_iop_module_so_t * module)2568 void init_global(dt_iop_module_so_t *module)
2569 {
2570   const int program = 22; // filmic.cl, from programs.conf
2571   dt_iop_filmicrgb_global_data_t *gd
2572       = (dt_iop_filmicrgb_global_data_t *)malloc(sizeof(dt_iop_filmicrgb_global_data_t));
2573 
2574   module->data = gd;
2575   gd->kernel_filmic_rgb_split = dt_opencl_create_kernel(program, "filmicrgb_split");
2576   gd->kernel_filmic_rgb_chroma = dt_opencl_create_kernel(program, "filmicrgb_chroma");
2577   gd->kernel_filmic_mask = dt_opencl_create_kernel(program, "filmic_mask_clipped_pixels");
2578   gd->kernel_filmic_show_mask = dt_opencl_create_kernel(program, "filmic_show_mask");
2579   gd->kernel_filmic_inpaint_noise = dt_opencl_create_kernel(program, "filmic_inpaint_noise");
2580   gd->kernel_filmic_bspline_vertical = dt_opencl_create_kernel(program, "blur_2D_Bspline_vertical");
2581   gd->kernel_filmic_bspline_horizontal = dt_opencl_create_kernel(program, "blur_2D_Bspline_horizontal");
2582   gd->kernel_filmic_init_reconstruct = dt_opencl_create_kernel(program, "init_reconstruct");
2583   gd->kernel_filmic_wavelets_detail = dt_opencl_create_kernel(program, "wavelets_detail_level");
2584   gd->kernel_filmic_wavelets_reconstruct = dt_opencl_create_kernel(program, "wavelets_reconstruct");
2585   gd->kernel_filmic_compute_ratios = dt_opencl_create_kernel(program, "compute_ratios");
2586   gd->kernel_filmic_restore_ratios = dt_opencl_create_kernel(program, "restore_ratios");
2587 }
2588 
cleanup_global(dt_iop_module_so_t * module)2589 void cleanup_global(dt_iop_module_so_t *module)
2590 {
2591   dt_iop_filmicrgb_global_data_t *gd = (dt_iop_filmicrgb_global_data_t *)module->data;
2592   dt_opencl_free_kernel(gd->kernel_filmic_rgb_split);
2593   dt_opencl_free_kernel(gd->kernel_filmic_rgb_chroma);
2594   dt_opencl_free_kernel(gd->kernel_filmic_mask);
2595   dt_opencl_free_kernel(gd->kernel_filmic_show_mask);
2596   dt_opencl_free_kernel(gd->kernel_filmic_inpaint_noise);
2597   dt_opencl_free_kernel(gd->kernel_filmic_bspline_vertical);
2598   dt_opencl_free_kernel(gd->kernel_filmic_bspline_horizontal);
2599   dt_opencl_free_kernel(gd->kernel_filmic_init_reconstruct);
2600   dt_opencl_free_kernel(gd->kernel_filmic_wavelets_detail);
2601   dt_opencl_free_kernel(gd->kernel_filmic_wavelets_reconstruct);
2602   dt_opencl_free_kernel(gd->kernel_filmic_compute_ratios);
2603   dt_opencl_free_kernel(gd->kernel_filmic_restore_ratios);
2604   free(module->data);
2605   module->data = NULL;
2606 }
2607 
2608 
gui_reset(dt_iop_module_t * self)2609 void gui_reset(dt_iop_module_t *self)
2610 {
2611   dt_iop_color_picker_reset(self, TRUE);
2612 }
2613 
2614 #define LOGBASE 20.f
2615 
dt_cairo_draw_arrow(cairo_t * cr,double origin_x,double origin_y,double destination_x,double destination_y,gboolean show_head)2616 static inline void dt_cairo_draw_arrow(cairo_t *cr, double origin_x, double origin_y, double destination_x,
2617                                        double destination_y, gboolean show_head)
2618 {
2619   cairo_move_to(cr, origin_x, origin_y);
2620   cairo_line_to(cr, destination_x, destination_y);
2621   cairo_stroke(cr);
2622 
2623   if(show_head)
2624   {
2625     // arrow head is hard set to 45° - convert to radians
2626     const float angle_arrow = 45.f / 360.f * M_PI;
2627     const float angle_trunk = atan2f((destination_y - origin_y), (destination_x - origin_x));
2628     const float radius = DT_PIXEL_APPLY_DPI(3);
2629 
2630     const float x_1 = destination_x + radius / sinf(angle_arrow + angle_trunk);
2631     const float y_1 = destination_y + radius / cosf(angle_arrow + angle_trunk);
2632 
2633     const float x_2 = destination_x - radius / sinf(-angle_arrow + angle_trunk);
2634     const float y_2 = destination_y - radius / cosf(-angle_arrow + angle_trunk);
2635 
2636     cairo_move_to(cr, x_1, y_1);
2637     cairo_line_to(cr, destination_x, destination_y);
2638     cairo_line_to(cr, x_2, y_2);
2639     cairo_stroke(cr);
2640   }
2641 }
2642 
filmic_gui_draw_icon(cairo_t * cr,struct dt_iop_filmicrgb_gui_button_data_t * button,struct dt_iop_filmicrgb_gui_data_t * g)2643 void filmic_gui_draw_icon(cairo_t *cr, struct dt_iop_filmicrgb_gui_button_data_t *button,
2644                           struct dt_iop_filmicrgb_gui_data_t *g)
2645 {
2646   if(!g->gui_sizes_inited) return;
2647 
2648   cairo_save(cr);
2649 
2650   GdkRGBA color;
2651 
2652   // copy color
2653   color.red = darktable.bauhaus->graph_fg.red;
2654   color.green = darktable.bauhaus->graph_fg.green;
2655   color.blue = darktable.bauhaus->graph_fg.blue;
2656   color.alpha = darktable.bauhaus->graph_fg.alpha;
2657 
2658   if(button->mouse_hover)
2659   {
2660     // use graph_fg color as-is if mouse hover
2661     cairo_set_source_rgba(cr, color.red, color.green, color.blue, color.alpha);
2662   }
2663   else
2664   {
2665     // use graph_fg color with transparency else
2666     cairo_set_source_rgba(cr, color.red, color.green, color.blue, color.alpha * 0.5);
2667   }
2668 
2669   cairo_rectangle(cr, button->left, button->top, button->w - DT_PIXEL_APPLY_DPI(0.5),
2670                   button->h - DT_PIXEL_APPLY_DPI(0.5));
2671   cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(1.));
2672   cairo_stroke(cr);
2673   cairo_translate(cr, button->left + button->w / 2. - DT_PIXEL_APPLY_DPI(0.25),
2674                   button->top + button->h / 2. - DT_PIXEL_APPLY_DPI(0.25));
2675 
2676   const float scale = 0.85;
2677   cairo_scale(cr, scale, scale);
2678   button->icon(cr, -scale * button->w / 2., -scale * button->h / 2., scale * button->w, scale * button->h,
2679                CPF_STYLE_FLAT | CPF_DO_NOT_USE_BORDER, NULL);
2680   cairo_restore(cr);
2681 }
2682 
2683 
dt_iop_tonecurve_draw(GtkWidget * widget,cairo_t * crf,gpointer user_data)2684 static gboolean dt_iop_tonecurve_draw(GtkWidget *widget, cairo_t *crf, gpointer user_data)
2685 {
2686   dt_iop_module_t *self = (dt_iop_module_t *)user_data;
2687   dt_iop_filmicrgb_params_t *p = (dt_iop_filmicrgb_params_t *)self->params;
2688   dt_iop_filmicrgb_gui_data_t *g = (dt_iop_filmicrgb_gui_data_t *)self->gui_data;
2689   dt_iop_filmic_rgb_compute_spline(p, &g->spline);
2690 
2691   // Cache the graph objects to avoid recomputing all the view at each redraw
2692   gtk_widget_get_allocation(widget, &g->allocation);
2693 
2694   cairo_surface_t *cst =
2695     dt_cairo_image_surface_create(CAIRO_FORMAT_ARGB32, g->allocation.width, g->allocation.height);
2696   PangoFontDescription *desc =
2697     pango_font_description_copy_static(darktable.bauhaus->pango_font_desc);
2698   cairo_t *cr = cairo_create(cst);
2699   PangoLayout *layout = pango_cairo_create_layout(cr);
2700 
2701   pango_layout_set_font_description(layout, desc);
2702   pango_cairo_context_set_resolution(pango_layout_get_context(layout), darktable.gui->dpi);
2703   g->context = gtk_widget_get_style_context(widget);
2704 
2705   char text[256];
2706 
2707   // reduce a bit the font size
2708   const gint font_size = pango_font_description_get_size(desc);
2709   pango_font_description_set_size(desc, 0.95 * font_size);
2710   pango_layout_set_font_description(layout, desc);
2711 
2712   // Get the text line height for spacing
2713   g_strlcpy(text, "X", sizeof(text));
2714   pango_layout_set_text(layout, text, -1);
2715   pango_layout_get_pixel_extents(layout, &g->ink, NULL);
2716   g->line_height = g->ink.height;
2717 
2718   // Get the width of a minus sign for legend labels spacing
2719   g_strlcpy(text, "-", sizeof(text));
2720   pango_layout_set_text(layout, text, -1);
2721   pango_layout_get_pixel_extents(layout, &g->ink, NULL);
2722   g->sign_width = g->ink.width / 2.0;
2723 
2724   // Get the width of a zero for legend labels spacing
2725   g_strlcpy(text, "0", sizeof(text));
2726   pango_layout_set_text(layout, text, -1);
2727   pango_layout_get_pixel_extents(layout, &g->ink, NULL);
2728   g->zero_width = g->ink.width;
2729 
2730   // Set the sizes, margins and paddings
2731   g->inner_padding = DT_PIXEL_APPLY_DPI(4); // TODO: INNER_PADDING value as defined in bauhaus.c macros, sync them
2732   g->inset = g->inner_padding;
2733 
2734   float margin_left;
2735   float margin_bottom;
2736   if(g->gui_show_labels)
2737   {
2738     // leave room for labels
2739     margin_left = 3. * g->zero_width + 2. * g->inset;
2740     margin_bottom = 2. * g->line_height + 4. * g->inset;
2741   }
2742   else
2743   {
2744     margin_left = g->inset;
2745     margin_bottom = g->inset;
2746   }
2747 
2748   const float margin_top = 2. * g->line_height + g->inset;
2749   const float margin_right = darktable.bauhaus->quad_width + 2. * g->inset;
2750 
2751   g->graph_width = g->allocation.width - margin_right - margin_left;   // align the right border on sliders
2752   g->graph_height = g->allocation.height - margin_bottom - margin_top; // give room to nodes
2753 
2754   gtk_render_background(g->context, cr, 0, 0, g->allocation.width, g->allocation.height);
2755 
2756   // Init icons bounds and cache them for mouse events
2757   for(int i = 0; i < DT_FILMIC_GUI_BUTTON_LAST; i++)
2758   {
2759     // put the buttons in the right margin and increment vertical position
2760     g->buttons[i].right = g->allocation.width;
2761     g->buttons[i].left = g->buttons[i].right - darktable.bauhaus->quad_width;
2762     g->buttons[i].top = margin_top + i * (g->inset + darktable.bauhaus->quad_width);
2763     g->buttons[i].bottom = g->buttons[i].top + darktable.bauhaus->quad_width;
2764     g->buttons[i].w = g->buttons[i].right - g->buttons[i].left;
2765     g->buttons[i].h = g->buttons[i].bottom - g->buttons[i].top;
2766     g->buttons[i].state = GTK_STATE_FLAG_NORMAL;
2767   }
2768 
2769   g->gui_sizes_inited = TRUE;
2770 
2771   g->buttons[0].icon = dtgtk_cairo_paint_refresh;
2772   g->buttons[1].icon = dtgtk_cairo_paint_text_label;
2773 
2774   if(g->gui_hover)
2775   {
2776     for(int i = 0; i < DT_FILMIC_GUI_BUTTON_LAST; i++) filmic_gui_draw_icon(cr, &g->buttons[i], g);
2777   }
2778 
2779   const float grey = p->grey_point_source / 100.f;
2780   const float DR = p->white_point_source - p->black_point_source;
2781 
2782   // set the graph as the origin of the coordinates
2783   cairo_translate(cr, margin_left, margin_top);
2784 
2785   cairo_set_line_cap(cr, CAIRO_LINE_CAP_ROUND);
2786 
2787   // write the graph legend at GUI default size
2788   pango_font_description_set_size(desc, font_size);
2789   pango_layout_set_font_description(layout, desc);
2790   if(g->gui_mode == DT_FILMIC_GUI_LOOK)
2791     g_strlcpy(text, _("look only"), sizeof(text));
2792   else if(g->gui_mode == DT_FILMIC_GUI_BASECURVE)
2793     g_strlcpy(text, _("look + mapping (lin)"), sizeof(text));
2794   else if(g->gui_mode == DT_FILMIC_GUI_BASECURVE_LOG)
2795     g_strlcpy(text, _("look + mapping (log)"), sizeof(text));
2796   else if(g->gui_mode == DT_FILMIC_GUI_RANGES)
2797     g_strlcpy(text, _("dynamic range mapping"), sizeof(text));
2798 
2799   pango_layout_set_text(layout, text, -1);
2800   pango_layout_get_pixel_extents(layout, &g->ink, NULL);
2801 
2802   // legend background
2803   set_color(cr, darktable.bauhaus->graph_bg);
2804   cairo_rectangle(cr, g->allocation.width - margin_left - g->ink.width - g->ink.x - 2. * g->inset,
2805                   -g->line_height - g->inset - 0.5 * g->ink.height - g->ink.y - g->inset,
2806                   g->ink.width + 3. * g->inset, g->ink.height + 2. * g->inset);
2807   cairo_fill(cr);
2808 
2809   // legend text
2810   set_color(cr, darktable.bauhaus->graph_fg);
2811   cairo_move_to(cr, g->allocation.width - margin_left - g->ink.width - g->ink.x - g->inset,
2812                 -g->line_height - g->inset - 0.5 * g->ink.height - g->ink.y);
2813   pango_cairo_show_layout(cr, layout);
2814   cairo_stroke(cr);
2815 
2816   // reduce font size for the rest of the graph
2817   pango_font_description_set_size(desc, 0.95 * font_size);
2818   pango_layout_set_font_description(layout, desc);
2819 
2820   if(g->gui_mode != DT_FILMIC_GUI_RANGES)
2821   {
2822     // Draw graph background then border
2823     cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(0.5));
2824     cairo_rectangle(cr, 0, 0, g->graph_width, g->graph_height);
2825     set_color(cr, darktable.bauhaus->graph_bg);
2826     cairo_fill_preserve(cr);
2827     set_color(cr, darktable.bauhaus->graph_border);
2828     cairo_stroke(cr);
2829 
2830     // draw grid
2831     cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(0.5));
2832     set_color(cr, darktable.bauhaus->graph_border);
2833 
2834     // we need to tweak the coordinates system to match dt_draw_grid expectations
2835     cairo_save(cr);
2836     cairo_scale(cr, 1., -1.);
2837     cairo_translate(cr, 0., -g->graph_height);
2838 
2839     if(g->gui_mode == DT_FILMIC_GUI_LOOK || g->gui_mode == DT_FILMIC_GUI_BASECURVE)
2840       dt_draw_grid(cr, 4, 0, 0, g->graph_width, g->graph_height);
2841     else if(g->gui_mode == DT_FILMIC_GUI_BASECURVE_LOG)
2842       dt_draw_loglog_grid(cr, 4, 0, 0, g->graph_width, g->graph_height, LOGBASE);
2843 
2844     // reset coordinates
2845     cairo_restore(cr);
2846 
2847     // draw identity line
2848     cairo_move_to(cr, 0, g->graph_height);
2849     cairo_line_to(cr, g->graph_width, 0);
2850     cairo_stroke(cr);
2851 
2852     cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(2.));
2853 
2854     // Draw the saturation curve
2855     const float saturation = (2.0f * p->saturation / 100.0f + 1.0f);
2856     const float sigma_toe = powf(g->spline.latitude_min / 3.0f, 2.0f);
2857     const float sigma_shoulder = powf((1.0f - g->spline.latitude_max) / 3.0f, 2.0f);
2858 
2859     cairo_set_source_rgb(cr, .5, .5, .5);
2860 
2861     // prevent graph overflowing
2862     cairo_save(cr);
2863     cairo_rectangle(cr, -DT_PIXEL_APPLY_DPI(2.), -DT_PIXEL_APPLY_DPI(2.),
2864                     g->graph_width + 2. * DT_PIXEL_APPLY_DPI(2.), g->graph_height + 2. * DT_PIXEL_APPLY_DPI(2.));
2865     cairo_clip(cr);
2866 
2867     if(p->version == DT_FILMIC_COLORSCIENCE_V1)
2868     {
2869       cairo_move_to(cr, 0,
2870                     g->graph_height * (1.0 - filmic_desaturate_v1(0.0f, sigma_toe, sigma_shoulder, saturation)));
2871       for(int k = 1; k < 256; k++)
2872       {
2873         float x = k / 255.0;
2874         const float y = filmic_desaturate_v1(x, sigma_toe, sigma_shoulder, saturation);
2875 
2876         if(g->gui_mode == DT_FILMIC_GUI_BASECURVE)
2877           x = exp_tonemapping_v2(x, grey, p->black_point_source, DR);
2878         else if(g->gui_mode == DT_FILMIC_GUI_BASECURVE_LOG)
2879           x = dt_log_scale_axis(exp_tonemapping_v2(x, grey, p->black_point_source, DR), LOGBASE);
2880 
2881         cairo_line_to(cr, x * g->graph_width, g->graph_height * (1.0 - y));
2882       }
2883     }
2884     else if(p->version == DT_FILMIC_COLORSCIENCE_V2 || p->version == DT_FILMIC_COLORSCIENCE_V3)
2885     {
2886       cairo_move_to(cr, 0,
2887                     g->graph_height * (1.0 - filmic_desaturate_v2(0.0f, sigma_toe, sigma_shoulder, saturation)));
2888       for(int k = 1; k < 256; k++)
2889       {
2890         float x = k / 255.0;
2891         const float y = filmic_desaturate_v2(x, sigma_toe, sigma_shoulder, saturation);
2892 
2893         if(g->gui_mode == DT_FILMIC_GUI_BASECURVE)
2894           x = exp_tonemapping_v2(x, grey, p->black_point_source, DR);
2895         else if(g->gui_mode == DT_FILMIC_GUI_BASECURVE_LOG)
2896           x = dt_log_scale_axis(exp_tonemapping_v2(x, grey, p->black_point_source, DR), LOGBASE);
2897 
2898         cairo_line_to(cr, x * g->graph_width, g->graph_height * (1.0 - y));
2899       }
2900     }
2901     cairo_stroke(cr);
2902 
2903     // draw the tone curve
2904     float x_start = 0.f;
2905     if(g->gui_mode == DT_FILMIC_GUI_BASECURVE || g->gui_mode == DT_FILMIC_GUI_BASECURVE_LOG)
2906       x_start = log_tonemapping_v2(x_start, grey, p->black_point_source, DR);
2907 
2908     if(g->gui_mode == DT_FILMIC_GUI_BASECURVE_LOG) x_start = dt_log_scale_axis(x_start, LOGBASE);
2909 
2910     float y_start = clamp_simd(filmic_spline(x_start, g->spline.M1, g->spline.M2, g->spline.M3, g->spline.M4,
2911                                              g->spline.M5, g->spline.latitude_min, g->spline.latitude_max, g->spline.type));
2912 
2913     if(g->gui_mode == DT_FILMIC_GUI_BASECURVE)
2914       y_start = powf(y_start, p->output_power);
2915     else if(g->gui_mode == DT_FILMIC_GUI_BASECURVE_LOG)
2916       y_start = dt_log_scale_axis(powf(y_start, p->output_power), LOGBASE);
2917 
2918     cairo_move_to(cr, 0, g->graph_height * (1.0 - y_start));
2919 
2920     for(int k = 1; k < 256; k++)
2921     {
2922       // k / 255 step defines a linearly scaled space. This might produce large gaps in lowlights when using log
2923       // GUI scaling so we non-linearly rescale that step to get more points in lowlights
2924       float x = powf(k / 255.0f, 2.4f);
2925       float value = x;
2926 
2927       if(g->gui_mode == DT_FILMIC_GUI_BASECURVE || g->gui_mode == DT_FILMIC_GUI_BASECURVE_LOG)
2928         value = log_tonemapping_v2(x, grey, p->black_point_source, DR);
2929 
2930       if(g->gui_mode == DT_FILMIC_GUI_BASECURVE_LOG) x = dt_log_scale_axis(x, LOGBASE);
2931 
2932       float y = filmic_spline(value, g->spline.M1, g->spline.M2, g->spline.M3, g->spline.M4, g->spline.M5,
2933                               g->spline.latitude_min, g->spline.latitude_max, g->spline.type);
2934 
2935       if(y > g->spline.y[4])
2936       {
2937         y = fminf(y, 1.0f);
2938         cairo_set_source_rgb(cr, 0.75, .5, 0.);
2939       }
2940       else if(y < g->spline.y[0])
2941       {
2942         y = fmaxf(y, 0.f);
2943         cairo_set_source_rgb(cr, 0.75, .5, 0.);
2944       }
2945       else
2946       {
2947         set_color(cr, darktable.bauhaus->graph_fg);
2948       }
2949 
2950       if(g->gui_mode == DT_FILMIC_GUI_BASECURVE)
2951         y = powf(y, p->output_power);
2952       else if(g->gui_mode == DT_FILMIC_GUI_BASECURVE_LOG)
2953         y = dt_log_scale_axis(powf(y, p->output_power), LOGBASE);
2954 
2955       cairo_line_to(cr, x * g->graph_width, g->graph_height * (1.0 - y));
2956       cairo_stroke(cr);
2957       cairo_move_to(cr, x * g->graph_width, g->graph_height * (1.0 - y));
2958     }
2959 
2960     cairo_restore(cr);
2961 
2962     // draw nodes
2963 
2964     // special case for the grey node
2965     cairo_save(cr);
2966     cairo_rectangle(cr, -DT_PIXEL_APPLY_DPI(4.), -DT_PIXEL_APPLY_DPI(4.),
2967                     g->graph_width + 2. * DT_PIXEL_APPLY_DPI(4.), g->graph_height + 2. * DT_PIXEL_APPLY_DPI(4.));
2968     cairo_clip(cr);
2969     float x_grey = g->spline.x[2];
2970     float y_grey = g->spline.y[2];
2971 
2972     if(g->gui_mode == DT_FILMIC_GUI_BASECURVE)
2973     {
2974       x_grey = exp_tonemapping_v2(x_grey, grey, p->black_point_source, DR);
2975       y_grey = powf(y_grey, p->output_power);
2976     }
2977     else if(g->gui_mode == DT_FILMIC_GUI_BASECURVE_LOG)
2978     {
2979       x_grey = dt_log_scale_axis(exp_tonemapping_v2(x_grey, grey, p->black_point_source, DR), LOGBASE);
2980       y_grey = dt_log_scale_axis(powf(y_grey, p->output_power), LOGBASE);
2981     }
2982 
2983     cairo_set_source_rgb(cr, 0.75, 0.5, 0.0);
2984     cairo_arc(cr, x_grey * g->graph_width, (1.0 - y_grey) * g->graph_height, DT_PIXEL_APPLY_DPI(6), 0,
2985               2. * M_PI);
2986     cairo_fill(cr);
2987     cairo_stroke(cr);
2988 
2989     // latitude nodes
2990     float x_black = 0.f;
2991     float y_black = 0.f;
2992 
2993     float x_white = 1.f;
2994     float y_white = 1.f;
2995 
2996     set_color(cr, darktable.bauhaus->graph_fg);
2997     for(int k = 0; k < 5; k++)
2998     {
2999       if(k != 2) // k == 2 : grey point, already processed above
3000       {
3001         float x = g->spline.x[k];
3002         float y = g->spline.y[k];
3003 
3004         if(g->gui_mode == DT_FILMIC_GUI_BASECURVE)
3005         {
3006           x = exp_tonemapping_v2(x, grey, p->black_point_source, DR);
3007           y = powf(y, p->output_power);
3008         }
3009         else if(g->gui_mode == DT_FILMIC_GUI_BASECURVE_LOG)
3010         {
3011           x = dt_log_scale_axis(exp_tonemapping_v2(x, grey, p->black_point_source, DR), LOGBASE);
3012           y = dt_log_scale_axis(powf(y, p->output_power), LOGBASE);
3013         }
3014 
3015         // save the bounds of the curve to mark the axis graduation
3016         if(k == 0) // black point
3017         {
3018           x_black = x;
3019           y_black = y;
3020         }
3021         else if(k == 4) // white point
3022         {
3023           x_white = x;
3024           y_white = y;
3025         }
3026 
3027         // draw bullet
3028         cairo_arc(cr, x * g->graph_width, (1.0 - y) * g->graph_height, DT_PIXEL_APPLY_DPI(4), 0, 2. * M_PI);
3029         cairo_fill(cr);
3030         cairo_stroke(cr);
3031       }
3032     }
3033     cairo_restore(cr);
3034 
3035     if(g->gui_show_labels)
3036     {
3037       // position of the upper bound of x axis labels
3038       const float x_legend_top = g->graph_height + 0.5 * g->line_height;
3039 
3040       // mark the y axis graduation at grey spot
3041       set_color(cr, darktable.bauhaus->graph_fg);
3042       snprintf(text, sizeof(text), "%.0f", p->grey_point_target);
3043       pango_layout_set_text(layout, text, -1);
3044       pango_layout_get_pixel_extents(layout, &g->ink, NULL);
3045       cairo_move_to(cr, -2. * g->inset - g->ink.width - g->ink.x,
3046                     (1.0 - y_grey) * g->graph_height - 0.5 * g->ink.height - g->ink.y);
3047       pango_cairo_show_layout(cr, layout);
3048       cairo_stroke(cr);
3049 
3050       // mark the x axis graduation at grey spot
3051       set_color(cr, darktable.bauhaus->graph_fg);
3052       if(g->gui_mode == DT_FILMIC_GUI_LOOK)
3053         snprintf(text, sizeof(text), "%+.1f", 0.f);
3054       else if(g->gui_mode == DT_FILMIC_GUI_BASECURVE || g->gui_mode == DT_FILMIC_GUI_BASECURVE_LOG)
3055         snprintf(text, sizeof(text), "%.0f", p->grey_point_source);
3056 
3057       pango_layout_set_text(layout, text, -1);
3058       pango_layout_get_pixel_extents(layout, &g->ink, NULL);
3059       cairo_move_to(cr, x_grey * g->graph_width - 0.5 * g->ink.width - g->ink.x, x_legend_top);
3060       pango_cairo_show_layout(cr, layout);
3061       cairo_stroke(cr);
3062 
3063       // mark the y axis graduation at black spot
3064       set_color(cr, darktable.bauhaus->graph_fg);
3065       snprintf(text, sizeof(text), "%.0f", p->black_point_target);
3066       pango_layout_set_text(layout, text, -1);
3067       pango_layout_get_pixel_extents(layout, &g->ink, NULL);
3068       cairo_move_to(cr, -2. * g->inset - g->ink.width - g->ink.x,
3069                     (1.0 - y_black) * g->graph_height - 0.5 * g->ink.height - g->ink.y);
3070       pango_cairo_show_layout(cr, layout);
3071       cairo_stroke(cr);
3072 
3073       // mark the y axis graduation at black spot
3074       set_color(cr, darktable.bauhaus->graph_fg);
3075       snprintf(text, sizeof(text), "%.0f", p->white_point_target);
3076       pango_layout_set_text(layout, text, -1);
3077       pango_layout_get_pixel_extents(layout, &g->ink, NULL);
3078       cairo_move_to(cr, -2. * g->inset - g->ink.width - g->ink.x,
3079                     (1.0 - y_white) * g->graph_height - 0.5 * g->ink.height - g->ink.y);
3080       pango_cairo_show_layout(cr, layout);
3081       cairo_stroke(cr);
3082 
3083       // mark the x axis graduation at black spot
3084       set_color(cr, darktable.bauhaus->graph_fg);
3085       if(g->gui_mode == DT_FILMIC_GUI_LOOK)
3086         snprintf(text, sizeof(text), "%+.1f", p->black_point_source);
3087       else if(g->gui_mode == DT_FILMIC_GUI_BASECURVE || g->gui_mode == DT_FILMIC_GUI_BASECURVE_LOG)
3088         snprintf(text, sizeof(text), "%.0f", exp2f(p->black_point_source) * p->grey_point_source);
3089 
3090       pango_layout_set_text(layout, text, -1);
3091       pango_layout_get_pixel_extents(layout, &g->ink, NULL);
3092       cairo_move_to(cr, x_black * g->graph_width - 0.5 * g->ink.width - g->ink.x, x_legend_top);
3093       pango_cairo_show_layout(cr, layout);
3094       cairo_stroke(cr);
3095 
3096       // mark the x axis graduation at white spot
3097       set_color(cr, darktable.bauhaus->graph_fg);
3098       if(g->gui_mode == DT_FILMIC_GUI_LOOK)
3099         snprintf(text, sizeof(text), "%+.1f", p->white_point_source);
3100       else if(g->gui_mode == DT_FILMIC_GUI_BASECURVE || g->gui_mode == DT_FILMIC_GUI_BASECURVE_LOG)
3101       {
3102         if(x_white > 1.f)
3103           snprintf(text, sizeof(text), "%.0f →", 100.f); // this marks the bound of the graph, not the actual white
3104         else
3105           snprintf(text, sizeof(text), "%.0f", exp2f(p->white_point_source) * p->grey_point_source);
3106       }
3107 
3108       pango_layout_set_text(layout, text, -1);
3109       pango_layout_get_pixel_extents(layout, &g->ink, NULL);
3110       cairo_move_to(cr,
3111                     fminf(x_white, 1.f) * g->graph_width - 0.5 * g->ink.width - g->ink.x
3112                         + 2. * (x_white > 1.f) * g->sign_width,
3113                     x_legend_top);
3114       pango_cairo_show_layout(cr, layout);
3115       cairo_stroke(cr);
3116 
3117       // handle the case where white > 100 %, so the node is out of the graph.
3118       // we still want to display the value to get a hint.
3119       set_color(cr, darktable.bauhaus->graph_fg);
3120       if((g->gui_mode == DT_FILMIC_GUI_BASECURVE || g->gui_mode == DT_FILMIC_GUI_BASECURVE_LOG) && (x_white > 1.f))
3121       {
3122         // set to italic font
3123         PangoStyle backup = pango_font_description_get_style(desc);
3124         pango_font_description_set_style(desc, PANGO_STYLE_ITALIC);
3125         pango_layout_set_font_description(layout, desc);
3126 
3127         snprintf(text, sizeof(text), _("(%.0f %%)"), exp2f(p->white_point_source) * p->grey_point_source);
3128         pango_layout_set_text(layout, text, -1);
3129         pango_layout_get_pixel_extents(layout, &g->ink, NULL);
3130         cairo_move_to(cr, g->allocation.width - g->ink.width - g->ink.x - margin_left,
3131                       g->graph_height + 3. * g->inset + g->line_height - g->ink.y);
3132         pango_cairo_show_layout(cr, layout);
3133         cairo_stroke(cr);
3134 
3135         // restore font
3136         pango_font_description_set_style(desc, backup);
3137         pango_layout_set_font_description(layout, desc);
3138       }
3139 
3140       // mark the y axis legend
3141       set_color(cr, darktable.bauhaus->graph_fg);
3142       /* xgettext:no-c-format */
3143       g_strlcpy(text, _("% display"), sizeof(text));
3144       pango_layout_set_text(layout, text, -1);
3145       pango_layout_get_pixel_extents(layout, &g->ink, NULL);
3146       cairo_move_to(cr, -2. * g->inset - g->zero_width - g->ink.x,
3147                     -g->line_height - g->inset - 0.5 * g->ink.height - g->ink.y);
3148       pango_cairo_show_layout(cr, layout);
3149       cairo_stroke(cr);
3150 
3151       // mark the x axis legend
3152       set_color(cr, darktable.bauhaus->graph_fg);
3153       if(g->gui_mode == DT_FILMIC_GUI_LOOK)
3154         g_strlcpy(text, _("EV scene"), sizeof(text));
3155       else if(g->gui_mode == DT_FILMIC_GUI_BASECURVE || g->gui_mode == DT_FILMIC_GUI_BASECURVE_LOG)
3156       {
3157         /* xgettext:no-c-format */
3158         g_strlcpy(text, _("% camera"), sizeof(text));
3159       }
3160       pango_layout_set_text(layout, text, -1);
3161       pango_layout_get_pixel_extents(layout, &g->ink, NULL);
3162       cairo_move_to(cr, 0.5 * g->graph_width - 0.5 * g->ink.width - g->ink.x,
3163                     g->graph_height + 3. * g->inset + g->line_height - g->ink.y);
3164       pango_cairo_show_layout(cr, layout);
3165       cairo_stroke(cr);
3166     }
3167   }
3168   else
3169   {
3170     // mode ranges
3171     cairo_identity_matrix(cr); // reset coordinates
3172 
3173     // draw the dynamic range of display
3174     // if white = 100%, assume -11.69 EV because of uint8 output + sRGB OETF.
3175     // for uint10 output, white should be set to 400%, so anything above 100% increases DR
3176     // FIXME : if darktable becomes HDR-10bits compatible (for output), this needs to be updated
3177     const float display_DR = 12.f + log2f(p->white_point_target / 100.f);
3178 
3179     const float y_display = g->allocation.height / 3.f + g->line_height;
3180     const float y_scene = 2. * g->allocation.height / 3.f + g->line_height;
3181 
3182     const float display_top = y_display - g->line_height / 2;
3183     const float display_bottom = display_top + g->line_height;
3184 
3185     const float scene_top = y_scene - g->line_height / 2;
3186     const float scene_bottom = scene_top + g->line_height;
3187 
3188     float column_left;
3189 
3190     if(g->gui_show_labels)
3191     {
3192       // labels
3193       set_color(cr, darktable.bauhaus->graph_fg);
3194       g_strlcpy(text, _("display"), sizeof(text));
3195       pango_layout_set_text(layout, text, -1);
3196       pango_layout_get_pixel_extents(layout, &g->ink, NULL);
3197       cairo_move_to(cr, 0., y_display - 0.5 * g->ink.height - g->ink.y);
3198       pango_cairo_show_layout(cr, layout);
3199       cairo_stroke(cr);
3200       const float display_label_width = g->ink.width;
3201 
3202       // axis legend
3203       g_strlcpy(text, _("(%)"), sizeof(text));
3204       pango_layout_set_text(layout, text, -1);
3205       pango_layout_get_pixel_extents(layout, &g->ink, NULL);
3206       cairo_move_to(cr, 0.5 * display_label_width - 0.5 * g->ink.width - g->ink.x,
3207                     display_top - 4. * g->inset - g->ink.height - g->ink.y);
3208       pango_cairo_show_layout(cr, layout);
3209       cairo_stroke(cr);
3210 
3211       set_color(cr, darktable.bauhaus->graph_fg);
3212       g_strlcpy(text, _("scene"), sizeof(text));
3213       pango_layout_set_text(layout, text, -1);
3214       pango_layout_get_pixel_extents(layout, &g->ink, NULL);
3215       cairo_move_to(cr, 0., y_scene - 0.5 * g->ink.height - g->ink.y);
3216       pango_cairo_show_layout(cr, layout);
3217       cairo_stroke(cr);
3218       const float scene_label_width = g->ink.width;
3219 
3220       // axis legend
3221       g_strlcpy(text, _("(EV)"), sizeof(text));
3222       pango_layout_set_text(layout, text, -1);
3223       pango_layout_get_pixel_extents(layout, &g->ink, NULL);
3224       cairo_move_to(cr, 0.5 * scene_label_width - 0.5 * g->ink.width - g->ink.x,
3225                     scene_bottom + 2. * g->inset + 0. * g->ink.height + g->ink.y);
3226       pango_cairo_show_layout(cr, layout);
3227       cairo_stroke(cr);
3228 
3229       // arrow between labels
3230       cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(1.));
3231       dt_cairo_draw_arrow(cr, fminf(scene_label_width, display_label_width) / 2.f, y_scene - g->line_height,
3232                           fminf(scene_label_width, display_label_width) / 2.f,
3233                           y_display + g->line_height + g->inset, TRUE);
3234 
3235       column_left = fmaxf(display_label_width, scene_label_width) + g->inset;
3236     }
3237     else
3238       column_left = darktable.bauhaus->quad_width;
3239 
3240     const float column_right = g->allocation.width - column_left - darktable.bauhaus->quad_width;
3241 
3242     // compute dynamic ranges left and right to middle grey
3243     const float display_HL_EV = -log2f(p->grey_point_target / p->white_point_target); // compared to white EV
3244     const float display_LL_EV = display_DR - display_HL_EV;                           // compared to black EV
3245     const float display_real_black_EV
3246         = -fmaxf(log2f(p->black_point_target / p->grey_point_target),
3247                  -11.685887601778058f + display_HL_EV - log2f(p->white_point_target / 100.f));
3248     const float scene_HL_EV = p->white_point_source;  // compared to white EV
3249     const float scene_LL_EV = -p->black_point_source; // compared to black EV
3250 
3251     // compute the max width needed to fit both dynamic ranges and derivate the unit size of a GUI EV
3252     const float max_DR = ceilf(fmaxf(display_HL_EV, scene_HL_EV)) + ceilf(fmaxf(display_LL_EV, scene_LL_EV));
3253     const float EV = (column_right) / max_DR;
3254 
3255     // all greys are aligned vertically in GUI since they are the fulcrum of the transform
3256     // so, get their coordinates
3257     const float grey_EV = fmaxf(ceilf(display_HL_EV), ceilf(scene_HL_EV));
3258     const float grey_x = g->allocation.width - (grey_EV)*EV - darktable.bauhaus->quad_width;
3259 
3260     // similarly, get black/white coordinates from grey point
3261     const float display_black_x = grey_x - display_real_black_EV * EV;
3262     const float display_DR_start_x = grey_x - display_LL_EV * EV;
3263     const float display_white_x = grey_x + display_HL_EV * EV;
3264 
3265     const float scene_black_x = grey_x - scene_LL_EV * EV;
3266     const float scene_white_x = grey_x + scene_HL_EV * EV;
3267     const float scene_lat_bottom = grey_x + (g->spline.x[1] - g->spline.x[2]) * EV * DR;
3268     const float scene_lat_top = grey_x + (g->spline.x[3] - g->spline.x[2]) * EV * DR;
3269 
3270     // show EV zones for display - zones are aligned on 0% and 100%
3271     cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(1.));
3272 
3273     // latitude bounds - show contrast expansion
3274 
3275     // Compute usual filmic  mapping
3276     float display_lat_bottom = filmic_spline(g->spline.latitude_min, g->spline.M1, g->spline.M2, g->spline.M3, g->spline.M4,
3277                                   g->spline.M5, g->spline.latitude_min, g->spline.latitude_max, g->spline.type);
3278     display_lat_bottom = powf(fmaxf(display_lat_bottom, NORM_MIN), p->output_power); // clamp at -16 EV
3279 
3280     // rescale output to log scale
3281     display_lat_bottom = log2f(display_lat_bottom/ (p->grey_point_target / 100.f));
3282 
3283     // take clamping into account
3284     if(display_lat_bottom < 0.f) // clamp to - 8 EV (black)
3285       display_lat_bottom = fmaxf(display_lat_bottom, -display_real_black_EV);
3286     else if(display_lat_bottom > 0.f) // clamp to 0 EV (white)
3287       display_lat_bottom = fminf(display_lat_bottom, display_HL_EV);
3288 
3289     // get destination coordinate
3290     display_lat_bottom = grey_x + display_lat_bottom * EV;
3291 
3292     // Compute usual filmic  mapping
3293     float display_lat_top = filmic_spline(g->spline.latitude_max, g->spline.M1, g->spline.M2, g->spline.M3, g->spline.M4,
3294                                   g->spline.M5, g->spline.latitude_min, g->spline.latitude_max, g->spline.type);
3295     display_lat_top = powf(fmaxf(display_lat_top, NORM_MIN), p->output_power); // clamp at -16 EV
3296 
3297     // rescale output to log scale
3298     display_lat_top = log2f(display_lat_top / (p->grey_point_target / 100.f));
3299 
3300     // take clamping into account
3301     if(display_lat_top < 0.f) // clamp to - 8 EV (black)
3302       display_lat_top = fmaxf(display_lat_top, -display_real_black_EV);
3303     else if(display_lat_top > 0.f) // clamp to 0 EV (white)
3304       display_lat_top = fminf(display_lat_top, display_HL_EV);
3305 
3306     // get destination coordinate and draw
3307     display_lat_top = grey_x + display_lat_top * EV;
3308 
3309     cairo_move_to(cr, scene_lat_bottom, scene_top);
3310     cairo_line_to(cr, scene_lat_top, scene_top);
3311     cairo_line_to(cr, display_lat_top, display_bottom);
3312     cairo_line_to(cr, display_lat_bottom, display_bottom);
3313     cairo_line_to(cr, scene_lat_bottom, scene_top);
3314     set_color(cr, darktable.bauhaus->graph_bg);
3315     cairo_fill(cr);
3316 
3317     for(int i = 0; i < (int)ceilf(display_DR); i++)
3318     {
3319       // content
3320       const float shade = powf(exp2f(-11.f + (float)i), 1.f / 2.4f);
3321       cairo_set_source_rgb(cr, shade, shade, shade);
3322       cairo_rectangle(cr, display_DR_start_x + i * EV, display_top, EV, g->line_height);
3323       cairo_fill_preserve(cr);
3324 
3325       // borders
3326       cairo_set_source_rgb(cr, 0.75, .5, 0.);
3327       cairo_stroke(cr);
3328     }
3329 
3330     // middle grey display
3331     cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(2.));
3332     cairo_move_to(cr, grey_x, display_bottom + 2. * g->inset);
3333     cairo_line_to(cr, grey_x, display_top - 2. * g->inset);
3334     cairo_stroke(cr);
3335 
3336     // show EV zones for scene - zones are aligned on grey
3337 
3338     for(int i = floorf(p->black_point_source); i < ceilf(p->white_point_source); i++)
3339     {
3340       // content
3341       cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(1.));
3342       const float shade = powf(0.1845f * exp2f((float)i), 1.f / 2.4f);
3343       const float x_temp = grey_x + i * EV;
3344       cairo_set_source_rgb(cr, shade, shade, shade);
3345       cairo_rectangle(cr, x_temp, scene_top, EV, g->line_height);
3346       cairo_fill_preserve(cr);
3347 
3348       // borders
3349       cairo_set_source_rgb(cr, 0.75, .5, 0.);
3350       cairo_stroke(cr);
3351 
3352       // arrows
3353       if(i == 0)
3354         cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(2.));
3355       else
3356         cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(1.));
3357 
3358       if((float)i > p->black_point_source && (float)i < p->white_point_source)
3359       {
3360         // Compute usual filmic  mapping
3361         const float normal_value = ((float)i - p->black_point_source) / DR;
3362         float y_temp = filmic_spline(normal_value, g->spline.M1, g->spline.M2, g->spline.M3, g->spline.M4,
3363                                      g->spline.M5, g->spline.latitude_min, g->spline.latitude_max, g->spline.type);
3364         y_temp = powf(fmaxf(y_temp, NORM_MIN), p->output_power); // clamp at -16 EV
3365 
3366         // rescale output to log scale
3367         y_temp = log2f(y_temp / (p->grey_point_target / 100.f));
3368 
3369         // take clamping into account
3370         if(y_temp < 0.f) // clamp to - 8 EV (black)
3371           y_temp = fmaxf(y_temp, -display_real_black_EV);
3372         else if(y_temp > 0.f) // clamp to 0 EV (white)
3373           y_temp = fminf(y_temp, display_HL_EV);
3374 
3375         // get destination coordinate and draw
3376         y_temp = grey_x + y_temp * EV;
3377         dt_cairo_draw_arrow(cr, x_temp, scene_top, y_temp, display_bottom, FALSE);
3378       }
3379     }
3380 
3381     cairo_set_line_width(cr, DT_PIXEL_APPLY_DPI(2.));
3382 
3383     // arrows for black and white
3384     float x_temp = grey_x + p->black_point_source * EV;
3385     float y_temp = grey_x - display_real_black_EV * EV;
3386     dt_cairo_draw_arrow(cr, x_temp, scene_top, y_temp, display_bottom, FALSE);
3387 
3388     x_temp = grey_x + p->white_point_source * EV;
3389     y_temp = grey_x + display_HL_EV * EV;
3390     dt_cairo_draw_arrow(cr, x_temp, scene_top, y_temp, display_bottom, FALSE);
3391 
3392     // draw white - grey - black ticks
3393 
3394     // black display
3395     cairo_move_to(cr, display_black_x, display_bottom);
3396     cairo_line_to(cr, display_black_x, display_top - 2. * g->inset);
3397     cairo_stroke(cr);
3398 
3399     // middle grey display
3400     cairo_move_to(cr, grey_x, display_bottom);
3401     cairo_line_to(cr, grey_x, display_top - 2. * g->inset);
3402     cairo_stroke(cr);
3403 
3404     // white display
3405     cairo_move_to(cr, display_white_x, display_bottom);
3406     cairo_line_to(cr, display_white_x, display_top - 2. * g->inset);
3407     cairo_stroke(cr);
3408 
3409     // black scene
3410     cairo_move_to(cr, scene_black_x, scene_bottom + 2. * g->inset);
3411     cairo_line_to(cr, scene_black_x, scene_top);
3412     cairo_stroke(cr);
3413 
3414     // middle grey scene
3415     cairo_move_to(cr, grey_x, scene_bottom + 2. * g->inset);
3416     cairo_line_to(cr, grey_x, scene_top);
3417     cairo_stroke(cr);
3418 
3419     // white scene
3420     cairo_move_to(cr, scene_white_x, scene_bottom + 2. * g->inset);
3421     cairo_line_to(cr, scene_white_x, scene_top);
3422     cairo_stroke(cr);
3423 
3424     // legends
3425     set_color(cr, darktable.bauhaus->graph_fg);
3426 
3427     // black scene legend
3428     snprintf(text, sizeof(text), "%+.1f", p->black_point_source);
3429     pango_layout_set_text(layout, text, -1);
3430     pango_layout_get_pixel_extents(layout, &g->ink, NULL);
3431     cairo_move_to(cr, scene_black_x - 0.5 * g->ink.width - g->ink.x,
3432                   scene_bottom + 2. * g->inset + 0. * g->ink.height + g->ink.y);
3433     pango_cairo_show_layout(cr, layout);
3434     cairo_stroke(cr);
3435 
3436     // grey scene legend
3437     snprintf(text, sizeof(text), "%+.1f", 0.f);
3438     pango_layout_set_text(layout, text, -1);
3439     pango_layout_get_pixel_extents(layout, &g->ink, NULL);
3440     cairo_move_to(cr, grey_x - 0.5 * g->ink.width - g->ink.x,
3441                   scene_bottom + 2. * g->inset + 0. * g->ink.height + g->ink.y);
3442     pango_cairo_show_layout(cr, layout);
3443     cairo_stroke(cr);
3444 
3445     // white scene legend
3446     snprintf(text, sizeof(text), "%+.1f", p->white_point_source);
3447     pango_layout_set_text(layout, text, -1);
3448     pango_layout_get_pixel_extents(layout, &g->ink, NULL);
3449     cairo_move_to(cr, scene_white_x - 0.5 * g->ink.width - g->ink.x,
3450                   scene_bottom + 2. * g->inset + 0. * g->ink.height + g->ink.y);
3451     pango_cairo_show_layout(cr, layout);
3452     cairo_stroke(cr);
3453 
3454     // black scene legend
3455     snprintf(text, sizeof(text), "%.0f", p->black_point_target);
3456     pango_layout_set_text(layout, text, -1);
3457     pango_layout_get_pixel_extents(layout, &g->ink, NULL);
3458     cairo_move_to(cr, display_black_x - 0.5 * g->ink.width - g->ink.x,
3459                   display_top - 4. * g->inset - g->ink.height - g->ink.y);
3460     pango_cairo_show_layout(cr, layout);
3461     cairo_stroke(cr);
3462 
3463     // grey scene legend
3464     snprintf(text, sizeof(text), "%.0f", p->grey_point_target);
3465     pango_layout_set_text(layout, text, -1);
3466     pango_layout_get_pixel_extents(layout, &g->ink, NULL);
3467     cairo_move_to(cr, grey_x - 0.5 * g->ink.width - g->ink.x,
3468                   display_top - 4. * g->inset - g->ink.height - g->ink.y);
3469     pango_cairo_show_layout(cr, layout);
3470     cairo_stroke(cr);
3471 
3472     // white scene legend
3473     snprintf(text, sizeof(text), "%.0f", p->white_point_target);
3474     pango_layout_set_text(layout, text, -1);
3475     pango_layout_get_pixel_extents(layout, &g->ink, NULL);
3476     cairo_move_to(cr, display_white_x - 0.5 * g->ink.width - g->ink.x,
3477                   display_top - 4. * g->inset - g->ink.height - g->ink.y);
3478     pango_cairo_show_layout(cr, layout);
3479     cairo_stroke(cr);
3480   }
3481 
3482   // restore font size
3483   pango_font_description_set_size(desc, font_size);
3484   pango_layout_set_font_description(layout, desc);
3485 
3486   cairo_destroy(cr);
3487   cairo_set_source_surface(crf, cst, 0, 0);
3488   cairo_paint(crf);
3489   cairo_surface_destroy(cst);
3490   g_object_unref(layout);
3491   pango_font_description_free(desc);
3492   return TRUE;
3493 }
3494 
area_button_press(GtkWidget * widget,GdkEventButton * event,gpointer user_data)3495 static gboolean area_button_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data)
3496 {
3497   dt_iop_module_t *self = (dt_iop_module_t *)user_data;
3498   if(darktable.gui->reset) return TRUE;
3499 
3500   dt_iop_filmicrgb_gui_data_t *g = (dt_iop_filmicrgb_gui_data_t *)self->gui_data;
3501 
3502   dt_iop_request_focus(self);
3503 
3504   if(g->active_button != DT_FILMIC_GUI_BUTTON_LAST)
3505   {
3506 
3507     if(event->button == 1 && event->type == GDK_2BUTTON_PRESS)
3508     {
3509       // double click resets view
3510       if(g->active_button == DT_FILMIC_GUI_BUTTON_TYPE)
3511       {
3512         g->gui_mode = DT_FILMIC_GUI_LOOK;
3513         gtk_widget_queue_draw(GTK_WIDGET(g->area));
3514         dt_conf_set_int("plugins/darkroom/filmicrgb/graph_view", g->gui_mode);
3515         return TRUE;
3516       }
3517       else
3518       {
3519         return FALSE;
3520       }
3521     }
3522     else if(event->button == 1)
3523     {
3524       // simple left click cycles through modes in positive direction
3525       if(g->active_button == DT_FILMIC_GUI_BUTTON_TYPE)
3526       {
3527         // cycle type of graph
3528         if(g->gui_mode == DT_FILMIC_GUI_RANGES)
3529           g->gui_mode = DT_FILMIC_GUI_LOOK;
3530         else
3531           g->gui_mode++;
3532 
3533         gtk_widget_queue_draw(GTK_WIDGET(g->area));
3534         dt_conf_set_int("plugins/darkroom/filmicrgb/graph_view", g->gui_mode);
3535         return TRUE;
3536       }
3537       else if(g->active_button == DT_FILMIC_GUI_BUTTON_LABELS)
3538       {
3539         g->gui_show_labels = !g->gui_show_labels;
3540         gtk_widget_queue_draw(GTK_WIDGET(g->area));
3541         dt_conf_set_int("plugins/darkroom/filmicrgb/graph_show_labels", g->gui_show_labels);
3542         return TRUE;
3543       }
3544       else
3545       {
3546         // we should never get there since (g->active_button != DT_FILMIC_GUI_BUTTON_LAST)
3547         // and any other case has been processed above.
3548         return FALSE;
3549       }
3550     }
3551     else if(event->button == 3)
3552     {
3553       // simple right click cycles through modes in negative direction
3554       if(g->active_button == DT_FILMIC_GUI_BUTTON_TYPE)
3555       {
3556         if(g->gui_mode == DT_FILMIC_GUI_LOOK)
3557           g->gui_mode = DT_FILMIC_GUI_RANGES;
3558         else
3559           g->gui_mode--;
3560 
3561         gtk_widget_queue_draw(GTK_WIDGET(g->area));
3562         dt_conf_set_int("plugins/darkroom/filmicrgb/graph_view", g->gui_mode);
3563         return TRUE;
3564       }
3565       else if(g->active_button == DT_FILMIC_GUI_BUTTON_LABELS)
3566       {
3567         g->gui_show_labels = !g->gui_show_labels;
3568         gtk_widget_queue_draw(GTK_WIDGET(g->area));
3569         dt_conf_set_int("plugins/darkroom/filmicrgb/graph_show_labels", g->gui_show_labels);
3570         return TRUE;
3571       }
3572       else
3573       {
3574         return FALSE;
3575       }
3576     }
3577   }
3578 
3579   return FALSE;
3580 }
3581 
area_enter_notify(GtkWidget * widget,GdkEventCrossing * event,gpointer user_data)3582 static gboolean area_enter_notify(GtkWidget *widget, GdkEventCrossing *event, gpointer user_data)
3583 {
3584   dt_iop_module_t *self = (dt_iop_module_t *)user_data;
3585   if(darktable.gui->reset) return 1;
3586   if(!self->enabled) return 0;
3587 
3588   dt_iop_filmicrgb_gui_data_t *g = (dt_iop_filmicrgb_gui_data_t *)self->gui_data;
3589   g->gui_hover = TRUE;
3590   gtk_widget_queue_draw(GTK_WIDGET(g->area));
3591   return TRUE;
3592 }
3593 
3594 
area_leave_notify(GtkWidget * widget,GdkEventCrossing * event,gpointer user_data)3595 static gboolean area_leave_notify(GtkWidget *widget, GdkEventCrossing *event, gpointer user_data)
3596 {
3597   dt_iop_module_t *self = (dt_iop_module_t *)user_data;
3598   if(darktable.gui->reset) return 1;
3599   if(!self->enabled) return 0;
3600 
3601   dt_iop_filmicrgb_gui_data_t *g = (dt_iop_filmicrgb_gui_data_t *)self->gui_data;
3602   g->gui_hover = FALSE;
3603   gtk_widget_queue_draw(GTK_WIDGET(g->area));
3604   return TRUE;
3605 }
3606 
area_motion_notify(GtkWidget * widget,GdkEventMotion * event,gpointer user_data)3607 static gboolean area_motion_notify(GtkWidget *widget, GdkEventMotion *event, gpointer user_data)
3608 {
3609   dt_iop_module_t *self = (dt_iop_module_t *)user_data;
3610   if(darktable.gui->reset) return 1;
3611 
3612   dt_iop_filmicrgb_gui_data_t *g = (dt_iop_filmicrgb_gui_data_t *)self->gui_data;
3613   if(!g->gui_sizes_inited) return FALSE;
3614 
3615   // get in-widget coordinates
3616   const float y = event->y;
3617   const float x = event->x;
3618 
3619   if(x > 0. && x < g->allocation.width && y > 0. && y < g->allocation.height) g->gui_hover = TRUE;
3620 
3621   gint save_active_button = g->active_button;
3622 
3623   if(g->gui_hover)
3624   {
3625     // find out which button is under the mouse
3626     gint found_something = FALSE;
3627     for(int i = 0; i < DT_FILMIC_GUI_BUTTON_LAST; i++)
3628     {
3629       // check if mouse in in the button's bounds
3630       if(x > g->buttons[i].left && x < g->buttons[i].right && y > g->buttons[i].top && y < g->buttons[i].bottom)
3631       {
3632         // yeah, mouse is over that button
3633         g->buttons[i].mouse_hover = TRUE;
3634         g->active_button = i;
3635         found_something = TRUE;
3636       }
3637       else
3638       {
3639         // no luck with this button
3640         g->buttons[i].mouse_hover = FALSE;
3641       }
3642     }
3643 
3644     if(!found_something) g->active_button = DT_FILMIC_GUI_BUTTON_LAST; // mouse is over no known button
3645 
3646     // update the tooltips
3647     if(g->active_button == DT_FILMIC_GUI_BUTTON_LAST && x < g->buttons[0].left)
3648     {
3649       // we are over the graph area
3650       gtk_widget_set_tooltip_text(GTK_WIDGET(g->area), _("use the parameters below to set the nodes.\n"
3651                                                          "the bright curve is the filmic tone mapping curve\n"
3652                                                          "the dark curve is the desaturation curve."));
3653     }
3654     else if(g->active_button == DT_FILMIC_GUI_BUTTON_LABELS)
3655     {
3656       gtk_widget_set_tooltip_text(GTK_WIDGET(g->area), _("toggle axis labels and values display."));
3657     }
3658     else if(g->active_button == DT_FILMIC_GUI_BUTTON_TYPE)
3659     {
3660       gtk_widget_set_tooltip_text(GTK_WIDGET(g->area), _("cycle through graph views.\n"
3661                                                          "left click: cycle forward.\n"
3662                                                          "right click: cycle backward.\n"
3663                                                          "double click: reset to look view."));
3664     }
3665     else
3666     {
3667       gtk_widget_set_tooltip_text(GTK_WIDGET(g->area), "");
3668     }
3669 
3670     if(save_active_button != g->active_button) gtk_widget_queue_draw(GTK_WIDGET(g->area));
3671     return TRUE;
3672   }
3673   else
3674   {
3675     g->active_button = DT_FILMIC_GUI_BUTTON_LAST;
3676     if(save_active_button != g->active_button) (GTK_WIDGET(g->area));
3677     return FALSE;
3678   }
3679 }
3680 
area_scroll_callback(GtkWidget * widget,GdkEventScroll * event,gpointer user_data)3681 static gboolean area_scroll_callback(GtkWidget *widget, GdkEventScroll *event, gpointer user_data)
3682 {
3683   if(dt_gui_ignore_scroll(event)) return FALSE;
3684 
3685   if(dt_modifier_is(event->state, GDK_CONTROL_MASK))
3686   {
3687     int delta_y;
3688     if(dt_gui_get_scroll_unit_deltas(event, NULL, &delta_y))
3689     {
3690       //adjust aspect
3691       const int aspect = dt_conf_get_int("plugins/darkroom/filmicrgb/aspect_percent");
3692       dt_conf_set_int("plugins/darkroom/filmicrgb/aspect_percent", aspect + delta_y);
3693       dtgtk_drawing_area_set_aspect_ratio(widget, aspect / 100.0);
3694     }
3695     return TRUE; // Ensure that scrolling cannot move side panel when no delta
3696   }
3697   return FALSE;
3698 }
3699 
gui_init(dt_iop_module_t * self)3700 void gui_init(dt_iop_module_t *self)
3701 {
3702   dt_iop_filmicrgb_gui_data_t *g = IOP_GUI_ALLOC(filmicrgb);
3703 
3704   g->show_mask = FALSE;
3705   g->gui_mode = DT_FILMIC_GUI_LOOK;
3706   g->gui_show_labels = TRUE;
3707   g->gui_hover = FALSE;
3708   g->gui_sizes_inited = FALSE;
3709 
3710   // don't make the area square to safe some vertical space -- it's not interactive anyway
3711   const float aspect = dt_conf_get_int("plugins/darkroom/filmicrgb/aspect_percent") / 100.0;
3712   g->area = GTK_DRAWING_AREA(dtgtk_drawing_area_new_with_aspect_ratio(aspect));
3713 
3714   gtk_widget_set_can_focus(GTK_WIDGET(g->area), TRUE);
3715   gtk_widget_add_events(GTK_WIDGET(g->area), GDK_BUTTON_PRESS_MASK | GDK_ENTER_NOTIFY_MASK | GDK_LEAVE_NOTIFY_MASK
3716                                                  | GDK_POINTER_MOTION_MASK | darktable.gui->scroll_mask);
3717   g_signal_connect(G_OBJECT(g->area), "draw", G_CALLBACK(dt_iop_tonecurve_draw), self);
3718   g_signal_connect(G_OBJECT(g->area), "button-press-event", G_CALLBACK(area_button_press), self);
3719   g_signal_connect(G_OBJECT(g->area), "leave-notify-event", G_CALLBACK(area_leave_notify), self);
3720   g_signal_connect(G_OBJECT(g->area), "enter-notify-event", G_CALLBACK(area_enter_notify), self);
3721   g_signal_connect(G_OBJECT(g->area), "motion-notify-event", G_CALLBACK(area_motion_notify), self);
3722   g_signal_connect(G_OBJECT(g->area), "scroll-event", G_CALLBACK(area_scroll_callback), self);
3723 
3724   // Init GTK notebook
3725   g->notebook = GTK_NOTEBOOK(gtk_notebook_new());
3726 
3727   // Page SCENE
3728   self->widget = dt_ui_notebook_page(g->notebook, _("scene"), NULL);
3729 
3730   g->grey_point_source
3731       = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, dt_bauhaus_slider_from_params(self, "grey_point_source"));
3732   dt_bauhaus_slider_set_soft_range(g->grey_point_source, .1, 36.0);
3733   dt_bauhaus_slider_set_format(g->grey_point_source, "%.2f %%");
3734   gtk_widget_set_tooltip_text(g->grey_point_source,
3735                               _("adjust to match the average luminance of the image's subject.\n"
3736                                 "the value entered here will then be remapped to 18.45%.\n"
3737                                 "decrease the value to increase the overall brightness."));
3738 
3739   // White slider
3740   g->white_point_source
3741       = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, dt_bauhaus_slider_from_params(self, "white_point_source"));
3742   dt_bauhaus_slider_set_soft_range(g->white_point_source, 2.0, 8.0);
3743   dt_bauhaus_slider_set_format(g->white_point_source, _("%+.2f EV"));
3744   gtk_widget_set_tooltip_text(g->white_point_source,
3745                               _("number of stops between middle gray and pure white.\n"
3746                                 "this is a reading a lightmeter would give you on the scene.\n"
3747                                 "adjust so highlights clipping is avoided"));
3748 
3749   // Black slider
3750   g->black_point_source
3751       = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, dt_bauhaus_slider_from_params(self, "black_point_source"));
3752   dt_bauhaus_slider_set_soft_range(g->black_point_source, -14.0, -3);
3753   dt_bauhaus_slider_set_format(g->black_point_source, _("%+.2f EV"));
3754   gtk_widget_set_tooltip_text(
3755       g->black_point_source, _("number of stops between middle gray and pure black.\n"
3756                                "this is a reading a lightmeter would give you on the scene.\n"
3757                                "increase to get more contrast.\ndecrease to recover more details in low-lights."));
3758 
3759   // Dynamic range scaling
3760   g->security_factor = dt_bauhaus_slider_from_params(self, "security_factor");
3761   dt_bauhaus_slider_set_soft_max(g->security_factor, 50);
3762   dt_bauhaus_slider_set_format(g->security_factor, "%+.2f %%");
3763   gtk_widget_set_tooltip_text(g->security_factor, _("symmetrically enlarge or shrink the computed dynamic range.\n"
3764                                                     "useful to give a safety margin to extreme luminances."));
3765 
3766   // Auto tune slider
3767   g->auto_button = dt_color_picker_new(self, DT_COLOR_PICKER_AREA, dt_bauhaus_combobox_new(self));
3768   dt_bauhaus_widget_set_label(g->auto_button, NULL, N_("auto tune levels"));
3769   gtk_widget_set_tooltip_text(g->auto_button, _("try to optimize the settings with some statistical assumptions.\n"
3770                                                 "this will fit the luminance range inside the histogram bounds.\n"
3771                                                 "works better for landscapes and evenly-lit pictures\n"
3772                                                 "but fails for high-keys, low-keys and high-ISO pictures.\n"
3773                                                 "this is not an artificial intelligence, but a simple guess.\n"
3774                                                 "ensure you understand its assumptions before using it."));
3775   gtk_box_pack_start(GTK_BOX(self->widget), g->auto_button, FALSE, FALSE, 0);
3776 
3777   // Page RECONSTRUCT
3778   self->widget = dt_ui_notebook_page(g->notebook, _("reconstruct"), NULL);
3779 
3780   GtkWidget *label = dt_ui_section_label_new(_("highlights clipping"));
3781   GtkStyleContext *context = gtk_widget_get_style_context(GTK_WIDGET(label));
3782   gtk_style_context_add_class(context, "section_label_top");
3783   gtk_box_pack_start(GTK_BOX(self->widget), label, FALSE, FALSE, 0);
3784 
3785   g->reconstruct_threshold = dt_bauhaus_slider_from_params(self, "reconstruct_threshold");
3786   dt_bauhaus_slider_set_format(g->reconstruct_threshold, _("%+.2f EV"));
3787   gtk_widget_set_tooltip_text(g->reconstruct_threshold,
3788                               _("set the exposure threshold upon which\n"
3789                                 "clipped highlights get reconstructed.\n"
3790                                 "values are relative to the scene white point.\n"
3791                                 "0 EV means the threshold is the same as the scene white point.\n"
3792                                 "decrease to include more areas,\n"
3793                                 "increase to exclude more areas."));
3794 
3795   g->reconstruct_feather = dt_bauhaus_slider_from_params(self, "reconstruct_feather");
3796   dt_bauhaus_slider_set_format(g->reconstruct_feather, _("%+.2f EV"));
3797   gtk_widget_set_tooltip_text(g->reconstruct_feather,
3798                               _("soften the transition between clipped highlights and valid pixels.\n"
3799                                 "decrease to make the transition harder and sharper,\n"
3800                                 "increase to make the transition softer and blurrier."));
3801 
3802   // Highlight Reconstruction Mask
3803   g->show_highlight_mask = dt_bauhaus_combobox_new(self);
3804   dt_bauhaus_widget_set_label(g->show_highlight_mask, NULL, N_("display highlight reconstruction mask"));
3805   dt_bauhaus_widget_set_quad_paint(g->show_highlight_mask, dtgtk_cairo_paint_showmask,
3806                                    CPF_STYLE_FLAT | CPF_DO_NOT_USE_BORDER, NULL);
3807   dt_bauhaus_widget_set_quad_toggle(g->show_highlight_mask, TRUE);
3808   g_signal_connect(G_OBJECT(g->show_highlight_mask), "quad-pressed", G_CALLBACK(show_mask_callback), self);
3809   gtk_box_pack_start(GTK_BOX(self->widget), g->show_highlight_mask, FALSE, FALSE, 0);
3810 
3811   label = dt_ui_section_label_new(_("balance"));
3812   gtk_box_pack_start(GTK_BOX(self->widget), label, FALSE, FALSE, 0);
3813 
3814   g->reconstruct_structure_vs_texture = dt_bauhaus_slider_from_params(self, "reconstruct_structure_vs_texture");
3815   dt_bauhaus_slider_set_step(g->reconstruct_structure_vs_texture, 0.1);
3816   dt_bauhaus_slider_set_format(g->reconstruct_structure_vs_texture, "%.2f %%");
3817   gtk_widget_set_tooltip_text(g->reconstruct_structure_vs_texture,
3818                               /* xgettext:no-c-format */
3819                               _("decide which reconstruction strategy to favor,\n"
3820                                 "between inpainting a smooth color gradient,\n"
3821                                 "or trying to recover the textured details.\n"
3822                                 "0% is an equal mix of both.\n"
3823                                 "increase if at least one RGB channel is not clipped.\n"
3824                                 "decrease if all RGB channels are clipped over large areas."));
3825 
3826   g->reconstruct_bloom_vs_details = dt_bauhaus_slider_from_params(self, "reconstruct_bloom_vs_details");
3827   dt_bauhaus_slider_set_step(g->reconstruct_bloom_vs_details, 0.1);
3828   dt_bauhaus_slider_set_format(g->reconstruct_bloom_vs_details, "%.2f %%");
3829   gtk_widget_set_tooltip_text(g->reconstruct_bloom_vs_details,
3830                               /* xgettext:no-c-format */
3831                               _("decide which reconstruction strategy to favor,\n"
3832                                 "between blooming highlights like film does,\n"
3833                                 "or trying to recover sharp details.\n"
3834                                 "0% is an equal mix of both.\n"
3835                                 "increase if you want more details.\n"
3836                                 "decrease if you want more blur."));
3837 
3838   // Bloom threshold
3839   g->reconstruct_grey_vs_color = dt_bauhaus_slider_from_params(self, "reconstruct_grey_vs_color");
3840   dt_bauhaus_slider_set_step(g->reconstruct_grey_vs_color, 0.1);
3841   dt_bauhaus_slider_set_format(g->reconstruct_grey_vs_color, "%.2f %%");
3842   gtk_widget_set_tooltip_text(g->reconstruct_grey_vs_color,
3843                               /* xgettext:no-c-format */
3844                               _("decide which reconstruction strategy to favor,\n"
3845                                 "between recovering monochromatic highlights,\n"
3846                                 "or trying to recover colorful highlights.\n"
3847                                 "0% is an equal mix of both.\n"
3848                                 "increase if you want more color.\n"
3849                                 "decrease if you see magenta or out-of-gamut highlights."));
3850 
3851   // Page LOOK
3852   self->widget = dt_ui_notebook_page(g->notebook, _("look"), NULL);
3853 
3854   g->contrast = dt_bauhaus_slider_from_params(self, N_("contrast"));
3855   dt_bauhaus_slider_set_soft_range(g->contrast, 1.0, 2.0);
3856   dt_bauhaus_slider_set_digits(g->contrast, 3);
3857   dt_bauhaus_slider_set_step(g->contrast, .01);
3858   gtk_widget_set_tooltip_text(g->contrast, _("slope of the linear part of the curve\n"
3859                                              "affects mostly the mid-tones"));
3860 
3861   // brightness slider
3862   g->output_power = dt_bauhaus_slider_from_params(self, "output_power");
3863   gtk_widget_set_tooltip_text(g->output_power, _("equivalent to paper grade in analog.\n"
3864                                                  "increase to make highlights brighter and less compressed.\n"
3865                                                  "decrease to mute highlights."));
3866 
3867   g->latitude = dt_bauhaus_slider_from_params(self, N_("latitude"));
3868   dt_bauhaus_slider_set_soft_range(g->latitude, 5.0, 50.0);
3869   dt_bauhaus_slider_set_format(g->latitude, "%.2f %%");
3870   gtk_widget_set_tooltip_text(g->latitude,
3871                               _("width of the linear domain in the middle of the curve,\n"
3872                                 "in percent of the dynamic range (white exposure - black exposure).\n"
3873                                 "increase to get more contrast and less desaturation at extreme luminances,\n"
3874                                 "decrease otherwise. no desaturation happens in the latitude range.\n"
3875                                 "this has no effect on mid-tones."));
3876 
3877   g->balance = dt_bauhaus_slider_from_params(self, "balance");
3878   dt_bauhaus_slider_set_format(g->balance, "%.2f %%");
3879   gtk_widget_set_tooltip_text(g->balance, _("slides the latitude along the slope\n"
3880                                             "to give more room to shadows or highlights.\n"
3881                                             "use it if you need to protect the details\n"
3882                                             "at one extremity of the histogram."));
3883 
3884   g->saturation = dt_bauhaus_slider_from_params(self, "saturation");
3885   dt_bauhaus_slider_set_soft_max(g->saturation, 50.0);
3886   dt_bauhaus_slider_set_format(g->saturation, "%.2f %%");
3887   gtk_widget_set_tooltip_text(g->saturation, _("desaturates the output of the module\n"
3888                                                "specifically at extreme luminances.\n"
3889                                                "increase if shadows and/or highlights are under-saturated."));
3890 
3891   // Page DISPLAY
3892   self->widget = dt_ui_notebook_page(g->notebook, _("display"), NULL);
3893 
3894   // Black slider
3895   g->black_point_target = dt_bauhaus_slider_from_params(self, "black_point_target");
3896   dt_bauhaus_slider_set_step(g->black_point_target, .001);
3897   dt_bauhaus_slider_set_digits(g->black_point_target, 4);
3898   dt_bauhaus_slider_set_format(g->black_point_target, "%.4f %%");
3899   gtk_widget_set_tooltip_text(g->black_point_target, _("luminance of output pure black, "
3900                                                        "this should be 0%\nexcept if you want a faded look"));
3901 
3902   g->grey_point_target = dt_bauhaus_slider_from_params(self, "grey_point_target");
3903   dt_bauhaus_slider_set_step(g->grey_point_target, .01);
3904   dt_bauhaus_slider_set_digits(g->grey_point_target, 4);
3905   dt_bauhaus_slider_set_format(g->grey_point_target, "%.4f %%");
3906   gtk_widget_set_tooltip_text(g->grey_point_target,
3907                               _("middle gray value of the target display or color space.\n"
3908                                 "you should never touch that unless you know what you are doing."));
3909 
3910   g->white_point_target = dt_bauhaus_slider_from_params(self, "white_point_target");
3911   dt_bauhaus_slider_set_soft_max(g->white_point_target, 100.0);
3912   dt_bauhaus_slider_set_step(g->white_point_target, .01);
3913   dt_bauhaus_slider_set_digits(g->white_point_target, 4);
3914   dt_bauhaus_slider_set_format(g->white_point_target, "%.4f %%");
3915   gtk_widget_set_tooltip_text(g->white_point_target, _("luminance of output pure white, "
3916                                                        "this should be 100%\nexcept if you want a faded look"));
3917 
3918   // Page OPTIONS
3919   self->widget = dt_ui_notebook_page(g->notebook, _("options"), NULL);
3920 
3921   // Color science
3922   g->version = dt_bauhaus_combobox_from_params(self, "version");
3923   gtk_widget_set_tooltip_text(g->version,
3924                               _("v3 is darktable 3.0 desaturation method, same as color balance.\n"
3925                                 "v4 is a newer desaturation method, based on spectral purity of light."));
3926 
3927   g->preserve_color = dt_bauhaus_combobox_from_params(self, "preserve_color");
3928   gtk_widget_set_tooltip_text(g->preserve_color, _("ensure the original color are preserved.\n"
3929                                                    "may reinforce chromatic aberrations and chroma noise,\n"
3930                                                    "so ensure they are properly corrected elsewhere.\n"));
3931 
3932   // Curve type
3933   g->highlights = dt_bauhaus_combobox_from_params(self, "highlights");
3934   gtk_widget_set_tooltip_text(g->highlights, _("choose the desired curvature of the filmic spline in highlights.\n"
3935                                                "hard uses a high curvature resulting in more tonal compression.\n"
3936                                                "soft uses a low curvature resulting in less tonal compression."));
3937 
3938   g->shadows = dt_bauhaus_combobox_from_params(self, "shadows");
3939   gtk_widget_set_tooltip_text(g->shadows, _("choose the desired curvature of the filmic spline in shadows.\n"
3940                                             "hard uses a high curvature resulting in more tonal compression.\n"
3941                                             "soft uses a low curvature resulting in less tonal compression."));
3942 
3943   g->custom_grey = dt_bauhaus_toggle_from_params(self, "custom_grey");
3944   gtk_widget_set_tooltip_text(g->custom_grey, _("enable to input custom middle-gray values.\n"
3945                                                 "this is not recommended in general.\n"
3946                                                 "fix the global exposure in the exposure module instead.\n"
3947                                                 "disable to use standard 18.45 %% middle gray."));
3948 
3949   g->auto_hardness = dt_bauhaus_toggle_from_params(self, "auto_hardness");
3950   gtk_widget_set_tooltip_text(
3951       g->auto_hardness, _("enable to auto-set the look hardness depending on the scene white and black points.\n"
3952                           "this keeps the middle gray on the identity line and improves fast tuning.\n"
3953                           "disable if you want a manual control."));
3954 
3955   g->high_quality_reconstruction = dt_bauhaus_slider_from_params(self, "high_quality_reconstruction");
3956   gtk_widget_set_tooltip_text(g->high_quality_reconstruction,
3957                               _("run extra passes of chromaticity reconstruction.\n"
3958                                 "more iterations means more color propagation from neighbourhood.\n"
3959                                 "this will be slower but will yield more neutral highlights.\n"
3960                                 "it also helps with difficult cases of magenta highlights."));
3961 
3962   // Highlight noise
3963   g->noise_level = dt_bauhaus_slider_from_params(self, "noise_level");
3964   gtk_widget_set_tooltip_text(g->noise_level, _("add statistical noise in reconstructed highlights.\n"
3965                                                 "this avoids highlights to look too smooth\n"
3966                                                 "when the picture is noisy overall,\n"
3967                                                 "so they blend with the rest of the picture."));
3968 
3969   // Noise distribution
3970   g->noise_distribution = dt_bauhaus_combobox_from_params(self, "noise_distribution");
3971   dt_bauhaus_combobox_add(g->noise_distribution, _("uniform"));
3972   dt_bauhaus_combobox_add(g->noise_distribution, _("gaussian"));
3973   dt_bauhaus_combobox_add(g->noise_distribution, _("poissonian"));
3974   gtk_widget_set_tooltip_text(g->noise_distribution, _("choose the statistical distribution of noise.\n"
3975                                                        "this is useful to match natural sensor noise pattern.\n"));
3976 
3977   // start building top level widget
3978   self->widget = gtk_box_new(GTK_ORIENTATION_VERTICAL, DT_BAUHAUS_SPACE);
3979 
3980   gtk_box_pack_start(GTK_BOX(self->widget), GTK_WIDGET(g->area), TRUE, TRUE, 0);
3981   gtk_box_pack_start(GTK_BOX(self->widget), GTK_WIDGET(g->notebook), FALSE, FALSE, 0);
3982 }
3983 
gui_changed(dt_iop_module_t * self,GtkWidget * w,void * previous)3984 void gui_changed(dt_iop_module_t *self, GtkWidget *w, void *previous)
3985 {
3986   dt_iop_filmicrgb_params_t *p = (dt_iop_filmicrgb_params_t *)self->params;
3987   dt_iop_filmicrgb_gui_data_t *g = (dt_iop_filmicrgb_gui_data_t *)self->gui_data;
3988 
3989   if(!w || w == g->auto_hardness || w == g->security_factor || w == g->grey_point_source
3990      || w == g->black_point_source || w == g->white_point_source)
3991   {
3992     ++darktable.gui->reset;
3993 
3994     if(w == g->security_factor || w == g->grey_point_source)
3995     {
3996       float prev = *(float *)previous;
3997       if(w == g->security_factor)
3998       {
3999         float ratio = (p->security_factor - prev) / (prev + 100.0f);
4000 
4001         float EVmin = p->black_point_source;
4002         EVmin = EVmin + ratio * EVmin;
4003 
4004         float EVmax = p->white_point_source;
4005         EVmax = EVmax + ratio * EVmax;
4006 
4007         p->white_point_source = EVmax;
4008         p->black_point_source = EVmin;
4009       }
4010       else
4011       {
4012         float grey_var = log2f(prev / p->grey_point_source);
4013         p->black_point_source = p->black_point_source - grey_var;
4014         p->white_point_source = p->white_point_source + grey_var;
4015       }
4016 
4017       dt_bauhaus_slider_set_soft(g->white_point_source, p->white_point_source);
4018       dt_bauhaus_slider_set_soft(g->black_point_source, p->black_point_source);
4019     }
4020 
4021     if(p->auto_hardness)
4022       p->output_power = logf(p->grey_point_target / 100.0f)
4023                         / logf(-p->black_point_source / (p->white_point_source - p->black_point_source));
4024 
4025     gtk_widget_set_visible(GTK_WIDGET(g->output_power), !p->auto_hardness);
4026     dt_bauhaus_slider_set_soft(g->output_power, p->output_power);
4027 
4028     --darktable.gui->reset;
4029   }
4030 
4031   if(!w || w == g->version)
4032   {
4033     if(p->version == DT_FILMIC_COLORSCIENCE_V1)
4034       dt_bauhaus_widget_set_label(g->saturation, NULL, N_("extreme luminance saturation"));
4035     else if(p->version == DT_FILMIC_COLORSCIENCE_V2 || p->version == DT_FILMIC_COLORSCIENCE_V3)
4036       dt_bauhaus_widget_set_label(g->saturation, NULL, N_("middle tones saturation"));
4037   }
4038 
4039   if(!w || w == g->reconstruct_bloom_vs_details)
4040   {
4041     if(p->reconstruct_bloom_vs_details == -100.f)
4042     {
4043       // user disabled the reconstruction in favor of full blooming
4044       // so the structure vs. texture setting doesn't make any difference
4045       // make it insensitive to not confuse users
4046       gtk_widget_set_sensitive(g->reconstruct_structure_vs_texture, FALSE);
4047     }
4048     else
4049     {
4050       gtk_widget_set_sensitive(g->reconstruct_structure_vs_texture, TRUE);
4051     }
4052   }
4053 
4054   if(!w || w == g->custom_grey)
4055   {
4056     gtk_widget_set_visible(g->grey_point_source, p->custom_grey);
4057     gtk_widget_set_visible(g->grey_point_target, p->custom_grey);
4058   }
4059 
4060   if(w) gtk_widget_queue_draw(self->widget);
4061 }
4062 
4063 // modelines: These editor modelines have been set for all relevant files by tools/update_modelines.sh
4064 // vim: shiftwidth=2 expandtab tabstop=2 cindent
4065 // kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified;
4066