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