From f793fc0480fcae0b034c3b00f7f99ba364c739e1 Mon Sep 17 00:00:00 2001
From: Niklas Haas <git@haasn.xyz>
Date: Thu, 28 May 2020 16:31:33 +0200
Subject: [PATCH] shaders/colorspace: implement ITU-R BT.2390

We make this the default tone mapping function because it's the de-facto
standard in the industry. Unfortunately, it's quite a bit heavier than
the other algorithms due to the extra PQ round trip needed during tone
mapping.

It's entirely possible that we could make the choice of whether to do
things in PQ space or in linear light a choice completely independent of
the tone mapping function itself, since arguably PQ's "perceptual
uniformity" quality makes it a suitable space to do tone mapping in
regardless of what function we use.

That being said, I don't currently want to consider the headache of
testing this all, so let's just implement it for BT.2390 and call it a
day.
---
 meson.build                                 |  2 +-
 src/include/libplacebo/shaders/colorspace.h |  6 ++-
 src/shaders/colorspace.c                    | 58 ++++++++++++++++++---
 src/tests/gpu_tests.h                       |  2 +-
 4 files changed, 59 insertions(+), 9 deletions(-)

diff --git a/meson.build b/meson.build
index 3c1e5f9ac..0f55f354a 100644
--- a/meson.build
+++ b/meson.build
@@ -2,7 +2,7 @@ project('libplacebo', ['c', 'cpp'],
   license: 'LGPL2.1+',
   default_options: ['c_std=c99', 'cpp_std=c++11', 'warning_level=2'],
   meson_version: '>=0.49',
-  version: '2.67.0',
+  version: '2.68.0',
 )
 
 # Version number
diff --git a/src/include/libplacebo/shaders/colorspace.h b/src/include/libplacebo/shaders/colorspace.h
index e9fed237f..562ef9218 100644
--- a/src/include/libplacebo/shaders/colorspace.h
+++ b/src/include/libplacebo/shaders/colorspace.h
@@ -194,6 +194,10 @@ enum pl_tone_mapping_algorithm {
     // as an aditional scaling coefficient to make the image (linearly)
     // brighter or darker. Defaults to 1.0.
     PL_TONE_MAPPING_LINEAR,
+
+    // EETF from the ITU-R Report BT.2390, a hermite spline roll-off with
+    // linear segment. Not configurable.
+    PL_TONE_MAPPING_BT_2390,
 };
 
 struct pl_color_map_params {
@@ -204,7 +208,7 @@ struct pl_color_map_params {
     // Algorithm and configuration used for tone-mapping. For non-tunable
     // algorithms, the `param` is ignored. If the tone mapping parameter is
     // left as 0.0, the tone-mapping curve's preferred default parameter will
-    // be used. The default algorithm is PL_TONE_MAPPING_HABLE.
+    // be used. The default algorithm is PL_TONE_MAPPING_BT_2390.
     enum pl_tone_mapping_algorithm tone_mapping_algo;
     float tone_mapping_param;
 
diff --git a/src/shaders/colorspace.c b/src/shaders/colorspace.c
index 722722ac6..3331f075d 100644
--- a/src/shaders/colorspace.c
+++ b/src/shaders/colorspace.c
@@ -751,9 +751,18 @@ bool pl_shader_detect_peak(struct pl_shader *sh,
     return true;
 }
 
+static inline float pq_delinearize(float x)
+{
+    x *= PL_COLOR_SDR_WHITE / 10000.0;
+    x = powf(x, PQ_M1);
+    x = (PQ_C1 + PQ_C2 * x) / (1.0 + PQ_C3 * x);
+    x = pow(x, PQ_M2);
+    return x;
+}
+
 const struct pl_color_map_params pl_color_map_default_params = {
     .intent                 = PL_INTENT_RELATIVE_COLORIMETRIC,
-    .tone_mapping_algo      = PL_TONE_MAPPING_HABLE,
+    .tone_mapping_algo      = PL_TONE_MAPPING_BT_2390,
     .desaturation_strength  = 0.75,
     .desaturation_exponent  = 1.50,
     .desaturation_base      = 0.18,
@@ -791,11 +800,13 @@ static void pl_shader_tone_map(struct pl_shader *sh, struct pl_color_space src,
         }
     }
 
-    // Rescale the input in order to bring it into a representation where
-    // 1.0 represents the dst_peak. This is because all of the tone mapping
-    // algorithms are defined in such a way that they map to the range [0.0, 1.0].
+    // Rescale the input in order to bring it into a representation where 1.0
+    // represents the dst_peak. This is because (almost) all of the tone
+    // mapping algorithms are defined in such a way that they map to the range
+    // [0.0, 1.0].
+    bool need_norm = params->tone_mapping_algo != PL_TONE_MAPPING_BT_2390;
     float dst_range = dst.sig_peak * dst.sig_scale;
-    if (dst_range > 1.0) {
+    if (dst_range > 1.0 && need_norm) {
         GLSL("color.rgb *= 1.0 / %f; \n"
              "sig_peak *= 1.0 / %f;  \n",
              dst_range, dst_range);
@@ -873,6 +884,41 @@ static void pl_shader_tone_map(struct pl_shader *sh, struct pl_color_space src,
         GLSL("sig *= %f / sig_peak;\n", PL_DEF(param, 1.0));
         break;
 
+    case PL_TONE_MAPPING_BT_2390:
+        // We first need to encode both sig and sig_peak into PQ space
+        GLSL("vec4 sig_pq = vec4(sig.rgb, sig_peak);                            \n"
+             "sig_pq *= vec4(1.0/%f);                                           \n"
+             "sig_pq = pow(sig_pq, vec4(%f));                                   \n"
+             "sig_pq = (vec4(%f) + vec4(%f) * sig_pq)                           \n"
+             "          / (vec4(1.0) + vec4(%f) * sig_pq);                      \n"
+             "sig_pq = pow(sig_pq, vec4(%f));                                   \n",
+             10000 / PL_COLOR_SDR_WHITE, PQ_M1, PQ_C1, PQ_C2, PQ_C3, PQ_M2);
+        // Encode both the signal and the target brightness to be relative to
+        // the source peak brightness, and figure out the target peak in this space
+        GLSL("float scale = 1.0 / sig_pq.a;                                     \n"
+             "sig_pq.rgb *= vec3(scale);                                        \n"
+             "float maxLum = %f * scale;                                        \n",
+             pq_delinearize(dst_range));
+        // Apply piece-wise hermite spline
+        GLSL("float ks = 1.5 * maxLum - 0.5;                                    \n"
+             "vec3 tb = (sig_pq.rgb - vec3(ks)) / vec3(1.0 - ks);               \n"
+             "vec3 tb2 = tb * tb;                                               \n"
+             "vec3 tb3 = tb2 * tb;                                              \n"
+             "vec3 pb = (2.0 * tb3 - 3.0 * tb2 + vec3(1.0)) * vec3(ks) +        \n"
+             "          (tb3 - 2.0 * tb2 + tb) * vec3(1.0 - ks) +               \n"
+             "          (-2.0 * tb3 + 3.0 * tb2) * vec3(maxLum);                \n"
+             "sig = mix(pb, sig_pq.rgb, %s(lessThan(sig_pq.rgb, vec3(ks))));    \n",
+             sh_bvec(sh, 3));
+        // Convert back from PQ space to linear light
+        GLSL("sig *= vec3(sig_pq.a);                                            \n"
+             "sig = pow(sig, vec3(1.0/%f));                                     \n"
+             "sig = max(sig - vec3(%f), 0.0) /                                  \n"
+             "          (vec3(%f) - vec3(%f) * sig);                            \n"
+             "sig = pow(sig, vec3(1.0/%f));                                     \n"
+             "sig *= vec3(%f);                                                  \n",
+             PQ_M2, PQ_C1, PQ_C2, PQ_C3, PQ_M1, 10000.0 / PL_COLOR_SDR_WHITE);
+        break;
+
     default:
         abort();
     }
@@ -895,7 +941,7 @@ static void pl_shader_tone_map(struct pl_shader *sh, struct pl_color_space src,
     }
 
     // Undo the normalization by `dst_peak`
-    if (dst_range > 1.0)
+    if (dst_range > 1.0 && need_norm)
         GLSL("color.rgb *= %f; \n", dst_range);
 }
 
diff --git a/src/tests/gpu_tests.h b/src/tests/gpu_tests.h
index 995fa063a..22184ba5b 100644
--- a/src/tests/gpu_tests.h
+++ b/src/tests/gpu_tests.h
@@ -733,7 +733,7 @@ static void pl_render_tests(const struct pl_gpu *gpu)
     // Test HDR stuff
     image.color.sig_scale = 10.0;
     target.color.sig_scale = 2.0;
-    TEST_PARAMS(color_map, tone_mapping_algo, PL_TONE_MAPPING_LINEAR);
+    TEST_PARAMS(color_map, tone_mapping_algo, PL_TONE_MAPPING_BT_2390);
     TEST_PARAMS(color_map, desaturation_strength, 1);
     image.color.sig_scale = target.color.sig_scale = 0.0;
 
-- 
GitLab