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()); |
John Reck | 115195e | 2023-02-01 20:57:44 -0500 | [diff] [blame] | 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; |
John Reck | 7beba3c | 2023-03-07 20:18:26 -0500 | [diff] [blame] | 86 | auto shader = |
| 87 | SkGainmapShader::Make(image, src, sampling, gainmapImage, gainmapSrc, sampling, |
| 88 | gainmapInfo, dst, targetSdrHdrRatio, destColorspace); |
John Reck | 115195e | 2023-02-01 20:57:44 -0500 | [diff] [blame] | 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 | |
John Reck | 7beba3c | 2023-03-07 20:18:26 -0500 | [diff] [blame] | 96 | #ifdef __ANDROID__ |
| 97 | |
| 98 | static constexpr char gGainmapSKSL[] = R"SKSL( |
Alec Mouri | 0d83185 | 2024-06-04 03:28:39 +0000 | [diff] [blame] | 99 | uniform shader linearBase; |
John Reck | 7beba3c | 2023-03-07 20:18:26 -0500 | [diff] [blame] | 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) { |
Alec Mouri | 0d83185 | 2024-06-04 03:28:39 +0000 | [diff] [blame] | 121 | if (W == 0.0) { |
| 122 | return base.eval(coord); |
| 123 | } |
| 124 | |
| 125 | half4 S = linearBase.eval(coord); |
John Reck | 7beba3c | 2023-03-07 20:18:26 -0500 | [diff] [blame] | 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 | |
| 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 | |
| 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 | |
| 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. |
Alec Mouri | 0d83185 | 2024-06-04 03:28:39 +0000 | [diff] [blame] | 194 | auto linearBaseImageShader = baseImage->makeRawShader(tileModeX, tileModeY, samplingOptions) |
| 195 | ->makeWithColorFilter(colorXformSdrToGainmap); |
| 196 | |
| 197 | auto baseImageShader = baseImage->makeShader(tileModeX, tileModeY, samplingOptions); |
John Reck | 7beba3c | 2023-03-07 20:18:26 -0500 | [diff] [blame] | 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 | |
Alec Mouri | 0d83185 | 2024-06-04 03:28:39 +0000 | [diff] [blame] | 211 | mBuilder.child("linearBase") = std::move(linearBaseImageShader); |
John Reck | 7beba3c | 2023-03-07 20:18:26 -0500 | [diff] [blame] | 212 | mBuilder.child("base") = std::move(baseImageShader); |
| 213 | mBuilder.child("gainmap") = std::move(gainmapImageShader); |
| 214 | mBuilder.child("workingSpaceToLinearSrgb") = std::move(colorXformGainmapToDst); |
| 215 | } |
| 216 | |
| 217 | void setupGenericUniforms(const sk_sp<const SkImage>& gainmapImage, |
| 218 | const SkGainmapInfo& gainmapInfo) { |
Nolan Scobie | d84b3da | 2024-04-18 20:49:08 +0000 | [diff] [blame] | 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}); |
John Reck | 7beba3c | 2023-03-07 20:18:26 -0500 | [diff] [blame] | 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 | |
| 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); |
John Reck | 4ef70c2 | 2023-08-09 16:07:57 -0400 | [diff] [blame] | 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) { |
Nolan Scobie | d84b3da | 2024-04-18 20:49:08 +0000 | [diff] [blame] | 261 | W = (std::log(targetHdrSdrRatio) - |
| 262 | std::log(mGainmapInfo.fDisplayRatioSdr)) / |
| 263 | (std::log(mGainmapInfo.fDisplayRatioHdr) - |
| 264 | std::log(mGainmapInfo.fDisplayRatioSdr)); |
John Reck | 4ef70c2 | 2023-08-09 16:07:57 -0400 | [diff] [blame] | 265 | } else { |
| 266 | W = 1.f; |
| 267 | } |
| 268 | } |
John Reck | 7beba3c | 2023-03-07 20:18:26 -0500 | [diff] [blame] | 269 | mBuilder.uniform("W") = W; |
| 270 | uniforms = mBuilder.uniforms(); |
| 271 | } |
| 272 | return uniforms; |
| 273 | } |
| 274 | |
| 275 | public: |
| 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 | |
| 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 | |
| 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 | |
| 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 | |
John Reck | 115195e | 2023-02-01 20:57:44 -0500 | [diff] [blame] | 320 | } // namespace android::uirenderer |