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/renderengine/skia/filters/LinearEffect.cpp b/libs/renderengine/skia/filters/LinearEffect.cpp
index c3a5a60..b43a3aa 100644
--- a/libs/renderengine/skia/filters/LinearEffect.cpp
+++ b/libs/renderengine/skia/filters/LinearEffect.cpp
@@ -104,7 +104,7 @@
         uniform float4x4 in_rgbToXyz;
         uniform float4x4 in_xyzToRgb;
         float3 ToXYZ(float3 rgb) {
-            return clamp((in_rgbToXyz * float4(rgb, 1.0)).rgb, 0.0, 1.0);
+            return (in_rgbToXyz * float4(rgb, 1.0)).rgb;
         }
 
         float3 ToRGB(float3 xyz) {
diff --git a/libs/renderengine/tests/RenderEngineTest.cpp b/libs/renderengine/tests/RenderEngineTest.cpp
index c2c05f4..5bc08ac 100644
--- a/libs/renderengine/tests/RenderEngineTest.cpp
+++ b/libs/renderengine/tests/RenderEngineTest.cpp
@@ -27,6 +27,9 @@
 #include <renderengine/ExternalTexture.h>
 #include <renderengine/RenderEngine.h>
 #include <sync/sync.h>
+#include <system/graphics-base-v1.0.h>
+#include <tonemap/tonemap.h>
+#include <ui/ColorSpace.h>
 #include <ui/PixelFormat.h>
 
 #include <chrono>
@@ -282,6 +285,13 @@
 
     void expectBufferColor(const Rect& rect, uint8_t r, uint8_t g, uint8_t b, uint8_t a,
                            uint8_t tolerance = 0) {
+        auto generator = [=](Point) { return ubyte4(r, g, b, a); };
+        expectBufferColor(rect, generator, tolerance);
+    }
+
+    using ColorGenerator = std::function<ubyte4(Point location)>;
+
+    void expectBufferColor(const Rect& rect, ColorGenerator generator, uint8_t tolerance = 0) {
         auto colorCompare = [tolerance](const uint8_t* colorA, const uint8_t* colorB) {
             auto colorBitCompare = [tolerance](uint8_t a, uint8_t b) {
                 uint8_t tmp = a >= b ? a - b : b - a;
@@ -290,10 +300,10 @@
             return std::equal(colorA, colorA + 4, colorB, colorBitCompare);
         };
 
-        expectBufferColor(rect, r, g, b, a, colorCompare);
+        expectBufferColor(rect, generator, colorCompare);
     }
 
-    void expectBufferColor(const Rect& region, uint8_t r, uint8_t g, uint8_t b, uint8_t a,
+    void expectBufferColor(const Rect& region, ColorGenerator generator,
                            std::function<bool(const uint8_t* a, const uint8_t* b)> colorCompare) {
         uint8_t* pixels;
         mBuffer->getBuffer()->lock(GRALLOC_USAGE_SW_READ_OFTEN | GRALLOC_USAGE_SW_WRITE_OFTEN,
@@ -304,19 +314,22 @@
             const uint8_t* src = pixels +
                     (mBuffer->getBuffer()->getStride() * (region.top + j) + region.left) * 4;
             for (int32_t i = 0; i < region.getWidth(); i++) {
-                const uint8_t expected[4] = {r, g, b, a};
-                bool equal = colorCompare(src, expected);
-                EXPECT_TRUE(equal)
+                const auto location = Point(region.left + i, region.top + j);
+                const ubyte4 colors = generator(location);
+                const uint8_t expected[4] = {colors.r, colors.g, colors.b, colors.a};
+                bool colorMatches = colorCompare(src, expected);
+                EXPECT_TRUE(colorMatches)
                         << GetParam()->name().c_str() << ": "
-                        << "pixel @ (" << region.left + i << ", " << region.top + j << "): "
-                        << "expected (" << static_cast<uint32_t>(r) << ", "
-                        << static_cast<uint32_t>(g) << ", " << static_cast<uint32_t>(b) << ", "
-                        << static_cast<uint32_t>(a) << "), "
+                        << "pixel @ (" << location.x << ", " << location.y << "): "
+                        << "expected (" << static_cast<uint32_t>(colors.r) << ", "
+                        << static_cast<uint32_t>(colors.g) << ", "
+                        << static_cast<uint32_t>(colors.b) << ", "
+                        << static_cast<uint32_t>(colors.a) << "), "
                         << "got (" << static_cast<uint32_t>(src[0]) << ", "
                         << static_cast<uint32_t>(src[1]) << ", " << static_cast<uint32_t>(src[2])
                         << ", " << static_cast<uint32_t>(src[3]) << ")";
                 src += 4;
-                if (!equal && ++fails >= maxFails) {
+                if (!colorMatches && ++fails >= maxFails) {
                     break;
                 }
             }
@@ -328,10 +341,11 @@
     }
 
     void expectAlpha(const Rect& rect, uint8_t a) {
+        auto generator = [=](Point) { return ubyte4(0, 0, 0, a); };
         auto colorCompare = [](const uint8_t* colorA, const uint8_t* colorB) {
             return colorA[3] == colorB[3];
         };
-        expectBufferColor(rect, 0.0f /* r */, 0.0f /*g */, 0.0f /* b */, a, colorCompare);
+        expectBufferColor(rect, generator, colorCompare);
     }
 
     void expectShadowColor(const renderengine::LayerSettings& castingLayer,
@@ -1099,7 +1113,7 @@
     layer.source.buffer.buffer = buf;
     layer.source.buffer.textureName = texName;
     // Transform coordinates to only be inside the red quadrant.
-    layer.source.buffer.textureTransform = mat4::scale(vec4(0.2, 0.2, 1, 1));
+    layer.source.buffer.textureTransform = mat4::scale(vec4(0.2f, 0.2f, 1.f, 1.f));
     layer.alpha = 1.0f;
     layer.geometry.boundaries = Rect(1, 1).toFloatRect();
 
@@ -1281,7 +1295,8 @@
     settings.clip = fullscreenRect();
 
     // 255, 255, 255, 255 is full opaque white.
-    const ubyte4 backgroundColor(255.f, 255.f, 255.f, 255.f);
+    const ubyte4 backgroundColor(static_cast<uint8_t>(255), static_cast<uint8_t>(255),
+                                 static_cast<uint8_t>(255), static_cast<uint8_t>(255));
     // Create layer with given color.
     renderengine::LayerSettings bgLayer;
     bgLayer.sourceDataspace = ui::Dataspace::V0_SRGB_LINEAR;
@@ -1615,7 +1630,8 @@
 TEST_P(RenderEngineTest, drawLayers_fillShadow_castsWithoutCasterLayer) {
     initializeRenderEngine();
 
-    const ubyte4 backgroundColor(255, 255, 255, 255);
+    const ubyte4 backgroundColor(static_cast<uint8_t>(255), static_cast<uint8_t>(255),
+                                 static_cast<uint8_t>(255), static_cast<uint8_t>(255));
     const float shadowLength = 5.0f;
     Rect casterBounds(DEFAULT_DISPLAY_WIDTH / 3.0f, DEFAULT_DISPLAY_HEIGHT / 3.0f);
     casterBounds.offsetBy(shadowLength + 1, shadowLength + 1);
@@ -1630,8 +1646,10 @@
 TEST_P(RenderEngineTest, drawLayers_fillShadow_casterLayerMinSize) {
     initializeRenderEngine();
 
-    const ubyte4 casterColor(255, 0, 0, 255);
-    const ubyte4 backgroundColor(255, 255, 255, 255);
+    const ubyte4 casterColor(static_cast<uint8_t>(255), static_cast<uint8_t>(0),
+                             static_cast<uint8_t>(0), static_cast<uint8_t>(255));
+    const ubyte4 backgroundColor(static_cast<uint8_t>(255), static_cast<uint8_t>(255),
+                                 static_cast<uint8_t>(255), static_cast<uint8_t>(255));
     const float shadowLength = 5.0f;
     Rect casterBounds(1, 1);
     casterBounds.offsetBy(shadowLength + 1, shadowLength + 1);
@@ -1649,8 +1667,10 @@
 TEST_P(RenderEngineTest, drawLayers_fillShadow_casterColorLayer) {
     initializeRenderEngine();
 
-    const ubyte4 casterColor(255, 0, 0, 255);
-    const ubyte4 backgroundColor(255, 255, 255, 255);
+    const ubyte4 casterColor(static_cast<uint8_t>(255), static_cast<uint8_t>(0),
+                             static_cast<uint8_t>(0), static_cast<uint8_t>(255));
+    const ubyte4 backgroundColor(static_cast<uint8_t>(255), static_cast<uint8_t>(255),
+                                 static_cast<uint8_t>(255), static_cast<uint8_t>(255));
     const float shadowLength = 5.0f;
     Rect casterBounds(DEFAULT_DISPLAY_WIDTH / 3.0f, DEFAULT_DISPLAY_HEIGHT / 3.0f);
     casterBounds.offsetBy(shadowLength + 1, shadowLength + 1);
@@ -1669,8 +1689,10 @@
 TEST_P(RenderEngineTest, drawLayers_fillShadow_casterOpaqueBufferLayer) {
     initializeRenderEngine();
 
-    const ubyte4 casterColor(255, 0, 0, 255);
-    const ubyte4 backgroundColor(255, 255, 255, 255);
+    const ubyte4 casterColor(static_cast<uint8_t>(255), static_cast<uint8_t>(0),
+                             static_cast<uint8_t>(0), static_cast<uint8_t>(255));
+    const ubyte4 backgroundColor(static_cast<uint8_t>(255), static_cast<uint8_t>(255),
+                                 static_cast<uint8_t>(255), static_cast<uint8_t>(255));
     const float shadowLength = 5.0f;
     Rect casterBounds(DEFAULT_DISPLAY_WIDTH / 3.0f, DEFAULT_DISPLAY_HEIGHT / 3.0f);
     casterBounds.offsetBy(shadowLength + 1, shadowLength + 1);
@@ -1690,8 +1712,10 @@
 TEST_P(RenderEngineTest, drawLayers_fillShadow_casterWithRoundedCorner) {
     initializeRenderEngine();
 
-    const ubyte4 casterColor(255, 0, 0, 255);
-    const ubyte4 backgroundColor(255, 255, 255, 255);
+    const ubyte4 casterColor(static_cast<uint8_t>(255), static_cast<uint8_t>(0),
+                             static_cast<uint8_t>(0), static_cast<uint8_t>(255));
+    const ubyte4 backgroundColor(static_cast<uint8_t>(255), static_cast<uint8_t>(255),
+                                 static_cast<uint8_t>(255), static_cast<uint8_t>(255));
     const float shadowLength = 5.0f;
     Rect casterBounds(DEFAULT_DISPLAY_WIDTH / 3.0f, DEFAULT_DISPLAY_HEIGHT / 3.0f);
     casterBounds.offsetBy(shadowLength + 1, shadowLength + 1);
@@ -2027,6 +2051,155 @@
         expectBufferColor(rect, 0, 255, 0, 255);
     }
 }
+
+double EOTF_PQ(double channel) {
+    float m1 = (2610.0 / 4096.0) / 4.0;
+    float m2 = (2523.0 / 4096.0) * 128.0;
+    float c1 = (3424.0 / 4096.0);
+    float c2 = (2413.0 / 4096.0) * 32.0;
+    float c3 = (2392.0 / 4096.0) * 32.0;
+
+    float tmp = std::pow(std::clamp(channel, 0.0, 1.0), 1.0 / m2);
+    tmp = std::fmax(tmp - c1, 0.0) / (c2 - c3 * tmp);
+    return std::pow(tmp, 1.0 / m1);
+}
+
+vec3 EOTF_PQ(vec3 color) {
+    return vec3(EOTF_PQ(color.r), EOTF_PQ(color.g), EOTF_PQ(color.b));
+}
+
+double OETF_sRGB(double channel) {
+    return channel <= 0.0031308 ? channel * 12.92 : (pow(channel, 1.0 / 2.4) * 1.055) - 0.055;
+}
+
+int sign(float in) {
+    return in >= 0.0 ? 1 : -1;
+}
+
+vec3 OETF_sRGB(vec3 linear) {
+    return vec3(sign(linear.r) * OETF_sRGB(linear.r), sign(linear.g) * OETF_sRGB(linear.g),
+                sign(linear.b) * OETF_sRGB(linear.b));
+}
+
+TEST_P(RenderEngineTest, test_tonemapPQMatches) {
+    if (!GetParam()->useColorManagement()) {
+        return;
+    }
+
+    if (GetParam()->type() == renderengine::RenderEngine::RenderEngineType::GLES) {
+        return;
+    }
+
+    initializeRenderEngine();
+
+    constexpr int32_t kGreyLevels = 256;
+
+    const auto rect = Rect(0, 0, kGreyLevels, 1);
+    const renderengine::DisplaySettings display{
+            .physicalDisplay = rect,
+            .clip = rect,
+            .maxLuminance = 750.0f,
+            .outputDataspace = ui::Dataspace::DISPLAY_P3,
+    };
+
+    auto buf = std::make_shared<
+            renderengine::ExternalTexture>(new GraphicBuffer(kGreyLevels, 1,
+                                                             HAL_PIXEL_FORMAT_RGBA_8888, 1,
+                                                             GRALLOC_USAGE_SW_READ_OFTEN |
+                                                                     GRALLOC_USAGE_SW_WRITE_OFTEN |
+                                                                     GRALLOC_USAGE_HW_RENDER |
+                                                                     GRALLOC_USAGE_HW_TEXTURE,
+                                                             "input"),
+                                           *mRE,
+                                           renderengine::ExternalTexture::Usage::READABLE |
+                                                   renderengine::ExternalTexture::Usage::WRITEABLE);
+    ASSERT_EQ(0, buf->getBuffer()->initCheck());
+
+    {
+        uint8_t* pixels;
+        buf->getBuffer()->lock(GRALLOC_USAGE_SW_READ_OFTEN | GRALLOC_USAGE_SW_WRITE_OFTEN,
+                               reinterpret_cast<void**>(&pixels));
+
+        uint8_t color = 0;
+        for (int32_t j = 0; j < buf->getBuffer()->getHeight(); j++) {
+            uint8_t* dest = pixels + (buf->getBuffer()->getStride() * j * 4);
+            for (int32_t i = 0; i < buf->getBuffer()->getWidth(); i++) {
+                dest[0] = color;
+                dest[1] = color;
+                dest[2] = color;
+                dest[3] = 255;
+                color++;
+                dest += 4;
+            }
+        }
+        buf->getBuffer()->unlock();
+    }
+
+    mBuffer = std::make_shared<
+            renderengine::ExternalTexture>(new GraphicBuffer(kGreyLevels, 1,
+                                                             HAL_PIXEL_FORMAT_RGBA_8888, 1,
+                                                             GRALLOC_USAGE_SW_READ_OFTEN |
+                                                                     GRALLOC_USAGE_SW_WRITE_OFTEN |
+                                                                     GRALLOC_USAGE_HW_RENDER |
+                                                                     GRALLOC_USAGE_HW_TEXTURE,
+                                                             "output"),
+                                           *mRE,
+                                           renderengine::ExternalTexture::Usage::READABLE |
+                                                   renderengine::ExternalTexture::Usage::WRITEABLE);
+    ASSERT_EQ(0, mBuffer->getBuffer()->initCheck());
+
+    const renderengine::LayerSettings layer{
+            .geometry.boundaries = rect.toFloatRect(),
+            .source =
+                    renderengine::PixelSource{
+                            .buffer =
+                                    renderengine::Buffer{
+                                            .buffer = std::move(buf),
+                                            .usePremultipliedAlpha = true,
+                                    },
+                    },
+            .alpha = 1.0f,
+            .sourceDataspace = static_cast<ui::Dataspace>(HAL_DATASPACE_STANDARD_BT2020 |
+                                                          HAL_DATASPACE_TRANSFER_ST2084 |
+                                                          HAL_DATASPACE_RANGE_FULL),
+    };
+
+    std::vector<renderengine::LayerSettings> layers{layer};
+    invokeDraw(display, layers);
+
+    ColorSpace displayP3 = ColorSpace::DisplayP3();
+    ColorSpace bt2020 = ColorSpace::BT2020();
+
+    tonemap::Metadata metadata{.displayMaxLuminance = 750.0f};
+
+    auto generator = [=](Point location) {
+        const double normColor = static_cast<double>(location.x) / (kGreyLevels - 1);
+        const vec3 rgb = vec3(normColor, normColor, normColor);
+
+        const vec3 linearRGB = EOTF_PQ(rgb);
+
+        static constexpr float kMaxPQLuminance = 10000.f;
+        const vec3 xyz = bt2020.getRGBtoXYZ() * linearRGB * kMaxPQLuminance;
+        const double gain =
+                tonemap::getToneMapper()
+                        ->lookupTonemapGain(static_cast<aidl::android::hardware::graphics::common::
+                                                                Dataspace>(
+                                                    HAL_DATASPACE_STANDARD_BT2020 |
+                                                    HAL_DATASPACE_TRANSFER_ST2084 |
+                                                    HAL_DATASPACE_RANGE_FULL),
+                                            static_cast<aidl::android::hardware::graphics::common::
+                                                                Dataspace>(
+                                                    ui::Dataspace::DISPLAY_P3),
+                                            linearRGB * 10000.0, xyz, metadata);
+        const vec3 scaledXYZ = xyz * gain / metadata.displayMaxLuminance;
+
+        const vec3 targetRGB = OETF_sRGB(displayP3.getXYZtoRGB() * scaledXYZ) * 255;
+        return ubyte4(static_cast<uint8_t>(targetRGB.r), static_cast<uint8_t>(targetRGB.g),
+                      static_cast<uint8_t>(targetRGB.b), 255);
+    };
+
+    expectBufferColor(Rect(kGreyLevels, 1), generator, 2);
+}
 } // namespace renderengine
 } // namespace android
 
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