750 lines
37 KiB
C++
750 lines
37 KiB
C++
/*
|
|
* 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
|