xref: /aosp_15_r20/external/skia/src/gpu/graphite/geom/AnalyticBlurMask.cpp (revision c8dee2aa9b3f27cf6c858bd81872bdeb2c07ed17)
1 /*
2  * Copyright 2024 Google LLC
3  *
4  * Use of this source code is governed by a BSD-style license that can be
5  * found in the LICENSE file.
6  */
7 
8 #include "src/gpu/graphite/geom/AnalyticBlurMask.h"
9 
10 #include "include/core/SkBitmap.h"
11 #include "include/core/SkMatrix.h"
12 #include "include/core/SkRRect.h"
13 #include "include/core/SkRect.h"
14 #include "include/core/SkScalar.h"
15 #include "include/core/SkSize.h"
16 #include "include/gpu/graphite/Recorder.h"
17 #include "include/private/base/SkAlign.h"
18 #include "include/private/base/SkAssert.h"
19 #include "include/private/base/SkFloatingPoint.h"
20 #include "include/private/base/SkMacros.h"
21 #include "include/private/base/SkPoint_impl.h"
22 #include "src/base/SkFloatBits.h"
23 #include "src/core/SkRRectPriv.h"
24 #include "src/gpu/BlurUtils.h"
25 #include "src/gpu/ResourceKey.h"
26 #include "src/gpu/graphite/Caps.h"
27 #include "src/gpu/graphite/ProxyCache.h"
28 #include "src/gpu/graphite/RecorderPriv.h"
29 #include "src/gpu/graphite/geom/Transform_graphite.h"
30 #include "src/sksl/SkSLUtil.h"
31 
32 #include <algorithm>
33 #include <cmath>
34 #include <cstdint>
35 #include <cstring>
36 
37 namespace skgpu::graphite {
38 
39 namespace {
40 
outset_bounds(const SkMatrix & localToDevice,float devSigma,const SkRect & srcRect)41 std::optional<Rect> outset_bounds(const SkMatrix& localToDevice,
42                                   float devSigma,
43                                   const SkRect& srcRect) {
44     float outsetX = 3.0f * devSigma;
45     float outsetY = 3.0f * devSigma;
46     if (localToDevice.isScaleTranslate()) {
47         outsetX /= std::fabs(localToDevice.getScaleX());
48         outsetY /= std::fabs(localToDevice.getScaleY());
49     } else {
50         SkSize scale;
51         if (!localToDevice.decomposeScale(&scale, nullptr)) {
52             return std::nullopt;
53         }
54         outsetX /= scale.width();
55         outsetY /= scale.height();
56     }
57     return srcRect.makeOutset(outsetX, outsetY);
58 }
59 
60 }  // anonymous namespace
61 
Make(Recorder * recorder,const Transform & localToDeviceTransform,float deviceSigma,const SkRRect & srcRRect)62 std::optional<AnalyticBlurMask> AnalyticBlurMask::Make(Recorder* recorder,
63                                                        const Transform& localToDeviceTransform,
64                                                        float deviceSigma,
65                                                        const SkRRect& srcRRect) {
66     // TODO: Implement SkMatrix functionality used below for Transform.
67     SkMatrix localToDevice = localToDeviceTransform;
68 
69     if (srcRRect.isRect() && localToDevice.preservesRightAngles()) {
70         return MakeRect(recorder, localToDevice, deviceSigma, srcRRect.rect());
71     }
72 
73     SkRRect devRRect;
74     const bool devRRectIsValid = srcRRect.transform(localToDevice, &devRRect);
75     if (devRRectIsValid && SkRRectPriv::IsCircle(devRRect)) {
76         return MakeCircle(recorder, localToDevice, deviceSigma, srcRRect.rect(), devRRect.rect());
77     }
78 
79     // A local-space circle transformed by a rotation matrix will fail SkRRect::transform since it
80     // only supports scale + translate matrices, but is still a valid circle that can be blurred.
81     if (SkRRectPriv::IsCircle(srcRRect) && localToDevice.isSimilarity()) {
82         const SkRect srcRect = srcRRect.rect();
83         const SkPoint devCenter = localToDevice.mapPoint(srcRect.center());
84         const float devRadius = localToDevice.mapVector(0.0f, srcRect.width() / 2.0f).length();
85         const SkRect devRect = {devCenter.x() - devRadius,
86                                 devCenter.y() - devRadius,
87                                 devCenter.x() + devRadius,
88                                 devCenter.y() + devRadius};
89         return MakeCircle(recorder, localToDevice, deviceSigma, srcRect, devRect);
90     }
91 
92     if (devRRectIsValid && SkRRectPriv::IsSimpleCircular(devRRect) &&
93         localToDevice.isScaleTranslate()) {
94         return MakeRRect(recorder, localToDevice, deviceSigma, srcRRect, devRRect);
95     }
96 
97     return std::nullopt;
98 }
99 
MakeRect(Recorder * recorder,const SkMatrix & localToDevice,float devSigma,const SkRect & srcRect)100 std::optional<AnalyticBlurMask> AnalyticBlurMask::MakeRect(Recorder* recorder,
101                                                            const SkMatrix& localToDevice,
102                                                            float devSigma,
103                                                            const SkRect& srcRect) {
104     SkASSERT(srcRect.isSorted());
105 
106     SkRect devRect;
107     SkMatrix devToScaledShape;
108     if (localToDevice.rectStaysRect()) {
109         // We can do everything in device space when the src rect projects to a rect in device
110         // space.
111         SkAssertResult(localToDevice.mapRect(&devRect, srcRect));
112 
113     } else {
114         // The view matrix may scale, perhaps anisotropically. But we want to apply our device space
115         // sigma to the delta of frag coord from the rect edges. Factor out the scaling to define a
116         // space that is purely rotation / translation from device space (and scale from src space).
117         // We'll meet in the middle: pre-scale the src rect to be in this space and then apply the
118         // inverse of the rotation / translation portion to the frag coord.
119         SkMatrix m;
120         SkSize scale;
121         if (!localToDevice.decomposeScale(&scale, &m)) {
122             return std::nullopt;
123         }
124         if (!m.invert(&devToScaledShape)) {
125             return std::nullopt;
126         }
127         devRect = {srcRect.left() * scale.width(),
128                    srcRect.top() * scale.height(),
129                    srcRect.right() * scale.width(),
130                    srcRect.bottom() * scale.height()};
131     }
132 
133     if (!recorder->priv().caps()->shaderCaps()->fFloatIs32Bits) {
134         // We promote the math that gets us into the Gaussian space to full float when the rect
135         // coords are large. If we don't have full float then fail. We could probably clip the rect
136         // to an outset device bounds instead.
137         if (std::fabs(devRect.left()) > 16000.0f || std::fabs(devRect.top()) > 16000.0f ||
138             std::fabs(devRect.right()) > 16000.0f || std::fabs(devRect.bottom()) > 16000.0f) {
139             return std::nullopt;
140         }
141     }
142 
143     const float sixSigma = 6.0f * devSigma;
144     const int tableWidth = ComputeIntegralTableWidth(sixSigma);
145     UniqueKey key;
146     {
147         static const UniqueKey::Domain kRectBlurDomain = UniqueKey::GenerateDomain();
148         UniqueKey::Builder builder(&key, kRectBlurDomain, 1, "BlurredRectIntegralTable");
149         builder[0] = tableWidth;
150     }
151     sk_sp<TextureProxy> integral = recorder->priv().proxyCache()->findOrCreateCachedProxy(
152             recorder, key, &tableWidth,
153             [](const void* context) {
154                 int tableWidth = *static_cast<const int*>(context);
155                 return CreateIntegralTable(tableWidth);
156             });
157 
158     if (!integral) {
159         return std::nullopt;
160     }
161 
162     // In the fast variant we think of the midpoint of the integral texture as aligning with the
163     // closest rect edge both in x and y. To simplify texture coord calculation we inset the rect so
164     // that the edge of the inset rect corresponds to t = 0 in the texture. It actually simplifies
165     // things a bit in the !isFast case, too.
166     const float threeSigma = 3.0f * devSigma;
167     const Rect shapeData = Rect(devRect.left() + threeSigma,
168                                 devRect.top() + threeSigma,
169                                 devRect.right() - threeSigma,
170                                 devRect.bottom() - threeSigma);
171 
172     // In our fast variant we find the nearest horizontal and vertical edges and for each do a
173     // lookup in the integral texture for each and multiply them. When the rect is less than 6*sigma
174     // wide then things aren't so simple and we have to consider both the left and right edge of the
175     // rectangle (and similar in y).
176     const bool isFast = shapeData.left() <= shapeData.right() && shapeData.top() <= shapeData.bot();
177 
178     const float invSixSigma = 1.0f / sixSigma;
179 
180     // Determine how much to outset the draw bounds to ensure we hit pixels within 3*sigma.
181     std::optional<Rect> drawBounds = outset_bounds(localToDevice, devSigma, srcRect);
182     if (!drawBounds) {
183         return std::nullopt;
184     }
185 
186     return AnalyticBlurMask(*drawBounds,
187                             SkM44(devToScaledShape),
188                             ShapeType::kRect,
189                             shapeData,
190                             {static_cast<float>(isFast), invSixSigma},
191                             integral);
192 }
193 
quantize(float deviceSpaceFloat)194 static float quantize(float deviceSpaceFloat) {
195     // Snap the device-space value to the nearest 1/32 to increase cache hits w/o impacting the
196     // visible output since it should be hard to see a change limited to 1/32 of a pixel.
197     return SkScalarRoundToInt(deviceSpaceFloat * 32.f) / 32.f;
198 }
199 
MakeCircle(Recorder * recorder,const SkMatrix & localToDevice,float devSigma,const SkRect & srcRect,const SkRect & devRect)200 std::optional<AnalyticBlurMask> AnalyticBlurMask::MakeCircle(Recorder* recorder,
201                                                              const SkMatrix& localToDevice,
202                                                              float devSigma,
203                                                              const SkRect& srcRect,
204                                                              const SkRect& devRect) {
205     const float radius = devRect.width() / 2.0f;
206     if (!SkIsFinite(radius) || radius < SK_ScalarNearlyZero) {
207         return std::nullopt;
208     }
209 
210     // Pack profile-dependent properties and derived values into a struct that can be passed into
211     // findOrCreateCachedProxy to lazily invoke the profile creation bitmap factories.
212     struct DerivedParams {
213         float fQuantizedRadius;
214         float fQuantizedDevSigma;
215 
216         float fSolidRadius;
217         float fTextureRadius;
218 
219         bool  fUseHalfPlaneApprox;
220 
221         DerivedParams(float devSigma, float radius)
222                 : fQuantizedRadius(quantize(radius))
223                 , fQuantizedDevSigma(quantize(devSigma)) {
224             SkASSERT(fQuantizedRadius > 0.f); // quantization shouldn't have rounded to 0
225 
226             // When sigma is really small this becomes a equivalent to convolving a Gaussian with a
227             // half-plane. Similarly, in the extreme high ratio cases circle becomes a point WRT to
228             // the Guassian and the profile texture is a just a Gaussian evaluation. However, we
229             // haven't yet implemented this latter optimization.
230             constexpr float kHalfPlaneThreshold = 0.1f;
231             const float sigmaToRadiusRatio = std::min(fQuantizedDevSigma / fQuantizedRadius, 8.0f);
232             if (sigmaToRadiusRatio <= kHalfPlaneThreshold) {
233                 fUseHalfPlaneApprox = true;
234                 fSolidRadius = fQuantizedRadius - 3.0f * fQuantizedDevSigma;
235                 fTextureRadius = 6.0f * fQuantizedDevSigma;
236             } else {
237                 fUseHalfPlaneApprox = false;
238                 fQuantizedDevSigma = fQuantizedRadius * sigmaToRadiusRatio;
239                 fSolidRadius = 0.0f;
240                 fTextureRadius = fQuantizedRadius + 3.0f * fQuantizedDevSigma;
241             }
242         }
243     } params{devSigma, radius};
244 
245     UniqueKey key;
246     {
247         static const UniqueKey::Domain kCircleBlurDomain = UniqueKey::GenerateDomain();
248         UniqueKey::Builder builder(&key, kCircleBlurDomain, 2, "BlurredCircleIntegralTable");
249         if (params.fUseHalfPlaneApprox) {
250             // There only ever needs to be one half plane approximation table, so store {0,0} into
251             // the key, which never arises under normal use because we reject radius = 0 above.
252             builder[0] = SkFloat2Bits(0.f);
253             builder[1] = SkFloat2Bits(0.f);
254         } else {
255             builder[0] = SkFloat2Bits(params.fQuantizedDevSigma);
256             builder[1] = SkFloat2Bits(params.fQuantizedRadius);
257         }
258     }
259     sk_sp<TextureProxy> profile = recorder->priv().proxyCache()->findOrCreateCachedProxy(
260             recorder, key, &params,
261             [](const void* context) {
262                 constexpr int kProfileTextureWidth = 512;
263                 const DerivedParams* params = static_cast<const DerivedParams*>(context);
264                 if (params->fUseHalfPlaneApprox) {
265                     return CreateHalfPlaneProfile(kProfileTextureWidth);
266                 } else {
267                     // Rescale params to the size of the texture we're creating.
268                     const float scale = kProfileTextureWidth / params->fTextureRadius;
269                     return CreateCircleProfile(params->fQuantizedDevSigma * scale,
270                                                params->fQuantizedRadius * scale,
271                                                kProfileTextureWidth);
272                 }
273             });
274 
275     if (!profile) {
276         return std::nullopt;
277     }
278 
279     // In the shader we calculate an index into the blur profile
280     // "i = (length(fragCoords - circleCenter) - solidRadius + 0.5) / textureRadius" as
281     // "i = length((fragCoords - circleCenter) / textureRadius) -
282     //      (solidRadius - 0.5) / textureRadius"
283     // to avoid passing large values to length() that would overflow. We precalculate
284     // "1 / textureRadius" and "(solidRadius - 0.5) / textureRadius" here.
285     const Rect shapeData = Rect(devRect.centerX(),
286                                 devRect.centerY(),
287                                 1.0f / params.fTextureRadius,
288                                 (params.fSolidRadius - 0.5f) / params.fTextureRadius);
289 
290     // Determine how much to outset the draw bounds to ensure we hit pixels within 3*sigma.
291     std::optional<Rect> drawBounds = outset_bounds(localToDevice,
292                                                    params.fQuantizedDevSigma,
293                                                    srcRect);
294     if (!drawBounds) {
295         return std::nullopt;
296     }
297 
298     constexpr float kUnusedBlurData = 0.0f;
299     return AnalyticBlurMask(*drawBounds,
300                             SkM44(),
301                             ShapeType::kCircle,
302                             shapeData,
303                             {kUnusedBlurData, kUnusedBlurData},
304                             profile);
305 }
306 
MakeRRect(Recorder * recorder,const SkMatrix & localToDevice,float devSigma,const SkRRect & srcRRect,const SkRRect & devRRect)307 std::optional<AnalyticBlurMask> AnalyticBlurMask::MakeRRect(Recorder* recorder,
308                                                             const SkMatrix& localToDevice,
309                                                             float devSigma,
310                                                             const SkRRect& srcRRect,
311                                                             const SkRRect& devRRect) {
312     const int devBlurRadius = 3 * SkScalarCeilToInt(devSigma - 1.0f / 6.0f);
313 
314     const SkVector& devRadiiUL = devRRect.radii(SkRRect::kUpperLeft_Corner);
315     const SkVector& devRadiiUR = devRRect.radii(SkRRect::kUpperRight_Corner);
316     const SkVector& devRadiiLR = devRRect.radii(SkRRect::kLowerRight_Corner);
317     const SkVector& devRadiiLL = devRRect.radii(SkRRect::kLowerLeft_Corner);
318 
319     const int devLeft = SkScalarCeilToInt(std::max<float>(devRadiiUL.fX, devRadiiLL.fX));
320     const int devTop = SkScalarCeilToInt(std::max<float>(devRadiiUL.fY, devRadiiUR.fY));
321     const int devRight = SkScalarCeilToInt(std::max<float>(devRadiiUR.fX, devRadiiLR.fX));
322     const int devBot = SkScalarCeilToInt(std::max<float>(devRadiiLL.fY, devRadiiLR.fY));
323 
324     // This is a conservative check for nine-patchability.
325     const SkRect& devOrig = devRRect.getBounds();
326     if (devOrig.fLeft + devLeft + devBlurRadius >= devOrig.fRight - devRight - devBlurRadius ||
327         devOrig.fTop + devTop + devBlurRadius >= devOrig.fBottom - devBot - devBlurRadius) {
328         return std::nullopt;
329     }
330 
331     const int newRRWidth = 2 * devBlurRadius + devLeft + devRight + 1;
332     const int newRRHeight = 2 * devBlurRadius + devTop + devBot + 1;
333 
334     const SkRect newRect = SkRect::MakeXYWH(SkIntToScalar(devBlurRadius),
335                                             SkIntToScalar(devBlurRadius),
336                                             SkIntToScalar(newRRWidth),
337                                             SkIntToScalar(newRRHeight));
338     SkVector newRadii[4];
339     newRadii[0] = {SkScalarCeilToScalar(devRadiiUL.fX), SkScalarCeilToScalar(devRadiiUL.fY)};
340     newRadii[1] = {SkScalarCeilToScalar(devRadiiUR.fX), SkScalarCeilToScalar(devRadiiUR.fY)};
341     newRadii[2] = {SkScalarCeilToScalar(devRadiiLR.fX), SkScalarCeilToScalar(devRadiiLR.fY)};
342     newRadii[3] = {SkScalarCeilToScalar(devRadiiLL.fX), SkScalarCeilToScalar(devRadiiLL.fY)};
343 
344     // NOTE: SkRRect does not satisfy std::has_unique_object_representation because NaN's in float
345     // values violate that, but all SkRRects that get here will be finite so it's not really a
346     // an issue for hashing the data directly.
347     SK_BEGIN_REQUIRE_DENSE
348     struct DerivedParams {
349         SkRRect fRRectToDraw;
350         SkISize fDimensions;
351         float   fDevSigma;
352     } params;
353     SK_END_REQUIRE_DENSE
354 
355     params.fRRectToDraw.setRectRadii(newRect, newRadii);
356     params.fDimensions =
357             SkISize::Make(newRRWidth + 2 * devBlurRadius, newRRHeight + 2 * devBlurRadius);
358     params.fDevSigma = devSigma;
359 
360     // TODO(b/343684954, b/338032240): This is just generating a blurred rrect mask image on the CPU
361     // and uploading it. We should either generate them on the GPU and cache them here, or if we
362     // have a general-purpose blur mask cache, then there's no reason rrects couldn't just use that
363     // since this "analytic" blur isn't actually simplifying work like the circle and rect case.
364     // That would also allow us to support arbitrary blurred rrects and not just ninepatch rrects.
365     static const UniqueKey::Domain kRRectBlurDomain = UniqueKey::GenerateDomain();
366     UniqueKey key;
367     {
368         static constexpr int kKeySize = sizeof(DerivedParams) / sizeof(uint32_t);
369         static_assert(SkIsAlign4(sizeof(DerivedParams)));
370         // TODO: We should discretize the sigma to perceptibly meaningful changes to the table,
371         // as well as the underlying the round rect geometry.
372         UniqueKey::Builder builder(&key, kRRectBlurDomain, kKeySize, "BlurredRRectNinePatch");
373         memcpy(&builder[0], &params, sizeof(DerivedParams));
374     }
375     sk_sp<TextureProxy> ninePatch = recorder->priv().proxyCache()->findOrCreateCachedProxy(
376             recorder, key, &params,
377             [](const void* context) {
378                 const DerivedParams* params = static_cast<const DerivedParams*>(context);
379                 return CreateRRectBlurMask(params->fRRectToDraw,
380                                            params->fDimensions,
381                                            params->fDevSigma);
382             });
383 
384     if (!ninePatch) {
385         return std::nullopt;
386     }
387 
388     const float blurRadius = 3.0f * SkScalarCeilToScalar(devSigma - 1.0f / 6.0f);
389     const float edgeSize = 2.0f * blurRadius + SkRRectPriv::GetSimpleRadii(devRRect).fX + 0.5f;
390     const Rect shapeData = devRRect.rect().makeOutset(blurRadius, blurRadius);
391 
392     // Determine how much to outset the draw bounds to ensure we hit pixels within 3*sigma.
393     std::optional<Rect> drawBounds = outset_bounds(localToDevice, devSigma, srcRRect.rect());
394     if (!drawBounds) {
395         return std::nullopt;
396     }
397 
398     constexpr float kUnusedBlurData = 0.0f;
399     return AnalyticBlurMask(*drawBounds,
400                             SkM44(),
401                             ShapeType::kRRect,
402                             shapeData,
403                             {edgeSize, kUnusedBlurData},
404                             ninePatch);
405 }
406 
407 }  // namespace skgpu::graphite
408