Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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.

Expand Down
7 changes: 7 additions & 0 deletions data/darktableconfig.xml.in
Original file line number Diff line number Diff line change
Expand Up @@ -2684,6 +2684,13 @@
<shortdescription>always use LittleCMS 2 to apply output color profile</shortdescription>
<longdescription>this is slower than the default.</longdescription>
</dtconfig>
<dtconfig>
<name>plugins/lighttable/export/pq_peak_nits</name>
<type min="203" max="10000">int</type>
<default>1000</default>
<shortdescription>PQ peak luminance</shortdescription>
<longdescription>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.</longdescription>
</dtconfig>
<dtconfig>
<name>plugins/lighttable/export/high_quality_processing</name>
<type>bool</type>
Expand Down
49 changes: 49 additions & 0 deletions data/kernels/agx.cl
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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);
Expand All @@ -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, &params->hdr);
}

if(params->restore_hue)
{
hsv_pixel = RGB_2_HSV(transformed_pixel);
Expand Down
18 changes: 18 additions & 0 deletions src/cli/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -102,6 +103,8 @@ fprintf(stderr, "darktable %s\n"
" --icc-file <file> specify icc filename, default to NONE\n"
" --icc-intent <intent> specify icc intent, default to LAST\n"
" use --help icc-intent for list of supported intents\n"
" --pq-peak <nits> 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"
Expand Down Expand Up @@ -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++)
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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++)
{
Expand Down
30 changes: 30 additions & 0 deletions src/common/iop_profile.c
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
}
Expand Down
4 changes: 4 additions & 0 deletions src/common/iop_profile.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 *
Expand Down
7 changes: 7 additions & 0 deletions src/control/jobs/control_jobs.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions src/control/jobs/control_jobs.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions src/develop/pixelpipe_cache.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions src/develop/pixelpipe_hb.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
3 changes: 3 additions & 0 deletions src/develop/pixelpipe_hb.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading
Loading