blob: bc71c018988c111c8e340046966c6acee79b73eb [file] [log] [blame]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "cc/paint/tone_map_util.h"
#include <cmath>
#include <string_view>
#include <utility>
#include "cc/paint/paint_image.h"
#include "third_party/skia/include/core/SkCanvas.h"
#include "third_party/skia/include/core/SkColorFilter.h"
#include "third_party/skia/include/core/SkColorSpace.h"
#include "third_party/skia/include/core/SkPaint.h"
#include "third_party/skia/include/effects/SkColorMatrix.h"
#include "third_party/skia/include/effects/SkRuntimeEffect.h"
#include "ui/gfx/hdr_metadata_agtm.h"
namespace cc {
namespace {
// The HLG OOTF filter applies reference HLG opto-optical transfer function as
// described in Table 5 of ITU-R BT.2100-3, using the value of gamma=1.2.
// The working space of this shader has premultiplied alpha, and so the
// computed value of Y must be un-multiplied.
static constexpr char kHlgOotfSKSL[] =
"half4 main(half4 color) {\n"
" half3 L = half3(0.2627, 0.6780, 0.0593);\n"
" half Y = dot(L, color.rgb);\n"
" if (Y > 0.0) {\n"
" if (color.a > 0.0) {\n"
" Y /= color.a;\n"
" }\n"
" color.rgb *= pow(Y, 0.2);\n"
" }\n"
" return color;\n"
"}\n";
sk_sp<SkColorFilter> GetHlgOotfFilter() {
static const SkRuntimeEffect* effect =
SkRuntimeEffect::MakeForColorFilter(
SkString(kHlgOotfSKSL, sizeof(kHlgOotfSKSL) - 1),
/*options=*/{})
.effect.release();
CHECK(effect);
sk_sp<SkColorFilter> filter = effect->makeColorFilter(nullptr);
CHECK(filter);
return filter->makeWithWorkingColorSpace(SkColorSpace::MakeRGB(
SkNamedTransferFn::kLinear, SkNamedGamut::kRec2020));
}
// Scale the color values in linear space.
sk_sp<SkColorFilter> GetLinearScaleFilter(float s) {
if (s == 1.f) {
return nullptr;
}
SkColorMatrix scale_matrix;
scale_matrix.setScale(s, s, s);
sk_sp<SkColorFilter> filter =
SkColorFilters::Matrix(scale_matrix, SkColorFilters::Clamp::kNo);
CHECK(filter);
return filter->makeWithWorkingColorSpace(SkColorSpace::MakeRGB(
SkNamedTransferFn::kLinear, SkNamedGamut::kRec2020));
}
// The Reinhard tone mapping function performs the tone mapping described in:
// https://docs.google.com/document/d/17T2ek1i2R7tXdfHCnM-i5n6__RoYe0JyMfKmTEjoGR8/edit?usp=sharing
// And further detailed in:
// https://colab.research.google.com/drive/1hI10nq6L6ru_UFvz7-f7xQaQp0qarz_K
// The working space of this shader is premultiplied, and so the computed value
// of M (maxRGB) must be un-multiplied.
static constexpr char kReinhardToneMapSKSL[] =
"uniform half a;\n"
"uniform half b;\n"
"half4 main(half4 color) {\n"
" half M = max(max(color.r, color.g), color.b);\n"
" if (M > 0.0) {\n"
" if (color.a > 0.0) {\n"
" M /= color.a;\n"
" }\n"
" color.rgb *= (1.0 + a * M) / (1.0 + b * M);\n"
" }\n"
" return color;\n"
"}\n";
sk_sp<SkColorFilter> GetReinhardToneMapFilter(
float content_max_component_value,
float target_max_component_value) {
static const SkRuntimeEffect* effect =
SkRuntimeEffect::MakeForColorFilter(
SkString(kReinhardToneMapSKSL, sizeof(kReinhardToneMapSKSL) - 1),
/*options=*/{})
.effect.release();
CHECK(effect);
// Computation of the uniforms a and b is derived in
// https://colab.research.google.com/drive/1hI10nq6L6ru_UFvz7-f7xQaQp0qarz_K
float uniforms[2] = {0.f, 0.f};
if (content_max_component_value > target_max_component_value) {
uniforms[0] = target_max_component_value /
(content_max_component_value * content_max_component_value);
uniforms[1] = 1.f / target_max_component_value;
}
auto uniforms_data = SkData::MakeWithCopy(uniforms, sizeof(uniforms));
sk_sp<SkColorFilter> filter = effect->makeColorFilter(uniforms_data);
CHECK(filter);
return filter->makeWithWorkingColorSpace(SkColorSpace::MakeRGB(
SkNamedTransferFn::kLinear, SkNamedGamut::kRec2020));
}
// AGTM tone mapping shader.
static constexpr char kAgtmToneMapSKSL[] =
"uniform half altr_i_weight;\n"
"uniform half altr_j_weight;\n"
"uniform half4 altr_i_mix_rgbx;\n"
"uniform half4 altr_j_mix_rgbx;\n"
"uniform half4 altr_i_mix_Mmcx;\n"
"uniform half4 altr_j_mix_Mmcx;\n"
"uniform shader altr_i_curve;\n"
"uniform shader altr_j_curve;\n"
"uniform half gain_min;\n"
"uniform half gain_span;\n"
"uniform half gain_application_offset;\n"
"uniform half curve_scale;\n"
"\n"
"half3 EvalComponentMixing(half3 color, bool j) {\n"
" half4 rgbx = j ? altr_j_mix_rgbx : altr_i_mix_rgbx;\n"
" half4 Mmcx = j ? altr_j_mix_Mmcx : altr_i_mix_Mmcx;\n"
" half M = max(max(color.r, color.g), color.b);\n"
" half m = min(min(color.r, color.g), color.b);\n"
" half common = dot(rgbx.rgb, color) +\n"
" Mmcx[0] * M +\n"
" Mmcx[1] * m;\n"
" return Mmcx[2] * color + half3(common);\n"
"}\n"
"half EvalGain(half color, bool j) {\n"
" half2 tc = half2(curve_scale * color + 0.5, 0.5);\n"
" half y = j ? altr_j_curve.eval(tc).a : altr_i_curve.eval(tc).a;\n"
" return gain_min + gain_span * y;\n"
"}\n"
"half4 main(half4 color) {\n"
" half3 G = half3(0.0);\n"
" if (altr_i_weight > 0.0) {\n"
" half3 mixed = EvalComponentMixing(color.rgb, false);\n"
" G += altr_i_weight * half3(EvalGain(mixed.r, false),\n"
" EvalGain(mixed.g, false),\n"
" EvalGain(mixed.b, false));\n"
" }\n"
" if (altr_j_weight > 0.0) {\n"
" half3 mixed = EvalComponentMixing(color.rgb, true);\n"
" G += altr_j_weight * half3(EvalGain(mixed.r, true),\n"
" EvalGain(mixed.g, true),\n"
" EvalGain(mixed.b, true));\n"
" }\n"
" color.rgb *= exp2(G);\n"
" return color;\n"
"}\n";
sk_sp<SkColorFilter> GetAgtmFilter(const gfx::HdrMetadataAgtmParsed& params,
float H_target) {
auto result = SkRuntimeEffect::MakeForColorFilter(
SkString(kAgtmToneMapSKSL, sizeof(kAgtmToneMapSKSL) - 1),
/*options=*/{});
CHECK(result.effect) << result.errorText.c_str();
SkRuntimeShaderBuilder builder(result.effect);
builder.uniform("gain_min") = params.gain_min;
builder.uniform("gain_span") = params.gain_span;
builder.uniform("gain_application_offset") = params.gain_application_offset;
// Set the alternate representation weightings and parameters.
size_t altr_i = gfx::HdrMetadataAgtmParsed::kBaselineIndex;
size_t altr_j = gfx::HdrMetadataAgtmParsed::kBaselineIndex;
float altr_i_weight = 0.f;
float altr_j_weight = 0.f;
params.ComputeAlternateWeights(H_target, altr_i, altr_i_weight, altr_j,
altr_j_weight);
builder.uniform("altr_i_weight") = altr_i_weight;
if (altr_i == gfx::HdrMetadataAgtmParsed::kBaselineIndex) {
DCHECK_EQ(altr_i_weight, 0.f);
DCHECK_EQ(altr_j, gfx::HdrMetadataAgtmParsed::kBaselineIndex);
builder.child("altr_i_curve") = nullptr;
builder.uniform("altr_i_mix_rgbx") = SkColor4f();
builder.uniform("altr_i_mix_Mmcx") = SkColor4f();
} else {
const auto& altr = params.alternates[altr_i];
builder.child("altr_i_curve") =
altr.curve->makeRawShader(SkSamplingOptions(SkFilterMode::kLinear));
builder.uniform("altr_i_mix_rgbx") = altr.mix_rgbx;
builder.uniform("altr_i_mix_Mmcx") = altr.mix_Mmcx;
builder.uniform("curve_scale") =
params.baseline_max_component / (altr.curve->width() - 1.f);
}
builder.uniform("altr_j_weight") = altr_j_weight;
if (altr_j == gfx::HdrMetadataAgtmParsed::kBaselineIndex) {
DCHECK_EQ(altr_j_weight, 0.f);
builder.child("altr_j_curve") = nullptr;
builder.uniform("altr_j_mix_rgbx") = SkColor4f();
builder.uniform("altr_j_mix_Mmcx") = SkColor4f();
} else {
const auto& altr = params.alternates[altr_j];
builder.child("altr_j_curve") =
altr.curve->makeRawShader(SkSamplingOptions(SkFilterMode::kLinear));
builder.uniform("altr_j_mix_rgbx") = altr.mix_rgbx;
builder.uniform("altr_j_mix_Mmcx") = altr.mix_Mmcx;
}
auto filter = builder.makeColorFilter();
CHECK(filter);
return filter->makeWithWorkingColorSpace(params.gain_application_color_space);
}
} // namespace
bool ToneMapUtil::UseGlobalToneMapFilter(const SkImage* image,
const SkColorSpace* dst_color_space) {
if (!image) {
return false;
}
// Workaround for crbug.com/337538021: Disable tone mapping when the source
// and destination spaces are the same, to avoid applying tone mapping when
// uploading HLG or PQ frames to textures.
if (SkColorSpace::Equals(image->colorSpace(), dst_color_space)) {
return false;
}
return UseGlobalToneMapFilter(image->colorSpace());
}
bool ToneMapUtil::UseGlobalToneMapFilter(const SkColorSpace* cs) {
if (!cs) {
return false;
}
skcms_TransferFunction fn;
cs->transferFn(&fn);
return skcms_TransferFunction_isHLGish(&fn) ||
skcms_TransferFunction_isPQish(&fn) ||
skcms_TransferFunction_isHLG(&fn) || skcms_TransferFunction_isPQ(&fn);
}
void ToneMapUtil::AddGlobalToneMapFilterToPaint(
SkPaint& paint,
const SkImage* image,
const std::optional<gfx::HDRMetadata>& metadata,
float target_hdr_headroom) {
if (!image || !image->colorSpace()) {
return;
}
skcms_TransferFunction trfn;
image->colorSpace()->transferFn(&trfn);
gfx::HdrMetadataAgtmParsed agtm;
const bool agtm_parsed = metadata.has_value() && metadata->agtm.has_value() &&
agtm.Parse(metadata->agtm.value());
// The remainder of the function will construct `filter` to perform all
// transformations (scaling, OOTF, and tone mapping).
sk_sp<SkColorFilter> filter;
// Several stages will use the reference white luminance. Compute it ahead
// of time.
auto compute_reference_white_luminance = [&]() {
// AGTM metadata gets priority.
if (agtm_parsed) {
return agtm.hdr_reference_white;
}
// Then NDWL.
if (metadata.has_value() && metadata->ndwl.has_value() &&
metadata->ndwl->nits > 0.f) {
return metadata->ndwl->nits;
}
// Then defer to the source color space.
if (skcms_TransferFunction_isPQ(&trfn) ||
skcms_TransferFunction_isHLG(&trfn)) {
return trfn.a;
}
// Then use the default.
return gfx::ColorSpace::kDefaultSDRWhiteLevel;
};
const float reference_white_luminance = compute_reference_white_luminance();
// The HLG or PQ SkColorSpace may have a white level baked into it. Re-scale
// to be relative to the white level from the metadata, and apply the
// reference HLG OOTF (if needed).
if (skcms_TransferFunction_isPQish(&trfn)) {
// Scale so that we are using the reference inverse OETF, which maps [0,1]
// to [0,1]. Then scale by the reference display luminance (10,000 nits),
// and divide by white luminance.
const float trfn_max = skcms_TransferFunction_eval(&trfn, 1.f);
filter =
GetLinearScaleFilter(10000.f / reference_white_luminance / trfn_max);
} else if (skcms_TransferFunction_isHLGish(&trfn)) {
// Scale so that we are using the reference inverse OETF, which maps [0,1]
// to [0,1] (instead of [0,1] to [0,12] or something else).
const float trfn_max = skcms_TransferFunction_eval(&trfn, 1.f);
auto pre_ootf_scale = GetLinearScaleFilter(1.f / trfn_max);
// Apply the reference OOTF on this.
auto ootf = GetHlgOotfFilter();
// Scale by the reference display luminance (1000 nits), and divide by the
// white luminance.
auto post_ootf_scale =
GetLinearScaleFilter(1000.f / reference_white_luminance);
// Set `filter` to the three operations in sequence.
filter = SkColorFilters::Compose(
post_ootf_scale, SkColorFilters::Compose(ootf, pre_ootf_scale));
} else if (skcms_TransferFunction_isPQ(&trfn) ||
skcms_TransferFunction_isHLG(&trfn)) {
// Override the white value specified in the color space.
filter = GetLinearScaleFilter(trfn.a / reference_white_luminance);
}
// Apply tone mapping.
if (agtm_parsed) {
auto tone_map_filter = GetAgtmFilter(agtm, target_hdr_headroom);
filter = SkColorFilters::Compose(tone_map_filter, std::move(filter));
} else {
const float content_max_luminance =
gfx::HDRMetadata::GetContentMaxLuminance(metadata);
auto tone_map_filter = GetReinhardToneMapFilter(
content_max_luminance / reference_white_luminance,
std::exp2(target_hdr_headroom));
filter = SkColorFilters::Compose(tone_map_filter, std::move(filter));
}
// Perform the original filter after tone mapping.
paint.setColorFilter(
SkColorFilters::Compose(paint.refColorFilter(), std::move(filter)));
}
} // namespace cc