diff options
-rw-r--r-- | libs/renderengine/tests/RenderEngineTest.cpp | 3 | ||||
-rw-r--r-- | libs/shaders/shaders.cpp | 21 | ||||
-rw-r--r-- | libs/tonemap/include/tonemap/tonemap.h | 3 | ||||
-rw-r--r-- | libs/tonemap/tonemap.cpp | 103 |
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; |