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     if (targetSdrHdrRatio > 1.f && gainmapImage) {
77         SkPaint gainmapPaint = *paint;
78         float sX = gainmapImage->width() / (float)image->width();
79         float sY = gainmapImage->height() / (float)image->height();
80         SkRect gainmapSrc = src;
81         // TODO: Tweak rounding?
82         gainmapSrc.fLeft *= sX;
83         gainmapSrc.fRight *= sX;
84         gainmapSrc.fTop *= sY;
85         gainmapSrc.fBottom *= sY;
86         auto shader =
87                 SkGainmapShader::Make(image, src, sampling, gainmapImage, gainmapSrc, sampling,
88                                       gainmapInfo, dst, targetSdrHdrRatio, destColorspace);
89         gainmapPaint.setShader(shader);
90         c->drawRect(dst, gainmapPaint);
91     } else
92 #endif
93         c->drawImageRect(image.get(), src, dst, sampling, paint, constraint);
94 }
95 
96 #ifdef __ANDROID__
97 
98 static constexpr char gGainmapSKSL[] = R"SKSL(
99     uniform shader linearBase;
100     uniform shader base;
101     uniform shader gainmap;
102     uniform colorFilter workingSpaceToLinearSrgb;
103     uniform half4 logRatioMin;
104     uniform half4 logRatioMax;
105     uniform half4 gainmapGamma;
106     uniform half4 epsilonSdr;
107     uniform half4 epsilonHdr;
108     uniform half W;
109     uniform int gainmapIsAlpha;
110     uniform int gainmapIsRed;
111     uniform int singleChannel;
112     uniform int noGamma;
113 
114     half4 toDest(half4 working) {
115         half4 ls = workingSpaceToLinearSrgb.eval(working);
116         vec3 dest = fromLinearSrgb(ls.rgb);
117         return half4(dest.r, dest.g, dest.b, ls.a);
118     }
119 
120     half4 main(float2 coord) {
121         if (W == 0.0) {
122             return base.eval(coord);
123         }
124 
125         half4 S = linearBase.eval(coord);
126         half4 G = gainmap.eval(coord);
127         if (gainmapIsAlpha == 1) {
128             G = half4(G.a, G.a, G.a, 1.0);
129         }
130         if (gainmapIsRed == 1) {
131             G = half4(G.r, G.r, G.r, 1.0);
132         }
133         if (singleChannel == 1) {
134             half L;
135             if (noGamma == 1) {
136                 L = mix(logRatioMin.r, logRatioMax.r, G.r);
137             } else {
138                 L = mix(logRatioMin.r, logRatioMax.r, pow(G.r, gainmapGamma.r));
139             }
140             half3 H = (S.rgb + epsilonSdr.rgb) * exp(L * W) - epsilonHdr.rgb;
141             return toDest(half4(H.r, H.g, H.b, S.a));
142         } else {
143             half3 L;
144             if (noGamma == 1) {
145                 L = mix(logRatioMin.rgb, logRatioMax.rgb, G.rgb);
146             } else {
147                 L = mix(logRatioMin.rgb, logRatioMax.rgb, pow(G.rgb, gainmapGamma.rgb));
148             }
149             half3 H = (S.rgb + epsilonSdr.rgb) * exp(L * W) - epsilonHdr.rgb;
150             return toDest(half4(H.r, H.g, H.b, S.a));
151         }
152     }
153 )SKSL";
154 
gainmap_apply_effect()155 static sk_sp<SkRuntimeEffect> gainmap_apply_effect() {
156     static const SkRuntimeEffect* effect = []() -> SkRuntimeEffect* {
157         auto buildResult = SkRuntimeEffect::MakeForShader(SkString(gGainmapSKSL), {});
158         if (buildResult.effect) {
159             return buildResult.effect.release();
160         } else {
161             LOG_ALWAYS_FATAL("Failed to build gainmap shader: %s", buildResult.errorText.c_str());
162         }
163     }();
164     SkASSERT(effect);
165     return sk_ref_sp(effect);
166 }
167 
all_channels_equal(const SkColor4f & c)168 static bool all_channels_equal(const SkColor4f& c) {
169     return c.fR == c.fG && c.fR == c.fB;
170 }
171 
172 class DeferredGainmapShader {
173 private:
174     sk_sp<SkRuntimeEffect> mShader{gainmap_apply_effect()};
175     SkRuntimeShaderBuilder mBuilder{mShader};
176     SkGainmapInfo mGainmapInfo;
177     std::mutex mUniformGuard;
178 
setupChildren(const sk_sp<const SkImage> & baseImage,const sk_sp<const SkImage> & gainmapImage,SkTileMode tileModeX,SkTileMode tileModeY,const SkSamplingOptions & samplingOptions)179     void setupChildren(const sk_sp<const SkImage>& baseImage,
180                        const sk_sp<const SkImage>& gainmapImage, SkTileMode tileModeX,
181                        SkTileMode tileModeY, const SkSamplingOptions& samplingOptions) {
182         sk_sp<SkColorSpace> baseColorSpace =
183                 baseImage->colorSpace() ? baseImage->refColorSpace() : SkColorSpace::MakeSRGB();
184 
185         // Determine the color space in which the gainmap math is to be applied.
186         sk_sp<SkColorSpace> gainmapMathColorSpace = baseColorSpace->makeLinearGamma();
187 
188         // Create a color filter to transform from the base image's color space to the color space
189         // in which the gainmap is to be applied.
190         auto colorXformSdrToGainmap =
191                 SkColorFilterPriv::MakeColorSpaceXform(baseColorSpace, gainmapMathColorSpace);
192 
193         // The base image shader will convert into the color space in which the gainmap is applied.
194         auto linearBaseImageShader = baseImage->makeRawShader(tileModeX, tileModeY, samplingOptions)
195                                              ->makeWithColorFilter(colorXformSdrToGainmap);
196 
197         auto baseImageShader = baseImage->makeShader(tileModeX, tileModeY, samplingOptions);
198 
199         // The gainmap image shader will ignore any color space that the gainmap has.
200         const SkMatrix gainmapRectToDstRect =
201                 SkMatrix::RectToRect(SkRect::MakeWH(gainmapImage->width(), gainmapImage->height()),
202                                      SkRect::MakeWH(baseImage->width(), baseImage->height()));
203         auto gainmapImageShader = gainmapImage->makeRawShader(tileModeX, tileModeY, samplingOptions,
204                                                               &gainmapRectToDstRect);
205 
206         // Create a color filter to transform from the color space in which the gainmap is applied
207         // to the intermediate destination color space.
208         auto colorXformGainmapToDst = SkColorFilterPriv::MakeColorSpaceXform(
209                 gainmapMathColorSpace, SkColorSpace::MakeSRGBLinear());
210 
211         mBuilder.child("linearBase") = std::move(linearBaseImageShader);
212         mBuilder.child("base") = std::move(baseImageShader);
213         mBuilder.child("gainmap") = std::move(gainmapImageShader);
214         mBuilder.child("workingSpaceToLinearSrgb") = std::move(colorXformGainmapToDst);
215     }
216 
setupGenericUniforms(const sk_sp<const SkImage> & gainmapImage,const SkGainmapInfo & gainmapInfo)217     void setupGenericUniforms(const sk_sp<const SkImage>& gainmapImage,
218                               const SkGainmapInfo& gainmapInfo) {
219         const SkColor4f logRatioMin({std::log(gainmapInfo.fGainmapRatioMin.fR),
220                                      std::log(gainmapInfo.fGainmapRatioMin.fG),
221                                      std::log(gainmapInfo.fGainmapRatioMin.fB), 1.f});
222         const SkColor4f logRatioMax({std::log(gainmapInfo.fGainmapRatioMax.fR),
223                                      std::log(gainmapInfo.fGainmapRatioMax.fG),
224                                      std::log(gainmapInfo.fGainmapRatioMax.fB), 1.f});
225         const int noGamma = gainmapInfo.fGainmapGamma.fR == 1.f &&
226                             gainmapInfo.fGainmapGamma.fG == 1.f &&
227                             gainmapInfo.fGainmapGamma.fB == 1.f;
228         const uint32_t colorTypeFlags = SkColorTypeChannelFlags(gainmapImage->colorType());
229         const int gainmapIsAlpha = colorTypeFlags == kAlpha_SkColorChannelFlag;
230         const int gainmapIsRed = colorTypeFlags == kRed_SkColorChannelFlag;
231         const int singleChannel = all_channels_equal(gainmapInfo.fGainmapGamma) &&
232                                   all_channels_equal(gainmapInfo.fGainmapRatioMin) &&
233                                   all_channels_equal(gainmapInfo.fGainmapRatioMax) &&
234                                   (colorTypeFlags == kGray_SkColorChannelFlag ||
235                                    colorTypeFlags == kAlpha_SkColorChannelFlag ||
236                                    colorTypeFlags == kRed_SkColorChannelFlag);
237         mBuilder.uniform("logRatioMin") = logRatioMin;
238         mBuilder.uniform("logRatioMax") = logRatioMax;
239         mBuilder.uniform("gainmapGamma") = gainmapInfo.fGainmapGamma;
240         mBuilder.uniform("epsilonSdr") = gainmapInfo.fEpsilonSdr;
241         mBuilder.uniform("epsilonHdr") = gainmapInfo.fEpsilonHdr;
242         mBuilder.uniform("noGamma") = noGamma;
243         mBuilder.uniform("singleChannel") = singleChannel;
244         mBuilder.uniform("gainmapIsAlpha") = gainmapIsAlpha;
245         mBuilder.uniform("gainmapIsRed") = gainmapIsRed;
246     }
247 
build(float targetHdrSdrRatio)248     sk_sp<const SkData> build(float targetHdrSdrRatio) {
249         sk_sp<const SkData> uniforms;
250         {
251             // If we are called concurrently from multiple threads, we need to guard the call
252             // to writableUniforms() which mutates mUniform. This is otherwise safe because
253             // writeableUniforms() will make a copy if it's not unique before mutating
254             // This can happen if a BitmapShader is used on multiple canvas', such as a
255             // software + hardware canvas, which is otherwise valid as SkShader is "immutable"
256             std::lock_guard _lock(mUniformGuard);
257             // Compute the weight parameter that will be used to blend between the images.
258             float W = 0.f;
259             if (targetHdrSdrRatio > mGainmapInfo.fDisplayRatioSdr) {
260                 if (targetHdrSdrRatio < mGainmapInfo.fDisplayRatioHdr) {
261                     W = (std::log(targetHdrSdrRatio) -
262                          std::log(mGainmapInfo.fDisplayRatioSdr)) /
263                         (std::log(mGainmapInfo.fDisplayRatioHdr) -
264                          std::log(mGainmapInfo.fDisplayRatioSdr));
265                 } else {
266                     W = 1.f;
267                 }
268             }
269             mBuilder.uniform("W") = W;
270             uniforms = mBuilder.uniforms();
271         }
272         return uniforms;
273     }
274 
275 public:
DeferredGainmapShader(const sk_sp<const SkImage> & image,const sk_sp<const SkImage> & gainmapImage,const SkGainmapInfo & gainmapInfo,SkTileMode tileModeX,SkTileMode tileModeY,const SkSamplingOptions & sampling)276     explicit DeferredGainmapShader(const sk_sp<const SkImage>& image,
277                                    const sk_sp<const SkImage>& gainmapImage,
278                                    const SkGainmapInfo& gainmapInfo, SkTileMode tileModeX,
279                                    SkTileMode tileModeY, const SkSamplingOptions& sampling) {
280         mGainmapInfo = gainmapInfo;
281         setupChildren(image, gainmapImage, tileModeX, tileModeY, sampling);
282         setupGenericUniforms(gainmapImage, gainmapInfo);
283     }
284 
Make(const sk_sp<const SkImage> & image,const sk_sp<const SkImage> & gainmapImage,const SkGainmapInfo & gainmapInfo,SkTileMode tileModeX,SkTileMode tileModeY,const SkSamplingOptions & sampling)285     static sk_sp<SkShader> Make(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         auto deferredHandler = std::make_shared<DeferredGainmapShader>(
290                 image, gainmapImage, gainmapInfo, tileModeX, tileModeY, sampling);
291         auto callback =
292                 [deferredHandler](const SkRuntimeEffectPriv::UniformsCallbackContext& renderContext)
293                 -> sk_sp<const SkData> {
294             return deferredHandler->build(getTargetHdrSdrRatio(renderContext.fDstColorSpace));
295         };
296         return SkRuntimeEffectPriv::MakeDeferredShader(deferredHandler->mShader.get(), callback,
297                                                        deferredHandler->mBuilder.children());
298     }
299 };
300 
MakeGainmapShader(const sk_sp<const SkImage> & image,const sk_sp<const SkImage> & gainmapImage,const SkGainmapInfo & gainmapInfo,SkTileMode tileModeX,SkTileMode tileModeY,const SkSamplingOptions & sampling)301 sk_sp<SkShader> MakeGainmapShader(const sk_sp<const SkImage>& image,
302                                   const sk_sp<const SkImage>& gainmapImage,
303                                   const SkGainmapInfo& gainmapInfo, SkTileMode tileModeX,
304                                   SkTileMode tileModeY, const SkSamplingOptions& sampling) {
305     return DeferredGainmapShader::Make(image, gainmapImage, gainmapInfo, tileModeX, tileModeY,
306                                        sampling);
307 }
308 
309 #else  // __ANDROID__
310 
MakeGainmapShader(const sk_sp<const SkImage> & image,const sk_sp<const SkImage> & gainmapImage,const SkGainmapInfo & gainmapInfo,SkTileMode tileModeX,SkTileMode tileModeY,const SkSamplingOptions & sampling)311 sk_sp<SkShader> MakeGainmapShader(const sk_sp<const SkImage>& image,
312                                   const sk_sp<const SkImage>& gainmapImage,
313                                   const SkGainmapInfo& gainmapInfo, SkTileMode tileModeX,
314                                   SkTileMode tileModeY, const SkSamplingOptions& sampling) {
315         return nullptr;
316 }
317 
318 #endif  // __ANDROID__
319 
320 }  // namespace android::uirenderer