summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Alec Mouri <alecmouri@google.com> 2022-03-04 23:41:38 +0000
committer Alec Mouri <alecmouri@google.com> 2022-03-18 23:54:08 +0000
commit7a577450e536aa1e99f229a0cb3d3531c82e8a8d (patch)
tree324da642f325123cf62f9c688f481614942a9d1e
parent1a3c5456dd9e05642c3d0f388102c511e80b5108 (diff)
Push HLG OOTF down to libtonemap.
Usage of current display brightness may be vendor-configured when the display brightness is very low, so keep the OOTF in libtonemap as part of the reference implementation. Concretely, this means that: * The BT2100 recommended OOTF for HLG->output format is moved from ScaleLuminance in libshaders to be the first part of the tonemapping operator in libtonemap * The inverse OOTF for input format->HLG is moved from NormalizeLuminance in libshaders to the end of the tonemapping operator in libtonemp * Current display brightness is only taken into account in the default tonemapping for Android T. The historic tonemapper does not take into account current display brightness, as it treats the "nominal peak brightness" of the display as 1000 nits instead of the current brightness. Also add a default lower-bound for using the current display brightness, because not having a bound looks really terrible on existing shipping devices Bug: 208933319 Test: builds Test: HLG test video looks okay Test: HDR10 test video didn't break Change-Id: I4f489c68f635a8ecc4d497b98c32e91c297d0765
-rw-r--r--libs/renderengine/tests/RenderEngineTest.cpp3
-rw-r--r--libs/shaders/shaders.cpp21
-rw-r--r--libs/tonemap/include/tonemap/tonemap.h3
-rw-r--r--libs/tonemap/tonemap.cpp103
4 files changed, 100 insertions, 30 deletions
diff --git a/libs/renderengine/tests/RenderEngineTest.cpp b/libs/renderengine/tests/RenderEngineTest.cpp
index ec1bd470b5..38ae2fd4ed 100644
--- a/libs/renderengine/tests/RenderEngineTest.cpp
+++ b/libs/renderengine/tests/RenderEngineTest.cpp
@@ -2619,8 +2619,7 @@ TEST_P(RenderEngineTest, test_tonemapHLGMatches) {
[](vec3 color) { return EOTF_HLG(color); },
[](vec3 color, float currentLuminaceNits) {
static constexpr float kMaxHLGLuminance = 1000.f;
- static const float kHLGGamma = 1.2 + 0.42 * std::log10(currentLuminaceNits / 1000);
- return color * kMaxHLGLuminance * std::pow(color.y, kHLGGamma - 1);
+ return color * kMaxHLGLuminance;
});
}
diff --git a/libs/shaders/shaders.cpp b/libs/shaders/shaders.cpp
index 0d77519ab7..5935589fdf 100644
--- a/libs/shaders/shaders.cpp
+++ b/libs/shaders/shaders.cpp
@@ -185,9 +185,8 @@ void generateLuminanceScalesForOOTF(ui::Dataspace inputDataspace, ui::Dataspace
break;
case HAL_DATASPACE_TRANSFER_HLG:
shader.append(R"(
- uniform float in_hlgGamma;
float3 ScaleLuminance(float3 xyz) {
- return xyz * 1000.0 * pow(xyz.y, in_hlgGamma - 1);
+ return xyz * 1000.0;
}
)");
break;
@@ -228,10 +227,8 @@ static void generateLuminanceNormalizationForOOTF(ui::Dataspace outputDataspace,
break;
case HAL_DATASPACE_TRANSFER_HLG:
shader.append(R"(
- uniform float in_hlgGamma;
float3 NormalizeLuminance(float3 xyz) {
- return xyz / 1000.0 *
- pow(xyz.y / 1000.0, (1 - in_hlgGamma) / (in_hlgGamma));
+ return xyz / 1000.0;
}
)");
break;
@@ -451,11 +448,6 @@ std::vector<uint8_t> buildUniformValue(T value) {
return result;
}
-// Refer to BT2100-2
-float computeHlgGamma(float currentDisplayBrightnessNits) {
- return 1.2 + 0.42 * std::log10(currentDisplayBrightnessNits / 1000);
-}
-
} // namespace
std::string buildLinearEffectSkSL(const LinearEffect& linearEffect) {
@@ -493,12 +485,6 @@ std::vector<tonemap::ShaderUniform> buildLinearEffectUniforms(const LinearEffect
colorTransform * mat4(outputColorSpace.getXYZtoRGB()))});
}
- if ((linearEffect.inputDataspace & HAL_DATASPACE_TRANSFER_MASK) == HAL_DATASPACE_TRANSFER_HLG) {
- uniforms.push_back(
- {.name = "in_hlgGamma",
- .value = buildUniformValue<float>(computeHlgGamma(currentDisplayLuminanceNits))});
- }
-
tonemap::Metadata metadata{.displayMaxLuminance = maxDisplayLuminance,
// If the input luminance is unknown, use display luminance (aka,
// no-op any luminance changes)
@@ -506,6 +492,9 @@ std::vector<tonemap::ShaderUniform> buildLinearEffectUniforms(const LinearEffect
// uncalibrated displays
.contentMaxLuminance =
maxLuminance > 0 ? maxLuminance : maxDisplayLuminance,
+ .currentDisplayLuminance = currentDisplayLuminanceNits > 0
+ ? currentDisplayLuminanceNits
+ : maxDisplayLuminance,
.buffer = buffer};
for (const auto uniform : tonemap::getToneMapper()->generateShaderSkSLUniforms(metadata)) {
diff --git a/libs/tonemap/include/tonemap/tonemap.h b/libs/tonemap/include/tonemap/tonemap.h
index e380323dd3..c51016db56 100644
--- a/libs/tonemap/include/tonemap/tonemap.h
+++ b/libs/tonemap/include/tonemap/tonemap.h
@@ -49,6 +49,9 @@ struct Metadata {
// The maximum luminance of the content in nits
float contentMaxLuminance = 0.0;
+ // The current brightness of the display in nits
+ float currentDisplayLuminance = 0.0;
+
// Reference to an AHardwareBuffer.
// Devices that support gralloc 4.0 and higher may attach metadata onto a
// particular frame's buffer, including metadata used by HDR-standards like
diff --git a/libs/tonemap/tonemap.cpp b/libs/tonemap/tonemap.cpp
index c4f46bd97c..19e1eea12d 100644
--- a/libs/tonemap/tonemap.cpp
+++ b/libs/tonemap/tonemap.cpp
@@ -48,6 +48,22 @@ std::vector<uint8_t> buildUniformValue(T 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(
@@ -79,11 +95,27 @@ public:
// HLG output.
program.append(R"(
float libtonemap_ToneMapTargetNits(vec3 xyz) {
- return clamp(xyz.y, 0.0, 1000.0);
+ 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"(
@@ -91,6 +123,8 @@ public:
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
@@ -153,6 +187,21 @@ public:
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.
@@ -178,7 +227,7 @@ public:
if (nits <= x0) {
// scale [0.0, x0] to [0.0, y0] linearly
float slope = y0 / x0;
- return nits * slope;
+ nits = nits * slope;
} else if (nits <= x1) {
// scale [x0, x1] to [y0, y1] using a curve
float t = (nits - x0) / (x1 - x0);
@@ -196,7 +245,7 @@ public:
2.0 * (1.0 - t) * t * c3 + t * t * y3;
}
- return nits;
+ return nits * libtonemap_applyBaseOOTFGain(nits);
}
)");
break;
@@ -264,12 +313,17 @@ public:
// 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) {
@@ -362,6 +416,10 @@ public:
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
@@ -411,6 +469,7 @@ public:
program.append(R"(
uniform float in_libtonemap_displayMaxLuminance;
uniform float in_libtonemap_inputMaxLuminance;
+ uniform float in_libtonemap_hlgGamma;
)");
switch (sourceDataspaceInt & kTransferMask) {
case kTransferST2084:
@@ -428,7 +487,10 @@ public:
// HLG output.
program.append(R"(
float libtonemap_ToneMapTargetNits(float maxRGB) {
- return clamp(maxRGB, 0.0, 1000.0);
+ 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;
@@ -497,8 +559,15 @@ public:
break;
case kTransferHLG:
switch (destinationDataspaceInt & kTransferMask) {
- // HLG -> HDR does not tone-map at all
+ // 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) {
@@ -507,13 +576,14 @@ public:
)");
break;
default:
- // libshaders follows BT2100 OOTF, but with a nominal peak display luminance
- // of 1000 nits. Renormalize to max display luminance if we're tone-mapping
- // down to SDR, as libshaders normalizes all SDR output from [0,
- // maxDisplayLumins] -> [0, 1]
+ // 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 * in_libtonemap_displayMaxLuminance / 1000.0;
+ return maxRGB
+ * pow(maxRGB / 1000.0, in_libtonemap_hlgGamma - 1)
+ * in_libtonemap_displayMaxLuminance / 1000.0;
}
)");
break;
@@ -545,11 +615,14 @@ public:
// Hardcode the max content luminance to a "reasonable" level
static const constexpr float kContentMaxLuminance = 4000.f;
std::vector<ShaderUniform> uniforms;
- uniforms.reserve(2);
+ 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;
}
@@ -580,6 +653,8 @@ public:
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});
@@ -603,6 +678,7 @@ public:
// 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;
@@ -630,11 +706,14 @@ public:
case kTransferHLG:
switch (destinationDataspaceInt & kTransferMask) {
case kTransferST2084:
+ targetNits = maxRGB * pow(maxRGB / 1000.0, hlgGamma - 1);
+ break;
case kTransferHLG:
targetNits = maxRGB;
break;
default:
- targetNits = maxRGB * metadata.displayMaxLuminance / 1000.0;
+ targetNits = maxRGB * pow(maxRGB / 1000.0, hlgGamma - 1) *
+ metadata.displayMaxLuminance / 1000.0;
break;
}
break;