John Reck | 115195e | 2023-02-01 20:57:44 -0500 | [diff] [blame] | 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__ |
John Reck | 7beba3c | 2023-03-07 20:18:26 -0500 | [diff] [blame] | 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" |
John Reck | 115195e | 2023-02-01 20:57:44 -0500 | [diff] [blame] | 31 | #include "renderthread/CanvasContext.h" |
John Reck | 7beba3c | 2023-03-07 20:18:26 -0500 | [diff] [blame] | 32 | #include "src/core/SkColorFilterPriv.h" |
| 33 | #include "src/core/SkImageInfoPriv.h" |
| 34 | #include "src/core/SkRuntimeEffectPriv.h" |
Nolan Scobie | d84b3da | 2024-04-18 20:49:08 +0000 | [diff] [blame] | 35 | |
| 36 | #include <cmath> |
John Reck | 115195e | 2023-02-01 20:57:44 -0500 | [diff] [blame] | 37 | #endif |
| 38 | |
| 39 | namespace android::uirenderer { |
| 40 | |
| 41 | using namespace renderthread; |
| 42 | |
John Reck | 45fd4a5 | 2023-04-20 13:40:18 -0400 | [diff] [blame] | 43 | float getTargetHdrSdrRatio(const SkColorSpace* destColorspace) { |
John Reck | 7beba3c | 2023-03-07 20:18:26 -0500 | [diff] [blame] | 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; |
John Reck | 7beba3c | 2023-03-07 20:18:26 -0500 | [diff] [blame] | 59 | #ifdef __ANDROID__ |
John Reck | aa584a3 | 2023-08-23 17:08:37 -0400 | [diff] [blame] | 60 | } else if (RenderThread::isCurrent()) { |
John Reck | 7beba3c | 2023-03-07 20:18:26 -0500 | [diff] [blame] | 61 | CanvasContext* context = CanvasContext::getActiveContext(); |
| 62 | return context ? context->targetSdrHdrRatio() : 1.f; |
John Reck | 7beba3c | 2023-03-07 20:18:26 -0500 | [diff] [blame] | 63 | #endif |
| 64 | } |
John Reck | aa584a3 | 2023-08-23 17:08:37 -0400 | [diff] [blame] | 65 | return 1.f; |
John Reck | 7beba3c | 2023-03-07 20:18:26 -0500 | [diff] [blame] | 66 | } |
| 67 | |
John Reck | 115195e | 2023-02-01 20:57:44 -0500 | [diff] [blame] | 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__ |
John Reck | 7beba3c | 2023-03-07 20:18:26 -0500 | [diff] [blame] | 74 | auto destColorspace = c->imageInfo().refColorSpace(); |
| 75 | float targetSdrHdrRatio = getTargetHdrSdrRatio(destColorspace.get()); |
Alec Mouri | e84cc9e | 2024-10-17 14:56:25 +0000 | [diff] [blame^] | 76 | const bool baseImageHdr = gainmapInfo.fBaseImageType == SkGainmapInfo::BaseImageType::kHDR; |
| 77 | if (gainmapImage && ((baseImageHdr && targetSdrHdrRatio < gainmapInfo.fDisplayRatioHdr) || |
| 78 | (!baseImageHdr && targetSdrHdrRatio > gainmapInfo.fDisplayRatioSdr))) { |
John Reck | 115195e | 2023-02-01 20:57:44 -0500 | [diff] [blame] | 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; |
John Reck | 7beba3c | 2023-03-07 20:18:26 -0500 | [diff] [blame] | 88 | auto shader = |
| 89 | SkGainmapShader::Make(image, src, sampling, gainmapImage, gainmapSrc, sampling, |
| 90 | gainmapInfo, dst, targetSdrHdrRatio, destColorspace); |
John Reck | 115195e | 2023-02-01 20:57:44 -0500 | [diff] [blame] | 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 | |
John Reck | 7beba3c | 2023-03-07 20:18:26 -0500 | [diff] [blame] | 98 | #ifdef __ANDROID__ |
| 99 | |
| 100 | static constexpr char gGainmapSKSL[] = R"SKSL( |
Alec Mouri | 0d83185 | 2024-06-04 03:28:39 +0000 | [diff] [blame] | 101 | uniform shader linearBase; |
John Reck | 7beba3c | 2023-03-07 20:18:26 -0500 | [diff] [blame] | 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) { |
Alec Mouri | 0d83185 | 2024-06-04 03:28:39 +0000 | [diff] [blame] | 123 | if (W == 0.0) { |
| 124 | return base.eval(coord); |
| 125 | } |
| 126 | |
| 127 | half4 S = linearBase.eval(coord); |
John Reck | 7beba3c | 2023-03-07 20:18:26 -0500 | [diff] [blame] | 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 | |
| 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 | |
| 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 | |
| 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. |
Alec Mouri | e84cc9e | 2024-10-17 14:56:25 +0000 | [diff] [blame^] | 188 | sk_sp<SkColorSpace> gainmapMathColorSpace = |
| 189 | mGainmapInfo.fGainmapMathColorSpace |
| 190 | ? mGainmapInfo.fGainmapMathColorSpace->makeLinearGamma() |
| 191 | : baseColorSpace->makeLinearGamma(); |
John Reck | 7beba3c | 2023-03-07 20:18:26 -0500 | [diff] [blame] | 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. |
Alec Mouri | 0d83185 | 2024-06-04 03:28:39 +0000 | [diff] [blame] | 199 | auto linearBaseImageShader = baseImage->makeRawShader(tileModeX, tileModeY, samplingOptions) |
| 200 | ->makeWithColorFilter(colorXformSdrToGainmap); |
| 201 | |
| 202 | auto baseImageShader = baseImage->makeShader(tileModeX, tileModeY, samplingOptions); |
John Reck | 7beba3c | 2023-03-07 20:18:26 -0500 | [diff] [blame] | 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 | |
Alec Mouri | 0d83185 | 2024-06-04 03:28:39 +0000 | [diff] [blame] | 216 | mBuilder.child("linearBase") = std::move(linearBaseImageShader); |
John Reck | 7beba3c | 2023-03-07 20:18:26 -0500 | [diff] [blame] | 217 | mBuilder.child("base") = std::move(baseImageShader); |
| 218 | mBuilder.child("gainmap") = std::move(gainmapImageShader); |
| 219 | mBuilder.child("workingSpaceToLinearSrgb") = std::move(colorXformGainmapToDst); |
| 220 | } |
| 221 | |
| 222 | void setupGenericUniforms(const sk_sp<const SkImage>& gainmapImage, |
| 223 | const SkGainmapInfo& gainmapInfo) { |
Nolan Scobie | d84b3da | 2024-04-18 20:49:08 +0000 | [diff] [blame] | 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}); |
John Reck | 7beba3c | 2023-03-07 20:18:26 -0500 | [diff] [blame] | 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 | |
| 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); |
John Reck | 4ef70c2 | 2023-08-09 16:07:57 -0400 | [diff] [blame] | 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) { |
Nolan Scobie | d84b3da | 2024-04-18 20:49:08 +0000 | [diff] [blame] | 266 | W = (std::log(targetHdrSdrRatio) - |
| 267 | std::log(mGainmapInfo.fDisplayRatioSdr)) / |
| 268 | (std::log(mGainmapInfo.fDisplayRatioHdr) - |
| 269 | std::log(mGainmapInfo.fDisplayRatioSdr)); |
John Reck | 4ef70c2 | 2023-08-09 16:07:57 -0400 | [diff] [blame] | 270 | } else { |
| 271 | W = 1.f; |
| 272 | } |
| 273 | } |
Alec Mouri | e84cc9e | 2024-10-17 14:56:25 +0000 | [diff] [blame^] | 274 | |
| 275 | if (mGainmapInfo.fBaseImageType == SkGainmapInfo::BaseImageType::kHDR) { |
| 276 | W -= 1.f; |
| 277 | } |
John Reck | 7beba3c | 2023-03-07 20:18:26 -0500 | [diff] [blame] | 278 | mBuilder.uniform("W") = W; |
| 279 | uniforms = mBuilder.uniforms(); |
| 280 | } |
| 281 | return uniforms; |
| 282 | } |
| 283 | |
| 284 | public: |
| 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 | |
| 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 | |
| 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 | |
| 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 | |
Alec Mouri | e84cc9e | 2024-10-17 14:56:25 +0000 | [diff] [blame^] | 329 | } // namespace android::uirenderer |