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