blob: 76c0aa2554feb7dc275a416ee81558f01215f264 [file] [log] [blame]
/*
* Copyright 2025 Google LLC.
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "include/core/SkBitmap.h"
#include "include/core/SkColorFilter.h"
#include "include/effects/SkRuntimeEffect.h"
#include "include/private/SkHdrMetadata.h"
#include "src/codec/SkCodecPriv.h"
#include "src/codec/SkHdrAgtmPriv.h"
namespace {
// The maximum and minimum HDR headroom values allowed by the specification.
static constexpr float kMinHdrHeadroom = 0.f;
static constexpr float kMaxHdrHeadroom = 6.f;
// The maximum linear value is exp2(kMaxHdrHeadroom) = 64.
static constexpr float kMaxLinearHdrHeadroom = 64.f;
static bool in_unit_interval(float x) {
return x >= 0.f && x <= 1.f;
}
// AGTM tone mapping shader.
static constexpr char gAgtmSKSL[] =
"uniform half scale_factor;" // The scale to apply in linear space
"uniform shader curve_xym;" // The texture containing control points.
"uniform half weight_i;" // The weight of gain curve "i"
"uniform half4 mix_rgbx_i;" // The red,green,blue mixing coefficients.
"uniform half4 mix_Mmcx_i;" // The max,min,component mixing coefficients.
"uniform half curve_texcoord_y_i;" // The y texture coordinate at which to sample curve_xym.
"uniform half curve_N_cp_i;" // The number of control points.
"uniform half weight_j;" // All the same parameters, for gain curve "j"
"uniform half4 mix_rgbx_j;"
"uniform half4 mix_Mmcx_j;"
"uniform half curve_texcoord_y_j;"
"uniform half curve_N_cp_j;"
// Shader equivalent of AgtmHelpers::EvaluateComponentMixingFunction.
"half3 EvalComponentMixing(half3 color, half4 rgbx, half4 Mmcx) {"
"half common = dot(rgbx.rgb, color) +"
"Mmcx[0] * max(max(color.r, color.g), color.b) +"
"Mmcx[1] * min(min(color.r, color.g), color.b);"
"return Mmcx[2] * color + half3(common);"
"}"
// Shader equivalent of AgtmHelpers::EvaluateGainCurve.
"half EvalGainCurve(half x, half curve_texcoord_y, half curve_N_cp) {"
// Handle points to the left of the first control point.
"half c_min = 0.0;"
"half4 xym_min = curve_xym.eval(half2(c_min + 0.5, curve_texcoord_y));"
"if (x <= xym_min.x) {"
"return xym_min.y;"
"}"
// Handle points after the last control point.
"half c_max = curve_N_cp - 1.0;"
"half4 xym_max = curve_xym.eval(half2(c_max + 0.5, curve_texcoord_y));"
"if (x >= xym_max.x) {"
"return xym_max.y + log2(xym_max.x / x);"
"}"
// Binary search for the interval containing x. This will require sampling at most
// log2(32)=5 more control points.
"for (int step = 0; step < 5; ++step) {"
// Early-out if we've already found the interval.
"if (c_max - c_min <= 1.0) {"
"break;"
"}"
// Test the midpoint and replace one of the endpoints with it.
"half c_mid = ceil(0.5 * (c_min + c_max));"
"half4 xym_mid = curve_xym.eval(half2(c_mid + 0.5, curve_texcoord_y));"
"if (x == xym_mid.x) {"
// If we hit a control point exactly, then just return.
"return xym_mid.y;"
"} else if (x < xym_mid.x) {"
"c_max = c_mid;"
"xym_max = xym_mid;"
"} else {"
"c_min = c_mid;"
"xym_min = xym_mid;"
"}"
"}"
// Evaluate the cubic.
"half h = xym_max.x - xym_min.x;"
"half mHat_min = xym_min.z * h;"
"half mHat_max = xym_max.z * h;"
"half c3 = 2.0 * xym_min.y + mHat_min - 2.0 * xym_max.y + mHat_max;"
"half c2 = -3.0 * xym_min.y + 3.0 * xym_max.y - 2.0 * mHat_min - mHat_max;"
"half c1 = mHat_min;"
"half c0 = xym_min.y;"
"half t = (x - xym_min.x) / h;"
"return ((c3*t + c2)*t + c1)*t + c0;"
"}"
// Shader equivalent of AgtmHelpers::EvaluateColorGainFunction.
"half3 EvalColorGainFunction(half3 color,"
"half4 mix_rgbx, half4 mix_Mmcx,"
"float curve_texcoord_y, float curve_N_cp) {"
"half3 M = EvalComponentMixing(color, mix_rgbx, mix_Mmcx);"
"if (mix_Mmcx.b == 0.0) {"
// If the kComponent coefficient is zero, only evalute the curve once.
"return half3(EvalGainCurve(M.r, curve_texcoord_y, curve_N_cp));"
"}"
"return half3(EvalGainCurve(M.r, curve_texcoord_y, curve_N_cp),"
"EvalGainCurve(M.g, curve_texcoord_y, curve_N_cp),"
"EvalGainCurve(M.b, curve_texcoord_y, curve_N_cp));"
"}"
// Shader equivalent of AgtmHelpers::ApplyGain.
"half4 main(half4 color) {"
"color.rgb *= scale_factor;"
"if (weight_i > 0.0) {"
// Unpremultiply alpha is needed.
"float a_inv = (color.a == 0.0) ? 1.0 : 1.0 / color.a;"
"half3 G = half3(0.0);"
"G += weight_i * EvalColorGainFunction(color.rgb * a_inv,"
"mix_rgbx_i, mix_Mmcx_i,"
"curve_texcoord_y_i, curve_N_cp_i);"
"if (weight_j > 0.0) {"
"G += weight_j * EvalColorGainFunction(color.rgb * a_inv,"
"mix_rgbx_j, mix_Mmcx_j,"
"curve_texcoord_y_j, curve_N_cp_j);"
"}"
"color.rgb *= exp2(G);"
"}"
"return color;"
"}";
static sk_sp<SkRuntimeEffect> agtm_runtime_effect() {
auto init_lambda = []() {
auto result = SkRuntimeEffect::MakeForColorFilter(SkString(gAgtmSKSL), {});
SkASSERTF(result.effect, "Agtm shader log:\n%s\n", result.errorText.c_str());
return result.effect.release();
};
static SkRuntimeEffect* effect = init_lambda();
return sk_ref_sp(effect);
}
} // namespace
namespace skhdr {
SkColor4f AgtmHelpers::EvaluateComponentMixingFunction(
const AdaptiveGlobalToneMap::ComponentMixingFunction& mix, const SkColor4f& c) {
// This implements that math in Formula (9).
float common = mix.fRed * c.fR + mix.fGreen * c.fG + mix.fBlue * c.fB +
mix.fMax * std::max(std::max(c.fR, c.fG), c.fB) +
mix.fMin * std::min(std::max(c.fR, c.fG), c.fB);
// Optimization for when all components are the same.
if (mix.fComponent == 0.f) {
return {common, common, common, c.fA};
}
// Formula (10).
return {mix.fComponent * c.fR + common,
mix.fComponent * c.fG + common,
mix.fComponent * c.fB + common,
c.fA};
}
namespace AgtmHelpers {
float EvaluateGainCurve(const AdaptiveGlobalToneMap::GainCurve& gainCurve, float x) {
auto& cp = gainCurve.fControlPoints;
size_t N = cp.size();
// This implements that math in Formula (11).
SkASSERT(N > 0 && N <= 32);
// Handle points off of the left endpoint.
size_t i = 0;
if (x <= cp[i].fX) {
return cp[i].fY;
}
// Handle points off of the right endpoint.
size_t j = N - 1;
if (x >= cp[j].fX) {
return cp[j].fY + std::log2(cp[j].fX / x);
}
// Binary search for i, j bracket in which we find x.
while (j - i > 1) {
size_t m = (i + j) / 2;
if (x < cp[m].fX) {
j = m;
} else {
i = m;
}
}
// Cache short names for the parameters for computing the cubic coefficients.
const float x_i = cp[i].fX;
const float y_i = cp[i].fY;
const float x_j = cp[j].fX;
const float y_j = cp[j].fY;
const float h_i = x_j - x_i;
const float mHat_i = cp[i].fM * h_i;
const float mHat_j = cp[j].fM * h_i;
// Handle intervals that are a point.
if (h_i == 0.f) {
return y_i;
}
// Compute the coefficients and evaluate the polynomial.
const float c3 = 2.f * y_i + mHat_i - 2.f * y_j + mHat_j;
const float c2 = -3.f * y_i + 3.f * y_j - 2.f * mHat_i - mHat_j;
const float c1 = mHat_i;
const float c0 = y_i;
const float t = (x - x_i) / h_i;
return ((c3*t + c2)*t + c1)*t + c0;
}
SkColor4f EvaluateColorGainFunction(
const AdaptiveGlobalToneMap::ColorGainFunction& gain, const SkColor4f& c) {
SkColor4f m = EvaluateComponentMixingFunction(gain.fComponentMixing, c);
SkColor4f result = {0.f, 0.f, 0.f, c.fA};
result.fR = EvaluateGainCurve(gain.fGainCurve, m.fR);
if (m.fR == m.fG && m.fG == m.fB) {
result.fG = result.fR;
result.fB = result.fR;
} else {
result.fG = EvaluateGainCurve(gain.fGainCurve, m.fG);
result.fB = EvaluateGainCurve(gain.fGainCurve, m.fB);
}
return result;
}
void PopulateSlopeFromPCHIP(AdaptiveGlobalToneMap::GainCurve& gainCurve) {
auto& cp = gainCurve.fControlPoints;
size_t N = cp.size();
// Compute the interval width (h) and piecewise linear slope (s).
float s[AdaptiveGlobalToneMap::GainCurve::kMaxNumControlPoints];
float h[AdaptiveGlobalToneMap::GainCurve::kMaxNumControlPoints];
for (size_t i = 0; i < N - 1; ++i) {
h[i] = cp[i+1].fX - cp[i].fX;
}
for (size_t i = 0; i < N - 1; ++i) {
s[i] = (cp[i+1].fY - cp[i].fY) / h[i];
}
// Handle the left and right control points.
if (N >= 3) {
// Formula (C.7) and Formula (C.8).
cp[0].fM = ((2 * h[0] + h[1] ) * s[0] - h[0] * s[1] ) / (h[0] + h[1] );
cp[N-1].fM = ((2 * h[N-2] + h[N-3]) * s[N-2] - h[N-2] * s[N-3]) / (h[N-2] + h[N-3]);
} else if (N == 2) {
cp[0].fM = s[0];
cp[N-1].fM = s[0];
} else {
cp[0].fM = 0.f;
cp[N-1].fM = 0.f;
}
// Populate internal control points.
for (size_t i = 1; i <= N - 2; ++i) {
// Formula (C.9).
if (s[i-1] * s[i] < 0.f) {
cp[i].fM = 0.f;
} else {
float num = 3 * (h[i-1] + h[i]) * s[i-1] * s[i];
float den = (2 * h[i-1] + h[i]) * s[i-1] + (h[i-1] + 2 * h[i]) * s[i];
cp[i].fM = num / den;
}
}
}
sk_sp<SkImage>
MakeGainCurveXYMImage(const AdaptiveGlobalToneMap::HeadroomAdaptiveToneMap& hatm) {
if (hatm.fAlternateImages.empty()) {
return nullptr;
}
size_t maxNumControlPoints = 1;
for (const auto& alt : hatm.fAlternateImages) {
maxNumControlPoints = std::max(maxNumControlPoints,
alt.fColorGainFunction.fGainCurve.fControlPoints.size());
}
// Write the X, Y, and M values of the control points into the colors of the rows.
SkBitmap bm32;
bm32.allocPixels(SkImageInfo::Make(
AdaptiveGlobalToneMap::GainCurve::kMaxNumControlPoints, hatm.fAlternateImages.size(),
kRGBA_F32_SkColorType, kPremul_SkAlphaType));
for (size_t a = 0; a < hatm.fAlternateImages.size(); ++a) {
const auto& alt = hatm.fAlternateImages[a];
const auto& curve = alt.fColorGainFunction.fGainCurve;
for (size_t c = 0; c < curve.fControlPoints.size(); ++c) {
float* xymX = reinterpret_cast<float*>(bm32.getAddr(c, a));
xymX[0] = curve.fControlPoints[c].fX;
xymX[1] = curve.fControlPoints[c].fY;
xymX[2] = curve.fControlPoints[c].fM;
xymX[3] = 1.f;
}
}
// Convert from F32 to F16 for use on the GPU.
SkBitmap bm16;
bm16.allocPixels(bm32.info().makeColorType(kRGBA_F16_SkColorType));
if (!bm32.readPixels(bm16.pixmap())) {
return nullptr;
}
bm16.setImmutable();
return SkImages::RasterFromBitmap(bm16);
}
void PopulateUsingRwtmo(AdaptiveGlobalToneMap::HeadroomAdaptiveToneMap& hatm) {
hatm.fGainApplicationSpacePrimaries = SkNamedPrimaries::kRec2020;
if (hatm.fBaselineHdrHeadroom == 0.f) {
hatm.fAlternateImages.clear();
return;
}
// Set the two alternate image headrooms using Formula (C.1).
hatm.fAlternateImages.resize(2);
hatm.fAlternateImages[0].fHdrHeadroom = 0.f;
hatm.fAlternateImages[1].fHdrHeadroom =
std::log2(8.f / 3.f) * std::min(hatm.fBaselineHdrHeadroom / std::log2(1000/203.f), 1.f);
for (size_t a = 0; a < hatm.fAlternateImages.size(); ++a) {
auto& gain = hatm.fAlternateImages[a].fColorGainFunction;
gain = AdaptiveGlobalToneMap::ColorGainFunction();
// Use maxRGB for applying the curve.
gain.fComponentMixing.fMax = 1.f;
// Compute the image of white under the tone mapping from Formula (C.2).
const float yWhite =
(a == 1) ? 1.f
: 1.f - 0.5f * std::min(hatm.fBaselineHdrHeadroom / std::log2(1000/203.f), 1.f);
// Compute the Bezier control points using Formula (C.3).
const float kappa = 0.65f;
const float xKnee = 1.f;
const float yKnee = yWhite;
const float xMax = std::exp2(hatm.fBaselineHdrHeadroom);
const float yMax = std::exp2(hatm.fAlternateImages[a].fHdrHeadroom);
const float xMid = (1.f - kappa) * xKnee + kappa * (xKnee * yMax / yKnee);
const float yMid = (1.f - kappa) * yKnee + kappa * yMax;
// Compute the cubic coefficients using Formula (C.5).
const float xA = xKnee - 2.f * xMid + xMax;
const float yA = yKnee - 2.f * yMid + yMax;
const float xB = 2.f * xMid - 2.f * xKnee;
const float yB = 2.f * yMid - 2.f * yKnee;
const float xC = xKnee;
const float yC = yKnee;
auto& cubic = gain.fGainCurve;
cubic.fControlPoints.resize(8);
for (size_t c = 0; c < cubic.fControlPoints.size(); ++c) {
// Compute the linear domain curve values using Formula (C.4).
const float t = c / (cubic.fControlPoints.size() - 1.f);
const float x = xC + t * (xB + t * xA);
const float y = yC + t * (yB + t * yA);
const float m = (2.f * yA * t + yB) / (2.f * xA * t + xB);
// Compute the log domain curve values using Formula (C.6).
cubic.fControlPoints[c].fX = x;
cubic.fControlPoints[c].fY = std::log2(y / x);
cubic.fControlPoints[c].fM = (x * m - y) / (std::log(2.f) * x * y);
}
}
}
Weighting ComputeWeighting(const AdaptiveGlobalToneMap::HeadroomAdaptiveToneMap& hatm,
float targetedHdrHeadroom) {
Weighting result;
// Create the list of HDR headrooms including the baseline image and all alternate images, as
// described Clause 6.2.5 Computation of the headroom-adaptive tone map.
// Let N be the length of the combined list.
size_t N = 0;
// Let H be the sorted list of HDR headrooms.
float H[AdaptiveGlobalToneMap::HeadroomAdaptiveToneMap::kMaxNumAlternateImages + 1];
// Let indices list the index of each entry of H in fAlternateHdrHeadroom. The index for
// fBaselineHdrHeadroom is Weighting::kInvalidIndex.
size_t indices[AdaptiveGlobalToneMap::HeadroomAdaptiveToneMap::kMaxNumAlternateImages + 1];
for (size_t i = 0; i < hatm.fAlternateImages.size(); ++i) {
if (N == i && hatm.fBaselineHdrHeadroom < hatm.fAlternateImages[i].fHdrHeadroom) {
// Insert the baseline HDR headroom before the indices as they are visited.
indices[N] = Weighting::kInvalidIndex;
H[N++] = hatm.fBaselineHdrHeadroom;
}
indices[N] = i;
H[N++] = hatm.fAlternateImages[i].fHdrHeadroom;
}
if (N == hatm.fAlternateImages.size()) {
// Insert the baseline HDR headroom at the end if it has not yet been inserted.
indices[N] = Weighting::kInvalidIndex;
H[N++] = hatm.fBaselineHdrHeadroom;
}
// Find the indices for the contributing images.
if (targetedHdrHeadroom <= H[0]) {
// One case of Formula (2), for the left endpoint.
result.fWeight[0] = 1.f;
result.fAlternateImageIndex[0] = indices[0];
} else if (targetedHdrHeadroom >= H[N-1]) {
// The other case of Formula (2), for the right endpoint.
result.fWeight[0] = 1.f;
result.fAlternateImageIndex[0] = indices[N-1];
} else {
// The case of Formula (3).
size_t i = 0;
for (i = 0; i < N - 1; ++i) {
if (H[i] <= targetedHdrHeadroom && targetedHdrHeadroom <= H[i+1]) {
break;
}
}
result.fWeight[0] = (targetedHdrHeadroom - H[i+1]) / (H[i] - H[i+1]);
result.fWeight[1] = 1.f - result.fWeight[0];
result.fAlternateImageIndex[0] = indices[i];
result.fAlternateImageIndex[1] = indices[i+1];
}
// The baseline image always has a gain of 0, so set the weight for the baseline image to 0.
for (size_t i = 0; i < 2; ++i) {
if (result.fAlternateImageIndex[i] == Weighting::kInvalidIndex) {
result.fWeight[i] = 0;
} else if (result.fWeight[i] == 0) {
result.fAlternateImageIndex[i] = Weighting::kInvalidIndex;
}
}
// Sort the weights so that the first weight is always greater.
if (result.fWeight[1] > result.fWeight[0]) {
std::swap(result.fWeight[0], result.fWeight[1]);
std::swap(result.fAlternateImageIndex[0], result.fAlternateImageIndex[1]);
}
return result;
}
void ApplyGain(const AdaptiveGlobalToneMap::HeadroomAdaptiveToneMap& hatm,
SkSpan<SkColor4f> colors,
float targetedHdrHeadroom) {
SkASSERT(Validate(hatm));
// This function implements Formula (4). It creates special cases for one or both of the weights
// being zero.
const auto weighting = AgtmHelpers::ComputeWeighting(hatm, targetedHdrHeadroom);
if (weighting.fWeight[0] == 0.f) {
// If no weight is non-zero, then no gain will be applied. Leave the points unchanged.
return;
} else if (weighting.fWeight[1] == 0.f) {
// Special case the case of there being only one weighted gain function.
const auto& gain =
hatm.fAlternateImages[weighting.fAlternateImageIndex[0]].fColorGainFunction;
const float w = weighting.fWeight[0];
for (auto& C : colors) {
SkColor4f G = AgtmHelpers::EvaluateColorGainFunction(gain, C);
C = {
C.fR * std::exp2(w * G.fR),
C.fG * std::exp2(w * G.fG),
C.fB * std::exp2(w * G.fB),
C.fA,
};
}
} else {
// The general case of two weighted gain functions.
const auto& gain0 =
hatm.fAlternateImages[weighting.fAlternateImageIndex[0]].fColorGainFunction;
const float w0 = weighting.fWeight[0];
const auto& gain1 =
hatm.fAlternateImages[weighting.fAlternateImageIndex[1]].fColorGainFunction;
const float w1 = weighting.fWeight[1];
for (auto& C : colors) {
SkColor4f G0 = AgtmHelpers::EvaluateColorGainFunction(gain0, C);
SkColor4f G1 = AgtmHelpers::EvaluateColorGainFunction(gain1, C);
C = {
C.fR * std::exp2(w0 * G0.fR + w1 * G1.fR),
C.fG * std::exp2(w0 * G0.fG + w1 * G1.fG),
C.fB * std::exp2(w0 * G0.fB + w1 * G1.fB),
C.fA,
};
}
}
}
sk_sp<SkColorSpace> GetGainApplicationSpace(
const AdaptiveGlobalToneMap::HeadroomAdaptiveToneMap& hatm) {
skcms_Matrix3x3 toXYZD50;
if (!hatm.fGainApplicationSpacePrimaries.toXYZD50(&toXYZD50)) {
return nullptr;
}
return SkColorSpace::MakeRGB(SkNamedTransferFn::kLinear, toXYZD50);
}
sk_sp<SkColorFilter> MakeColorFilter(
const AdaptiveGlobalToneMap::HeadroomAdaptiveToneMap& hatm,
float targetedHdrHeadroom,
float scaleFactor) {
const auto weighting = ComputeWeighting(hatm, targetedHdrHeadroom);
auto effect = agtm_runtime_effect();
if (!effect) {
return nullptr;
}
SkRuntimeShaderBuilder builder(effect);
builder.uniform("scale_factor") = scaleFactor;
for (size_t a = 0; a < 2; ++a) {
const char* weight_str[2] = {"weight_i", "weight_j"};
builder.uniform(weight_str[a]) = weighting.fWeight[a];
if (weighting.fWeight[a] == 0.f) {
continue;
}
const auto& gain = hatm.fAlternateImages[
weighting.fAlternateImageIndex[a]].fColorGainFunction;
const char* mix_rgbx_str[2] = {"mix_rgbx_i", "mix_rgbx_j"};
builder.uniform(mix_rgbx_str[a]) = SkColor4f({
gain.fComponentMixing.fRed,
gain.fComponentMixing.fGreen,
gain.fComponentMixing.fBlue,
0.f,
});
const char* mix_Mmcx_str[2] = {"mix_Mmcx_i", "mix_Mmcx_j"};
builder.uniform(mix_Mmcx_str[a]) = SkColor4f({
gain.fComponentMixing.fMax,
gain.fComponentMixing.fMin,
gain.fComponentMixing.fComponent,
0.f,
});
const char* curve_texcoord_y_str[2] = {"curve_texcoord_y_i", "curve_texcoord_y_j"};
builder.uniform(curve_texcoord_y_str[a]) = (weighting.fAlternateImageIndex[a] + 0.5f);
const char* curve_N_cp_str[2] = {"curve_N_cp_i", "curve_N_cp_j"};
builder.uniform(curve_N_cp_str[a]) = static_cast<float>(
gain.fGainCurve.fControlPoints.size());
}
if (auto gainCurvesXYM = MakeGainCurveXYMImage(hatm)) {
builder.child("curve_xym") = gainCurvesXYM->makeRawShader(
SkSamplingOptions(SkFilterMode::kNearest));
}
auto gainApplicationColorSpace = GetGainApplicationSpace(hatm);
if (!gainApplicationColorSpace) {
return nullptr;
}
auto filter = builder.makeColorFilter();
SkASSERT(filter);
return filter->makeWithWorkingColorSpace(gainApplicationColorSpace);
}
// Return the maximum luminance from CLLI, MDCV, or a default.
static float get_max_luminance(const Metadata& metadata) {
if (metadata.getContentLightLevelInformation(nullptr)) {
ContentLightLevelInformation clli;
if (metadata.getContentLightLevelInformation(&clli) && clli.fMaxCLL > 0.f) {
return clli.fMaxCLL;
}
}
if (metadata.getMasteringDisplayColorVolume(nullptr)) {
MasteringDisplayColorVolume mdcv;
if (metadata.getMasteringDisplayColorVolume(&mdcv) &&
mdcv.fMaximumDisplayMasteringLuminance > 0.f) {
return mdcv.fMaximumDisplayMasteringLuminance;
}
}
return 1000.f;
}
bool PopulateToneMapAgtmParams(const Metadata& metadata,
const SkColorSpace* inputColorSpace,
AdaptiveGlobalToneMap* outAgtm,
float* outScaleFactor) {
// If `inputColorSpace` is HLG or PQ, find the HDR reference white value. When the shader
// starts, this is the luminance that will have been mapped to 1.0. We will populate
// `outScaleFactor` with a scale such that the AGTM HDR reference white luminance (if specified
// will be mapped to 1.0).
bool inputIsPqOrHlg = false;
float inputPqOrHlgWhite = AdaptiveGlobalToneMap::kDefaultHdrReferenceWhite;
if (inputColorSpace) {
skcms_TransferFunction trfn;
inputColorSpace->transferFn(&trfn);
switch (skcms_TransferFunction_getType(&trfn)) {
case skcms_TFType_PQ:
case skcms_TFType_HLG:
inputIsPqOrHlg = true;
inputPqOrHlgWhite = trfn.a;
break;
default:
break;
}
}
AdaptiveGlobalToneMap agtm;
auto& hatm = agtm.fHeadroomAdaptiveToneMap;
bool hadAgtmMetadata = metadata.getAdaptiveGlobalToneMap(&agtm);
// SDR content that does not specify an inverse tone mapping will not have a default tone
// mapping added.
if (!inputIsPqOrHlg) {
if (!hadAgtmMetadata || !hatm.has_value()) {
return false;
}
}
// If no AGTM was specified, populate the HDR reference white from the input color space.
if (!hadAgtmMetadata) {
agtm.fHdrReferenceWhite = inputPqOrHlgWhite;
}
// If no tone mapping was specified, then use RWTMO with the baseline HDR headroom computed
// from the CLLI and MDCV metadata.
if (!hatm.has_value()) {
hatm = {{
.fBaselineHdrHeadroom = std::log2(
std::max(get_max_luminance(metadata) / agtm.fHdrReferenceWhite, 1.f))
}};
AgtmHelpers::PopulateUsingRwtmo(hatm.value());
}
if (outAgtm) {
*outAgtm = agtm;
}
if (outScaleFactor) {
*outScaleFactor = inputIsPqOrHlg ? inputPqOrHlgWhite / agtm.fHdrReferenceWhite : 1.f;
}
return true;
}
bool Validate(const AdaptiveGlobalToneMap& agtm) {
if (agtm.fHdrReferenceWhite < 0.f || agtm.fHdrReferenceWhite > 10000.f) {
SkCodecPrintf("Agtm validation failed: HdrReferenceWhite invalid\n");
return false;
}
if (agtm.fHeadroomAdaptiveToneMap.has_value()) {
if (!Validate(agtm.fHeadroomAdaptiveToneMap.value())) {
return false;
}
}
return true;
}
bool Validate(const AdaptiveGlobalToneMap::HeadroomAdaptiveToneMap& hatm) {
// Enforce the constraints listed in Clause 6.2.2.
{
// The value of H_baseline shall be in the interval [0, 6].
if (hatm.fBaselineHdrHeadroom < kMinHdrHeadroom ||
hatm.fBaselineHdrHeadroom > kMaxHdrHeadroom) {
SkCodecPrintf("Agtm validation failed: H_baseline invalid\n");
return false;
}
// The value of N_alt shall be in the set {0,1,2,3,4}.
if (hatm.fAlternateImages.size() >
AdaptiveGlobalToneMap::HeadroomAdaptiveToneMap::kMaxNumAlternateImages) {
SkCodecPrintf("Agtm validation failed: NumAlternateImages invalid\n");
return false;
}
// H_alt,i shall not equal H_baseline.
for (const auto& alt : hatm.fAlternateImages) {
if (alt.fHdrHeadroom == hatm.fBaselineHdrHeadroom) {
SkCodecPrintf("Agtm validation failed: Alternate headroom equal to baseline\n");
return false;
}
}
// H_{alt,i} shall be strictly less than H_{alt,i+1}
for (size_t a = 1; a < hatm.fAlternateImages.size(); ++a) {
if (hatm.fAlternateImages[a].fHdrHeadroom <=
hatm.fAlternateImages[a - 1].fHdrHeadroom) {
SkCodecPrintf("Agtm validation failed: Alternate headrooms not ascending\n");
return false;
}
}
// Chromaticity coordinates must be in the unit interval.
if (!in_unit_interval(hatm.fGainApplicationSpacePrimaries.fRX) ||
!in_unit_interval(hatm.fGainApplicationSpacePrimaries.fRY) ||
!in_unit_interval(hatm.fGainApplicationSpacePrimaries.fGX) ||
!in_unit_interval(hatm.fGainApplicationSpacePrimaries.fGY) ||
!in_unit_interval(hatm.fGainApplicationSpacePrimaries.fBX) ||
!in_unit_interval(hatm.fGainApplicationSpacePrimaries.fBY) ||
!in_unit_interval(hatm.fGainApplicationSpacePrimaries.fWX) ||
!in_unit_interval(hatm.fGainApplicationSpacePrimaries.fWY)) {
SkCodecPrintf("Agtm validation failed: chromaticities not in unit interval\n");
return false;
}
}
// Enforce the constraints listed in Clause 6.2.3 that are required for the metadata to be
// encodable. The specification indicates that "implementations may adjust metadata items so
// that they satisfy these constraints" for the other constraints.
for (const auto& alt : hatm.fAlternateImages) {
const auto& curve = alt.fColorGainFunction.fGainCurve;
for (const auto& cp : curve.fControlPoints) {
if (alt.fHdrHeadroom > hatm.fBaselineHdrHeadroom) {
// If H_{alt,i} > H_baseline then GainCurve_i(x) >= 0.
if (cp.fY < 0.f) {
SkCodecPrintf("Agtm validation failed: negative Y value in inverse tone map\n");
return false;
}
} else {
// If H_{alt,i} < H_baseline then GainCurve_i(x) <= 0.
if (cp.fY > 0.f) {
SkCodecPrintf("Agtm validation failed: positive Y value in tone map\n");
return false;
}
}
}
}
// Enforce the constraints listed in Clause 6.4.2.
for (const auto& alt : hatm.fAlternateImages) {
const auto& mix = alt.fColorGainFunction.fComponentMixing;
// All components shall be in the interval [0, 1].
if (!in_unit_interval(mix.fRed) || !in_unit_interval(mix.fGreen) ||
!in_unit_interval(mix.fBlue) || !in_unit_interval(mix.fMax) ||
!in_unit_interval(mix.fMin) || !in_unit_interval(mix.fComponent)) {
SkCodecPrintf("Agtm validation failed: mix coefficients not in unit interval\n");
return false;
}
// The sum of all components shall equal 1. The coefficients are encoded in steps of
// 0.00001. Allow the sum to be at most one step away from 1.
const float sum = mix.fRed + mix.fGreen + mix.fBlue + mix.fMax + mix.fMin + mix.fComponent;
if (sum < 0.99999f || sum > 1.00001f) {
SkCodecPrintf("Agtm validation failed: mix coefficients don't sum to 1\n");
return false;
}
}
// Enforce the constraints listed in Clause 6.5.2.
for (const auto& alt : hatm.fAlternateImages) {
const auto& curve = alt.fColorGainFunction.fGainCurve;
// The value of N_cp must be in the set {1,...,32}.
if (curve.fControlPoints.size() < AdaptiveGlobalToneMap::GainCurve::kMinNumControlPoints ||
curve.fControlPoints.size() > AdaptiveGlobalToneMap::GainCurve::kMaxNumControlPoints) {
SkCodecPrintf("Agtm validation failed: invalid number of control points\n");
return false;
}
for (size_t c = 0; c < curve.fControlPoints.size(); ++c) {
// The value of x_i shall be in the interval [0, 64].
if (curve.fControlPoints[c].fX < 0.f ||
curve.fControlPoints[c].fX > kMaxLinearHdrHeadroom) {
SkCodecPrintf("Agtm validation failed: invalid X value\n");
return false;
}
// The value of y_i shall be in the interval [-6, 6].
if (curve.fControlPoints[c].fY < -kMaxHdrHeadroom ||
curve.fControlPoints[c].fY > kMaxHdrHeadroom) {
SkCodecPrintf("Agtm validation failed: invalid Y value\n");
return false;
}
if (c > 0) {
// It shall be the case that x_i <= x_{i+1}.
if (curve.fControlPoints[c].fX < curve.fControlPoints[c - 1].fX) {
SkCodecPrintf("Agtm validation failed: curve X is not non-decreasing\n");
return false;
}
// If x_i == x_{i+1} then it shall be the case that y_i == y_{i+1}.
if (curve.fControlPoints[c].fX == curve.fControlPoints[c - 1].fX &&
curve.fControlPoints[c].fY != curve.fControlPoints[c - 1].fY) {
SkCodecPrintf("Agtm validation failed: curve has Y discontinuity\n");
return false;
}
}
}
}
return true;
}
} // namespace AgtmHelpers
SkString AdaptiveGlobalToneMap::toString() const {
SkString result = SkStringPrintf("{hdrReferenceWhite:%f", fHdrReferenceWhite);
if (!fHeadroomAdaptiveToneMap.has_value()) {
result += "}";
return result;
}
auto& hatm = fHeadroomAdaptiveToneMap.value();
result += SkStringPrintf(", baselineHdrHeadroom:%f", hatm.fBaselineHdrHeadroom);
result += ", alternateHdrHeadrooms:[";
for (size_t a = 0; a < hatm.fAlternateImages.size(); ++a) {
result += SkStringPrintf("%f", hatm.fAlternateImages[a].fHdrHeadroom);
if (a != hatm.fAlternateImages.size() - 1) {
result += ", ";
}
}
result += "]}";
return result;
}
} // namespace skhdr