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, ¶ms,
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], ¶ms, sizeof(DerivedParams));
374 }
375 sk_sp<TextureProxy> ninePatch = recorder->priv().proxyCache()->findOrCreateCachedProxy(
376 recorder, key, ¶ms,
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