diff options
author | 2021-10-15 20:59:33 -0700 | |
---|---|---|
committer | 2021-11-18 12:33:04 -0800 | |
commit | 4049b53017f5cfdac2c1afecc6d7b4571dd4dce6 (patch) | |
tree | 8e3324e726c070bd9e9f81877955219dbf2015f6 | |
parent | 5184f413900601d737488eaf9c53dcfe8ae39d16 (diff) |
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
-rw-r--r-- | libs/renderengine/skia/filters/LinearEffect.cpp | 2 | ||||
-rw-r--r-- | libs/renderengine/tests/RenderEngineTest.cpp | 217 | ||||
-rw-r--r-- | libs/tonemap/Android.bp | 6 | ||||
-rw-r--r-- | libs/tonemap/include/tonemap/tonemap.h | 18 | ||||
-rw-r--r-- | libs/tonemap/tests/Android.bp | 1 | ||||
-rw-r--r-- | libs/tonemap/tonemap.cpp | 258 |
6 files changed, 477 insertions, 25 deletions
diff --git a/libs/renderengine/skia/filters/LinearEffect.cpp b/libs/renderengine/skia/filters/LinearEffect.cpp index c3a5a60e13..b43a3aa5ce 100644 --- a/libs/renderengine/skia/filters/LinearEffect.cpp +++ b/libs/renderengine/skia/filters/LinearEffect.cpp @@ -104,7 +104,7 @@ static void generateXYZTransforms(SkString& shader) { 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 c2c05f41b7..5bc08ac089 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 @@ public: 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 @@ public: 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 @@ public: 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 @@ public: } 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 @@ void RenderEngineTest::fillRedBufferTextureTransform() { 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 @@ TEST_P(RenderEngineTest, drawLayers_withoutBuffers_withColorTransform) { 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_fillBuffer_withoutPremultiplyingAlpha) { 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_castsWithoutCasterLayer) { 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_casterLayerMinSize) { 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_casterColorLayer) { 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_casterOpaqueBufferLayer) { 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 @@ TEST_P(RenderEngineTest, test_isOpaque) { 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 231a342852..5360fe2b07 100644 --- a/libs/tonemap/Android.bp +++ b/libs/tonemap/Android.bp @@ -30,7 +30,13 @@ cc_library_static { 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 d350e160ca..bd7b72d186 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 @@ struct Metadata { 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 @@ public: // 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 e58d519224..f46f3fa27d 100644 --- a/libs/tonemap/tests/Android.bp +++ b/libs/tonemap/tests/Android.bp @@ -31,6 +31,7 @@ cc_test { "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 2cec773eb3..c2372fe828 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 @@ public: .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 @@ public: .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 @@ ToneMapper* getToneMapper() { return sToneMapper.get(); } - } // namespace android::tonemap
\ No newline at end of file |