Add CPU implementation for tone-mapping curves.

This allows for library implementations that do not wish to place a
dependency on a GPU driver to build a lookup table.

A secondary use-case, which is included in this CL, is to allow for
building unit-tests for checking the validity of the tone-mapping curve.
See the newly added test in RenderEngineTest which validates the PQ
tone-mapping curve by checking grey values.

Bug: 200310159
Test: librenderengine_test
Change-Id: Ic765485c22c53b4dc58a2bc8db42fd51ac7f2eea
diff --git a/libs/tonemap/Android.bp b/libs/tonemap/Android.bp
index 231a342..5360fe2 100644
--- a/libs/tonemap/Android.bp
+++ b/libs/tonemap/Android.bp
@@ -30,7 +30,13 @@
 
     shared_libs: [
         "android.hardware.graphics.common-V3-ndk",
+        "liblog",
     ],
+
+    static_libs: [
+        "libmath",
+    ],
+
     srcs: [
         "tonemap.cpp",
     ],
diff --git a/libs/tonemap/include/tonemap/tonemap.h b/libs/tonemap/include/tonemap/tonemap.h
index d350e16..bd7b72d 100644
--- a/libs/tonemap/include/tonemap/tonemap.h
+++ b/libs/tonemap/include/tonemap/tonemap.h
@@ -17,6 +17,7 @@
 #pragma once
 
 #include <aidl/android/hardware/graphics/common/Dataspace.h>
+#include <math/vec3.h>
 
 #include <string>
 #include <vector>
@@ -48,7 +49,9 @@
 class ToneMapper {
 public:
     virtual ~ToneMapper() {}
-    // Constructs a tonemap shader whose shader language is SkSL
+    // Constructs a tonemap shader whose shader language is SkSL, which tonemaps from an
+    // input whose dataspace is described by sourceDataspace, to an output whose dataspace
+    // is described by destinationDataspace
     //
     // The returned shader string *must* contain a function with the following signature:
     // float libtonemap_LookupTonemapGain(vec3 linearRGB, vec3 xyz);
@@ -94,6 +97,19 @@
     // assume that there are predefined floats in_libtonemap_displayMaxLuminance and
     // in_libtonemap_inputMaxLuminance inside of the body of the tone-mapping shader.
     virtual std::vector<ShaderUniform> generateShaderSkSLUniforms(const Metadata& metadata) = 0;
+
+    // CPU implementation of the tonemapping gain. This must match the GPU implementation returned
+    // by generateTonemapGainShaderSKSL() above, with some epsilon difference to account for
+    // differences in hardware precision.
+    //
+    // The gain is computed assuming an input described by sourceDataspace, tonemapped to an output
+    // described by destinationDataspace. To compute the gain, the input colors are provided by
+    // linearRGB, which is the RGB colors in linear space. The colors in XYZ space are also
+    // provided. Metadata is also provided for helping to compute the tonemapping curve.
+    virtual double lookupTonemapGain(
+            aidl::android::hardware::graphics::common::Dataspace sourceDataspace,
+            aidl::android::hardware::graphics::common::Dataspace destinationDataspace,
+            vec3 linearRGB, vec3 xyz, const Metadata& metadata) = 0;
 };
 
 // Retrieves a tonemapper instance.
diff --git a/libs/tonemap/tests/Android.bp b/libs/tonemap/tests/Android.bp
index e58d519..f46f3fa 100644
--- a/libs/tonemap/tests/Android.bp
+++ b/libs/tonemap/tests/Android.bp
@@ -31,6 +31,7 @@
         "android.hardware.graphics.common-V3-ndk",
     ],
     static_libs: [
+        "libmath",
         "libgmock",
         "libgtest",
         "libtonemap",
diff --git a/libs/tonemap/tonemap.cpp b/libs/tonemap/tonemap.cpp
index 2cec773..c2372fe 100644
--- a/libs/tonemap/tonemap.cpp
+++ b/libs/tonemap/tonemap.cpp
@@ -16,6 +16,7 @@
 
 #include <tonemap/tonemap.h>
 
+#include <algorithm>
 #include <cstdint>
 #include <mutex>
 #include <type_traits>
@@ -234,9 +235,163 @@
                             .value = buildUniformValue<float>(metadata.contentMaxLuminance)});
         return uniforms;
     }
+
+    double lookupTonemapGain(
+            aidl::android::hardware::graphics::common::Dataspace sourceDataspace,
+            aidl::android::hardware::graphics::common::Dataspace destinationDataspace,
+            vec3 /* linearRGB */, vec3 xyz, const Metadata& metadata) override {
+        if (xyz.y <= 0.0) {
+            return 1.0;
+        }
+        const int32_t sourceDataspaceInt = static_cast<int32_t>(sourceDataspace);
+        const int32_t destinationDataspaceInt = static_cast<int32_t>(destinationDataspace);
+
+        double targetNits = 0.0;
+        switch (sourceDataspaceInt & kTransferMask) {
+            case kTransferST2084:
+            case kTransferHLG:
+                switch (destinationDataspaceInt & kTransferMask) {
+                    case kTransferST2084:
+                        targetNits = xyz.y;
+                        break;
+                    case kTransferHLG:
+                        // PQ has a wider luminance range (10,000 nits vs. 1,000 nits) than HLG, so
+                        // we'll clamp the luminance range in case we're mapping from PQ input to
+                        // HLG output.
+                        targetNits = std::clamp(xyz.y, 0.0f, 1000.0f);
+                        break;
+                    default:
+                        // Here we're mapping from HDR to SDR content, so interpolate using a
+                        // Hermitian polynomial onto the smaller luminance range.
+
+                        targetNits = xyz.y;
+                        // if the max input luminance is less than what we can output then
+                        // no tone mapping is needed as all color values will be in range.
+                        if (metadata.contentMaxLuminance > metadata.displayMaxLuminance) {
+                            // three control points
+                            const double x0 = 10.0;
+                            const double y0 = 17.0;
+                            double x1 = metadata.displayMaxLuminance * 0.75;
+                            double y1 = x1;
+                            double x2 = x1 + (metadata.contentMaxLuminance - x1) / 2.0;
+                            double y2 = y1 + (metadata.displayMaxLuminance - y1) * 0.75;
+
+                            // horizontal distances between the last three control points
+                            double h12 = x2 - x1;
+                            double h23 = metadata.contentMaxLuminance - x2;
+                            // tangents at the last three control points
+                            double m1 = (y2 - y1) / h12;
+                            double m3 = (metadata.displayMaxLuminance - y2) / h23;
+                            double m2 = (m1 + m3) / 2.0;
+
+                            if (targetNits < x0) {
+                                // scale [0.0, x0] to [0.0, y0] linearly
+                                double slope = y0 / x0;
+                                targetNits *= slope;
+                            } else if (targetNits < x1) {
+                                // scale [x0, x1] to [y0, y1] linearly
+                                double slope = (y1 - y0) / (x1 - x0);
+                                targetNits = y0 + (targetNits - x0) * slope;
+                            } else if (targetNits < x2) {
+                                // scale [x1, x2] to [y1, y2] using Hermite interp
+                                double t = (targetNits - x1) / h12;
+                                targetNits = (y1 * (1.0 + 2.0 * t) + h12 * m1 * t) * (1.0 - t) *
+                                                (1.0 - t) +
+                                        (y2 * (3.0 - 2.0 * t) + h12 * m2 * (t - 1.0)) * t * t;
+                            } else {
+                                // scale [x2, maxInLumi] to [y2, maxOutLumi] using Hermite interp
+                                double t = (targetNits - x2) / h23;
+                                targetNits = (y2 * (1.0 + 2.0 * t) + h23 * m2 * t) * (1.0 - t) *
+                                                (1.0 - t) +
+                                        (metadata.displayMaxLuminance * (3.0 - 2.0 * t) +
+                                         h23 * m3 * (t - 1.0)) *
+                                                t * t;
+                            }
+                        }
+                        break;
+                }
+                break;
+            default:
+                // source is SDR
+                switch (destinationDataspaceInt & kTransferMask) {
+                    case kTransferST2084:
+                    case kTransferHLG: {
+                        // Map from SDR onto an HDR output buffer
+                        // Here we use a polynomial curve to map from [0, displayMaxLuminance] onto
+                        // [0, maxOutLumi] which is hard-coded to be 3000 nits.
+                        const double maxOutLumi = 3000.0;
+
+                        double x0 = 5.0;
+                        double y0 = 2.5;
+                        double x1 = metadata.displayMaxLuminance * 0.7;
+                        double y1 = maxOutLumi * 0.15;
+                        double x2 = metadata.displayMaxLuminance * 0.9;
+                        double y2 = maxOutLumi * 0.45;
+                        double x3 = metadata.displayMaxLuminance;
+                        double y3 = maxOutLumi;
+
+                        double c1 = y1 / 3.0;
+                        double c2 = y2 / 2.0;
+                        double c3 = y3 / 1.5;
+
+                        targetNits = xyz.y;
+
+                        if (targetNits <= x0) {
+                            // scale [0.0, x0] to [0.0, y0] linearly
+                            double slope = y0 / x0;
+                            targetNits *= slope;
+                        } else if (targetNits <= x1) {
+                            // scale [x0, x1] to [y0, y1] using a curve
+                            double t = (targetNits - x0) / (x1 - x0);
+                            targetNits = (1.0 - t) * (1.0 - t) * y0 + 2.0 * (1.0 - t) * t * c1 +
+                                    t * t * y1;
+                        } else if (targetNits <= x2) {
+                            // scale [x1, x2] to [y1, y2] using a curve
+                            double t = (targetNits - x1) / (x2 - x1);
+                            targetNits = (1.0 - t) * (1.0 - t) * y1 + 2.0 * (1.0 - t) * t * c2 +
+                                    t * t * y2;
+                        } else {
+                            // scale [x2, x3] to [y2, y3] using a curve
+                            double t = (targetNits - x2) / (x3 - x2);
+                            targetNits = (1.0 - t) * (1.0 - t) * y2 + 2.0 * (1.0 - t) * t * c3 +
+                                    t * t * y3;
+                        }
+                    } break;
+                    default:
+                        // For completeness, this is tone-mapping from SDR to SDR, where this is
+                        // just a no-op.
+                        targetNits = xyz.y;
+                        break;
+                }
+        }
+
+        return targetNits / xyz.y;
+    }
 };
 
 class ToneMapper13 : public ToneMapper {
+private:
+    double OETF_ST2084(double nits) {
+        nits = nits / 10000.0;
+        double m1 = (2610.0 / 4096.0) / 4.0;
+        double m2 = (2523.0 / 4096.0) * 128.0;
+        double c1 = (3424.0 / 4096.0);
+        double c2 = (2413.0 / 4096.0) * 32.0;
+        double c3 = (2392.0 / 4096.0) * 32.0;
+
+        double tmp = std::pow(nits, m1);
+        tmp = (c1 + c2 * tmp) / (1.0 + c3 * tmp);
+        return std::pow(tmp, m2);
+    }
+
+    double OETF_HLG(double nits) {
+        nits = nits / 1000.0;
+        const double a = 0.17883277;
+        const double b = 0.28466892;
+        const double c = 0.55991073;
+        return nits <= 1.0 / 12.0 ? std::sqrt(3.0 * nits) : a * std::log(12.0 * nits - b) + c;
+    }
+
 public:
     std::string generateTonemapGainShaderSkSL(
             aidl::android::hardware::graphics::common::Dataspace sourceDataspace,
@@ -386,6 +541,108 @@
                             .value = buildUniformValue<float>(kContentMaxLuminance)});
         return uniforms;
     }
+
+    double lookupTonemapGain(
+            aidl::android::hardware::graphics::common::Dataspace sourceDataspace,
+            aidl::android::hardware::graphics::common::Dataspace destinationDataspace,
+            vec3 linearRGB, vec3 /* xyz */, const Metadata& metadata) override {
+        double maxRGB = std::max({linearRGB.r, linearRGB.g, linearRGB.b});
+
+        if (maxRGB <= 0.0) {
+            return 1.0;
+        }
+
+        const int32_t sourceDataspaceInt = static_cast<int32_t>(sourceDataspace);
+        const int32_t destinationDataspaceInt = static_cast<int32_t>(destinationDataspace);
+
+        double targetNits = 0.0;
+        switch (sourceDataspaceInt & kTransferMask) {
+            case kTransferST2084:
+            case kTransferHLG:
+                switch (destinationDataspaceInt & kTransferMask) {
+                    case kTransferST2084:
+                        targetNits = maxRGB;
+                        break;
+                    case kTransferHLG:
+                        // PQ has a wider luminance range (10,000 nits vs. 1,000 nits) than HLG, so
+                        // we'll clamp the luminance range in case we're mapping from PQ input to
+                        // HLG output.
+                        targetNits = std::clamp(maxRGB, 0.0, 1000.0);
+                        break;
+                    default:
+                        // Here we're mapping from HDR to SDR content, so interpolate using a
+                        // Hermitian polynomial onto the smaller luminance range.
+
+                        double maxInLumi = 4000;
+                        double maxOutLumi = metadata.displayMaxLuminance;
+
+                        targetNits = maxRGB;
+
+                        double x1 = maxOutLumi * 0.65;
+                        double y1 = x1;
+
+                        double x3 = maxInLumi;
+                        double y3 = maxOutLumi;
+
+                        double x2 = x1 + (x3 - x1) * 4.0 / 17.0;
+                        double y2 = maxOutLumi * 0.9;
+
+                        double greyNorm1 = 0.0;
+                        double greyNorm2 = 0.0;
+                        double greyNorm3 = 0.0;
+
+                        if ((sourceDataspaceInt & kTransferMask) == kTransferST2084) {
+                            greyNorm1 = OETF_ST2084(x1);
+                            greyNorm2 = OETF_ST2084(x2);
+                            greyNorm3 = OETF_ST2084(x3);
+                        } else if ((sourceDataspaceInt & kTransferMask) == kTransferHLG) {
+                            greyNorm1 = OETF_HLG(x1);
+                            greyNorm2 = OETF_HLG(x2);
+                            greyNorm3 = OETF_HLG(x3);
+                        }
+
+                        double slope2 = (y2 - y1) / (greyNorm2 - greyNorm1);
+                        double slope3 = (y3 - y2) / (greyNorm3 - greyNorm2);
+
+                        if (targetNits < x1) {
+                            break;
+                        }
+
+                        if (targetNits > maxInLumi) {
+                            targetNits = maxOutLumi;
+                            break;
+                        }
+
+                        double greyNits = 0.0;
+                        if ((sourceDataspaceInt & kTransferMask) == kTransferST2084) {
+                            greyNits = OETF_ST2084(targetNits);
+                        } else if ((sourceDataspaceInt & kTransferMask) == kTransferHLG) {
+                            greyNits = OETF_HLG(targetNits);
+                        }
+
+                        if (greyNits <= greyNorm2) {
+                            targetNits = (greyNits - greyNorm2) * slope2 + y2;
+                        } else if (greyNits <= greyNorm3) {
+                            targetNits = (greyNits - greyNorm3) * slope3 + y3;
+                        } else {
+                            targetNits = maxOutLumi;
+                        }
+                        break;
+                }
+                break;
+            default:
+                switch (destinationDataspaceInt & kTransferMask) {
+                    case kTransferST2084:
+                    case kTransferHLG:
+                    default:
+                        targetNits = maxRGB;
+                        break;
+                }
+                break;
+        }
+
+        return targetNits / maxRGB;
+    }
 };
 
 } // namespace
@@ -406,5 +663,4 @@
 
     return sToneMapper.get();
 }
-
 } // namespace android::tonemap
\ No newline at end of file