diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 4af89ce3e3e..dc8ec53da1b 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -230,6 +230,17 @@ changes (where available). ## Other Changes +- AgX and sigmoid now adapt to HDR output, so one edit exports to + SDR, HLG and PQ alike. HLG output peaks at 1000 nits, and PQ output + at the new "PQ peak" setting of the export module, 1000 nits by + default, which export presets store and darktable-cli sets with + `--pq-peak`. With AgX, shadows and midtones keep their SDR brightness + and highlights extend to the peak, following Blender's AgX HDR, with + a new "HDR purity" slider for the highlights' saturation. Sigmoid + scales its target white to the peak, up to 1600% (3248 nits). SDR + output is unchanged. Existing edits render as before, and a "follow + output peak" toggle in each module opts them in. + - PNG exports using a PQ output color profile now include content light level metadata (MaxCLL and MaxFALL) for HDR tone mapping. @@ -360,6 +371,9 @@ changes (where available). opened: going back in the history showed masks from before the step that added them. +- Fixed AgX rendering everything above the pivot black when pivot + target output was 100% and shoulder power was 7 or more. + - Fixed shift-click on the selected entry of the history stack, which did nothing instead of revealing that module in the right-hand panel. diff --git a/data/darktableconfig.xml.in b/data/darktableconfig.xml.in index a427550de6d..a2fadd76060 100644 --- a/data/darktableconfig.xml.in +++ b/data/darktableconfig.xml.in @@ -2684,6 +2684,13 @@ always use LittleCMS 2 to apply output color profile this is slower than the default. + + plugins/lighttable/export/pq_peak_nits + int + 1000 + PQ peak luminance + the brightest highlight a PQ (HDR) output should reach, in nits. tone mappers that follow the output map their white to this peak. HLG output always peaks at 1000 nits and SDR output at the 203 nit reference white. + plugins/lighttable/export/high_quality_processing bool diff --git a/data/kernels/agx.cl b/data/kernels/agx.cl index 19c3b01dfde..49a1b7e923f 100644 --- a/data/kernels/agx.cl +++ b/data/kernels/agx.cl @@ -21,6 +21,19 @@ #define _epsilon 1E-6f +// Must match hdr_params_t in C code +typedef struct dt_iop_agx_hdr_params_t +{ + int enabled; + float headroom; + float purity; + float log_min; + float pivot_x; + float pivot_y; + float toe_scale; + float shoulder_scale; +} dt_iop_agx_hdr_params_t; + // Must match tone_mapping_params_t in C code typedef struct dt_iop_agx_tone_mapping_params_t { @@ -55,6 +68,7 @@ typedef struct dt_iop_agx_tone_mapping_params_t float look_original_hue_mix_ratio; int look_tuned; int restore_hue; + dt_iop_agx_hdr_params_t hdr; } dt_iop_agx_tone_mapping_params_t; static inline void _agx_compress_into_gamut(float4 *pixel) @@ -175,6 +189,36 @@ static inline float _agx_lerp_hue(const float original_hue, const float processe return mixed_hue - floor(mixed_hue); } +// see _hdr_darken() in agx.c +static inline float _agx_hdr_darken(const float x, const dt_iop_agx_hdr_params_t *hdr) +{ + const float log_x = clamp(1.0f - dtcl_log2(fmax(x, 1e-12f)) / hdr->log_min, 0.0f, 1.0f); + const float u = log_x - hdr->pivot_x; + const float log_y = u < 0.0f + ? _agx_scaled_sigmoid(log_x, hdr->toe_scale, 1.0f, 3.0f, hdr->pivot_x, hdr->pivot_y) + : hdr->pivot_y + hdr->shoulder_scale * u / (hdr->shoulder_scale + u); + return x > 0.0f ? dtcl_exp2(hdr->log_min * (1.0f - log_y)) : 0.0f; +} + +// see _agx_hdr() in agx.c +static inline void _agx_hdr(float4 *pixel, const dt_iop_agx_hdr_params_t *hdr) +{ + const float4 hsv_before = RGB_2_HSV(*pixel); + float4 darkened = *pixel; + darkened.x = _agx_hdr_darken(pixel->x, hdr); + darkened.y = _agx_hdr_darken(pixel->y, hdr); + darkened.z = _agx_hdr_darken(pixel->z, hdr); + float4 hsv_after = RGB_2_HSV(darkened); + + hsv_after.x = _agx_lerp_hue(hsv_before.x, hsv_after.x, 1.0f - hdr->purity); + hsv_after.y = hsv_before.y + hdr->purity * (hsv_after.y - hsv_before.y); + darkened = HSV_2_RGB(hsv_after); + + pixel->x = hdr->headroom * darkened.x; + pixel->y = hdr->headroom * darkened.y; + pixel->z = hdr->headroom * darkened.z; +} + static inline void _agx_tone_mapping(float4 *rgb_in_out, const dt_iop_agx_tone_mapping_params_t *params, constant float *rendering_to_xyz) { float4 hsv_pixel = (float4)(0.0f, 0.0f, 0.0f, 0.0f); @@ -199,6 +243,11 @@ static inline void _agx_tone_mapping(float4 *rgb_in_out, const dt_iop_agx_tone_m transformed_pixel.y = dtcl_pow(fmax(0.0f, transformed_pixel.y), params->curve_gamma); transformed_pixel.z = dtcl_pow(fmax(0.0f, transformed_pixel.z), params->curve_gamma); + if(params->hdr.enabled) + { + _agx_hdr(&transformed_pixel, ¶ms->hdr); + } + if(params->restore_hue) { hsv_pixel = RGB_2_HSV(transformed_pixel); diff --git a/src/cli/main.c b/src/cli/main.c index a4bdeea5e49..ca827673ffe 100644 --- a/src/cli/main.c +++ b/src/cli/main.c @@ -34,6 +34,7 @@ #include "common/history.h" #include "common/image.h" #include "common/image_cache.h" +#include "common/iop_profile.h" #include "common/points.h" #include "control/conf.h" #include "develop/imageop.h" @@ -102,6 +103,8 @@ fprintf(stderr, "darktable %s\n" " --icc-file specify icc filename, default to NONE\n" " --icc-intent specify icc intent, default to LAST\n" " use --help icc-intent for list of supported intents\n" + " --pq-peak peak luminance of PQ output, 203 to 10000,\n" + " default from the export module's setting\n" " --print-paths print the resolved configdir, cachedir, tmpdir,\n" " datadir, moduledir, localedir and library paths\n" " for this install, then exit\n" @@ -250,6 +253,7 @@ int main(int argc, char *arg[]) dt_colorspaces_color_profile_type_t icc_type = DT_COLORSPACE_NONE; gchar *icc_filename = NULL; dt_iop_color_intent_t icc_intent = DT_INTENT_LAST; + int pq_peak_nits = 0; int k; for(k = 1; k < argc; k++) @@ -438,6 +442,17 @@ int main(int argc, char *arg[]) exit(1); } } + else if(!strcmp(arg[k], "--pq-peak") && argc > k + 1) + { + k++; + pq_peak_nits = atoi(arg[k]); + if(pq_peak_nits < DT_HDR_REFERENCE_WHITE_NITS || pq_peak_nits > 10000) + { + fprintf(stderr, _("incorrect PQ peak for --pq-peak: '%s'\n"), arg[k]); + usage(arg[0]); + exit(1); + } + } else if(!strcmp(arg[k], "-v") || !strcmp(arg[k], "--verbose")) { verbose = TRUE; @@ -880,6 +895,9 @@ int main(int argc, char *arg[]) // TODO: add a callback to set the bpp without going through the config + // the exports below run in this thread + dt_ioppr_set_thread_pq_peak_nits(pq_peak_nits); + int num = 1, res = 0; for(GList *iter = id_list; iter; iter = g_list_next(iter), num++) { diff --git a/src/common/iop_profile.c b/src/common/iop_profile.c index bbe0a39b205..442a11d2f35 100644 --- a/src/common/iop_profile.c +++ b/src/common/iop_profile.c @@ -806,6 +806,35 @@ dt_ioppr_set_pipe_input_profile_info(struct dt_develop_t *dev, return profile_info; } +// per thread, so that an export job keeps the peak it was started with +// without writing the setting shared with the export module and the darkroom +static __thread int _thread_pq_peak_nits = 0; + +void dt_ioppr_set_thread_pq_peak_nits(const int nits) +{ + _thread_pq_peak_nits = nits; +} + +static float _output_headroom(const dt_colorspaces_color_profile_type_t type) +{ + switch(dt_colorspaces_hdr_transfer(type)) + { + case DT_HDR_PQ: + { + const int nits = _thread_pq_peak_nits > 0 + ? _thread_pq_peak_nits + : dt_conf_get_int("plugins/lighttable/export/pq_peak_nits"); + return fmaxf(1.0f, nits / DT_HDR_REFERENCE_WHITE_NITS); + } + case DT_HDR_HLG_REC2020: + case DT_HDR_HLG_P3: + // colorout's OOTF clips HLG at the nominal 1000 nit display + return DT_HDR_HLG_PEAK_NITS / DT_HDR_REFERENCE_WHITE_NITS; + default: + return 1.0f; + } +} + dt_iop_order_iccprofile_info_t * dt_ioppr_set_pipe_output_profile_info(struct dt_develop_t *dev, struct dt_dev_pixelpipe_t *pipe, @@ -834,6 +863,7 @@ dt_ioppr_set_pipe_output_profile_info(struct dt_develop_t *dev, profile_info = dt_ioppr_add_profile_info_to_list(dev, DT_COLORSPACE_SRGB, "", intent); } pipe->output_profile_info = profile_info; + pipe->output_headroom = _output_headroom(profile_info->type); return profile_info; } diff --git a/src/common/iop_profile.h b/src/common/iop_profile.h index e645538fd92..13f28f0c421 100644 --- a/src/common/iop_profile.h +++ b/src/common/iop_profile.h @@ -116,6 +116,10 @@ dt_ioppr_set_pipe_output_profile_info(struct dt_develop_t *dev, const char *filename, const int intent); +/** set the PQ peak, in nits, for the pipes processed by the calling thread, + or 0 to read it from the export module's setting again */ +void dt_ioppr_set_thread_pq_peak_nits(const int nits); + /** Record the export profile, used as a cache-identity tag. Returns the (possibly NULL) interned profile-info pointer, no fallback for sRGB in case of failure. */ dt_iop_order_iccprofile_info_t * diff --git a/src/control/jobs/control_jobs.c b/src/control/jobs/control_jobs.c index f7c446f1bac..60fde57a422 100644 --- a/src/control/jobs/control_jobs.c +++ b/src/control/jobs/control_jobs.c @@ -105,6 +105,7 @@ typedef struct dt_control_export_t dt_colorspaces_color_profile_type_t icc_type; gchar *icc_filename; dt_iop_color_intent_t icc_intent; + int pq_peak_nits; gchar *metadata_export; } dt_control_export_t; @@ -2096,6 +2097,9 @@ static int32_t _control_export_job_run(dt_job_t *job) GList *t = params->index; double prev_time = 0; + // the peak set when the export was queued, not the setting's current value + dt_ioppr_set_thread_pq_peak_nits(settings->pq_peak_nits); + while(t && !_job_cancelled(job)) { const dt_imgid_t imgid = GPOINTER_TO_INT(t->data); @@ -2148,6 +2152,7 @@ static int32_t _control_export_job_run(dt_job_t *job) fraction += 1.0 / total; _update_progress(job, fraction, &prev_time); } + dt_ioppr_set_thread_pq_peak_nits(0); g_list_free_full(metadata.list, g_free); if(mstorage->finalize_store) mstorage->finalize_store(mstorage, sdata); @@ -2747,6 +2752,7 @@ void dt_control_export(GList *imgid_list, const dt_colorspaces_color_profile_type_t icc_type, const gchar *icc_filename, const dt_iop_color_intent_t icc_intent, + const int pq_peak_nits, const gchar *metadata_export) { dt_job_t *job = dt_control_job_create(&_control_export_job_run, "export"); @@ -2803,6 +2809,7 @@ void dt_control_export(GList *imgid_list, data->icc_type = icc_type; data->icc_filename = g_strdup(icc_filename); data->icc_intent = icc_intent; + data->pq_peak_nits = pq_peak_nits; data->metadata_export = g_strdup(metadata_export); dt_control_job_add_progress(job, _("export images"), TRUE); diff --git a/src/control/jobs/control_jobs.h b/src/control/jobs/control_jobs.h index 5544a255de1..8cf519dc216 100644 --- a/src/control/jobs/control_jobs.h +++ b/src/control/jobs/control_jobs.h @@ -64,6 +64,7 @@ void dt_control_export(GList *imgid_list, const dt_colorspaces_color_profile_type_t icc_type, const gchar *icc_filename, const dt_iop_color_intent_t icc_intent, + const int pq_peak_nits, const gchar *metadata_export); void dt_control_merge_hdr(void); void dt_control_import(GList *imgs, const char *datetime_override, const gboolean inplace); diff --git a/src/develop/pixelpipe_cache.c b/src/develop/pixelpipe_cache.c index a1bd507d814..9fa7278863e 100644 --- a/src/develop/pixelpipe_cache.c +++ b/src/develop/pixelpipe_cache.c @@ -122,6 +122,8 @@ static dt_hash_t _dev_pixelpipe_cache_basichash(dt_dev_pixelpipe_t *pipe, hash = dt_hash(hash, &pipe->work_profile_info, sizeof(pipe->work_profile_info)); hash = dt_hash(hash, &pipe->output_profile_info, sizeof(pipe->output_profile_info)); hash = dt_hash(hash, &pipe->export_profile_info, sizeof(pipe->export_profile_info)); + // the PQ peak is a preference, so the output profile alone does not fix the headroom + hash = dt_hash(hash, &pipe->output_headroom, sizeof(pipe->output_headroom)); // go through all modules up to position and compute a hash using the operation and params. GList *pieces = pipe->nodes; diff --git a/src/develop/pixelpipe_hb.c b/src/develop/pixelpipe_hb.c index 5abcda2f3ec..9928aeae99f 100644 --- a/src/develop/pixelpipe_hb.c +++ b/src/develop/pixelpipe_hb.c @@ -318,6 +318,7 @@ static gboolean _dev_pixelpipe_init_cached(dt_dev_pixelpipe_t *pipe, pipe->input_profile_info = NULL; pipe->output_profile_info = NULL; pipe->export_profile_info = NULL; + pipe->output_headroom = 1.0f; pipe->runs = 0; pipe->bcache_data = NULL; pipe->bcache_size = 0; diff --git a/src/develop/pixelpipe_hb.h b/src/develop/pixelpipe_hb.h index bd97cfca498..b1e5c2d1480 100644 --- a/src/develop/pixelpipe_hb.h +++ b/src/develop/pixelpipe_hb.h @@ -237,6 +237,9 @@ typedef struct dt_dev_pixelpipe_t struct dt_iop_order_iccprofile_info_t *output_profile_info; /** used only as a cache-identity tag to invalidate the cache **/ struct dt_iop_order_iccprofile_info_t *export_profile_info; + /** peak luminance of the output relative to the 203 nit reference white, + 1.0 for SDR; set with the output profile, read by tone mappers **/ + float output_headroom; // instances of pixelpipe, stored in GList of dt_dev_pixelpipe_iop_t GList *nodes; diff --git a/src/iop/agx.c b/src/iop/agx.c index f39ae9a4360..04159e5f005 100644 --- a/src/iop/agx.c +++ b/src/iop/agx.c @@ -38,7 +38,7 @@ #include #include -DT_MODULE_INTROSPECTION(7, dt_iop_agx_params_t) +DT_MODULE_INTROSPECTION(8, dt_iop_agx_params_t) const char *name() { @@ -152,6 +152,10 @@ typedef struct dt_iop_agx_params_t // v5 gboolean completely_reverse_primaries; // $DEFAULT: FALSE $DESCRIPTION: "reverse all" + + // v8 + gboolean target_white_from_output; // $DEFAULT: TRUE $DESCRIPTION: "follow output peak" + float hdr_purity; // $MIN: 0.f $MAX: 1.f $DEFAULT: 0.5f $DESCRIPTION: "HDR purity" } dt_iop_agx_params_t; typedef struct dt_iop_basic_curve_controls_t @@ -168,8 +172,13 @@ typedef struct dt_iop_agx_gui_data_t GtkNotebook *notebook; GtkWidget *auto_gamma; GtkWidget *curve_gamma; + GtkWidget *target_white_from_output; + GtkWidget *hdr_purity; GtkDrawingArea *graph_drawing_area; + // the darkroom full pipe's headroom, see _ui_pipe_finished() + float output_headroom; + dt_gui_collapsible_section_t look_section; dt_gui_collapsible_section_t graph_section; dt_gui_collapsible_section_t advanced_section; @@ -201,6 +210,19 @@ typedef struct dt_iop_agx_gui_data_t GtkWidget *set_post_curve_primaries_from_pre_button; } dt_iop_agx_gui_data_t; +// HDR output, see _set_hdr_params() +typedef struct hdr_params_t +{ + gboolean enabled; + float headroom; + float purity; + float log_min; + float pivot_x; + float pivot_y; + float toe_scale; + float shoulder_scale; +} hdr_params_t; + typedef struct tone_mapping_params_t { float black_relative_ev; @@ -249,6 +271,8 @@ typedef struct tone_mapping_params_t float look_original_hue_mix_ratio; gboolean look_tuned; gboolean restore_hue; + + hdr_params_t hdr; } tone_mapping_params_t; typedef struct primaries_params_t @@ -269,6 +293,8 @@ typedef struct dt_iop_agx_data_t { tone_mapping_params_t tone_mapping_params; primaries_params_t primaries_params; + // to rebuild the curve for the output headroom, only known at process time + dt_iop_agx_params_t params; } dt_iop_agx_data_t; static void _set_scene_referred_default_params(dt_iop_agx_params_t *p); @@ -294,6 +320,21 @@ int legacy_params(dt_iop_module_t *self, return 0; } + if(old_version == 7) + { + dt_iop_agx_params_t *np = calloc(1, sizeof(dt_iop_agx_params_t)); + memcpy(np, old_params, offsetof(dt_iop_agx_params_t, target_white_from_output)); + // existing edits keep their white at the reference white and do not + // follow the output's peak, so that they render as before + np->target_white_from_output = FALSE; + np->hdr_purity = 0.5f; + *new_params = np; + *new_params_size = sizeof(dt_iop_agx_params_t); + *new_version = 8; + + return 0; + } + return 1; } @@ -541,12 +582,15 @@ static inline float _line(const float x, // the actual 'rise' the curve needs to cover. const float actual_rise = fmaxf(_epsilon, limit_y - transition_y); - const float transformed_projected_rise = powf(projected_rise, -power); - const float transformed_actual_rise = powf(actual_rise, -power); + // in double: when the pivot reaches target white, the rise is clamped to + // _epsilon and powf() overflows to inf for powers above about 6.4, which + // gives a zero scale and NaN in _scaled_sigmoid() + const double transformed_projected_rise = pow(projected_rise, -power); + const double transformed_actual_rise = pow(actual_rise, -power); - const float base = fmaxf(_epsilon, transformed_actual_rise - transformed_projected_rise); + const double base = fmax(_epsilon, transformed_actual_rise - transformed_projected_rise); - const float scale_value = powf(base, -1.f / power); + const float scale_value = pow(base, -1.0 / power); // avoid 'explosions' return fminf(1e9f, scale_value); @@ -839,7 +883,8 @@ static inline float _calculate_slope_gamma_compensation(const float gamma, static tone_mapping_params_t _calculate_tone_mapping_params(const dt_iop_agx_params_t *p) { - tone_mapping_params_t tone_mapping_params; + tone_mapping_params_t tone_mapping_params = { 0 }; + tone_mapping_params.hdr.headroom = 1.f; // look tone_mapping_params.look_lift = p->look_lift; @@ -960,6 +1005,53 @@ static tone_mapping_params_t _calculate_tone_mapping_params(const dt_iop_agx_par return tone_mapping_params; } +// Blender's AgX HDR (AgXHLG.py in github.com/EaryChow/AgX_LUT_Gen) darkens +// the linear curve output with a second sigmoid in log2 space: 20 EV below +// the pivot up to 1.0, the peak, maps to [0, 1]. It moves the pivot output +// down by the headroom, so the pivot keeps its SDR brightness, with slope 1 +// and power 3 below it and power 1 above it +static void _set_hdr_params(hdr_params_t *hdr, + const dt_iop_agx_params_t *p, + const float headroom) +{ + const float pivot = CLAMPF(p->curve_pivot_y_linear_output, _epsilon, 0.99f); + + hdr->enabled = TRUE; + hdr->headroom = headroom; + hdr->purity = p->hdr_purity; + hdr->log_min = log2f(pivot) - 20.f; + hdr->pivot_x = 1.f - log2f(pivot) / hdr->log_min; + hdr->pivot_y = 1.f - log2f(pivot / headroom) / hdr->log_min; + hdr->toe_scale = -_scale(1.f, 1.f, 1.f - hdr->pivot_x, 1.f - hdr->pivot_y, 1.f, 3.f); + // _scale() cannot give the negative scale of a convex segment: with + // power 1 the shoulder is the hyperbola through the pivot and (1, 1) + const float dx = 1.f - hdr->pivot_x; + const float dy = 1.f - hdr->pivot_y; + hdr->shoulder_scale = -dx * dy / (dy - dx); +} + +// when following the output, the curve's output, target white included, +// is relative to the output's peak, and HDR output gets a harder shoulder +// and _agx_hdr(), as in Blender's AgX HDR. SDR output is unchanged +static tone_mapping_params_t _pipe_tone_mapping_params(const dt_iop_agx_params_t *p, + const float headroom) +{ + if(!p->target_white_from_output) + return _calculate_tone_mapping_params(p); + + const float hdr_headroom = fmaxf(1.f, headroom); + dt_iop_agx_params_t pipe_p = *p; + // capped at the slider's maximum: well above it, powf() in _sigmoid() + // overflows, e.g. for a high pivot target and the highest PQ peaks + pipe_p.curve_shoulder_power = fminf(pipe_p.curve_shoulder_power + * powf(hdr_headroom, log10f(2.f)), 10.f); + + tone_mapping_params_t tone_mapping_params = _calculate_tone_mapping_params(&pipe_p); + if(hdr_headroom > 1.f) + _set_hdr_params(&tone_mapping_params.hdr, &pipe_p, hdr_headroom); + return tone_mapping_params; +} + static primaries_params_t _get_primaries_params(const dt_iop_agx_params_t *p) { primaries_params_t primaries_params; @@ -1057,6 +1149,43 @@ static void _adjust_relative_exposure_from_exposure_params(dt_iop_module_t *self _update_pivot_x(old_black_ev, old_white_ev, self, p); } +// x is linear, relative to the peak +static inline float _hdr_darken(const float x, + const hdr_params_t *hdr) +{ + const float log_x = CLIP(1.f - log2f(fmaxf(x, 1e-12f)) / hdr->log_min); + const float u = log_x - hdr->pivot_x; + const float log_y = + u < 0.f + ? _scaled_sigmoid(log_x, hdr->toe_scale, 1.f, 3.f, + hdr->pivot_x, hdr->pivot_y) + : hdr->pivot_y + + hdr->shoulder_scale * u / (hdr->shoulder_scale + u); + return x > 0.f ? exp2f(hdr->log_min * (1.f - log_y)) : 0.f; +} + +static inline void _agx_hdr(dt_aligned_pixel_t pixel_in_out, + const hdr_params_t *hdr) +{ + dt_aligned_pixel_t hsv_before = { 0.f }; + dt_aligned_pixel_t hsv_after = { 0.f }; + dt_aligned_pixel_t darkened = { 0.f }; + + dt_RGB_2_HSV(pixel_in_out, hsv_before); + for_three_channels(k, aligned(pixel_in_out, darkened : 16)) + darkened[k] = _hdr_darken(pixel_in_out[k], hdr); + dt_RGB_2_HSV(darkened, hsv_after); + + // keep part of the hue and saturation shift of the per-channel darkening + hsv_after[0] = _lerp_hue(hsv_before[0], hsv_after[0], 1.f - hdr->purity); + hsv_after[1] = hsv_before[1] + hdr->purity * (hsv_after[1] - hsv_before[1]); + dt_HSV_2_RGB(hsv_after, darkened); + + // back to 1.0 = reference white + for_three_channels(k, aligned(pixel_in_out, darkened : 16)) + pixel_in_out[k] = hdr->headroom * darkened[k]; +} + static void _agx_tone_mapping(dt_aligned_pixel_t rgb_in_out, const tone_mapping_params_t *params, const dt_colormatrix_t rendering_to_xyz_transposed) @@ -1084,6 +1213,9 @@ static void _agx_tone_mapping(dt_aligned_pixel_t rgb_in_out, transformed_pixel[k] = powf(fmaxf(0.f, transformed_pixel[k]), params->curve_gamma); } + if(params->hdr.enabled) + _agx_hdr(transformed_pixel, ¶ms->hdr); + // get post-curve chroma angle if(params->restore_hue) { @@ -1103,6 +1235,16 @@ static void _agx_tone_mapping(dt_aligned_pixel_t rgb_in_out, } } +// the curve, plus the darkening of HDR output, gamma-encoded and relative to the peak +static float _rendered_curve(const float x, + const tone_mapping_params_t *params) +{ + const float y = _apply_curve(x, params); + return params->hdr.enabled + ? powf(_hdr_darken(powf(y, params->curve_gamma), ¶ms->hdr), 1.f / params->curve_gamma) + : y; +} + static void _apply_auto_black_exposure(const dt_iop_module_t *self) { dt_iop_agx_params_t *p = self->params; @@ -1188,6 +1330,8 @@ static void _apply_auto_pivot_xy(dt_iop_module_t *self, const dt_iop_order_iccpr const float range = p->range_white_relative_ev - p->range_black_relative_ev; const float picked_pivot_x = (picked_ev - p->range_black_relative_ev) / range; + // the SDR curve, also with HDR output: pivot target output is an SDR + // value, and the pick must not depend on the display it was made on const tone_mapping_params_t tone_mapping_params = _calculate_tone_mapping_params(p); @@ -1378,6 +1522,9 @@ void process(dt_iop_module_t *self, rendering_profile.nonlinearlut = FALSE; // no LUT for this linear transform const gboolean base_working_same_profile = pipe_work_profile == base_profile; + const tone_mapping_params_t tone_mapping_params = d->params.target_white_from_output + ? _pipe_tone_mapping_params(&d->params, piece->pipe->output_headroom) + : d->tone_mapping_params; DT_OMP_FOR() for(size_t k = 0; k < 4 * n_pixels; k += 4) @@ -1409,7 +1556,7 @@ void process(dt_iop_module_t *self, dt_apply_transposed_color_matrix(base_rgb, base_to_rendering_transposed, rendering_rgb); // Apply the tone mapping curve and look adjustments - _agx_tone_mapping(rendering_rgb, &d->tone_mapping_params, + _agx_tone_mapping(rendering_rgb, &tone_mapping_params, rendering_profile.matrix_in_transposed); // Convert from internal rendering space back to pipe working space @@ -1427,7 +1574,9 @@ static gboolean _agx_draw_curve(GtkWidget *widget, const dt_iop_agx_params_t *p = self->params; dt_iop_agx_gui_data_t *g = self->gui_data; - const tone_mapping_params_t tone_mapping_params = _calculate_tone_mapping_params(p); + // draw the curve the center view renders + const tone_mapping_params_t tone_mapping_params = + _pipe_tone_mapping_params(p, g->output_headroom); gtk_widget_get_allocation(widget, &g->allocation); g->allocation.height -= DT_RESIZE_HANDLE_SIZE; @@ -1530,7 +1679,8 @@ static gboolean _agx_draw_curve(GtkWidget *widget, cairo_identity_matrix(cr); // Reset transformations for text set_color(cr, darktable.bauhaus->graph_fg); // Use standard text color - snprintf(text, sizeof(text), "%.0f%%", 100.0f * y_linear); // Format the linear value + // the top of the graph is the peak, so HDR output labels it above 100% + snprintf(text, sizeof(text), "%.0f%%", 100.0f * y_linear * tone_mapping_params.hdr.headroom); pango_layout_set_text(layout, text, -1); pango_layout_get_pixel_extents(layout, &g->ink, NULL); @@ -1614,11 +1764,11 @@ static gboolean _agx_draw_curve(GtkWidget *widget, const int steps = 200; // draw the main curve - cairo_move_to(cr, 0, _apply_curve(0, &tone_mapping_params) * graph_height); + cairo_move_to(cr, 0, _rendered_curve(0, &tone_mapping_params) * graph_height); for(int k = 1; k <= steps; k++) { const float x_norm = (float)k / steps; - const float y_norm = _apply_curve(x_norm, &tone_mapping_params); + const float y_norm = _rendered_curve(x_norm, &tone_mapping_params); cairo_line_to(cr, x_norm * graph_width, y_norm * graph_height); } cairo_stroke(cr); @@ -1628,11 +1778,11 @@ static gboolean _agx_draw_curve(GtkWidget *widget, { cairo_set_source_rgb(cr, 0.75, .5, 0.); const int toe_end_step = ceilf(tone_mapping_params.toe_transition_x * steps); - cairo_move_to(cr, 0, _apply_curve(0, &tone_mapping_params) * graph_height); + cairo_move_to(cr, 0, _rendered_curve(0, &tone_mapping_params) * graph_height); for(int k = 1; k <= toe_end_step; k++) { const float x_norm = (float)k / steps; - const float y_norm = _apply_curve(x_norm, &tone_mapping_params); + const float y_norm = _rendered_curve(x_norm, &tone_mapping_params); cairo_line_to(cr, x_norm * graph_width, y_norm * graph_height); } cairo_stroke(cr); @@ -1643,18 +1793,18 @@ static gboolean _agx_draw_curve(GtkWidget *widget, cairo_set_source_rgb(cr, 0.75, .5, 0.); const int shoulder_start_step = floorf(tone_mapping_params.shoulder_transition_x * steps); float x_norm = (float)shoulder_start_step / steps; - float y_norm = _apply_curve(x_norm, &tone_mapping_params); + float y_norm = _rendered_curve(x_norm, &tone_mapping_params); cairo_move_to(cr, x_norm * graph_width, y_norm * graph_height); for(int k = shoulder_start_step + 1; k <= steps; k++) { x_norm = (float)k / steps; - y_norm = _apply_curve(x_norm, &tone_mapping_params); + y_norm = _rendered_curve(x_norm, &tone_mapping_params); cairo_line_to(cr, x_norm * graph_width, y_norm * graph_height); } cairo_stroke(cr); } - // draw the toe start, shoulder start, pivot + // draw the toe start, shoulder start, pivot, on the curve as rendered cairo_save(cr); // restore line width and color for points cairo_set_line_width(cr, line_width); @@ -1666,21 +1816,24 @@ static gboolean _agx_draw_curve(GtkWidget *widget, cairo_clip(cr); const float x_toe_graph = tone_mapping_params.toe_transition_x * graph_width; - const float y_toe_graph = tone_mapping_params.toe_transition_y * graph_height; + const float y_toe_graph = + _rendered_curve(tone_mapping_params.toe_transition_x, &tone_mapping_params) * graph_height; set_color(cr, darktable.bauhaus->graph_fg_active); cairo_arc(cr, x_toe_graph, y_toe_graph, DT_PIXEL_APPLY_DPI(4), 0, 2. * M_PI); cairo_fill(cr); cairo_stroke(cr); const float x_shoulder_graph = tone_mapping_params.shoulder_transition_x * graph_width; - const float y_shoulder_graph = tone_mapping_params.shoulder_transition_y * graph_height; + const float y_shoulder_graph = + _rendered_curve(tone_mapping_params.shoulder_transition_x, &tone_mapping_params) * graph_height; set_color(cr, darktable.bauhaus->graph_fg_active); cairo_arc(cr, x_shoulder_graph, y_shoulder_graph, DT_PIXEL_APPLY_DPI(4), 0, 2. * M_PI); cairo_fill(cr); cairo_stroke(cr); const float x_pivot_graph = tone_mapping_params.pivot_x * graph_width; - const float y_pivot_graph = tone_mapping_params.pivot_y * graph_height; + const float y_pivot_graph = + _rendered_curve(tone_mapping_params.pivot_x, &tone_mapping_params) * graph_height; set_color(cr, darktable.bauhaus->graph_fg_active); cairo_arc(cr, x_pivot_graph, y_pivot_graph, DT_PIXEL_APPLY_DPI(4), 0, 2. * M_PI); cairo_fill(cr); @@ -1732,7 +1885,8 @@ static void _update_curve_warnings(dt_iop_module_t *self) if(!g) return; const gboolean warnings_enabled = dt_conf_get_bool("plugins/darkroom/agx/enable_curve_warnings"); - const tone_mapping_params_t params = _calculate_tone_mapping_params(p); + const tone_mapping_params_t params = + _pipe_tone_mapping_params(p, g->output_headroom); dt_bauhaus_widget_set_quad_paint(g->basic_curve_controls.curve_toe_power, params.need_convex_toe && warnings_enabled @@ -1747,6 +1901,9 @@ static void _update_redraw_dynamic_gui(dt_iop_module_t* const self, const dt_iop_agx_params_t* const p) { gtk_widget_set_visible(g->curve_gamma, !p->auto_gamma); + // only edits made before HDR output existed have it off + gtk_widget_set_visible(g->target_white_from_output, !p->target_white_from_output); + gtk_widget_set_visible(g->hdr_purity, p->target_white_from_output); gtk_widget_set_visible(g->primaries_controls_vbox, !p->disable_primaries_adjustments); const gboolean post_curve_primaries_available = !p->completely_reverse_primaries && !p->disable_primaries_adjustments; gtk_widget_set_visible(g->post_curve_primaries_controls_vbox, post_curve_primaries_available); @@ -2000,7 +2157,24 @@ static GtkWidget* _create_advanced_box(dt_iop_module_t *self, dt_bauhaus_slider_set_format(slider, "%"); dt_bauhaus_slider_set_digits(slider, 2); dt_bauhaus_slider_set_factor(slider, 100.f); - gtk_widget_set_tooltip_text(slider, _("max linear output power")); + gtk_widget_set_tooltip_text(slider, _("max linear output power, relative to the peak of the output")); + + g->target_white_from_output = + dt_bauhaus_toggle_from_params(section, "target_white_from_output"); + gtk_widget_set_tooltip_text + (g->target_white_from_output, + _("this edit predates HDR output and keeps its white at the 203 nit\n" + "reference white. turn on to extend the highlights to the peak of\n" + "an HDR output (1000 nits for HLG, the PQ peak set in export),\n" + "as in Blender's AgX HDR. SDR output does not change")); + + g->hdr_purity = dt_bauhaus_slider_from_params(section, "hdr_purity"); + dt_bauhaus_slider_set_format(g->hdr_purity, "%"); + dt_bauhaus_slider_set_digits(g->hdr_purity, 0); + dt_bauhaus_slider_set_factor(g->hdr_purity, 100.f); + gtk_widget_set_tooltip_text(g->hdr_purity, + _("for HDR output, how much of the extra saturation and hue shift\n" + "from extending the highlights to the peak is kept")); // Toe length slider = dt_bauhaus_slider_from_params(section, "curve_linear_ratio_below_pivot"); @@ -2475,9 +2649,33 @@ static void _notebook_page_changed(GtkNotebook *notebook, } } +// a new display profile or PQ peak changes the curve of the darkroom view +static void _ui_pipe_finished(gpointer instance, + dt_iop_module_t *self) +{ + dt_iop_agx_gui_data_t *g = self->gui_data; + dt_dev_pixelpipe_t *pipe = self->dev->full.pipe; + if(!g || !pipe) return; + + // read on every finished run, cache hits included, since AgX's + // process() does not run then. colorout sets the headroom while + // synching under busy_mutex; a pipe that is busy again will signal + // once more, and a blocking lock could deadlock against process() + if(dt_pthread_mutex_trylock(&pipe->busy_mutex)) return; + const float headroom = pipe->output_headroom; + dt_pthread_mutex_unlock(&pipe->busy_mutex); + + if(headroom == g->output_headroom) return; + + g->output_headroom = headroom; + _update_curve_warnings(self); + gtk_widget_queue_draw(GTK_WIDGET(g->graph_drawing_area)); +} + void gui_init(dt_iop_module_t *self) { dt_iop_agx_gui_data_t *g = IOP_GUI_ALLOC(agx); + g->output_headroom = 1.f; static dt_action_def_t notebook_def = {}; g->notebook = dt_ui_notebook_new(¬ebook_def); @@ -2509,10 +2707,14 @@ void gui_init(dt_iop_module_t *self) // Finally, add the remaining sections to the settings page _add_look_box(settings_section, g); _create_primaries_page(self, g); + + DT_CONTROL_SIGNAL_HANDLE(DT_SIGNAL_DEVELOP_UI_PIPE_FINISHED, _ui_pipe_finished); } static void _set_default_curve_and_look_params(dt_iop_agx_params_t *p) { + p->target_white_from_output = TRUE; + p->hdr_purity = 0.5f; p->look_slope = 1.f; p->look_brightness = 1.f; p->look_lift = 0.f; @@ -2729,6 +2931,7 @@ void commit_params(dt_iop_module_t *self, // Calculate curve parameters once processing_params->tone_mapping_params = _calculate_tone_mapping_params(p); processing_params->primaries_params = _get_primaries_params(p); + processing_params->params = *p; } void reload_defaults(dt_iop_module_t *self) @@ -2843,11 +3046,14 @@ int process_cl(dt_iop_module_t *self, } const int base_working_same_profile = pipe_work_profile == base_profile; + const tone_mapping_params_t tone_mapping_params = d->params.target_white_from_output + ? _pipe_tone_mapping_params(&d->params, piece->pipe->output_headroom) + : d->tone_mapping_params; err = dt_opencl_enqueue_kernel_2d_args( devid, gd->kernel_agx, width, height, CLARG(dev_in), CLARG(dev_out), CLARG(width), CLARG(height), - CLARG(d->tone_mapping_params), + CLARG(tone_mapping_params), CLARG(dev_pipe_to_base), CLARG(dev_base_to_rendering), CLARG(dev_rendering_to_pipe), diff --git a/src/iop/sigmoid.c b/src/iop/sigmoid.c index bfdba9731e3..f11252e0077 100644 --- a/src/iop/sigmoid.c +++ b/src/iop/sigmoid.c @@ -31,7 +31,7 @@ #include #include -DT_MODULE_INTROSPECTION(3, dt_iop_sigmoid_params_t) +DT_MODULE_INTROSPECTION(4, dt_iop_sigmoid_params_t) #define MIDDLE_GREY 0.1845f @@ -70,6 +70,7 @@ typedef struct dt_iop_sigmoid_params_t float blue_rotation; // $MIN: -0.4 $MAX: 0.4 $DEFAULT: 0.0 $DESCRIPTION: "blue rotation" float purity; // $MIN: 0.0 $MAX: 1.0 $DEFAULT: 0.0 $DESCRIPTION: "recover purity" dt_iop_sigmoid_base_primaries_t base_primaries; // $DEFAULT: DT_SIGMOID_WORK_PROFILE $DESCRIPTION: "base primaries" + gboolean white_target_from_output; // $DEFAULT: TRUE $DESCRIPTION: "follow output peak" } dt_iop_sigmoid_params_t; int legacy_params(dt_iop_module_t *self, @@ -153,6 +154,20 @@ int legacy_params(dt_iop_module_t *self, return 0; } + else if(old_version == 3) + { + dt_iop_sigmoid_params_t *n = calloc(1, sizeof(dt_iop_sigmoid_params_t)); + memcpy(n, old_params, sizeof(dt_iop_sigmoid_params_v3_t)); + // existing edits keep their target white and do not follow the + // output's peak, so that they render as before + n->white_target_from_output = FALSE; + + *new_params = n; + *new_params_size = sizeof(dt_iop_sigmoid_params_t); + *new_version = 4; + + return 0; + } return 1; } @@ -171,11 +186,16 @@ typedef struct dt_iop_sigmoid_data_t float rotation[3]; float purity; dt_iop_sigmoid_base_primaries_t base_primaries; + // to rebuild the curve for the output headroom, only known at process time + gboolean white_target_from_output; + float middle_grey_contrast; + float contrast_skewness; } dt_iop_sigmoid_data_t; typedef struct dt_iop_sigmoid_gui_data_t { GtkWidget *color_processing_list, *hue_preservation_slider; + GtkWidget *white_target_toggle; dt_gui_collapsible_section_t display_luminance_section, primaries_section; } dt_iop_sigmoid_gui_data_t; @@ -248,6 +268,7 @@ void init_presets(dt_iop_module_so_t *self) // others dt_iop_sigmoid_params_t p = { 0 }; + p.white_target_from_output = TRUE; p.display_white_target = 100.0f; p.display_black_target = 0.0152f; p.color_processing = DT_SIGMOID_METHOD_PER_CHANNEL; @@ -315,13 +336,9 @@ static inline float _generalized_loglogistic_sigmoid(const float value, return dt_isnan(paper_response) ? magnitude : paper_response; } -void commit_params(dt_iop_module_t *self, - dt_iop_params_t *p1, - dt_dev_pixelpipe_t *pipe, - dt_dev_pixelpipe_iop_t *piece) +// fit the curve to the contrast, skew, black target and the given white target ratio +static void _compute_curve(dt_iop_sigmoid_data_t *d, const float white_target) { - const dt_iop_sigmoid_params_t *params = (dt_iop_sigmoid_params_t *)p1; - dt_iop_sigmoid_data_t *module_data = piece->data; /* Calculate actual skew log logistic parameters to fulfill the following: * f(scene_zero) = display_black_target * f(scene_grey) = MIDDLE_GREY @@ -330,7 +347,7 @@ void commit_params(dt_iop_module_t *self, */ // Calculate a reference slope for no skew and a normalized display - const float ref_film_power = params->middle_grey_contrast; + const float ref_film_power = d->middle_grey_contrast; const float ref_paper_power = 1.0f; const float ref_magnitude = 1.0f; const float ref_film_fog = 0.0f; @@ -345,50 +362,80 @@ void commit_params(dt_iop_module_t *self, / 2.0f / delta; // Add skew - module_data->paper_power = powf(5.0f, -params->contrast_skewness); + d->paper_power = powf(5.0f, -d->contrast_skewness); // Slope at low film power const float temp_film_power = 1.0f; - const float temp_white_target = 0.01f * params->display_white_target; + const float temp_white_target = white_target; const float temp_white_grey_relation - = powf(temp_white_target / MIDDLE_GREY, 1.0f / module_data->paper_power) - 1.0f; + = powf(temp_white_target / MIDDLE_GREY, 1.0f / d->paper_power) - 1.0f; const float temp_paper_exposure = powf(MIDDLE_GREY, temp_film_power) * temp_white_grey_relation; const float temp_slope = (_generalized_loglogistic_sigmoid(MIDDLE_GREY + delta, temp_white_target, temp_paper_exposure, - ref_film_fog, temp_film_power, module_data->paper_power) + ref_film_fog, temp_film_power, d->paper_power) - _generalized_loglogistic_sigmoid(MIDDLE_GREY - delta, temp_white_target, temp_paper_exposure, - ref_film_fog, temp_film_power, module_data->paper_power)) + ref_film_fog, temp_film_power, d->paper_power)) / 2.0f / delta; // Figure out what film power fulfills the target slope // (linear when assuming display_black = 0.0) - module_data->film_power = ref_slope / temp_slope; + d->film_power = ref_slope / temp_slope; // Calculate the other parameters now that both film and paper power is known - module_data->white_target = 0.01f * params->display_white_target; - module_data->black_target = 0.01f * params->display_black_target; + d->white_target = white_target; const float white_grey_relation - = powf(module_data->white_target / MIDDLE_GREY, 1.0f / module_data->paper_power) - 1.0f; + = powf(d->white_target / MIDDLE_GREY, 1.0f / d->paper_power) - 1.0f; const float white_black_relation - = powf(module_data->black_target / module_data->white_target, -1.0f / module_data->paper_power) - 1.0f; - - module_data->film_fog = MIDDLE_GREY * powf(white_grey_relation, 1.0f / module_data->film_power) - / (powf(white_black_relation, 1.0f / module_data->film_power) - - powf(white_grey_relation, 1.0f / module_data->film_power)); - module_data->paper_exposure - = powf(module_data->film_fog + MIDDLE_GREY, module_data->film_power) * white_grey_relation; - - module_data->color_processing = params->color_processing; - module_data->hue_preservation = fminf(fmaxf(0.01f * params->hue_preservation, 0.0f), 1.0f); - - module_data->purity = params->purity; - module_data->inset[0] = params->red_inset; - module_data->inset[1] = params->green_inset; - module_data->inset[2] = params->blue_inset; - module_data->rotation[0] = params->red_rotation; - module_data->rotation[1] = params->green_rotation; - module_data->rotation[2] = params->blue_rotation; - module_data->base_primaries = params->base_primaries; + = powf(d->black_target / d->white_target, -1.0f / d->paper_power) - 1.0f; + + d->film_fog = MIDDLE_GREY * powf(white_grey_relation, 1.0f / d->film_power) + / (powf(white_black_relation, 1.0f / d->film_power) + - powf(white_grey_relation, 1.0f / d->film_power)); + d->paper_exposure + = powf(d->film_fog + MIDDLE_GREY, d->film_power) * white_grey_relation; +} + +void commit_params(dt_iop_module_t *self, + dt_iop_params_t *p1, + dt_dev_pixelpipe_t *pipe, + dt_dev_pixelpipe_iop_t *piece) +{ + const dt_iop_sigmoid_params_t *p = (dt_iop_sigmoid_params_t *)p1; + dt_iop_sigmoid_data_t *d = piece->data; + + d->middle_grey_contrast = p->middle_grey_contrast; + d->contrast_skewness = p->contrast_skewness; + d->black_target = 0.01f * p->display_black_target; + d->white_target_from_output = p->white_target_from_output; + _compute_curve(d, 0.01f * p->display_white_target); + + d->color_processing = p->color_processing; + d->hue_preservation = fminf(fmaxf(0.01f * p->hue_preservation, 0.0f), 1.0f); + + d->purity = p->purity; + d->inset[0] = p->red_inset; + d->inset[1] = p->green_inset; + d->inset[2] = p->blue_inset; + d->rotation[0] = p->red_rotation; + d->rotation[1] = p->green_rotation; + d->rotation[2] = p->blue_rotation; + d->base_primaries = p->base_primaries; +} + +// when following the output, target white is scaled by the output's headroom, +// so SDR output is unchanged +static const dt_iop_sigmoid_data_t *_pipe_data(const dt_dev_pixelpipe_iop_t *piece, + dt_iop_sigmoid_data_t *pipe_d) +{ + const dt_iop_sigmoid_data_t *d = piece->data; + const float headroom = piece->pipe->output_headroom; + if(!d->white_target_from_output || headroom <= 1.0f) return d; + + // capped at the slider's 1600% (3248 nits): the fit overflows for + // extreme contrast, skew and target black at the highest PQ peaks + *pipe_d = *d; + _compute_curve(pipe_d, fminf(d->white_target * headroom, 16.0f)); + return pipe_d; } static void _calculate_adjusted_primaries(const dt_iop_sigmoid_data_t *const module_data, @@ -563,23 +610,22 @@ static void _pixel_channel_order(const dt_aligned_pixel_t pix_in, dt_iop_sigmoid } } -void process_loglogistic_rgb_ratio(const dt_dev_pixelpipe_iop_t *piece, +void process_loglogistic_rgb_ratio(const dt_iop_sigmoid_data_t *d, const void *const ivoid, void *const ovoid, const dt_iop_roi_t *const roi_in, const dt_iop_roi_t *const roi_out) { - const dt_iop_sigmoid_data_t *module_data = piece->data; const float *const in = (const float *)ivoid; float *const out = (float *)ovoid; const size_t npixels = (size_t)roi_in->width * roi_in->height; - const float white_target = module_data->white_target; - const float black_target = module_data->black_target; - const float paper_exp = module_data->paper_exposure; - const float film_fog = module_data->film_fog; - const float contrast_power = module_data->film_power; - const float skew_power = module_data->paper_power; + const float white_target = d->white_target; + const float black_target = d->black_target; + const float paper_exp = d->paper_exposure; + const float film_fog = d->film_fog; + const float contrast_power = d->film_power; + const float skew_power = d->paper_power; DT_OMP_FOR() for(size_t k = 0; k < 4 * npixels; k += 4) @@ -702,27 +748,27 @@ static inline void _preserve_hue_and_energy(const dt_aligned_pixel_t pix_in, void process_loglogistic_per_channel(dt_develop_t *dev, const dt_dev_pixelpipe_iop_t *piece, + const dt_iop_sigmoid_data_t *d, const void *const ivoid, void *const ovoid, const dt_iop_roi_t *const roi_in, const dt_iop_roi_t *const roi_out) { - const dt_iop_sigmoid_data_t *module_data = piece->data; const float *const in = (const float *)ivoid; float *const out = (float *)ovoid; const size_t npixels = (size_t)roi_in->width * roi_in->height; - const float white_target = module_data->white_target; - const float paper_exp = module_data->paper_exposure; - const float film_fog = module_data->film_fog; - const float contrast_power = module_data->film_power; - const float skew_power = module_data->paper_power; - const float hue_preservation = module_data->hue_preservation; + const float white_target = d->white_target; + const float paper_exp = d->paper_exposure; + const float film_fog = d->film_fog; + const float contrast_power = d->film_power; + const float skew_power = d->paper_power; + const float hue_preservation = d->hue_preservation; const dt_iop_order_iccprofile_info_t *pipe_work_profile = dt_ioppr_get_pipe_work_profile_info(piece->pipe); - const dt_iop_order_iccprofile_info_t *base_profile = _get_base_profile(dev, pipe_work_profile, module_data->base_primaries); + const dt_iop_order_iccprofile_info_t *base_profile = _get_base_profile(dev, pipe_work_profile, d->base_primaries); dt_colormatrix_t pipe_to_base, base_to_rendering, rendering_to_pipe; - _calculate_adjusted_primaries(module_data, pipe_work_profile, base_profile, pipe_to_base, base_to_rendering, rendering_to_pipe); + _calculate_adjusted_primaries(d, pipe_work_profile, base_profile, pipe_to_base, base_to_rendering, rendering_to_pipe); DT_OMP_FOR() for(size_t k = 0; k < 4 * npixels; k += 4) @@ -769,15 +815,16 @@ void process(dt_iop_module_t *self, const dt_iop_roi_t *const roi_out) { // this is called for preview and full pipe separately, each with its own pixelpipe piece. - const dt_iop_sigmoid_data_t *module_data = piece->data; + dt_iop_sigmoid_data_t pipe_d; + const dt_iop_sigmoid_data_t *d = _pipe_data(piece, &pipe_d); - if(module_data->color_processing == DT_SIGMOID_METHOD_PER_CHANNEL) + if(d->color_processing == DT_SIGMOID_METHOD_PER_CHANNEL) { - process_loglogistic_per_channel(self->dev, piece, ivoid, ovoid, roi_in, roi_out); + process_loglogistic_per_channel(self->dev, piece, d, ivoid, ovoid, roi_in, roi_out); } else // DT_SIGMOID_METHOD_RGB_RATIO { - process_loglogistic_rgb_ratio(piece, ivoid, ovoid, roi_in, roi_out); + process_loglogistic_rgb_ratio(d, ivoid, ovoid, roi_in, roi_out); } } @@ -789,7 +836,8 @@ int process_cl(dt_iop_module_t *self, const dt_iop_roi_t *const roi_in, const dt_iop_roi_t *const roi_out) { - const dt_iop_sigmoid_data_t *const d = piece->data; + dt_iop_sigmoid_data_t pipe_d; + const dt_iop_sigmoid_data_t *const d = _pipe_data(piece, &pipe_d); const dt_iop_sigmoid_global_data_t *const gd = self->global_data; cl_int err = CL_MEM_OBJECT_ALLOCATION_FAILURE; @@ -866,6 +914,9 @@ void gui_changed(dt_iop_module_t *self, GtkWidget *w, void *previous) const dt_iop_sigmoid_gui_data_t *g = self->gui_data; const dt_iop_sigmoid_params_t *p = self->params; + // only edits made before HDR output existed have it off + gtk_widget_set_visible(g->white_target_toggle, !p->white_target_from_output); + if(!w || w == g->color_processing_list) { const gboolean is_per_channel = p->color_processing == DT_SIGMOID_METHOD_PER_CHANNEL; @@ -970,6 +1021,13 @@ void gui_init(dt_iop_module_t *self) dt_bauhaus_slider_set_format(slider, "%"); gtk_widget_set_tooltip_text(slider, _("the white luminance of the target display or print.\n" "can be used creatively for a faded look or blowing out whites earlier.")); + g->white_target_toggle = dt_bauhaus_toggle_from_params(self, "white_target_from_output"); + gtk_widget_set_tooltip_text + (g->white_target_toggle, + _("this edit predates HDR output and keeps target white as set.\n" + "turn on to scale target white to the peak of an HDR output\n" + "(1000 nits for HLG, the PQ peak set in export), up to 1600%\n" + "(3248 nits). SDR output does not change")); self->widget = main_box; } diff --git a/src/libs/export.c b/src/libs/export.c index f17bcc0b2a4..4ff8290c3d4 100644 --- a/src/libs/export.c +++ b/src/libs/export.c @@ -27,6 +27,7 @@ #include "control/conf.h" #include "control/control.h" #include "control/signal.h" +#include "develop/develop.h" #include "dtgtk/button.h" #include "dtgtk/stylemenu.h" #include "gui/accelerators.h" @@ -34,6 +35,7 @@ #include "imageio/imageio_module.h" #include "libs/lib.h" #include "libs/lib_api.h" +#include "views/view.h" #include #include @@ -42,7 +44,7 @@ #include -DT_MODULE(8) +DT_MODULE(9) typedef enum dt_export_batch_cols_t { @@ -64,7 +66,7 @@ typedef struct dt_lib_export_t GtkWidget *storage, *format; int format_lut[128]; uint32_t max_allowed_width , max_allowed_height; - GtkWidget *upscale, *profile, *intent, *style, *style_mode; + GtkWidget *upscale, *profile, *pq_peak, *intent, *style, *style_mode; dt_gui_collapsible_section_t cs; GtkWidget *batch_treeview; GtkButton *export_button, *batch_export_button; @@ -419,13 +421,14 @@ static void _export_with_current_settings(dt_lib_module_t *self) dt_conf_get_int(CONFIG_PREFIX "icctype"); gchar *icc_filename = dt_conf_get_string(CONFIG_PREFIX "iccprofile"); const dt_iop_color_intent_t icc_intent = dt_conf_get_int(CONFIG_PREFIX "iccintent"); + const int pq_peak_nits = dt_conf_get_int(CONFIG_PREFIX "pq_peak_nits"); GList *list = dt_act_on_get_images(TRUE, TRUE, TRUE); dt_control_export(list, max_width, max_height, format_index, storage_index, high_quality, upscale, scaledimension, is_scaling, scale_factor, export_masks, style, style_append, - icc_type, icc_filename, icc_intent, + icc_type, icc_filename, icc_intent, pq_peak_nits, d->metadata_export); g_free(icc_filename); @@ -442,6 +445,8 @@ static void _export_with_preset(const gchar *preset_name, dt_lib_module_t *self) // get current settings from the GUI int params_size; void *params = get_params(self, ¶ms_size); + // not in params when the current profile cannot be PQ + const int pq_peak_nits = dt_conf_get_int(CONFIG_PREFIX "pq_peak_nits"); sqlite3_stmt *stmt; DT_DEBUG_SQLITE3_PREPARE_V2( @@ -472,6 +477,7 @@ static void _export_with_preset(const gchar *preset_name, dt_lib_module_t *self) // restore previous settings set_params(self, params, params_size); + dt_bauhaus_slider_set(d->pq_peak, pq_peak_nits); } static void _export_button_clicked(GtkWidget *widget, dt_lib_module_t *self) @@ -735,6 +741,9 @@ void gui_reset(dt_lib_module_t *self) dt_confgen_get_int(CONFIG_PREFIX "iccintent", DT_DEFAULT) + 1); + dt_bauhaus_slider_set(d->pq_peak, dt_confgen_get_int(CONFIG_PREFIX "pq_peak_nits", + DT_DEFAULT)); + // iccprofile const int icctype = dt_confgen_get_int(CONFIG_PREFIX "icctype", DT_DEFAULT); gchar *iccfilename = dt_conf_get_string(CONFIG_PREFIX "iccprofile"); @@ -982,7 +991,18 @@ static void _storage_changed(GtkWidget *widget, dt_lib_module_t *self) dt_lib_gui_queue_update(self); } -static void _profile_changed(GtkWidget *widget, dt_lib_export_t *d) +// the PQ peak only matters when the export may be PQ +static gboolean _export_may_be_pq(const dt_colorspaces_color_profile_type_t icctype) +{ + return icctype == DT_COLORSPACE_NONE || dt_colorspaces_hdr_transfer(icctype) == DT_HDR_PQ; +} + +static void _update_pq_peak_visibility(dt_lib_export_t *d) +{ + gtk_widget_set_visible(d->pq_peak, _export_may_be_pq(dt_conf_get_int(CONFIG_PREFIX "icctype"))); +} + +static void _set_profile_conf(GtkWidget *widget) { int pos = dt_bauhaus_combobox_get(widget); if(pos > 0) @@ -1008,6 +1028,21 @@ static void _profile_changed(GtkWidget *widget, dt_lib_export_t *d) dt_conf_set_string(CONFIG_PREFIX "iccprofile", ""); } +static void _profile_changed(GtkWidget *widget, dt_lib_export_t *d) +{ + _set_profile_conf(widget); + _update_pq_peak_visibility(d); +} + +static void _pq_peak_changed(GtkWidget *widget, dt_lib_export_t *d) +{ + dt_conf_set_int(CONFIG_PREFIX "pq_peak_nits", (int)roundf(dt_bauhaus_slider_get(widget))); + // colorout reads the peak when it commits, and a darkroom on a PQ + // display renders with it too + if(dt_view_get_current() == DT_VIEW_DARKROOM) + dt_dev_reprocess_all(darktable.develop); +} + static void _dimensions_type_changed(GtkWidget *widget, dt_lib_export_t *d) { @@ -1617,6 +1652,20 @@ void gui_init(dt_lib_module_t *self) gtk_widget_set_tooltip_markup(d->profile, tooltip); g_free(tooltip); + // Add PQ peak slider + + d->pq_peak = dt_bauhaus_slider_new_action(DT_ACTION(self), 203.f, 10000.f, 1.f, + dt_confgen_get_int(CONFIG_PREFIX "pq_peak_nits", + DT_DEFAULT), 0); + dt_bauhaus_widget_set_label(d->pq_peak, NULL, N_("PQ peak")); + dt_bauhaus_slider_set_log_curve(d->pq_peak); + dt_bauhaus_slider_set_format(d->pq_peak, _(" nits")); + dt_bauhaus_slider_set(d->pq_peak, dt_conf_get_int(CONFIG_PREFIX "pq_peak_nits")); + gtk_widget_set_tooltip_text(d->pq_peak, + _("the brightest highlight of a PQ (HDR) export, in nits.\n" + "tone mappers that follow the output map their white to it.\n" + "HLG exports always peak at 1000 nits.")); + // Add intent combo DT_BAUHAUS_COMBOBOX_NEW_FULL @@ -1683,6 +1732,8 @@ void gui_init(dt_lib_module_t *self) // Set callback signals g_signal_connect(G_OBJECT(d->profile), "value-changed", G_CALLBACK(_profile_changed), (gpointer)d); + g_signal_connect(G_OBJECT(d->pq_peak), "value-changed", + G_CALLBACK(_pq_peak_changed), (gpointer)d); // Export button d->export_button = GTK_BUTTON(dt_action_button_new @@ -1699,7 +1750,7 @@ void gui_init(dt_lib_module_t *self) dt_ui_section_label_new(C_("section", "global options")), d->dimensions_type, d->px_size, d->print_size, d->scale, d->size_in_px, d->upscale, d->high_quality, d->export_masks, - d->profile, d->intent, + d->profile, d->pq_peak, d->intent, style_box, d->style_mode, d->export_button); @@ -1791,6 +1842,7 @@ void gui_init(dt_lib_module_t *self) } g_free(iccfilename); + _update_pq_peak_visibility(d); // style // set it to none if the var is not set or the style doesn't exist anymore @@ -1891,9 +1943,10 @@ void init_presets(dt_lib_module_t *self) // extract the interesting parts from the blob const char *buf = (const char *)op_params; - // skip 9*int32_t: max_width, max_height, upscale, high_quality, - // export_masks, iccintent, icctype, dimensions_type, print_dpi - buf += 9 * sizeof(int32_t); + // skip 10*int32_t: max_width, max_height, upscale, high_quality, + // export_masks, iccintent, icctype, dimensions_type, print_dpi, + // pq_peak_nits + buf += 10 * sizeof(int32_t); // skip scale string buf += strlen(buf) + 1; // skip metadata presets string @@ -2305,6 +2358,37 @@ void *legacy_params(dt_lib_module_t *self, *new_version = 8; return new_params; } + else if(old_version == 8) + { + // add pq_peak_nits + + // format of v8: + // - 9 x int32_t (max_width, max_height, upscale, high_quality, + // export_masks, iccintent, icctype, dimensions_type, print_dpi) + // - old rest + // format of v9: + // - 10 x int32_t (max_width, max_height, upscale, high_quality, + // export_masks, iccintent, icctype, dimensions_type, print_dpi, + // pq_peak_nits) + // - old rest + + if(old_params_size < sizeof(int32_t) * 9) return NULL; + + // 0: applying the preset leaves the PQ peak as it is, as before + const int32_t pq_peak_nits = 0; + + const size_t new_params_size = old_params_size + sizeof(int32_t); + void *new_params = calloc(1, new_params_size); + + memcpy(new_params, old_params, sizeof(int32_t) * 9); + memcpy((uint8_t *)new_params + sizeof(int32_t) * 9, &pq_peak_nits, sizeof(int32_t)); + memcpy((uint8_t *)new_params + sizeof(int32_t) * 10, + (uint8_t *)old_params + sizeof(int32_t) * 9, + old_params_size - sizeof(int32_t) * 9); + *new_size = new_params_size; + *new_version = 9; + return new_params; + } return NULL; } @@ -2349,6 +2433,10 @@ void *get_params(dt_lib_module_t *self, int *size) const int32_t upscale = dt_conf_get_bool(CONFIG_PREFIX "upscale") ? 1 : 0; const int32_t dimensions_type = dt_conf_get_int(CONFIG_PREFIX "dimensions_type"); const int32_t print_dpi = dt_conf_get_int(CONFIG_PREFIX "print_dpi"); + // 0 when the export cannot be PQ, so that applying the preset does not + // change a PQ peak that is hidden and unused for its profile + const int32_t pq_peak_nits = + _export_may_be_pq(icctype) ? dt_conf_get_int(CONFIG_PREFIX "pq_peak_nits") : 0; gchar *scale = dt_conf_get_string(CONFIG_PREFIX "resizing_factor"); const int32_t high_quality = dt_conf_get_bool(CONFIG_PREFIX "high_quality_processing") ? 1 : 0; @@ -2382,7 +2470,7 @@ void *get_params(dt_lib_module_t *self, int *size) + 4 * sizeof(int32_t) + fsize + ssize - + 9 * sizeof(int32_t) + + 10 * sizeof(int32_t) + strlen(scale) + 1 + strlen(iccfilename) + 1 + strlen(metadata_export) + 1; @@ -2409,6 +2497,8 @@ void *get_params(dt_lib_module_t *self, int *size) pos += sizeof(int32_t); memcpy(params + pos, &print_dpi, sizeof(int32_t)); pos += sizeof(int32_t); + memcpy(params + pos, &pq_peak_nits, sizeof(int32_t)); + pos += sizeof(int32_t); memcpy(params + pos, scale, strlen(scale) + 1); pos += strlen(scale) + 1; memcpy(params + pos, metadata_export, strlen(metadata_export) + 1); @@ -2476,6 +2566,8 @@ int set_params(dt_lib_module_t *self, buf += sizeof(int32_t); const int print_dpi = *(const int *)buf; buf += sizeof(int32_t); + const int pq_peak_nits = *(const int *)buf; + buf += sizeof(int32_t); const char *scale = buf; buf += strlen(scale) + 1; const char *metadata_export = buf; @@ -2534,7 +2626,7 @@ int set_params(dt_lib_module_t *self, + 4 * sizeof(int32_t) + fsize + ssize - + 9 * sizeof(int32_t) + + 10 * sizeof(int32_t) + strlen(scale) + 1 + strlen(metadata_export) + 1 + strlen(iccfilename) + 1) @@ -2559,6 +2651,7 @@ int set_params(dt_lib_module_t *self, dt_bauhaus_combobox_set(d->high_quality, high_quality ? 1 : 0); dt_bauhaus_combobox_set(d->export_masks, export_masks ? 1 : 0); dt_bauhaus_combobox_set(d->dimensions_type, dimensions_type); + if(pq_peak_nits > 0) dt_bauhaus_slider_set(d->pq_peak, pq_peak_nits); _size_update_display(d); // propagate to modules