Flutter Engine
The Flutter Engine
AnalyticBlurMask.cpp
Go to the documentation of this file.
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
9
16#include "src/gpu/BlurUtils.h"
21#include "src/sksl/SkSLUtil.h"
22
23namespace skgpu::graphite {
24
25namespace {
26
27std::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 {
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
48std::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
86std::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;
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 }
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
173 SkM44(devToScaledShape),
175 shapeData,
176 {static_cast<float>(isFast), invSixSigma},
177 integral);
178}
179
180static 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
186std::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 }
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;
286 SkM44(),
288 shapeData,
289 {kUnusedBlurData, kUnusedBlurData},
290 profile);
291}
292
293std::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.
334 struct DerivedParams {
335 SkRRect fRRectToDraw;
336 SkISize fDimensions;
337 float fDevSigma;
338 } params;
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 }
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;
386 SkM44(),
388 shapeData,
389 {edgeSize, kUnusedBlurData},
390 ninePatch);
391}
392
393} // namespace skgpu::graphite
SkAssertResult(font.textToGlyphs("Hello", 5, SkTextEncoding::kUTF8, glyphs, std::size(glyphs))==count)
static constexpr bool SkIsAlign4(T x)
Definition: SkAlign.h:20
#define SkASSERT(cond)
Definition: SkAssert.h:116
static uint32_t SkFloat2Bits(float value)
Definition: SkFloatBits.h:41
static bool SkIsFinite(T x, Pack... values)
#define SK_BEGIN_REQUIRE_DENSE
Definition: SkMacros.h:37
#define SK_END_REQUIRE_DENSE
Definition: SkMacros.h:38
#define SkScalarRoundToInt(x)
Definition: SkScalar.h:37
#define SkScalarCeilToInt(x)
Definition: SkScalar.h:36
#define SK_ScalarNearlyZero
Definition: SkScalar.h:99
#define SkIntToScalar(x)
Definition: SkScalar.h:57
#define SkScalarCeilToScalar(x)
Definition: SkScalar.h:31
Definition: SkM44.h:150
bool preservesRightAngles(SkScalar tol=SK_ScalarNearlyZero) const
Definition: SkMatrix.cpp:209
SkPoint mapPoint(SkPoint pt) const
Definition: SkMatrix.h:1374
bool rectStaysRect() const
Definition: SkMatrix.h:271
SkScalar getScaleX() const
Definition: SkMatrix.h:415
bool decomposeScale(SkSize *scale, SkMatrix *remaining=nullptr) const
Definition: SkMatrix.cpp:1559
SkScalar getScaleY() const
Definition: SkMatrix.h:422
bool isScaleTranslate() const
Definition: SkMatrix.h:236
bool isSimilarity(SkScalar tol=SK_ScalarNearlyZero) const
Definition: SkMatrix.cpp:180
bool mapRect(SkRect *dst, const SkRect &src, SkApplyPerspectiveClip pc=SkApplyPerspectiveClip::kYes) const
Definition: SkMatrix.cpp:1141
void mapVector(SkScalar dx, SkScalar dy, SkVector *result) const
Definition: SkMatrix.h:1524
static bool IsSimpleCircular(const SkRRect &rr)
Definition: SkRRectPriv.h:27
static SkVector GetSimpleRadii(const SkRRect &rr)
Definition: SkRRectPriv.h:22
static bool IsCircle(const SkRRect &rr)
Definition: SkRRectPriv.h:18
const SkRect & rect() const
Definition: SkRRect.h:264
SkVector radii(Corner corner) const
Definition: SkRRect.h:271
@ kUpperLeft_Corner
index of top-left corner radii
Definition: SkRRect.h:252
@ kLowerRight_Corner
index of bottom-right corner radii
Definition: SkRRect.h:254
@ kUpperRight_Corner
index of top-right corner radii
Definition: SkRRect.h:253
@ kLowerLeft_Corner
index of bottom-left corner radii
Definition: SkRRect.h:255
bool transform(const SkMatrix &matrix, SkRRect *dst) const
Definition: SkRRect.cpp:436
bool isRect() const
Definition: SkRRect.h:84
const SkRect & getBounds() const
Definition: SkRRect.h:279
static Domain GenerateDomain()
Definition: ResourceKey.cpp:27
static std::optional< AnalyticBlurMask > Make(Recorder *, const Transform &localToDevice, float deviceSigma, const SkRRect &srcRRect)
const SkSL::ShaderCaps * shaderCaps() const
Definition: Caps.h:75
sk_sp< TextureProxy > findOrCreateCachedProxy(Recorder *, const SkBitmap &, std::string_view label)
Definition: ProxyCache.cpp:74
const Caps * caps() const
Definition: RecorderPriv.h:31
AI float bot() const
Definition: Rect.h:79
AI float top() const
Definition: Rect.h:77
AI float left() const
Definition: Rect.h:76
AI float right() const
Definition: Rect.h:78
const EmbeddedViewParams * params
static float min(float r, float g, float b)
Definition: hsl.cpp:48
DlVertices::Builder Builder
TRect< Scalar > Rect
Definition: rect.h:769
static float quantize(float deviceSpaceFloat)
SkBitmap CreateRRectBlurMask(const SkRRect &rrectToDraw, const SkISize &dimensions, float sigma)
Definition: BlurUtils.cpp:316
int ComputeIntegralTableWidth(float sixSigma)
Definition: BlurUtils.cpp:62
SkBitmap CreateCircleProfile(float sigma, float radius, int profileWidth)
Definition: BlurUtils.cpp:188
SkBitmap CreateHalfPlaneProfile(int profileWidth)
Definition: BlurUtils.cpp:226
SkBitmap CreateIntegralTable(int width)
Definition: BlurUtils.cpp:38
const Scalar scale
Definition: SkSize.h:16
static constexpr SkISize Make(int32_t w, int32_t h)
Definition: SkSize.h:20
float fX
x-axis value
Definition: SkPoint_impl.h:164
float fY
y-axis value
Definition: SkPoint_impl.h:165
constexpr float y() const
Definition: SkPoint_impl.h:187
constexpr float x() const
Definition: SkPoint_impl.h:181
SkScalar fBottom
larger y-axis bounds
Definition: extension.cpp:17
constexpr float left() const
Definition: SkRect.h:734
constexpr float top() const
Definition: SkRect.h:741
SkScalar fLeft
smaller x-axis bounds
Definition: extension.cpp:14
SkRect makeOutset(float dx, float dy) const
Definition: SkRect.h:1002
static constexpr SkRect MakeXYWH(float x, float y, float w, float h)
Definition: SkRect.h:659
SkScalar fRight
larger x-axis bounds
Definition: extension.cpp:16
constexpr float centerX() const
Definition: SkRect.h:776
constexpr float right() const
Definition: SkRect.h:748
constexpr float centerY() const
Definition: SkRect.h:785
constexpr float width() const
Definition: SkRect.h:762
constexpr SkPoint center() const
Definition: SkRect.h:792
bool isSorted() const
Definition: SkRect.h:705
constexpr float bottom() const
Definition: SkRect.h:755
SkScalar fTop
smaller y-axis bounds
Definition: extension.cpp:15
bool fFloatIs32Bits
Definition: SkSLUtil.h:100
Definition: SkSize.h:52