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
3 changes: 3 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,9 @@ changes (where available).

## Bug Fixes

- Fixed overly dark PQ and HLG image imports and overly bright exports,
including HDR PNG and JPEG XL profile detection.

- Fixed HEIF export compilation with libheif versions older than 1.15.0,
and with versions older than 1.16.0 when libsharpyuv is available.

Expand Down
10 changes: 7 additions & 3 deletions data/kernels/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
FILE(GLOB DT_OPENCL_KERNELS "*.cl")
FILE(GLOB DT_OPENCL_EXTRA "programs.conf" "common.h" "rgb_norms.h" "noise_generator.h" "color_conversion.h" "colorspace.h" "grain.h" "bilinear.h")

set(DT_HDR_TRANSFER_HEADER "${CMAKE_SOURCE_DIR}/src/common/hdr-transfer.h")
list(APPEND DT_OPENCL_EXTRA "${DT_HDR_TRANSFER_HEADER}")
configure_file("${DT_HDR_TRANSFER_HEADER}" "${CMAKE_CURRENT_BINARY_DIR}/hdr-transfer.h" COPYONLY)

add_custom_target(testcompile_opencl_kernels ALL)

macro (testcompile_opencl_kernel IN)
Expand All @@ -15,16 +19,16 @@ macro (testcompile_opencl_kernel IN)

add_custom_command(
OUTPUT ${TOUCH}
COMMAND ${CLANG_OPENCL_COMPILER} -cc1 -cl-std=CL1.2 -isystem ${CLANG_OPENCL_INCLUDE_DIR} -D__IMAGE_SUPPORT__=1 -finclude-default-header -I${CMAKE_CURRENT_SOURCE_DIR} ${IN}
COMMAND ${CLANG_OPENCL_COMPILER} -cc1 -cl-std=CL1.2 -isystem ${CLANG_OPENCL_INCLUDE_DIR} -D__IMAGE_SUPPORT__=1 -finclude-default-header -I${CMAKE_CURRENT_SOURCE_DIR} -I${CMAKE_CURRENT_BINARY_DIR} ${IN}
COMMAND ${CMAKE_COMMAND} -E touch ${TOUCH} # will be empty!
DEPENDS ${IN}
DEPENDS ${IN} ${DT_OPENCL_EXTRA}
COMMENT "Test-compiling OpenCL program ${KERNAME}"
)

add_custom_target(
${KERNAME_OUT}
DEPENDS ${TOUCH} # will be empty!
DEPENDS ${IN}
DEPENDS ${IN} ${DT_OPENCL_EXTRA}
)

add_dependencies(testcompile_opencl_kernels ${KERNAME_OUT})
Expand Down
39 changes: 30 additions & 9 deletions data/kernels/basic.cl
Original file line number Diff line number Diff line change
Expand Up @@ -1308,7 +1308,7 @@ colorin_unbound (read_only image2d_t in, write_only image2d_t out, const int wid
global float *cmat, global float *lmat,
read_only image2d_t lutr, read_only image2d_t lutg, read_only image2d_t lutb,
const int blue_mapping, global const float (*const a)[3], global const float *corr,
const float scale, const int hue_div, const int sat_div, const int val_div, global const float *dev_hsm, const int encoding, const int use_hsm)
const float scale, const int hue_div, const int sat_div, const int val_div, global const float *dev_hsm, const int encoding, const int use_hsm, const int hdr_transfer)
{
const int x = get_global_id(0);
const int y = get_global_id(1);
Expand All @@ -1320,9 +1320,19 @@ colorin_unbound (read_only image2d_t in, write_only image2d_t out, const int wid
const float alpha = pixel.w;

float cam[3], XYZ[3];
cam[0] = lerp_lookup_unbounded0(lutr, pixel.x, a[0]);
cam[1] = lerp_lookup_unbounded0(lutg, pixel.y, a[1]);
cam[2] = lerp_lookup_unbounded0(lutb, pixel.z, a[2]);
if(hdr_transfer)
{
cam[0] = pixel.x;
cam[1] = pixel.y;
cam[2] = pixel.z;
dt_hdr_decode(cam, hdr_transfer);
}
else
{
cam[0] = lerp_lookup_unbounded0(lutr, pixel.x, a[0]);
cam[1] = lerp_lookup_unbounded0(lutg, pixel.y, a[1]);
cam[2] = lerp_lookup_unbounded0(lutb, pixel.z, a[2]);
}

if(blue_mapping)
{
Expand Down Expand Up @@ -1387,7 +1397,7 @@ colorin_clipping (read_only image2d_t in, write_only image2d_t out, const int wi
global float *cmat, global float *lmat,
read_only image2d_t lutr, read_only image2d_t lutg, read_only image2d_t lutb,
const int blue_mapping, global const float (*const a)[3], global const float *corr,
const float scale, const int hue_div, const int sat_div, const int val_div, global const float *dev_hsm, const int encoding, const int use_hsm)
const float scale, const int hue_div, const int sat_div, const int val_div, global const float *dev_hsm, const int encoding, const int use_hsm, const int hdr_transfer)
{
const int x = get_global_id(0);
const int y = get_global_id(1);
Expand All @@ -1399,9 +1409,19 @@ colorin_clipping (read_only image2d_t in, write_only image2d_t out, const int wi
const float alpha = pixel.w;

float cam[3], RGB[3], XYZ[3];
cam[0] = lerp_lookup_unbounded0(lutr, pixel.x, a[0]);
cam[1] = lerp_lookup_unbounded0(lutg, pixel.y, a[1]);
cam[2] = lerp_lookup_unbounded0(lutb, pixel.z, a[2]);
if(hdr_transfer)
{
cam[0] = pixel.x;
cam[1] = pixel.y;
cam[2] = pixel.z;
dt_hdr_decode(cam, hdr_transfer);
}
else
{
cam[0] = lerp_lookup_unbounded0(lutr, pixel.x, a[0]);
cam[1] = lerp_lookup_unbounded0(lutg, pixel.y, a[1]);
cam[2] = lerp_lookup_unbounded0(lutb, pixel.z, a[2]);
}

if(blue_mapping)
{
Expand Down Expand Up @@ -3092,7 +3112,7 @@ colorout (read_only image2d_t in,
read_only image2d_t lutr,
read_only image2d_t lutg,
read_only image2d_t lutb,
global const float (*const a)[3])
global const float (*const a)[3], const int hdr_transfer)
{
const int x = get_global_id(0);
const int y = get_global_id(1);
Expand All @@ -3110,6 +3130,7 @@ colorout (read_only image2d_t in,
rgb[i] = 0.0f;
for(int j=0;j<3;j++) rgb[i] += mat[3*i+j]*XYZ[j];
}
dt_hdr_encode(rgb, hdr_transfer);
pixel.x = lerp_lookup_unbounded0(lutr, rgb[0], a[0]);
pixel.y = lerp_lookup_unbounded0(lutg, rgb[1], a[1]);
pixel.z = lerp_lookup_unbounded0(lutb, rgb[2], a[2]);
Expand Down
36 changes: 26 additions & 10 deletions data/kernels/color_conversion.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

#include "common.h"
#include "colorspace.h"
#include "hdr-transfer.h"

// must be in synch with dt_iop_colorspace_type_t in colorspaces.h
typedef enum dt_iop_colorspace_type_t
Expand All @@ -41,6 +42,7 @@ typedef struct dt_colorspaces_iccprofile_info_cl_t
float unbounded_coeffs_out[3][3];
int nonlinearlut;
float grey;
int hdr_transfer;
} dt_colorspaces_iccprofile_info_cl_t;

static inline float lerp_lookup_unbounded(const float x, read_only image2d_t lut,
Expand Down Expand Up @@ -95,22 +97,36 @@ static inline float4 apply_trc_in(const float4 rgb_in,
constant const dt_colorspaces_iccprofile_info_cl_t *const profile_info,
read_only image2d_t lut)
{
const float R = lerp_lookup_unbounded(rgb_in.x, lut, profile_info->unbounded_coeffs_in[0], 0, profile_info->lutsize);
const float G = lerp_lookup_unbounded(rgb_in.y, lut, profile_info->unbounded_coeffs_in[1], 1, profile_info->lutsize);
const float B = lerp_lookup_unbounded(rgb_in.z, lut, profile_info->unbounded_coeffs_in[2], 2, profile_info->lutsize);
const float a = rgb_in.w;
return (float4)(R, G, B, a);
float rgb[3] = { rgb_in.x, rgb_in.y, rgb_in.z };
if(profile_info->hdr_transfer)
Comment thread
TurboGit marked this conversation as resolved.
{
dt_hdr_decode(rgb, profile_info->hdr_transfer);
}
else
{
rgb[0] = lerp_lookup_unbounded(rgb_in.x, lut, profile_info->unbounded_coeffs_in[0], 0, profile_info->lutsize);
rgb[1] = lerp_lookup_unbounded(rgb_in.y, lut, profile_info->unbounded_coeffs_in[1], 1, profile_info->lutsize);
rgb[2] = lerp_lookup_unbounded(rgb_in.z, lut, profile_info->unbounded_coeffs_in[2], 2, profile_info->lutsize);
}
return (float4)(rgb[0], rgb[1], rgb[2], rgb_in.w);
}

static inline float4 apply_trc_out(const float4 rgb_in,
constant const dt_colorspaces_iccprofile_info_cl_t *const profile_info,
read_only image2d_t lut)
{
const float R = lerp_lookup_unbounded(rgb_in.x, lut, profile_info->unbounded_coeffs_out[0], 3, profile_info->lutsize);
const float G = lerp_lookup_unbounded(rgb_in.y, lut, profile_info->unbounded_coeffs_out[1], 4, profile_info->lutsize);
const float B = lerp_lookup_unbounded(rgb_in.z, lut, profile_info->unbounded_coeffs_out[2], 5, profile_info->lutsize);
const float a = rgb_in.w;
return (float4)(R, G, B, a);
float rgb[3] = { rgb_in.x, rgb_in.y, rgb_in.z };
if(profile_info->hdr_transfer)
Comment thread
TurboGit marked this conversation as resolved.
{
dt_hdr_encode(rgb, profile_info->hdr_transfer);
}
else
{
rgb[0] = lerp_lookup_unbounded(rgb_in.x, lut, profile_info->unbounded_coeffs_out[0], 3, profile_info->lutsize);
rgb[1] = lerp_lookup_unbounded(rgb_in.y, lut, profile_info->unbounded_coeffs_out[1], 4, profile_info->lutsize);
rgb[2] = lerp_lookup_unbounded(rgb_in.z, lut, profile_info->unbounded_coeffs_out[2], 5, profile_info->lutsize);
}
return (float4)(rgb[0], rgb[1], rgb[2], rgb_in.w);
}

static inline float get_rgb_matrix_luminance(const float4 rgb,
Expand Down
19 changes: 19 additions & 0 deletions src/common/colorspaces.c
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

#include "common/colorspaces_inline_conversions.h"
#include "common/colorspaces.h"
#include "common/hdr-transfer.h"
#include "common/colormatrices.c"
#include "common/darktable.h"
#include "common/debug.h"
Expand Down Expand Up @@ -1065,6 +1066,24 @@ void dt_colorspaces_cleanup_profile(cmsHPROFILE p)
cmsCloseProfile(p);
}

cmsHPROFILE dt_colorspaces_linearize_profile(cmsHPROFILE profile)
{
cmsHPROFILE linear = dt_colorspaces_make_temporary_profile(profile);
if(!linear) return NULL;
cmsToneCurve *trc = cmsBuildGamma(NULL, 1.0);
const gboolean valid = trc
&& cmsWriteTag(linear, cmsSigRedTRCTag, trc)
&& cmsWriteTag(linear, cmsSigGreenTRCTag, trc)
&& cmsWriteTag(linear, cmsSigBlueTRCTag, trc);
cmsFreeToneCurve(trc);
if(!valid)
{
cmsCloseProfile(linear);
return NULL;
}
return linear;
}

cmsHPROFILE dt_colorspaces_make_temporary_profile(cmsHPROFILE profile)
{
cmsUInt32Number size;
Expand Down
22 changes: 22 additions & 0 deletions src/common/colorspaces.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

#pragma once

#include "common/hdr-transfer.h"

#include "common/darktable.h"
#include "common/dttypes.h"

Expand Down Expand Up @@ -109,6 +111,26 @@ typedef enum dt_colorspaces_color_profile_type_t
DT_COLORSPACE_LAST = 29
} dt_colorspaces_color_profile_type_t;

/* Built-in CICP HDR profiles use RGB transfer functions, not ICC shapers. */
static inline int dt_colorspaces_hdr_transfer(const dt_colorspaces_color_profile_type_t type)
{
switch(type)
{
case DT_COLORSPACE_PQ_REC2020:
Comment thread
TurboGit marked this conversation as resolved.
case DT_COLORSPACE_PQ_P3:
return DT_HDR_PQ;
case DT_COLORSPACE_HLG_REC2020:
return DT_HDR_HLG_REC2020;
case DT_COLORSPACE_HLG_P3:
return DT_HDR_HLG_P3;
default:
return DT_HDR_NONE;
}
}

/* Owned copy of a matrix profile with linear shapers, for HDR RGB math. */
cmsHPROFILE dt_colorspaces_linearize_profile(cmsHPROFILE profile);

typedef enum dt_colorspaces_color_mode_t
{
DT_PROFILE_NORMAL = 0,
Expand Down
118 changes: 118 additions & 0 deletions src/common/hdr-transfer.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
* HDR transfer functions shared by the CPU and OpenCL color pipelines.
* Copyright (C) 2026 darktable developers.
* SPDX-License-Identifier: GPL-3.0-or-later
*/
#pragma once

#if defined(__OPENCL_VERSION__) || defined(__OPENCL_C_VERSION__)
#define DT_HDR_POW pow
#define DT_HDR_EXP exp
#define DT_HDR_LOG log
#define DT_HDR_SQRT sqrt
#define DT_HDR_MIN fmin
#define DT_HDR_MAX fmax
#else
#include <math.h>
#define DT_HDR_POW powf
#define DT_HDR_EXP expf
#define DT_HDR_LOG logf
#define DT_HDR_SQRT sqrtf
#define DT_HDR_MIN fminf
#define DT_HDR_MAX fmaxf
#endif

#define DT_HDR_NONE 0
#define DT_HDR_PQ 1
#define DT_HDR_HLG_REC2020 2
#define DT_HDR_HLG_P3 3

/* BT.2408 reference white; BT.2100 nominal HLG display, zero black. */
#define DT_HDR_REFERENCE_WHITE_NITS 203.0f
#define DT_HDR_PQ_PEAK_NITS 10000.0f
#define DT_HDR_HLG_PEAK_NITS 1000.0f
#define DT_HDR_HLG_SYSTEM_GAMMA 1.2f
#define DT_HDR_PQ_M1 (2610.0f / 16384.0f)
#define DT_HDR_PQ_M2 (2523.0f / 32.0f)
#define DT_HDR_PQ_C1 (3424.0f / 4096.0f)
#define DT_HDR_PQ_C2 (2413.0f / 128.0f)
#define DT_HDR_PQ_C3 (2392.0f / 128.0f)
#define DT_HDR_HLG_A 0.17883277f
#define DT_HDR_HLG_B 0.28466892f
#define DT_HDR_HLG_C 0.55991073f

static inline float dt_hdr_pq_decode(const float signal)
{
const float e = DT_HDR_MIN(DT_HDR_MAX(signal, 0.0f), 1.0f);
const float p = DT_HDR_POW(e, 1.0f / DT_HDR_PQ_M2);
const float n = DT_HDR_MAX(p - DT_HDR_PQ_C1, 0.0f);
const float d = DT_HDR_PQ_C2 - DT_HDR_PQ_C3 * p;
return (DT_HDR_PQ_PEAK_NITS / DT_HDR_REFERENCE_WHITE_NITS)
* DT_HDR_POW(n / d, 1.0f / DT_HDR_PQ_M1);
}

static inline float dt_hdr_pq_encode(const float linear)
{
const float l = DT_HDR_MIN(DT_HDR_MAX(linear, 0.0f)
* (DT_HDR_REFERENCE_WHITE_NITS / DT_HDR_PQ_PEAK_NITS), 1.0f);
const float p = DT_HDR_POW(l, DT_HDR_PQ_M1);
return DT_HDR_POW((DT_HDR_PQ_C1 + DT_HDR_PQ_C2 * p)
/ (1.0f + DT_HDR_PQ_C3 * p), DT_HDR_PQ_M2);
}

static inline float dt_hdr_hlg_decode(const float signal)
{
const float e = DT_HDR_MIN(DT_HDR_MAX(signal, 0.0f), 1.0f);
return e <= 0.5f ? e * e / 3.0f
: (DT_HDR_EXP((e - DT_HDR_HLG_C) / DT_HDR_HLG_A) + DT_HDR_HLG_B) / 12.0f;
}

static inline float dt_hdr_hlg_encode(const float scene)
{
const float e = DT_HDR_MAX(scene, 0.0f);
return DT_HDR_MIN(e <= 1.0f / 12.0f ? DT_HDR_SQRT(3.0f * e)
: DT_HDR_HLG_A * DT_HDR_LOG(12.0f * e - DT_HDR_HLG_B) + DT_HDR_HLG_C, 1.0f);
}

static inline float dt_hdr_hlg_luminance(const float rgb[3], const int transfer)
{
/* D65 luminance coefficients, before chromatic adaptation to D50. */
return transfer == DT_HDR_HLG_P3
? 0.22897456f * rgb[0] + 0.69173852f * rgb[1] + 0.07928691f * rgb[2]
: 0.2627f * rgb[0] + 0.6780f * rgb[1] + 0.0593f * rgb[2];
}

static inline void dt_hdr_decode(float rgb[3], const int transfer)
{
if(transfer == DT_HDR_PQ)
{
for(int c = 0; c < 3; c++) rgb[c] = dt_hdr_pq_decode(rgb[c]);
}
else if(transfer != DT_HDR_NONE)
{
for(int c = 0; c < 3; c++) rgb[c] = dt_hdr_hlg_decode(rgb[c]);
const float y = dt_hdr_hlg_luminance(rgb, transfer);
const float scale = (DT_HDR_HLG_PEAK_NITS / DT_HDR_REFERENCE_WHITE_NITS)
* DT_HDR_POW(y, DT_HDR_HLG_SYSTEM_GAMMA - 1.0f);
for(int c = 0; c < 3; c++) rgb[c] *= scale;
}
}

static inline void dt_hdr_encode(float rgb[3], const int transfer)
{
if(transfer == DT_HDR_PQ)
{
for(int c = 0; c < 3; c++) rgb[c] = dt_hdr_pq_encode(rgb[c]);
}
else if(transfer != DT_HDR_NONE)
{
for(int c = 0; c < 3; c++) rgb[c] = DT_HDR_MAX(rgb[c], 0.0f);
const float y = dt_hdr_hlg_luminance(rgb, transfer);
const float scale = y > 0.0f
? DT_HDR_POW(DT_HDR_REFERENCE_WHITE_NITS / DT_HDR_HLG_PEAK_NITS,
1.0f / DT_HDR_HLG_SYSTEM_GAMMA)
* DT_HDR_POW(y, 1.0f / DT_HDR_HLG_SYSTEM_GAMMA - 1.0f)
: 0.0f;
for(int c = 0; c < 3; c++) rgb[c] = dt_hdr_hlg_encode(rgb[c] * scale);
}
}
Loading
Loading