| /* |
| * 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 <algorithm> |
| #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, |
| Android13, // Algorithm used in Android 13. |
| }; |
| |
| static const constexpr auto kToneMapAlgorithm = ToneMapAlgorithm::Android13; |
| |
| 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; |
| } |
| |
| // Refer to BT2100-2 |
| float computeHlgGamma(float currentDisplayBrightnessNits) { |
| // BT 2100-2's recommendation for taking into account the nominal max |
| // brightness of the display does not work when the current brightness is |
| // very low. For instance, the gamma becomes negative when the current |
| // brightness is between 1 and 2 nits, which would be a bad experience in a |
| // dark environment. Furthermore, BT2100-2 recommends applying |
| // channel^(gamma - 1) as its OOTF, which means that when the current |
| // brightness is lower than 335 nits then channel * channel^(gamma - 1) > |
| // channel, which makes dark scenes very bright. As a workaround for those |
| // problems, lower-bound the brightness to 500 nits. |
| constexpr float minBrightnessNits = 500.f; |
| currentDisplayBrightnessNits = std::max(minBrightnessNits, currentDisplayBrightnessNits); |
| return 1.2 + 0.42 * std::log10(currentDisplayBrightnessNits / 1000); |
| } |
| |
| 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) { |
| float nits = clamp(xyz.y, 0.0, 1000.0); |
| return nits * pow(nits / 1000.0, -0.2 / 1.2); |
| } |
| )"); |
| break; |
| default: |
| // HLG follows BT2100, but this tonemapping version |
| // does not take into account current display brightness |
| if ((sourceDataspaceInt & kTransferMask) == kTransferHLG) { |
| program.append(R"( |
| float libtonemap_applyBaseOOTFGain(float nits) { |
| return pow(nits, 0.2); |
| } |
| )"); |
| } else { |
| program.append(R"( |
| float libtonemap_applyBaseOOTFGain(float nits) { |
| return 1.0; |
| } |
| )"); |
| } |
| // 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; |
| |
| xyz = xyz * libtonemap_applyBaseOOTFGain(xyz.y); |
| |
| 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: |
| // HLG follows BT2100, but this tonemapping version |
| // does not take into account current display brightness |
| if ((destinationDataspaceInt & kTransferMask) == kTransferHLG) { |
| program.append(R"( |
| float libtonemap_applyBaseOOTFGain(float nits) { |
| return pow(nits / 1000.0, -0.2 / 1.2); |
| } |
| )"); |
| } else { |
| program.append(R"( |
| float libtonemap_applyBaseOOTFGain(float nits) { |
| return 1.0; |
| } |
| )"); |
| } |
| // 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; |
| nits = 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 * libtonemap_applyBaseOOTFGain(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; |
| } |
| |
| std::vector<Gain> lookupTonemapGain( |
| aidl::android::hardware::graphics::common::Dataspace sourceDataspace, |
| aidl::android::hardware::graphics::common::Dataspace destinationDataspace, |
| const std::vector<Color>& colors, const Metadata& metadata) override { |
| std::vector<Gain> gains; |
| gains.reserve(colors.size()); |
| |
| for (const auto [_, xyz] : colors) { |
| if (xyz.y <= 0.0) { |
| gains.push_back(1.0); |
| continue; |
| } |
| 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); |
| targetNits *= std::pow(targetNits / 1000.f, -0.2 / 1.2); |
| 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 ((sourceDataspaceInt & kTransferMask) == kTransferHLG) { |
| targetNits *= std::pow(targetNits, 0.2); |
| } |
| // 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; |
| } |
| |
| if ((destinationDataspaceInt & kTransferMask) == kTransferHLG) { |
| targetNits *= std::pow(targetNits / 1000.0, -0.2 / 1.2); |
| } |
| } break; |
| default: |
| // For completeness, this is tone-mapping from SDR to SDR, where this is |
| // just a no-op. |
| targetNits = xyz.y; |
| break; |
| } |
| } |
| gains.push_back(targetNits / xyz.y); |
| } |
| return gains; |
| } |
| }; |
| |
| 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, |
| 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; |
| // Input uniforms |
| program.append(R"( |
| uniform float in_libtonemap_displayMaxLuminance; |
| uniform float in_libtonemap_inputMaxLuminance; |
| uniform float in_libtonemap_hlgGamma; |
| )"); |
| switch (sourceDataspaceInt & kTransferMask) { |
| case kTransferST2084: |
| switch (destinationDataspaceInt & kTransferMask) { |
| case kTransferST2084: |
| program.append(R"( |
| float libtonemap_ToneMapTargetNits(float maxRGB) { |
| return 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. |
| program.append(R"( |
| float libtonemap_ToneMapTargetNits(float maxRGB) { |
| float nits = clamp(maxRGB, 0.0, 1000.0); |
| float gamma = (1 - in_libtonemap_hlgGamma) |
| / in_libtonemap_hlgGamma; |
| return nits * pow(nits / 1000.0, gamma); |
| } |
| )"); |
| break; |
| |
| default: |
| program.append(R"( |
| float libtonemap_OETFTone(float channel) { |
| channel = channel / 10000.0; |
| 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 = pow(channel, float(m1)); |
| tmp = (c1 + c2 * tmp) / (1.0 + c3 * tmp); |
| return pow(tmp, float(m2)); |
| } |
| |
| float libtonemap_ToneMapTargetNits(float maxRGB) { |
| float maxInLumi = in_libtonemap_inputMaxLuminance; |
| float maxOutLumi = in_libtonemap_displayMaxLuminance; |
| |
| float nits = maxRGB; |
| |
| float x1 = maxOutLumi * 0.65; |
| float y1 = x1; |
| |
| float x3 = maxInLumi; |
| float y3 = maxOutLumi; |
| |
| float x2 = x1 + (x3 - x1) * 4.0 / 17.0; |
| float y2 = maxOutLumi * 0.9; |
| |
| float greyNorm1 = libtonemap_OETFTone(x1); |
| float greyNorm2 = libtonemap_OETFTone(x2); |
| float greyNorm3 = libtonemap_OETFTone(x3); |
| |
| float slope1 = 0; |
| float slope2 = (y2 - y1) / (greyNorm2 - greyNorm1); |
| float slope3 = (y3 - y2 ) / (greyNorm3 - greyNorm2); |
| |
| if (nits < x1) { |
| return nits; |
| } |
| |
| if (nits > maxInLumi) { |
| return maxOutLumi; |
| } |
| |
| float greyNits = libtonemap_OETFTone(nits); |
| |
| if (greyNits <= greyNorm2) { |
| nits = (greyNits - greyNorm2) * slope2 + y2; |
| } else if (greyNits <= greyNorm3) { |
| nits = (greyNits - greyNorm3) * slope3 + y3; |
| } else { |
| nits = maxOutLumi; |
| } |
| |
| return nits; |
| } |
| )"); |
| break; |
| } |
| break; |
| case kTransferHLG: |
| switch (destinationDataspaceInt & kTransferMask) { |
| // HLG uses the OOTF from BT 2100. |
| case kTransferST2084: |
| program.append(R"( |
| float libtonemap_ToneMapTargetNits(float maxRGB) { |
| return maxRGB |
| * pow(maxRGB / 1000.0, in_libtonemap_hlgGamma - 1); |
| } |
| )"); |
| break; |
| case kTransferHLG: |
| program.append(R"( |
| float libtonemap_ToneMapTargetNits(float maxRGB) { |
| return maxRGB; |
| } |
| )"); |
| break; |
| default: |
| // Follow BT 2100 and renormalize to max display luminance if we're |
| // tone-mapping down to SDR, as libshaders normalizes all SDR output from |
| // [0, maxDisplayLumins] -> [0, 1] |
| program.append(R"( |
| float libtonemap_ToneMapTargetNits(float maxRGB) { |
| return maxRGB |
| * pow(maxRGB / 1000.0, in_libtonemap_hlgGamma - 1) |
| * in_libtonemap_displayMaxLuminance / 1000.0; |
| } |
| )"); |
| break; |
| } |
| break; |
| default: |
| // Inverse tone-mapping and SDR-SDR mapping is not supported. |
| program.append(R"( |
| float libtonemap_ToneMapTargetNits(float maxRGB) { |
| return maxRGB; |
| } |
| )"); |
| break; |
| } |
| |
| program.append(R"( |
| float libtonemap_LookupTonemapGain(vec3 linearRGB, vec3 xyz) { |
| float maxRGB = max(linearRGB.r, max(linearRGB.g, linearRGB.b)); |
| if (maxRGB <= 0.0) { |
| return 1.0; |
| } |
| return libtonemap_ToneMapTargetNits(maxRGB) / maxRGB; |
| } |
| )"); |
| return program; |
| } |
| |
| std::vector<ShaderUniform> generateShaderSkSLUniforms(const Metadata& metadata) override { |
| // Hardcode the max content luminance to a "reasonable" level |
| static const constexpr float kContentMaxLuminance = 4000.f; |
| std::vector<ShaderUniform> uniforms; |
| uniforms.reserve(3); |
| uniforms.push_back({.name = "in_libtonemap_displayMaxLuminance", |
| .value = buildUniformValue<float>(metadata.displayMaxLuminance)}); |
| uniforms.push_back({.name = "in_libtonemap_inputMaxLuminance", |
| .value = buildUniformValue<float>(kContentMaxLuminance)}); |
| uniforms.push_back({.name = "in_libtonemap_hlgGamma", |
| .value = buildUniformValue<float>( |
| computeHlgGamma(metadata.currentDisplayLuminance))}); |
| return uniforms; |
| } |
| |
| std::vector<Gain> lookupTonemapGain( |
| aidl::android::hardware::graphics::common::Dataspace sourceDataspace, |
| aidl::android::hardware::graphics::common::Dataspace destinationDataspace, |
| const std::vector<Color>& colors, const Metadata& metadata) override { |
| std::vector<Gain> gains; |
| gains.reserve(colors.size()); |
| |
| // Precompute constants for HDR->SDR tonemapping parameters |
| constexpr double maxInLumi = 4000; |
| const double maxOutLumi = metadata.displayMaxLuminance; |
| |
| const double x1 = maxOutLumi * 0.65; |
| const double y1 = x1; |
| |
| const double x3 = maxInLumi; |
| const double y3 = maxOutLumi; |
| |
| const double x2 = x1 + (x3 - x1) * 4.0 / 17.0; |
| const double y2 = maxOutLumi * 0.9; |
| |
| const double greyNorm1 = OETF_ST2084(x1); |
| const double greyNorm2 = OETF_ST2084(x2); |
| const double greyNorm3 = OETF_ST2084(x3); |
| |
| const double slope2 = (y2 - y1) / (greyNorm2 - greyNorm1); |
| const double slope3 = (y3 - y2) / (greyNorm3 - greyNorm2); |
| |
| const double hlgGamma = computeHlgGamma(metadata.currentDisplayLuminance); |
| |
| for (const auto [linearRGB, _] : colors) { |
| double maxRGB = std::max({linearRGB.r, linearRGB.g, linearRGB.b}); |
| |
| if (maxRGB <= 0.0) { |
| gains.push_back(1.0); |
| continue; |
| } |
| |
| 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: |
| 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); |
| targetNits *= pow(targetNits / 1000.0, (1 - hlgGamma) / (hlgGamma)); |
| break; |
| default: |
| targetNits = maxRGB; |
| if (targetNits < x1) { |
| break; |
| } |
| |
| if (targetNits > maxInLumi) { |
| targetNits = maxOutLumi; |
| break; |
| } |
| |
| const double greyNits = OETF_ST2084(targetNits); |
| |
| if (greyNits <= greyNorm2) { |
| targetNits = (greyNits - greyNorm2) * slope2 + y2; |
| } else if (greyNits <= greyNorm3) { |
| targetNits = (greyNits - greyNorm3) * slope3 + y3; |
| } else { |
| targetNits = maxOutLumi; |
| } |
| break; |
| } |
| break; |
| case kTransferHLG: |
| switch (destinationDataspaceInt & kTransferMask) { |
| case kTransferST2084: |
| targetNits = maxRGB * pow(maxRGB / 1000.0, hlgGamma - 1); |
| break; |
| case kTransferHLG: |
| targetNits = maxRGB; |
| break; |
| default: |
| targetNits = maxRGB * pow(maxRGB / 1000.0, hlgGamma - 1) * |
| metadata.displayMaxLuminance / 1000.0; |
| break; |
| } |
| break; |
| default: |
| targetNits = maxRGB; |
| break; |
| } |
| |
| gains.push_back(targetNits / maxRGB); |
| } |
| return gains; |
| } |
| }; |
| |
| } // 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; |
| case ToneMapAlgorithm::Android13: |
| sToneMapper = std::unique_ptr<ToneMapper>(new ToneMapper13()); |
| } |
| }); |
| |
| return sToneMapper.get(); |
| } |
| } // namespace android::tonemap |