blob: 3ebf7d19202db72a59f58d0a8996fabea6eb804a [file] [log] [blame]
/*
* Copyright (C) 2023 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 "GainmapRenderer.h"
#include <SkGainmapShader.h>
#include "Gainmap.h"
#include "Rect.h"
#include "utils/Trace.h"
#ifdef __ANDROID__
#include "include/core/SkColorSpace.h"
#include "include/core/SkImage.h"
#include "include/core/SkShader.h"
#include "include/effects/SkRuntimeEffect.h"
#include "include/private/SkGainmapInfo.h"
#include "renderthread/CanvasContext.h"
#include "src/core/SkColorFilterPriv.h"
#include "src/core/SkImageInfoPriv.h"
#include "src/core/SkRuntimeEffectPriv.h"
#endif
namespace android::uirenderer {
using namespace renderthread;
float getTargetHdrSdrRatio(const SkColorSpace* destColorspace) {
// We should always have a known destination colorspace. If we don't we must be in some
// legacy mode where we're lost and also definitely not going to HDR
if (destColorspace == nullptr) {
return 1.f;
}
constexpr float GenericSdrWhiteNits = 203.f;
constexpr float maxPQLux = 10000.f;
constexpr float maxHLGLux = 1000.f;
skcms_TransferFunction destTF;
destColorspace->transferFn(&destTF);
if (skcms_TransferFunction_isPQish(&destTF)) {
return maxPQLux / GenericSdrWhiteNits;
} else if (skcms_TransferFunction_isHLGish(&destTF)) {
return maxHLGLux / GenericSdrWhiteNits;
#ifdef __ANDROID__
} else if (RenderThread::isCurrent()) {
CanvasContext* context = CanvasContext::getActiveContext();
return context ? context->targetSdrHdrRatio() : 1.f;
#endif
}
return 1.f;
}
void DrawGainmapBitmap(SkCanvas* c, const sk_sp<const SkImage>& image, const SkRect& src,
const SkRect& dst, const SkSamplingOptions& sampling, const SkPaint* paint,
SkCanvas::SrcRectConstraint constraint,
const sk_sp<const SkImage>& gainmapImage, const SkGainmapInfo& gainmapInfo) {
ATRACE_CALL();
#ifdef __ANDROID__
auto destColorspace = c->imageInfo().refColorSpace();
float targetSdrHdrRatio = getTargetHdrSdrRatio(destColorspace.get());
if (targetSdrHdrRatio > 1.f && gainmapImage) {
SkPaint gainmapPaint = *paint;
float sX = gainmapImage->width() / (float)image->width();
float sY = gainmapImage->height() / (float)image->height();
SkRect gainmapSrc = src;
// TODO: Tweak rounding?
gainmapSrc.fLeft *= sX;
gainmapSrc.fRight *= sX;
gainmapSrc.fTop *= sY;
gainmapSrc.fBottom *= sY;
auto shader =
SkGainmapShader::Make(image, src, sampling, gainmapImage, gainmapSrc, sampling,
gainmapInfo, dst, targetSdrHdrRatio, destColorspace);
gainmapPaint.setShader(shader);
c->drawRect(dst, gainmapPaint);
} else
#endif
c->drawImageRect(image.get(), src, dst, sampling, paint, constraint);
}
#ifdef __ANDROID__
static constexpr char gGainmapSKSL[] = R"SKSL(
uniform shader base;
uniform shader gainmap;
uniform colorFilter workingSpaceToLinearSrgb;
uniform half4 logRatioMin;
uniform half4 logRatioMax;
uniform half4 gainmapGamma;
uniform half4 epsilonSdr;
uniform half4 epsilonHdr;
uniform half W;
uniform int gainmapIsAlpha;
uniform int gainmapIsRed;
uniform int singleChannel;
uniform int noGamma;
half4 toDest(half4 working) {
half4 ls = workingSpaceToLinearSrgb.eval(working);
vec3 dest = fromLinearSrgb(ls.rgb);
return half4(dest.r, dest.g, dest.b, ls.a);
}
half4 main(float2 coord) {
half4 S = base.eval(coord);
half4 G = gainmap.eval(coord);
if (gainmapIsAlpha == 1) {
G = half4(G.a, G.a, G.a, 1.0);
}
if (gainmapIsRed == 1) {
G = half4(G.r, G.r, G.r, 1.0);
}
if (singleChannel == 1) {
half L;
if (noGamma == 1) {
L = mix(logRatioMin.r, logRatioMax.r, G.r);
} else {
L = mix(logRatioMin.r, logRatioMax.r, pow(G.r, gainmapGamma.r));
}
half3 H = (S.rgb + epsilonSdr.rgb) * exp(L * W) - epsilonHdr.rgb;
return toDest(half4(H.r, H.g, H.b, S.a));
} else {
half3 L;
if (noGamma == 1) {
L = mix(logRatioMin.rgb, logRatioMax.rgb, G.rgb);
} else {
L = mix(logRatioMin.rgb, logRatioMax.rgb, pow(G.rgb, gainmapGamma.rgb));
}
half3 H = (S.rgb + epsilonSdr.rgb) * exp(L * W) - epsilonHdr.rgb;
return toDest(half4(H.r, H.g, H.b, S.a));
}
}
)SKSL";
static sk_sp<SkRuntimeEffect> gainmap_apply_effect() {
static const SkRuntimeEffect* effect = []() -> SkRuntimeEffect* {
auto buildResult = SkRuntimeEffect::MakeForShader(SkString(gGainmapSKSL), {});
if (buildResult.effect) {
return buildResult.effect.release();
} else {
LOG_ALWAYS_FATAL("Failed to build gainmap shader: %s", buildResult.errorText.c_str());
}
}();
SkASSERT(effect);
return sk_ref_sp(effect);
}
static bool all_channels_equal(const SkColor4f& c) {
return c.fR == c.fG && c.fR == c.fB;
}
class DeferredGainmapShader {
private:
sk_sp<SkRuntimeEffect> mShader{gainmap_apply_effect()};
SkRuntimeShaderBuilder mBuilder{mShader};
SkGainmapInfo mGainmapInfo;
std::mutex mUniformGuard;
void setupChildren(const sk_sp<const SkImage>& baseImage,
const sk_sp<const SkImage>& gainmapImage, SkTileMode tileModeX,
SkTileMode tileModeY, const SkSamplingOptions& samplingOptions) {
sk_sp<SkColorSpace> baseColorSpace =
baseImage->colorSpace() ? baseImage->refColorSpace() : SkColorSpace::MakeSRGB();
// Determine the color space in which the gainmap math is to be applied.
sk_sp<SkColorSpace> gainmapMathColorSpace = baseColorSpace->makeLinearGamma();
// Create a color filter to transform from the base image's color space to the color space
// in which the gainmap is to be applied.
auto colorXformSdrToGainmap =
SkColorFilterPriv::MakeColorSpaceXform(baseColorSpace, gainmapMathColorSpace);
// The base image shader will convert into the color space in which the gainmap is applied.
auto baseImageShader = baseImage->makeRawShader(tileModeX, tileModeY, samplingOptions)
->makeWithColorFilter(colorXformSdrToGainmap);
// The gainmap image shader will ignore any color space that the gainmap has.
const SkMatrix gainmapRectToDstRect =
SkMatrix::RectToRect(SkRect::MakeWH(gainmapImage->width(), gainmapImage->height()),
SkRect::MakeWH(baseImage->width(), baseImage->height()));
auto gainmapImageShader = gainmapImage->makeRawShader(tileModeX, tileModeY, samplingOptions,
&gainmapRectToDstRect);
// Create a color filter to transform from the color space in which the gainmap is applied
// to the intermediate destination color space.
auto colorXformGainmapToDst = SkColorFilterPriv::MakeColorSpaceXform(
gainmapMathColorSpace, SkColorSpace::MakeSRGBLinear());
mBuilder.child("base") = std::move(baseImageShader);
mBuilder.child("gainmap") = std::move(gainmapImageShader);
mBuilder.child("workingSpaceToLinearSrgb") = std::move(colorXformGainmapToDst);
}
void setupGenericUniforms(const sk_sp<const SkImage>& gainmapImage,
const SkGainmapInfo& gainmapInfo) {
const SkColor4f logRatioMin({sk_float_log(gainmapInfo.fGainmapRatioMin.fR),
sk_float_log(gainmapInfo.fGainmapRatioMin.fG),
sk_float_log(gainmapInfo.fGainmapRatioMin.fB), 1.f});
const SkColor4f logRatioMax({sk_float_log(gainmapInfo.fGainmapRatioMax.fR),
sk_float_log(gainmapInfo.fGainmapRatioMax.fG),
sk_float_log(gainmapInfo.fGainmapRatioMax.fB), 1.f});
const int noGamma = gainmapInfo.fGainmapGamma.fR == 1.f &&
gainmapInfo.fGainmapGamma.fG == 1.f &&
gainmapInfo.fGainmapGamma.fB == 1.f;
const uint32_t colorTypeFlags = SkColorTypeChannelFlags(gainmapImage->colorType());
const int gainmapIsAlpha = colorTypeFlags == kAlpha_SkColorChannelFlag;
const int gainmapIsRed = colorTypeFlags == kRed_SkColorChannelFlag;
const int singleChannel = all_channels_equal(gainmapInfo.fGainmapGamma) &&
all_channels_equal(gainmapInfo.fGainmapRatioMin) &&
all_channels_equal(gainmapInfo.fGainmapRatioMax) &&
(colorTypeFlags == kGray_SkColorChannelFlag ||
colorTypeFlags == kAlpha_SkColorChannelFlag ||
colorTypeFlags == kRed_SkColorChannelFlag);
mBuilder.uniform("logRatioMin") = logRatioMin;
mBuilder.uniform("logRatioMax") = logRatioMax;
mBuilder.uniform("gainmapGamma") = gainmapInfo.fGainmapGamma;
mBuilder.uniform("epsilonSdr") = gainmapInfo.fEpsilonSdr;
mBuilder.uniform("epsilonHdr") = gainmapInfo.fEpsilonHdr;
mBuilder.uniform("noGamma") = noGamma;
mBuilder.uniform("singleChannel") = singleChannel;
mBuilder.uniform("gainmapIsAlpha") = gainmapIsAlpha;
mBuilder.uniform("gainmapIsRed") = gainmapIsRed;
}
sk_sp<const SkData> build(float targetHdrSdrRatio) {
sk_sp<const SkData> uniforms;
{
// If we are called concurrently from multiple threads, we need to guard the call
// to writableUniforms() which mutates mUniform. This is otherwise safe because
// writeableUniforms() will make a copy if it's not unique before mutating
// This can happen if a BitmapShader is used on multiple canvas', such as a
// software + hardware canvas, which is otherwise valid as SkShader is "immutable"
std::lock_guard _lock(mUniformGuard);
// Compute the weight parameter that will be used to blend between the images.
float W = 0.f;
if (targetHdrSdrRatio > mGainmapInfo.fDisplayRatioSdr) {
if (targetHdrSdrRatio < mGainmapInfo.fDisplayRatioHdr) {
W = (sk_float_log(targetHdrSdrRatio) -
sk_float_log(mGainmapInfo.fDisplayRatioSdr)) /
(sk_float_log(mGainmapInfo.fDisplayRatioHdr) -
sk_float_log(mGainmapInfo.fDisplayRatioSdr));
} else {
W = 1.f;
}
}
mBuilder.uniform("W") = W;
uniforms = mBuilder.uniforms();
}
return uniforms;
}
public:
explicit DeferredGainmapShader(const sk_sp<const SkImage>& image,
const sk_sp<const SkImage>& gainmapImage,
const SkGainmapInfo& gainmapInfo, SkTileMode tileModeX,
SkTileMode tileModeY, const SkSamplingOptions& sampling) {
mGainmapInfo = gainmapInfo;
setupChildren(image, gainmapImage, tileModeX, tileModeY, sampling);
setupGenericUniforms(gainmapImage, gainmapInfo);
}
static sk_sp<SkShader> Make(const sk_sp<const SkImage>& image,
const sk_sp<const SkImage>& gainmapImage,
const SkGainmapInfo& gainmapInfo, SkTileMode tileModeX,
SkTileMode tileModeY, const SkSamplingOptions& sampling) {
auto deferredHandler = std::make_shared<DeferredGainmapShader>(
image, gainmapImage, gainmapInfo, tileModeX, tileModeY, sampling);
auto callback =
[deferredHandler](const SkRuntimeEffectPriv::UniformsCallbackContext& renderContext)
-> sk_sp<const SkData> {
return deferredHandler->build(getTargetHdrSdrRatio(renderContext.fDstColorSpace));
};
return SkRuntimeEffectPriv::MakeDeferredShader(deferredHandler->mShader.get(), callback,
deferredHandler->mBuilder.children());
}
};
sk_sp<SkShader> MakeGainmapShader(const sk_sp<const SkImage>& image,
const sk_sp<const SkImage>& gainmapImage,
const SkGainmapInfo& gainmapInfo, SkTileMode tileModeX,
SkTileMode tileModeY, const SkSamplingOptions& sampling) {
return DeferredGainmapShader::Make(image, gainmapImage, gainmapInfo, tileModeX, tileModeY,
sampling);
}
#else // __ANDROID__
sk_sp<SkShader> MakeGainmapShader(const sk_sp<const SkImage>& image,
const sk_sp<const SkImage>& gainmapImage,
const SkGainmapInfo& gainmapInfo, SkTileMode tileModeX,
SkTileMode tileModeY, const SkSamplingOptions& sampling) {
return nullptr;
}
#endif // __ANDROID__
} // namespace android::uirenderer