/* * 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 #include #include #include #include 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(aidl::android::hardware::graphics::common::Dataspace::TRANSFER_MASK); static const constexpr auto kTransferST2084 = static_cast(aidl::android::hardware::graphics::common::Dataspace::TRANSFER_ST2084); static const constexpr auto kTransferHLG = static_cast(aidl::android::hardware::graphics::common::Dataspace::TRANSFER_HLG); template ::value, bool> = true> std::vector buildUniformValue(T value) { std::vector 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(sourceDataspace); const int32_t destinationDataspaceInt = static_cast(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 generateShaderSkSLUniforms(const Metadata& metadata) override { std::vector uniforms; uniforms.reserve(2); uniforms.push_back({.name = "in_libtonemap_displayMaxLuminance", .value = buildUniformValue(metadata.displayMaxLuminance)}); uniforms.push_back({.name = "in_libtonemap_inputMaxLuminance", .value = buildUniformValue(metadata.contentMaxLuminance)}); return uniforms; } std::vector lookupTonemapGain( aidl::android::hardware::graphics::common::Dataspace sourceDataspace, aidl::android::hardware::graphics::common::Dataspace destinationDataspace, const std::vector& colors, const Metadata& metadata) override { std::vector 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(sourceDataspace); const int32_t destinationDataspaceInt = static_cast(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(sourceDataspace); const int32_t destinationDataspaceInt = static_cast(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 generateShaderSkSLUniforms(const Metadata& metadata) override { // Hardcode the max content luminance to a "reasonable" level static const constexpr float kContentMaxLuminance = 4000.f; std::vector uniforms; uniforms.reserve(3); uniforms.push_back({.name = "in_libtonemap_displayMaxLuminance", .value = buildUniformValue(metadata.displayMaxLuminance)}); uniforms.push_back({.name = "in_libtonemap_inputMaxLuminance", .value = buildUniformValue(kContentMaxLuminance)}); uniforms.push_back({.name = "in_libtonemap_hlgGamma", .value = buildUniformValue( computeHlgGamma(metadata.currentDisplayLuminance))}); return uniforms; } std::vector lookupTonemapGain( aidl::android::hardware::graphics::common::Dataspace sourceDataspace, aidl::android::hardware::graphics::common::Dataspace destinationDataspace, const std::vector& colors, const Metadata& metadata) override { std::vector 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(sourceDataspace); const int32_t destinationDataspaceInt = static_cast(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 sToneMapper; std::call_once(sOnce, [&] { switch (kToneMapAlgorithm) { case ToneMapAlgorithm::AndroidO: sToneMapper = std::unique_ptr(new ToneMapperO()); break; case ToneMapAlgorithm::Android13: sToneMapper = std::unique_ptr(new ToneMapper13()); } }); return sToneMapper.get(); } } // namespace android::tonemap