Add libtonemap library
libtonemap is a vendor-available static library which provides a single
point where the definitions of tone mapping operations can be shared.
This faciliates sharing tone mapping operations between system libraries
and vendor libraries without making invasive changes to the framework.
The canonical use-case for this library is matching the tone-mapping
curves between DPU composition and GPU composition for the display.
Although not done in this patch, this library may be linked into
libhwui, for propagating the tone mapping operation into TextureView.
The initial design for this library is to expose a Tonemapper class,
which can return:
1. A shader string describing the tonemapper operation, which may be
inserted into any other shader string.
2. A list of shader uniforms for binding to the final shader
A later patch will allow for computing the tone mapping curve on the
CPU, which is useful for unit testing, and optionally for generating a
LUT without firing up the GPU.
Bug: 200310159
Ignore-AOSP-First: new internal-only lib
Test: libtonemap_test
Change-Id: I7f1353e0f456ec5f371e31754b2965c9b44aa125
diff --git a/libs/renderengine/Android.bp b/libs/renderengine/Android.bp
index a62d2b9..ecfaef8 100644
--- a/libs/renderengine/Android.bp
+++ b/libs/renderengine/Android.bp
@@ -40,6 +40,10 @@
"libui",
"libutils",
],
+
+ static_libs: [
+ "libtonemap",
+ ],
local_include_dirs: ["include"],
export_include_dirs: ["include"],
}
@@ -97,7 +101,7 @@
"skia/filters/GaussianBlurFilter.cpp",
"skia/filters/KawaseBlurFilter.cpp",
"skia/filters/LinearEffect.cpp",
- "skia/filters/StretchShaderFactory.cpp"
+ "skia/filters/StretchShaderFactory.cpp",
],
}
diff --git a/libs/renderengine/benchmark/Android.bp b/libs/renderengine/benchmark/Android.bp
index 5968399..baa5054 100644
--- a/libs/renderengine/benchmark/Android.bp
+++ b/libs/renderengine/benchmark/Android.bp
@@ -23,7 +23,10 @@
cc_benchmark {
name: "librenderengine_bench",
- defaults: ["skia_deps", "surfaceflinger_defaults"],
+ defaults: [
+ "skia_deps",
+ "surfaceflinger_defaults",
+ ],
srcs: [
"main.cpp",
"Codec.cpp",
@@ -32,6 +35,7 @@
],
static_libs: [
"librenderengine",
+ "libtonemap",
],
cflags: [
"-DLOG_TAG=\"RenderEngineBench\"",
@@ -50,5 +54,5 @@
"libutils",
],
- data: [ "resources/*"],
+ data: ["resources/*"],
}
diff --git a/libs/renderengine/skia/filters/LinearEffect.cpp b/libs/renderengine/skia/filters/LinearEffect.cpp
index 73dadef..53136e4 100644
--- a/libs/renderengine/skia/filters/LinearEffect.cpp
+++ b/libs/renderengine/skia/filters/LinearEffect.cpp
@@ -19,6 +19,7 @@
#define ATRACE_TAG ATRACE_TAG_GRAPHICS
#include <SkString.h>
+#include <tonemap/tonemap.h>
#include <utils/Trace.h>
#include <optional>
@@ -32,6 +33,11 @@
namespace renderengine {
namespace skia {
+static aidl::android::hardware::graphics::common::Dataspace toAidlDataspace(
+ ui::Dataspace dataspace) {
+ return static_cast<aidl::android::hardware::graphics::common::Dataspace>(dataspace);
+}
+
static void generateEOTF(ui::Dataspace dataspace, SkString& shader) {
switch (dataspace & HAL_DATASPACE_TRANSFER_MASK) {
case HAL_DATASPACE_TRANSFER_ST2084:
@@ -127,159 +133,13 @@
default:
shader.append(R"(
float3 ScaleLuminance(float3 xyz) {
- return xyz * in_inputMaxLuminance;
+ return xyz * in_libtonemap_inputMaxLuminance;
}
)");
break;
}
}
-static void generateToneMapInterpolation(ui::Dataspace inputDataspace,
- ui::Dataspace outputDataspace, SkString& shader) {
- switch (inputDataspace & HAL_DATASPACE_TRANSFER_MASK) {
- case HAL_DATASPACE_TRANSFER_ST2084:
- case HAL_DATASPACE_TRANSFER_HLG:
- switch (outputDataspace & HAL_DATASPACE_TRANSFER_MASK) {
- case HAL_DATASPACE_TRANSFER_ST2084:
- shader.append(R"(
- float3 ToneMap(float3 xyz) {
- return xyz;
- }
- )");
- break;
- case HAL_DATASPACE_TRANSFER_HLG:
- // 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.
- shader.append(R"(
- float3 ToneMap(float3 xyz) {
- return clamp(xyz, 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.
- shader.append(R"(
- float3 ToneMap(float3 xyz) {
- float maxInLumi = in_inputMaxLuminance;
- float maxOutLumi = in_displayMaxLuminance;
-
- float nits = 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 (maxInLumi <= maxOutLumi) {
- return xyz;
- } else {
-
- // three control points
- const float x0 = 10.0;
- const float y0 = 17.0;
- float x1 = maxOutLumi * 0.75;
- float y1 = x1;
- float x2 = x1 + (maxInLumi - x1) / 2.0;
- float y2 = y1 + (maxOutLumi - y1) * 0.75;
-
- // horizontal distances between the last three control points
- float h12 = x2 - x1;
- float h23 = maxInLumi - x2;
- // tangents at the last three control points
- float m1 = (y2 - y1) / h12;
- float m3 = (maxOutLumi - y2) / h23;
- float m2 = (m1 + m3) / 2.0;
-
- if (nits < x0) {
- // scale [0.0, x0] to [0.0, y0] linearly
- float slope = y0 / x0;
- return xyz * slope;
- } else if (nits < x1) {
- // scale [x0, x1] to [y0, y1] linearly
- float slope = (y1 - y0) / (x1 - x0);
- nits = y0 + (nits - x0) * slope;
- } else if (nits < x2) {
- // scale [x1, x2] to [y1, y2] using Hermite interp
- float t = (nits - x1) / h12;
- nits = (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
- float t = (nits - x2) / h23;
- nits = (y2 * (1.0 + 2.0 * t) + h23 * m2 * t) * (1.0 - t) * (1.0 - t) +
- (maxOutLumi * (3.0 - 2.0 * t) + h23 * m3 * (t - 1.0)) * t * t;
- }
- }
-
- // color.y is greater than x0 and is thus non-zero
- return xyz * (nits / xyz.y);
- }
- )");
- break;
- }
- break;
- default:
- switch (outputDataspace & HAL_DATASPACE_TRANSFER_MASK) {
- case HAL_DATASPACE_TRANSFER_ST2084:
- case HAL_DATASPACE_TRANSFER_HLG:
- // 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.
- shader.append(R"(
- float3 ToneMap(float3 xyz) {
- const float maxOutLumi = 3000.0;
-
- const float x0 = 5.0;
- const float y0 = 2.5;
- float x1 = in_displayMaxLuminance * 0.7;
- float y1 = maxOutLumi * 0.15;
- float x2 = in_displayMaxLuminance * 0.9;
- float y2 = maxOutLumi * 0.45;
- float x3 = in_displayMaxLuminance;
- float y3 = maxOutLumi;
-
- float c1 = y1 / 3.0;
- float c2 = y2 / 2.0;
- float c3 = y3 / 1.5;
-
- float nits = xyz.y;
-
- if (nits <= x0) {
- // scale [0.0, x0] to [0.0, y0] linearly
- float slope = y0 / x0;
- return xyz * slope;
- } else if (nits <= x1) {
- // scale [x0, x1] to [y0, y1] using a curve
- float t = (nits - x0) / (x1 - x0);
- nits = (1.0 - t) * (1.0 - t) * y0 + 2.0 * (1.0 - t) * t * c1 + t * t * y1;
- } else if (nits <= x2) {
- // scale [x1, x2] to [y1, y2] using a curve
- float t = (nits - x1) / (x2 - x1);
- nits = (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
- float t = (nits - x2) / (x3 - x2);
- nits = (1.0 - t) * (1.0 - t) * y2 + 2.0 * (1.0 - t) * t * c3 + t * t * y3;
- }
-
- // xyz.y is greater than x0 and is thus non-zero
- return xyz * (nits / xyz.y);
- }
- )");
- break;
- default:
- // For completeness, this is tone-mapping from SDR to SDR, where this is just a
- // no-op.
- shader.append(R"(
- float3 ToneMap(float3 xyz) {
- return xyz;
- }
- )");
- break;
- }
- break;
- }
-}
-
// Normalizes from absolute light back to relative light (maps from [0, maxNits] back to [0, 1])
static void generateLuminanceNormalizationForOOTF(ui::Dataspace outputDataspace, SkString& shader) {
switch (outputDataspace & HAL_DATASPACE_TRANSFER_MASK) {
@@ -300,7 +160,7 @@
default:
shader.append(R"(
float3 NormalizeLuminance(float3 xyz) {
- return xyz / in_displayMaxLuminance;
+ return xyz / in_libtonemap_displayMaxLuminance;
}
)");
break;
@@ -309,19 +169,22 @@
static void generateOOTF(ui::Dataspace inputDataspace, ui::Dataspace outputDataspace,
SkString& shader) {
- // Input uniforms
- shader.append(R"(
- uniform float in_displayMaxLuminance;
- uniform float in_inputMaxLuminance;
- )");
+ shader.append(tonemap::getToneMapper()
+ ->generateTonemapGainShaderSkSL(toAidlDataspace(inputDataspace),
+ toAidlDataspace(outputDataspace))
+ .c_str());
generateLuminanceScalesForOOTF(inputDataspace, shader);
- generateToneMapInterpolation(inputDataspace, outputDataspace, shader);
generateLuminanceNormalizationForOOTF(outputDataspace, shader);
shader.append(R"(
- float3 OOTF(float3 xyz) {
- return NormalizeLuminance(ToneMap(ScaleLuminance(xyz)));
+ float3 OOTF(float3 linearRGB, float3 xyz) {
+ float3 scaledLinearRGB = ScaleLuminance(linearRGB);
+ float3 scaledXYZ = ScaleLuminance(xyz);
+
+ float gain = libtonemap_LookupTonemapGain(scaledLinearRGB, scaledXYZ);
+
+ return NormalizeLuminance(scaledXYZ * gain);
}
)");
}
@@ -399,7 +262,9 @@
)");
}
shader.append(R"(
- c.rgb = OETF(ToRGB(OOTF(ToXYZ(EOTF(c.rgb)))));
+ float3 linearRGB = EOTF(c.rgb);
+ float3 xyz = ToXYZ(linearRGB);
+ c.rgb = OETF(ToRGB(OOTF(linearRGB, xyz)));
)");
if (undoPremultipliedAlpha) {
shader.append(R"(
@@ -465,11 +330,20 @@
colorTransform * mat4(outputColorSpace.getXYZtoRGB());
}
- effectBuilder.uniform("in_displayMaxLuminance") = maxDisplayLuminance;
- // If the input luminance is unknown, use display luminance (aka, no-op any luminance changes)
- // This will be the case for eg screenshots in addition to uncalibrated displays
- effectBuilder.uniform("in_inputMaxLuminance") =
- maxLuminance > 0 ? maxLuminance : maxDisplayLuminance;
+ tonemap::Metadata metadata{.displayMaxLuminance = maxDisplayLuminance,
+ // If the input luminance is unknown, use display luminance (aka,
+ // no-op any luminance changes)
+ // This will be the case for eg screenshots in addition to
+ // uncalibrated displays
+ .contentMaxLuminance =
+ maxLuminance > 0 ? maxLuminance : maxDisplayLuminance};
+
+ const auto uniforms = tonemap::getToneMapper()->generateShaderSkSLUniforms(metadata);
+
+ for (const auto& uniform : uniforms) {
+ effectBuilder.uniform(uniform.name.c_str()).set(uniform.value.data(), uniform.value.size());
+ }
+
return effectBuilder.makeShader(nullptr, false);
}
diff --git a/libs/renderengine/tests/Android.bp b/libs/renderengine/tests/Android.bp
index d0e19dd..52b6c8f 100644
--- a/libs/renderengine/tests/Android.bp
+++ b/libs/renderengine/tests/Android.bp
@@ -23,7 +23,10 @@
cc_test {
name: "librenderengine_test",
- defaults: ["skia_deps", "surfaceflinger_defaults"],
+ defaults: [
+ "skia_deps",
+ "surfaceflinger_defaults",
+ ],
test_suites: ["device-tests"],
srcs: [
"RenderEngineTest.cpp",
@@ -36,6 +39,7 @@
"libgmock",
"librenderengine",
"librenderengine_mocks",
+ "libtonemap",
],
shared_libs: [
diff --git a/libs/tonemap/Android.bp b/libs/tonemap/Android.bp
new file mode 100644
index 0000000..231a342
--- /dev/null
+++ b/libs/tonemap/Android.bp
@@ -0,0 +1,37 @@
+// Copyright 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "frameworks_native_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["frameworks_native_license"],
+}
+
+cc_library_static {
+ name: "libtonemap",
+ vendor_available: true,
+
+ export_include_dirs: ["include"],
+ local_include_dirs: ["include"],
+
+ shared_libs: [
+ "android.hardware.graphics.common-V3-ndk",
+ ],
+ srcs: [
+ "tonemap.cpp",
+ ],
+}
diff --git a/libs/tonemap/OWNERS b/libs/tonemap/OWNERS
new file mode 100644
index 0000000..6d91da3
--- /dev/null
+++ b/libs/tonemap/OWNERS
@@ -0,0 +1,4 @@
+alecmouri@google.com
+jreck@google.com
+sallyqi@google.com
+scroggo@google.com
\ No newline at end of file
diff --git a/libs/tonemap/TEST_MAPPING b/libs/tonemap/TEST_MAPPING
new file mode 100644
index 0000000..00f83ba
--- /dev/null
+++ b/libs/tonemap/TEST_MAPPING
@@ -0,0 +1,10 @@
+{
+ "presubmit": [
+ {
+ "name": "librenderengine_test"
+ },
+ {
+ "name": "libtonemap_test"
+ }
+ ]
+}
diff --git a/libs/tonemap/include/tonemap/tonemap.h b/libs/tonemap/include/tonemap/tonemap.h
new file mode 100644
index 0000000..d350e16
--- /dev/null
+++ b/libs/tonemap/include/tonemap/tonemap.h
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <aidl/android/hardware/graphics/common/Dataspace.h>
+
+#include <string>
+#include <vector>
+
+namespace android::tonemap {
+
+// Describes a shader uniform
+// The shader uniform is intended to be passed into a SkRuntimeShaderBuilder, i.e.:
+//
+// SkRuntimeShaderBuilder builder;
+// builder.uniform(<uniform name>).set(<uniform value>.data(), <uniform value>.size());
+struct ShaderUniform {
+ // The name of the uniform, used for binding into a shader.
+ // The shader must contain a uniform whose name matches this.
+ std::string name;
+
+ // The value for the uniform, which should be bound to the uniform identified by <name>
+ std::vector<uint8_t> value;
+};
+
+// Describes metadata which may be used for constructing the shader uniforms.
+// This metadata should not be used for manipulating the source code of the shader program directly,
+// as otherwise caching by other system of these shaders may break.
+struct Metadata {
+ float displayMaxLuminance = 0.0;
+ float contentMaxLuminance = 0.0;
+};
+
+class ToneMapper {
+public:
+ virtual ~ToneMapper() {}
+ // Constructs a tonemap shader whose shader language is SkSL
+ //
+ // The returned shader string *must* contain a function with the following signature:
+ // float libtonemap_LookupTonemapGain(vec3 linearRGB, vec3 xyz);
+ //
+ // The arguments are:
+ // * linearRGB is the absolute nits of the RGB pixels in linear space
+ // * xyz is linearRGB converted into XYZ
+ //
+ // libtonemap_LookupTonemapGain() returns a float representing the amount by which to scale the
+ // absolute nits of the pixels. This function may be plugged into any existing SkSL shader, and
+ // is expected to look something like this:
+ //
+ // vec3 rgb = ...;
+ // // apply the EOTF based on the incoming dataspace to convert to linear nits.
+ // vec3 linearRGB = applyEOTF(rgb);
+ // // apply a RGB->XYZ matrix float3
+ // vec3 xyz = toXYZ(linearRGB);
+ // // Scale the luminance based on the content standard
+ // vec3 absoluteRGB = ScaleLuminance(linearRGB);
+ // vec3 absoluteXYZ = ScaleLuminance(xyz);
+ // float gain = libtonemap_LookupTonemapGain(absoluteRGB, absoluteXYZ);
+ // // Normalize the luminance back down to a [0, 1] range
+ // xyz = NormalizeLuminance(absoluteXYZ * gain);
+ // // apply a XYZ->RGB matrix and apply the output OETf.
+ // vec3 finalColor = applyOETF(ToRGB(xyz));
+ // ...
+ //
+ // Helper methods in this shader should be prefixed with "libtonemap_". Accordingly, libraries
+ // which consume this shader must *not* contain any methods prefixed with "libtonemap_" to
+ // guarantee that there are no conflicts in name resolution.
+ virtual std::string generateTonemapGainShaderSkSL(
+ aidl::android::hardware::graphics::common::Dataspace sourceDataspace,
+ aidl::android::hardware::graphics::common::Dataspace destinationDataspace) = 0;
+
+ // Constructs uniform descriptions that correspond to those that are generated for the tonemap
+ // shader. Uniforms must be prefixed with "in_libtonemap_". Libraries which consume this shader
+ // must not bind any new uniforms that begin with this prefix.
+ //
+ // Downstream shaders may assume the existence of the uniform in_libtonemap_displayMaxLuminance
+ // and in_libtonemap_inputMaxLuminance, in order to assist with scaling and normalizing
+ // luminance as described in the documentation for generateTonemapGainShaderSkSL(). That is,
+ // shaders plugging in a tone-mapping shader returned by generateTonemapGainShaderSkSL() may
+ // 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;
+};
+
+// Retrieves a tonemapper instance.
+// This instance is globally constructed.
+ToneMapper* getToneMapper();
+
+} // namespace android::tonemap
\ No newline at end of file
diff --git a/libs/tonemap/tests/Android.bp b/libs/tonemap/tests/Android.bp
new file mode 100644
index 0000000..e58d519
--- /dev/null
+++ b/libs/tonemap/tests/Android.bp
@@ -0,0 +1,38 @@
+// Copyright 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "frameworks_native_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["frameworks_native_license"],
+}
+
+cc_test {
+ name: "libtonemap_test",
+ test_suites: ["device-tests"],
+ srcs: [
+ "tonemap_test.cpp",
+ ],
+ shared_libs: [
+ "android.hardware.graphics.common-V3-ndk",
+ ],
+ static_libs: [
+ "libgmock",
+ "libgtest",
+ "libtonemap",
+ ],
+}
diff --git a/libs/tonemap/tests/tonemap_test.cpp b/libs/tonemap/tests/tonemap_test.cpp
new file mode 100644
index 0000000..7a7958f
--- /dev/null
+++ b/libs/tonemap/tests/tonemap_test.cpp
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+#include <tonemap/tonemap.h>
+#include <cmath>
+
+namespace android {
+
+using testing::HasSubstr;
+
+struct TonemapTest : public ::testing::Test {};
+
+TEST_F(TonemapTest, generateShaderSkSLUniforms_containsDefaultUniforms) {
+ static const constexpr float kDisplayMaxLuminance = 1.f;
+ static const constexpr float kContentMaxLuminance = 2.f;
+ tonemap::Metadata metadata{.displayMaxLuminance = kDisplayMaxLuminance,
+ .contentMaxLuminance = kContentMaxLuminance};
+ const auto uniforms = tonemap::getToneMapper()->generateShaderSkSLUniforms(metadata);
+
+ ASSERT_EQ(1, std::count_if(uniforms.cbegin(), uniforms.cend(), [](const auto& data) {
+ return data.name == "in_libtonemap_displayMaxLuminance";
+ }));
+ ASSERT_EQ(1, std::count_if(uniforms.cbegin(), uniforms.cend(), [](const auto& data) {
+ return data.name == "in_libtonemap_inputMaxLuminance";
+ }));
+
+ // Smoke check that metadata values are "real", specifically that they're non-zero and actually
+ // numbers. This is to help avoid shaders using these uniforms from dividing by zero or other
+ // catastrophic errors.
+ const auto& displayLum = std::find_if(uniforms.cbegin(), uniforms.cend(), [](const auto& data) {
+ return data.name == "in_libtonemap_displayMaxLuminance";
+ })->value;
+
+ float displayLumFloat = 0.f;
+ std::memcpy(&displayLumFloat, displayLum.data(), displayLum.size());
+ EXPECT_FALSE(std::isnan(displayLumFloat));
+ EXPECT_GT(displayLumFloat, 0);
+
+ const auto& contentLum = std::find_if(uniforms.cbegin(), uniforms.cend(), [](const auto& data) {
+ return data.name == "in_libtonemap_inputMaxLuminance";
+ })->value;
+
+ float contentLumFloat = 0.f;
+ std::memcpy(&contentLumFloat, contentLum.data(), contentLum.size());
+ EXPECT_FALSE(std::isnan(contentLumFloat));
+ EXPECT_GT(contentLumFloat, 0);
+}
+
+TEST_F(TonemapTest, generateTonemapGainShaderSkSL_containsEntryPoint) {
+ const auto shader =
+ tonemap::getToneMapper()
+ ->generateTonemapGainShaderSkSL(aidl::android::hardware::graphics::common::
+ Dataspace::BT2020_ITU_PQ,
+ aidl::android::hardware::graphics::common::
+ Dataspace::DISPLAY_P3);
+
+ // Other tests such as librenderengine_test will plug in the shader to check compilation.
+ EXPECT_THAT(shader, HasSubstr("float libtonemap_LookupTonemapGain(vec3 linearRGB, vec3 xyz)"));
+}
+
+} // namespace android
diff --git a/libs/tonemap/tonemap.cpp b/libs/tonemap/tonemap.cpp
new file mode 100644
index 0000000..350bca4
--- /dev/null
+++ b/libs/tonemap/tonemap.cpp
@@ -0,0 +1,256 @@
+/*
+ * Copyright 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <tonemap/tonemap.h>
+
+#include <cstdint>
+#include <mutex>
+#include <type_traits>
+
+namespace android::tonemap {
+
+namespace {
+
+// Flag containing the variant of tone map algorithm to use.
+enum class ToneMapAlgorithm {
+ AndroidO, // Default algorithm in place since Android O,
+};
+
+static const constexpr auto kToneMapAlgorithm = ToneMapAlgorithm::AndroidO;
+
+static const constexpr auto kTransferMask =
+ static_cast<int32_t>(aidl::android::hardware::graphics::common::Dataspace::TRANSFER_MASK);
+static const constexpr auto kTransferST2084 =
+ static_cast<int32_t>(aidl::android::hardware::graphics::common::Dataspace::TRANSFER_ST2084);
+static const constexpr auto kTransferHLG =
+ static_cast<int32_t>(aidl::android::hardware::graphics::common::Dataspace::TRANSFER_HLG);
+
+template <typename T, std::enable_if_t<std::is_trivially_copyable<T>::value, bool> = true>
+std::vector<uint8_t> buildUniformValue(T value) {
+ std::vector<uint8_t> result;
+ result.resize(sizeof(value));
+ std::memcpy(result.data(), &value, sizeof(value));
+ return result;
+}
+
+class ToneMapperO : public ToneMapper {
+public:
+ std::string generateTonemapGainShaderSkSL(
+ aidl::android::hardware::graphics::common::Dataspace sourceDataspace,
+ aidl::android::hardware::graphics::common::Dataspace destinationDataspace) override {
+ const int32_t sourceDataspaceInt = static_cast<int32_t>(sourceDataspace);
+ const int32_t destinationDataspaceInt = static_cast<int32_t>(destinationDataspace);
+
+ std::string program;
+ // Define required uniforms
+ program.append(R"(
+ uniform float in_libtonemap_displayMaxLuminance;
+ uniform float in_libtonemap_inputMaxLuminance;
+ )");
+ switch (sourceDataspaceInt & kTransferMask) {
+ case kTransferST2084:
+ case kTransferHLG:
+ switch (destinationDataspaceInt & kTransferMask) {
+ case kTransferST2084:
+ program.append(R"(
+ float libtonemap_ToneMapTargetNits(vec3 xyz) {
+ return 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.
+ program.append(R"(
+ float libtonemap_ToneMapTargetNits(vec3 xyz) {
+ return clamp(xyz.y, 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.
+ program.append(R"(
+ float libtonemap_ToneMapTargetNits(vec3 xyz) {
+ float maxInLumi = in_libtonemap_inputMaxLuminance;
+ float maxOutLumi = in_libtonemap_displayMaxLuminance;
+
+ float nits = 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 (maxInLumi <= maxOutLumi) {
+ return xyz.y;
+ } else {
+
+ // three control points
+ const float x0 = 10.0;
+ const float y0 = 17.0;
+ float x1 = maxOutLumi * 0.75;
+ float y1 = x1;
+ float x2 = x1 + (maxInLumi - x1) / 2.0;
+ float y2 = y1 + (maxOutLumi - y1) * 0.75;
+
+ // horizontal distances between the last three
+ // control points
+ float h12 = x2 - x1;
+ float h23 = maxInLumi - x2;
+ // tangents at the last three control points
+ float m1 = (y2 - y1) / h12;
+ float m3 = (maxOutLumi - y2) / h23;
+ float m2 = (m1 + m3) / 2.0;
+
+ if (nits < x0) {
+ // scale [0.0, x0] to [0.0, y0] linearly
+ float slope = y0 / x0;
+ return nits * slope;
+ } else if (nits < x1) {
+ // scale [x0, x1] to [y0, y1] linearly
+ float slope = (y1 - y0) / (x1 - x0);
+ nits = y0 + (nits - x0) * slope;
+ } else if (nits < x2) {
+ // scale [x1, x2] to [y1, y2] using Hermite interp
+ float t = (nits - x1) / h12;
+ nits = (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
+ float t = (nits - x2) / h23;
+ nits = (y2 * (1.0 + 2.0 * t) + h23 * m2 * t) *
+ (1.0 - t) * (1.0 - t) + (maxOutLumi *
+ (3.0 - 2.0 * t) + h23 * m3 *
+ (t - 1.0)) * t * t;
+ }
+ }
+
+ return nits;
+ }
+ )");
+ break;
+ }
+ break;
+ default:
+ 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.
+ program.append(R"(
+ float libtonemap_ToneMapTargetNits(vec3 xyz) {
+ const float maxOutLumi = 3000.0;
+
+ const float x0 = 5.0;
+ const float y0 = 2.5;
+ float x1 = in_libtonemap_displayMaxLuminance * 0.7;
+ float y1 = maxOutLumi * 0.15;
+ float x2 = in_libtonemap_displayMaxLuminance * 0.9;
+ float y2 = maxOutLumi * 0.45;
+ float x3 = in_libtonemap_displayMaxLuminance;
+ float y3 = maxOutLumi;
+
+ float c1 = y1 / 3.0;
+ float c2 = y2 / 2.0;
+ float c3 = y3 / 1.5;
+
+ float nits = xyz.y;
+
+ if (nits <= x0) {
+ // scale [0.0, x0] to [0.0, y0] linearly
+ float slope = y0 / x0;
+ return nits * slope;
+ } else if (nits <= x1) {
+ // scale [x0, x1] to [y0, y1] using a curve
+ float t = (nits - x0) / (x1 - x0);
+ nits = (1.0 - t) * (1.0 - t) * y0 +
+ 2.0 * (1.0 - t) * t * c1 + t * t * y1;
+ } else if (nits <= x2) {
+ // scale [x1, x2] to [y1, y2] using a curve
+ float t = (nits - x1) / (x2 - x1);
+ nits = (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
+ float t = (nits - x2) / (x3 - x2);
+ nits = (1.0 - t) * (1.0 - t) * y2 +
+ 2.0 * (1.0 - t) * t * c3 + t * t * y3;
+ }
+
+ return nits;
+ }
+ )");
+ break;
+ default:
+ // For completeness, this is tone-mapping from SDR to SDR, where this is
+ // just a no-op.
+ program.append(R"(
+ float libtonemap_ToneMapTargetNits(vec3 xyz) {
+ return xyz.y;
+ }
+ )");
+ break;
+ }
+ break;
+ }
+
+ program.append(R"(
+ float libtonemap_LookupTonemapGain(vec3 linearRGB, vec3 xyz) {
+ if (xyz.y <= 0.0) {
+ return 1.0;
+ }
+ return libtonemap_ToneMapTargetNits(xyz) / xyz.y;
+ }
+ )");
+ return program;
+ }
+
+ std::vector<ShaderUniform> generateShaderSkSLUniforms(const Metadata& metadata) override {
+ std::vector<ShaderUniform> uniforms;
+
+ uniforms.reserve(2);
+
+ uniforms.push_back({.name = "in_libtonemap_displayMaxLuminance",
+ .value = buildUniformValue<float>(metadata.displayMaxLuminance)});
+ uniforms.push_back({.name = "in_libtonemap_inputMaxLuminance",
+ .value = buildUniformValue<float>(metadata.contentMaxLuminance)});
+
+ return uniforms;
+ }
+};
+
+} // namespace
+
+ToneMapper* getToneMapper() {
+ static std::once_flag sOnce;
+ static std::unique_ptr<ToneMapper> sToneMapper;
+
+ std::call_once(sOnce, [&] {
+ switch (kToneMapAlgorithm) {
+ case ToneMapAlgorithm::AndroidO:
+ sToneMapper = std::unique_ptr<ToneMapper>(new ToneMapperO());
+ break;
+ }
+ });
+
+ return sToneMapper.get();
+}
+
+} // namespace android::tonemap
\ No newline at end of file
diff --git a/services/surfaceflinger/Android.bp b/services/surfaceflinger/Android.bp
index 56b8374..6691575 100644
--- a/services/surfaceflinger/Android.bp
+++ b/services/surfaceflinger/Android.bp
@@ -79,6 +79,7 @@
"libperfetto_client_experimental",
"librenderengine",
"libserviceutils",
+ "libtonemap",
"libtrace_proto",
"libaidlcommonsupport",
],
diff --git a/services/surfaceflinger/CompositionEngine/Android.bp b/services/surfaceflinger/CompositionEngine/Android.bp
index 83b4a25..aefc014 100644
--- a/services/surfaceflinger/CompositionEngine/Android.bp
+++ b/services/surfaceflinger/CompositionEngine/Android.bp
@@ -38,6 +38,7 @@
static_libs: [
"libmath",
"librenderengine",
+ "libtonemap",
"libtrace_proto",
"libaidlcommonsupport",
],
diff --git a/services/surfaceflinger/tests/unittests/Android.bp b/services/surfaceflinger/tests/unittests/Android.bp
index f152ced..1ac5680 100644
--- a/services/surfaceflinger/tests/unittests/Android.bp
+++ b/services/surfaceflinger/tests/unittests/Android.bp
@@ -142,6 +142,7 @@
"libtimestats",
"libtimestats_atoms_proto",
"libtimestats_proto",
+ "libtonemap",
"libtrace_proto",
"perfetto_trace_protos",
],