1 /*
2 * Copyright (C) 2023 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 #include "GainmapRenderer.h"
18
19 #include <SkGainmapShader.h>
20
21 #include "Gainmap.h"
22 #include "Rect.h"
23 #include "utils/Trace.h"
24
25 #ifdef __ANDROID__
26 #include "include/core/SkColorSpace.h"
27 #include "include/core/SkImage.h"
28 #include "include/core/SkShader.h"
29 #include "include/effects/SkRuntimeEffect.h"
30 #include "include/private/SkGainmapInfo.h"
31 #include "renderthread/CanvasContext.h"
32 #include "src/core/SkColorFilterPriv.h"
33 #include "src/core/SkImageInfoPriv.h"
34 #include "src/core/SkRuntimeEffectPriv.h"
35
36 #include <cmath>
37 #endif
38
39 namespace android::uirenderer {
40
41 using namespace renderthread;
42
getTargetHdrSdrRatio(const SkColorSpace * destColorspace)43 float getTargetHdrSdrRatio(const SkColorSpace* destColorspace) {
44 // We should always have a known destination colorspace. If we don't we must be in some
45 // legacy mode where we're lost and also definitely not going to HDR
46 if (destColorspace == nullptr) {
47 return 1.f;
48 }
49
50 constexpr float GenericSdrWhiteNits = 203.f;
51 constexpr float maxPQLux = 10000.f;
52 constexpr float maxHLGLux = 1000.f;
53 skcms_TransferFunction destTF;
54 destColorspace->transferFn(&destTF);
55 if (skcms_TransferFunction_isPQish(&destTF)) {
56 return maxPQLux / GenericSdrWhiteNits;
57 } else if (skcms_TransferFunction_isHLGish(&destTF)) {
58 return maxHLGLux / GenericSdrWhiteNits;
59 #ifdef __ANDROID__
60 } else if (RenderThread::isCurrent()) {
61 CanvasContext* context = CanvasContext::getActiveContext();
62 return context ? context->targetSdrHdrRatio() : 1.f;
63 #endif
64 }
65 return 1.f;
66 }
67
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)68 void DrawGainmapBitmap(SkCanvas* c, const sk_sp<const SkImage>& image, const SkRect& src,
69 const SkRect& dst, const SkSamplingOptions& sampling, const SkPaint* paint,
70 SkCanvas::SrcRectConstraint constraint,
71 const sk_sp<const SkImage>& gainmapImage, const SkGainmapInfo& gainmapInfo) {
72 ATRACE_CALL();
73 #ifdef __ANDROID__
74 auto destColorspace = c->imageInfo().refColorSpace();
75 float targetSdrHdrRatio = getTargetHdrSdrRatio(destColorspace.get());
76 const bool baseImageHdr = gainmapInfo.fBaseImageType == SkGainmapInfo::BaseImageType::kHDR;
77 if (gainmapImage && ((baseImageHdr && targetSdrHdrRatio < gainmapInfo.fDisplayRatioHdr) ||
78 (!baseImageHdr && targetSdrHdrRatio > gainmapInfo.fDisplayRatioSdr))) {
79 SkPaint gainmapPaint = *paint;
80 float sX = gainmapImage->width() / (float)image->width();
81 float sY = gainmapImage->height() / (float)image->height();
82 SkRect gainmapSrc = src;
83 // TODO: Tweak rounding?
84 gainmapSrc.fLeft *= sX;
85 gainmapSrc.fRight *= sX;
86 gainmapSrc.fTop *= sY;
87 gainmapSrc.fBottom *= sY;
88 auto shader =
89 SkGainmapShader::Make(image, src, sampling, gainmapImage, gainmapSrc, sampling,
90 gainmapInfo, dst, targetSdrHdrRatio, destColorspace);
91 gainmapPaint.setShader(shader);
92 c->drawRect(dst, gainmapPaint);
93 } else
94 #endif
95 c->drawImageRect(image.get(), src, dst, sampling, paint, constraint);
96 }
97
98 #ifdef __ANDROID__
99
100 static constexpr char gGainmapSKSL[] = R"SKSL(
101 uniform shader linearBase;
102 uniform shader base;
103 uniform shader gainmap;
104 uniform colorFilter workingSpaceToLinearSrgb;
105 uniform half4 logRatioMin;
106 uniform half4 logRatioMax;
107 uniform half4 gainmapGamma;
108 uniform half4 epsilonSdr;
109 uniform half4 epsilonHdr;
110 uniform half W;
111 uniform int gainmapIsAlpha;
112 uniform int gainmapIsRed;
113 uniform int singleChannel;
114 uniform int noGamma;
115
116 half4 toDest(half4 working) {
117 half4 ls = workingSpaceToLinearSrgb.eval(working);
118 vec3 dest = fromLinearSrgb(ls.rgb);
119 return half4(dest.r, dest.g, dest.b, ls.a);
120 }
121
122 half4 main(float2 coord) {
123 if (W == 0.0) {
124 return base.eval(coord);
125 }
126
127 half4 S = linearBase.eval(coord);
128 half4 G = gainmap.eval(coord);
129 if (gainmapIsAlpha == 1) {
130 G = half4(G.a, G.a, G.a, 1.0);
131 }
132 if (gainmapIsRed == 1) {
133 G = half4(G.r, G.r, G.r, 1.0);
134 }
135 if (singleChannel == 1) {
136 half L;
137 if (noGamma == 1) {
138 L = mix(logRatioMin.r, logRatioMax.r, G.r);
139 } else {
140 L = mix(logRatioMin.r, logRatioMax.r, pow(G.r, gainmapGamma.r));
141 }
142 half3 H = (S.rgb + epsilonSdr.rgb) * exp(L * W) - epsilonHdr.rgb;
143 return toDest(half4(H.r, H.g, H.b, S.a));
144 } else {
145 half3 L;
146 if (noGamma == 1) {
147 L = mix(logRatioMin.rgb, logRatioMax.rgb, G.rgb);
148 } else {
149 L = mix(logRatioMin.rgb, logRatioMax.rgb, pow(G.rgb, gainmapGamma.rgb));
150 }
151 half3 H = (S.rgb + epsilonSdr.rgb) * exp(L * W) - epsilonHdr.rgb;
152 return toDest(half4(H.r, H.g, H.b, S.a));
153 }
154 }
155 )SKSL";
156
gainmap_apply_effect()157 static sk_sp<SkRuntimeEffect> gainmap_apply_effect() {
158 static const SkRuntimeEffect* effect = []() -> SkRuntimeEffect* {
159 auto buildResult = SkRuntimeEffect::MakeForShader(SkString(gGainmapSKSL), {});
160 if (buildResult.effect) {
161 return buildResult.effect.release();
162 } else {
163 LOG_ALWAYS_FATAL("Failed to build gainmap shader: %s", buildResult.errorText.c_str());
164 }
165 }();
166 SkASSERT(effect);
167 return sk_ref_sp(effect);
168 }
169
all_channels_equal(const SkColor4f & c)170 static bool all_channels_equal(const SkColor4f& c) {
171 return c.fR == c.fG && c.fR == c.fB;
172 }
173
174 class DeferredGainmapShader {
175 private:
176 sk_sp<SkRuntimeEffect> mShader{gainmap_apply_effect()};
177 SkRuntimeShaderBuilder mBuilder{mShader};
178 SkGainmapInfo mGainmapInfo;
179 std::mutex mUniformGuard;
180
setupChildren(const sk_sp<const SkImage> & baseImage,const sk_sp<const SkImage> & gainmapImage,SkTileMode tileModeX,SkTileMode tileModeY,const SkSamplingOptions & samplingOptions)181 void setupChildren(const sk_sp<const SkImage>& baseImage,
182 const sk_sp<const SkImage>& gainmapImage, SkTileMode tileModeX,
183 SkTileMode tileModeY, const SkSamplingOptions& samplingOptions) {
184 sk_sp<SkColorSpace> baseColorSpace =
185 baseImage->colorSpace() ? baseImage->refColorSpace() : SkColorSpace::MakeSRGB();
186
187 // Determine the color space in which the gainmap math is to be applied.
188 sk_sp<SkColorSpace> gainmapMathColorSpace =
189 mGainmapInfo.fGainmapMathColorSpace
190 ? mGainmapInfo.fGainmapMathColorSpace->makeLinearGamma()
191 : baseColorSpace->makeLinearGamma();
192
193 // Create a color filter to transform from the base image's color space to the color space
194 // in which the gainmap is to be applied.
195 auto colorXformSdrToGainmap =
196 SkColorFilterPriv::MakeColorSpaceXform(baseColorSpace, gainmapMathColorSpace);
197
198 // The base image shader will convert into the color space in which the gainmap is applied.
199 auto linearBaseImageShader = baseImage->makeRawShader(tileModeX, tileModeY, samplingOptions)
200 ->makeWithColorFilter(colorXformSdrToGainmap);
201
202 auto baseImageShader = baseImage->makeShader(tileModeX, tileModeY, samplingOptions);
203
204 // The gainmap image shader will ignore any color space that the gainmap has.
205 const SkMatrix gainmapRectToDstRect =
206 SkMatrix::RectToRect(SkRect::MakeWH(gainmapImage->width(), gainmapImage->height()),
207 SkRect::MakeWH(baseImage->width(), baseImage->height()));
208 auto gainmapImageShader = gainmapImage->makeRawShader(tileModeX, tileModeY, samplingOptions,
209 &gainmapRectToDstRect);
210
211 // Create a color filter to transform from the color space in which the gainmap is applied
212 // to the intermediate destination color space.
213 auto colorXformGainmapToDst = SkColorFilterPriv::MakeColorSpaceXform(
214 gainmapMathColorSpace, SkColorSpace::MakeSRGBLinear());
215
216 mBuilder.child("linearBase") = std::move(linearBaseImageShader);
217 mBuilder.child("base") = std::move(baseImageShader);
218 mBuilder.child("gainmap") = std::move(gainmapImageShader);
219 mBuilder.child("workingSpaceToLinearSrgb") = std::move(colorXformGainmapToDst);
220 }
221
setupGenericUniforms(const sk_sp<const SkImage> & gainmapImage,const SkGainmapInfo & gainmapInfo)222 void setupGenericUniforms(const sk_sp<const SkImage>& gainmapImage,
223 const SkGainmapInfo& gainmapInfo) {
224 const SkColor4f logRatioMin({std::log(gainmapInfo.fGainmapRatioMin.fR),
225 std::log(gainmapInfo.fGainmapRatioMin.fG),
226 std::log(gainmapInfo.fGainmapRatioMin.fB), 1.f});
227 const SkColor4f logRatioMax({std::log(gainmapInfo.fGainmapRatioMax.fR),
228 std::log(gainmapInfo.fGainmapRatioMax.fG),
229 std::log(gainmapInfo.fGainmapRatioMax.fB), 1.f});
230 const int noGamma = gainmapInfo.fGainmapGamma.fR == 1.f &&
231 gainmapInfo.fGainmapGamma.fG == 1.f &&
232 gainmapInfo.fGainmapGamma.fB == 1.f;
233 const uint32_t colorTypeFlags = SkColorTypeChannelFlags(gainmapImage->colorType());
234 const int gainmapIsAlpha = colorTypeFlags == kAlpha_SkColorChannelFlag;
235 const int gainmapIsRed = colorTypeFlags == kRed_SkColorChannelFlag;
236 const int singleChannel = all_channels_equal(gainmapInfo.fGainmapGamma) &&
237 all_channels_equal(gainmapInfo.fGainmapRatioMin) &&
238 all_channels_equal(gainmapInfo.fGainmapRatioMax) &&
239 (colorTypeFlags == kGray_SkColorChannelFlag ||
240 colorTypeFlags == kAlpha_SkColorChannelFlag ||
241 colorTypeFlags == kRed_SkColorChannelFlag);
242 mBuilder.uniform("logRatioMin") = logRatioMin;
243 mBuilder.uniform("logRatioMax") = logRatioMax;
244 mBuilder.uniform("gainmapGamma") = gainmapInfo.fGainmapGamma;
245 mBuilder.uniform("epsilonSdr") = gainmapInfo.fEpsilonSdr;
246 mBuilder.uniform("epsilonHdr") = gainmapInfo.fEpsilonHdr;
247 mBuilder.uniform("noGamma") = noGamma;
248 mBuilder.uniform("singleChannel") = singleChannel;
249 mBuilder.uniform("gainmapIsAlpha") = gainmapIsAlpha;
250 mBuilder.uniform("gainmapIsRed") = gainmapIsRed;
251 }
252
build(float targetHdrSdrRatio)253 sk_sp<const SkData> build(float targetHdrSdrRatio) {
254 sk_sp<const SkData> uniforms;
255 {
256 // If we are called concurrently from multiple threads, we need to guard the call
257 // to writableUniforms() which mutates mUniform. This is otherwise safe because
258 // writeableUniforms() will make a copy if it's not unique before mutating
259 // This can happen if a BitmapShader is used on multiple canvas', such as a
260 // software + hardware canvas, which is otherwise valid as SkShader is "immutable"
261 std::lock_guard _lock(mUniformGuard);
262 // Compute the weight parameter that will be used to blend between the images.
263 float W = 0.f;
264 if (targetHdrSdrRatio > mGainmapInfo.fDisplayRatioSdr) {
265 if (targetHdrSdrRatio < mGainmapInfo.fDisplayRatioHdr) {
266 W = (std::log(targetHdrSdrRatio) -
267 std::log(mGainmapInfo.fDisplayRatioSdr)) /
268 (std::log(mGainmapInfo.fDisplayRatioHdr) -
269 std::log(mGainmapInfo.fDisplayRatioSdr));
270 } else {
271 W = 1.f;
272 }
273 }
274
275 if (mGainmapInfo.fBaseImageType == SkGainmapInfo::BaseImageType::kHDR) {
276 W -= 1.f;
277 }
278 mBuilder.uniform("W") = W;
279 uniforms = mBuilder.uniforms();
280 }
281 return uniforms;
282 }
283
284 public:
DeferredGainmapShader(const sk_sp<const SkImage> & image,const sk_sp<const SkImage> & gainmapImage,const SkGainmapInfo & gainmapInfo,SkTileMode tileModeX,SkTileMode tileModeY,const SkSamplingOptions & sampling)285 explicit DeferredGainmapShader(const sk_sp<const SkImage>& image,
286 const sk_sp<const SkImage>& gainmapImage,
287 const SkGainmapInfo& gainmapInfo, SkTileMode tileModeX,
288 SkTileMode tileModeY, const SkSamplingOptions& sampling) {
289 mGainmapInfo = gainmapInfo;
290 setupChildren(image, gainmapImage, tileModeX, tileModeY, sampling);
291 setupGenericUniforms(gainmapImage, gainmapInfo);
292 }
293
Make(const sk_sp<const SkImage> & image,const sk_sp<const SkImage> & gainmapImage,const SkGainmapInfo & gainmapInfo,SkTileMode tileModeX,SkTileMode tileModeY,const SkSamplingOptions & sampling)294 static sk_sp<SkShader> Make(const sk_sp<const SkImage>& image,
295 const sk_sp<const SkImage>& gainmapImage,
296 const SkGainmapInfo& gainmapInfo, SkTileMode tileModeX,
297 SkTileMode tileModeY, const SkSamplingOptions& sampling) {
298 auto deferredHandler = std::make_shared<DeferredGainmapShader>(
299 image, gainmapImage, gainmapInfo, tileModeX, tileModeY, sampling);
300 auto callback =
301 [deferredHandler](const SkRuntimeEffectPriv::UniformsCallbackContext& renderContext)
302 -> sk_sp<const SkData> {
303 return deferredHandler->build(getTargetHdrSdrRatio(renderContext.fDstColorSpace));
304 };
305 return SkRuntimeEffectPriv::MakeDeferredShader(deferredHandler->mShader.get(), callback,
306 deferredHandler->mBuilder.children());
307 }
308 };
309
MakeGainmapShader(const sk_sp<const SkImage> & image,const sk_sp<const SkImage> & gainmapImage,const SkGainmapInfo & gainmapInfo,SkTileMode tileModeX,SkTileMode tileModeY,const SkSamplingOptions & sampling)310 sk_sp<SkShader> MakeGainmapShader(const sk_sp<const SkImage>& image,
311 const sk_sp<const SkImage>& gainmapImage,
312 const SkGainmapInfo& gainmapInfo, SkTileMode tileModeX,
313 SkTileMode tileModeY, const SkSamplingOptions& sampling) {
314 return DeferredGainmapShader::Make(image, gainmapImage, gainmapInfo, tileModeX, tileModeY,
315 sampling);
316 }
317
318 #else // __ANDROID__
319
MakeGainmapShader(const sk_sp<const SkImage> & image,const sk_sp<const SkImage> & gainmapImage,const SkGainmapInfo & gainmapInfo,SkTileMode tileModeX,SkTileMode tileModeY,const SkSamplingOptions & sampling)320 sk_sp<SkShader> MakeGainmapShader(const sk_sp<const SkImage>& image,
321 const sk_sp<const SkImage>& gainmapImage,
322 const SkGainmapInfo& gainmapInfo, SkTileMode tileModeX,
323 SkTileMode tileModeY, const SkSamplingOptions& sampling) {
324 return nullptr;
325 }
326
327 #endif // __ANDROID__
328
329 } // namespace android::uirenderer
330