/src/skia/src/gpu/graphite/geom/AnalyticBlurMask.cpp
Line | Count | Source (jump to first uncovered line) |
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/core/SkRRectPriv.h" |
15 | | #include "src/gpu/BlurUtils.h" |
16 | | #include "src/gpu/graphite/Caps.h" |
17 | | #include "src/gpu/graphite/RecorderPriv.h" |
18 | | #include "src/gpu/graphite/geom/Transform_graphite.h" |
19 | | #include "src/sksl/SkSLUtil.h" |
20 | | |
21 | | namespace skgpu::graphite { |
22 | | |
23 | | namespace { |
24 | | |
25 | | std::optional<Rect> outset_bounds(const SkMatrix& localToDevice, |
26 | | float devSigma, |
27 | 0 | const SkRect& srcRect) { |
28 | 0 | float outsetX = 3.0f * devSigma; |
29 | 0 | float outsetY = 3.0f * devSigma; |
30 | 0 | if (localToDevice.isScaleTranslate()) { |
31 | 0 | outsetX /= std::fabs(localToDevice.getScaleX()); |
32 | 0 | outsetY /= std::fabs(localToDevice.getScaleY()); |
33 | 0 | } else { |
34 | 0 | SkSize scale; |
35 | 0 | if (!localToDevice.decomposeScale(&scale, nullptr)) { |
36 | 0 | return std::nullopt; |
37 | 0 | } |
38 | 0 | outsetX /= scale.width(); |
39 | 0 | outsetY /= scale.height(); |
40 | 0 | } |
41 | 0 | return srcRect.makeOutset(outsetX, outsetY); |
42 | 0 | } |
43 | | |
44 | | } // anonymous namespace |
45 | | |
46 | | std::optional<AnalyticBlurMask> AnalyticBlurMask::Make(Recorder* recorder, |
47 | | const Transform& localToDeviceTransform, |
48 | | float deviceSigma, |
49 | 0 | const SkRRect& srcRRect) { |
50 | | // TODO: Implement SkMatrix functionality used below for Transform. |
51 | 0 | SkMatrix localToDevice = localToDeviceTransform; |
52 | |
|
53 | 0 | if (srcRRect.isRect() && localToDevice.preservesRightAngles()) { |
54 | 0 | return MakeRect(recorder, localToDevice, deviceSigma, srcRRect.rect()); |
55 | 0 | } |
56 | | |
57 | 0 | SkRRect devRRect; |
58 | 0 | const bool devRRectIsValid = srcRRect.transform(localToDevice, &devRRect); |
59 | 0 | if (devRRectIsValid && SkRRectPriv::IsCircle(devRRect)) { |
60 | 0 | return MakeCircle(recorder, localToDevice, deviceSigma, srcRRect.rect(), devRRect.rect()); |
61 | 0 | } |
62 | | |
63 | | // A local-space circle transformed by a rotation matrix will fail SkRRect::transform since it |
64 | | // only supports scale + translate matrices, but is still a valid circle that can be blurred. |
65 | 0 | if (SkRRectPriv::IsCircle(srcRRect) && localToDevice.isSimilarity()) { |
66 | 0 | const SkRect srcRect = srcRRect.rect(); |
67 | 0 | const SkPoint devCenter = localToDevice.mapPoint(srcRect.center()); |
68 | 0 | const float devRadius = localToDevice.mapVector(0.0f, srcRect.width() / 2.0f).length(); |
69 | 0 | const SkRect devRect = {devCenter.x() - devRadius, |
70 | 0 | devCenter.y() - devRadius, |
71 | 0 | devCenter.x() + devRadius, |
72 | 0 | devCenter.y() + devRadius}; |
73 | 0 | return MakeCircle(recorder, localToDevice, deviceSigma, srcRect, devRect); |
74 | 0 | } |
75 | | |
76 | 0 | if (devRRectIsValid && SkRRectPriv::IsSimpleCircular(devRRect) && |
77 | 0 | localToDevice.isScaleTranslate()) { |
78 | 0 | return MakeRRect(recorder, localToDevice, deviceSigma, srcRRect, devRRect); |
79 | 0 | } |
80 | | |
81 | 0 | return std::nullopt; |
82 | 0 | } |
83 | | |
84 | | std::optional<AnalyticBlurMask> AnalyticBlurMask::MakeRect(Recorder* recorder, |
85 | | const SkMatrix& localToDevice, |
86 | | float devSigma, |
87 | 0 | const SkRect& srcRect) { |
88 | 0 | SkASSERT(srcRect.isSorted()); |
89 | |
|
90 | 0 | SkRect devRect; |
91 | 0 | SkMatrix devToScaledShape; |
92 | 0 | if (localToDevice.rectStaysRect()) { |
93 | | // We can do everything in device space when the src rect projects to a rect in device |
94 | | // space. |
95 | 0 | SkAssertResult(localToDevice.mapRect(&devRect, srcRect)); |
96 | |
|
97 | 0 | } else { |
98 | | // The view matrix may scale, perhaps anisotropically. But we want to apply our device space |
99 | | // sigma to the delta of frag coord from the rect edges. Factor out the scaling to define a |
100 | | // space that is purely rotation / translation from device space (and scale from src space). |
101 | | // We'll meet in the middle: pre-scale the src rect to be in this space and then apply the |
102 | | // inverse of the rotation / translation portion to the frag coord. |
103 | 0 | SkMatrix m; |
104 | 0 | SkSize scale; |
105 | 0 | if (!localToDevice.decomposeScale(&scale, &m)) { |
106 | 0 | return std::nullopt; |
107 | 0 | } |
108 | 0 | if (!m.invert(&devToScaledShape)) { |
109 | 0 | return std::nullopt; |
110 | 0 | } |
111 | 0 | devRect = {srcRect.left() * scale.width(), |
112 | 0 | srcRect.top() * scale.height(), |
113 | 0 | srcRect.right() * scale.width(), |
114 | 0 | srcRect.bottom() * scale.height()}; |
115 | 0 | } |
116 | | |
117 | 0 | if (!recorder->priv().caps()->shaderCaps()->fFloatIs32Bits) { |
118 | | // We promote the math that gets us into the Gaussian space to full float when the rect |
119 | | // coords are large. If we don't have full float then fail. We could probably clip the rect |
120 | | // to an outset device bounds instead. |
121 | 0 | if (std::fabs(devRect.left()) > 16000.0f || std::fabs(devRect.top()) > 16000.0f || |
122 | 0 | std::fabs(devRect.right()) > 16000.0f || std::fabs(devRect.bottom()) > 16000.0f) { |
123 | 0 | return std::nullopt; |
124 | 0 | } |
125 | 0 | } |
126 | | |
127 | 0 | const float sixSigma = 6.0f * devSigma; |
128 | 0 | SkBitmap integralBitmap = skgpu::CreateIntegralTable(sixSigma); |
129 | 0 | if (integralBitmap.empty()) { |
130 | 0 | return std::nullopt; |
131 | 0 | } |
132 | | |
133 | 0 | sk_sp<TextureProxy> integral = RecorderPriv::CreateCachedProxy(recorder, integralBitmap, |
134 | 0 | "BlurredRectIntegralTable"); |
135 | 0 | if (!integral) { |
136 | 0 | return std::nullopt; |
137 | 0 | } |
138 | | |
139 | | // In the fast variant we think of the midpoint of the integral texture as aligning with the |
140 | | // closest rect edge both in x and y. To simplify texture coord calculation we inset the rect so |
141 | | // that the edge of the inset rect corresponds to t = 0 in the texture. It actually simplifies |
142 | | // things a bit in the !isFast case, too. |
143 | 0 | const float threeSigma = 3.0f * devSigma; |
144 | 0 | const Rect shapeData = Rect(devRect.left() + threeSigma, |
145 | 0 | devRect.top() + threeSigma, |
146 | 0 | devRect.right() - threeSigma, |
147 | 0 | devRect.bottom() - threeSigma); |
148 | | |
149 | | // In our fast variant we find the nearest horizontal and vertical edges and for each do a |
150 | | // lookup in the integral texture for each and multiply them. When the rect is less than 6*sigma |
151 | | // wide then things aren't so simple and we have to consider both the left and right edge of the |
152 | | // rectangle (and similar in y). |
153 | 0 | const bool isFast = shapeData.left() <= shapeData.right() && shapeData.top() <= shapeData.bot(); |
154 | |
|
155 | 0 | const float invSixSigma = 1.0f / sixSigma; |
156 | | |
157 | | // Determine how much to outset the draw bounds to ensure we hit pixels within 3*sigma. |
158 | 0 | std::optional<Rect> drawBounds = outset_bounds(localToDevice, devSigma, srcRect); |
159 | 0 | if (!drawBounds) { |
160 | 0 | return std::nullopt; |
161 | 0 | } |
162 | | |
163 | 0 | return AnalyticBlurMask(*drawBounds, |
164 | 0 | SkM44(devToScaledShape), |
165 | 0 | ShapeType::kRect, |
166 | 0 | shapeData, |
167 | 0 | {static_cast<float>(isFast), invSixSigma}, |
168 | 0 | integral); |
169 | 0 | } Unexecuted instantiation: skgpu::graphite::AnalyticBlurMask::MakeRect(skgpu::graphite::Recorder*, SkMatrix const&, float, SkRect const&) Unexecuted instantiation: skgpu::graphite::AnalyticBlurMask::MakeRect(skgpu::graphite::Recorder*, SkMatrix const&, float, SkRect const&) |
170 | | |
171 | | std::optional<AnalyticBlurMask> AnalyticBlurMask::MakeCircle(Recorder* recorder, |
172 | | const SkMatrix& localToDevice, |
173 | | float devSigma, |
174 | | const SkRect& srcRect, |
175 | 0 | const SkRect& devRect) { |
176 | 0 | const float radius = devRect.width() / 2.0f; |
177 | 0 | if (!SkIsFinite(radius) || radius < SK_ScalarNearlyZero) { |
178 | 0 | return std::nullopt; |
179 | 0 | } |
180 | | |
181 | | // When sigma is really small this becomes a equivalent to convolving a Gaussian with a |
182 | | // half-plane. Similarly, in the extreme high ratio cases circle becomes a point WRT to the |
183 | | // Guassian and the profile texture is a just a Gaussian evaluation. However, we haven't yet |
184 | | // implemented this latter optimization. |
185 | 0 | constexpr float kHalfPlaneThreshold = 0.1f; |
186 | 0 | const float sigmaToRadiusRatio = std::min(devSigma / radius, 8.0f); |
187 | 0 | const bool useHalfPlaneApprox = sigmaToRadiusRatio <= kHalfPlaneThreshold; |
188 | |
|
189 | 0 | float solidRadius; |
190 | 0 | float textureRadius; |
191 | 0 | if (useHalfPlaneApprox) { |
192 | 0 | solidRadius = radius - 3.0f * devSigma; |
193 | 0 | textureRadius = 6.0f * devSigma; |
194 | 0 | } else { |
195 | 0 | devSigma = radius * sigmaToRadiusRatio; |
196 | 0 | solidRadius = 0.0f; |
197 | 0 | textureRadius = radius + 3.0f * devSigma; |
198 | 0 | } |
199 | |
|
200 | 0 | constexpr int kProfileTextureWidth = 512; |
201 | |
|
202 | 0 | SkBitmap profileBitmap; |
203 | 0 | if (useHalfPlaneApprox) { |
204 | 0 | profileBitmap = skgpu::CreateHalfPlaneProfile(kProfileTextureWidth); |
205 | 0 | } else { |
206 | | // Rescale params to the size of the texture we're creating. |
207 | 0 | const float scale = kProfileTextureWidth / textureRadius; |
208 | 0 | profileBitmap = |
209 | 0 | skgpu::CreateCircleProfile(devSigma * scale, radius * scale, kProfileTextureWidth); |
210 | 0 | } |
211 | 0 | if (profileBitmap.empty()) { |
212 | 0 | return std::nullopt; |
213 | 0 | } |
214 | | |
215 | 0 | sk_sp<TextureProxy> profile = RecorderPriv::CreateCachedProxy(recorder, profileBitmap, |
216 | 0 | "BlurredCircleIntegralTable"); |
217 | 0 | if (!profile) { |
218 | 0 | return std::nullopt; |
219 | 0 | } |
220 | | |
221 | | // In the shader we calculate an index into the blur profile |
222 | | // "i = (length(fragCoords - circleCenter) - solidRadius + 0.5) / textureRadius" as |
223 | | // "i = length((fragCoords - circleCenter) / textureRadius) - |
224 | | // (solidRadius - 0.5) / textureRadius" |
225 | | // to avoid passing large values to length() that would overflow. We precalculate |
226 | | // "1 / textureRadius" and "(solidRadius - 0.5) / textureRadius" here. |
227 | 0 | const Rect shapeData = Rect(devRect.centerX(), |
228 | 0 | devRect.centerY(), |
229 | 0 | 1.0f / textureRadius, |
230 | 0 | (solidRadius - 0.5f) / textureRadius); |
231 | | |
232 | | // Determine how much to outset the draw bounds to ensure we hit pixels within 3*sigma. |
233 | 0 | std::optional<Rect> drawBounds = outset_bounds(localToDevice, devSigma, srcRect); |
234 | 0 | if (!drawBounds) { |
235 | 0 | return std::nullopt; |
236 | 0 | } |
237 | | |
238 | 0 | constexpr float kUnusedBlurData = 0.0f; |
239 | 0 | return AnalyticBlurMask(*drawBounds, |
240 | 0 | SkM44(), |
241 | 0 | ShapeType::kCircle, |
242 | 0 | shapeData, |
243 | 0 | {kUnusedBlurData, kUnusedBlurData}, |
244 | 0 | profile); |
245 | 0 | } |
246 | | |
247 | | std::optional<AnalyticBlurMask> AnalyticBlurMask::MakeRRect(Recorder* recorder, |
248 | | const SkMatrix& localToDevice, |
249 | | float devSigma, |
250 | | const SkRRect& srcRRect, |
251 | 0 | const SkRRect& devRRect) { |
252 | 0 | const int devBlurRadius = 3 * SkScalarCeilToInt(devSigma - 1.0f / 6.0f); |
253 | |
|
254 | 0 | const SkVector& devRadiiUL = devRRect.radii(SkRRect::kUpperLeft_Corner); |
255 | 0 | const SkVector& devRadiiUR = devRRect.radii(SkRRect::kUpperRight_Corner); |
256 | 0 | const SkVector& devRadiiLR = devRRect.radii(SkRRect::kLowerRight_Corner); |
257 | 0 | const SkVector& devRadiiLL = devRRect.radii(SkRRect::kLowerLeft_Corner); |
258 | |
|
259 | 0 | const int devLeft = SkScalarCeilToInt(std::max<float>(devRadiiUL.fX, devRadiiLL.fX)); |
260 | 0 | const int devTop = SkScalarCeilToInt(std::max<float>(devRadiiUL.fY, devRadiiUR.fY)); |
261 | 0 | const int devRight = SkScalarCeilToInt(std::max<float>(devRadiiUR.fX, devRadiiLR.fX)); |
262 | 0 | const int devBot = SkScalarCeilToInt(std::max<float>(devRadiiLL.fY, devRadiiLR.fY)); |
263 | | |
264 | | // This is a conservative check for nine-patchability. |
265 | 0 | const SkRect& devOrig = devRRect.getBounds(); |
266 | 0 | if (devOrig.fLeft + devLeft + devBlurRadius >= devOrig.fRight - devRight - devBlurRadius || |
267 | 0 | devOrig.fTop + devTop + devBlurRadius >= devOrig.fBottom - devBot - devBlurRadius) { |
268 | 0 | return std::nullopt; |
269 | 0 | } |
270 | | |
271 | 0 | const int newRRWidth = 2 * devBlurRadius + devLeft + devRight + 1; |
272 | 0 | const int newRRHeight = 2 * devBlurRadius + devTop + devBot + 1; |
273 | |
|
274 | 0 | const SkRect newRect = SkRect::MakeXYWH(SkIntToScalar(devBlurRadius), |
275 | 0 | SkIntToScalar(devBlurRadius), |
276 | 0 | SkIntToScalar(newRRWidth), |
277 | 0 | SkIntToScalar(newRRHeight)); |
278 | 0 | SkVector newRadii[4]; |
279 | 0 | newRadii[0] = {SkScalarCeilToScalar(devRadiiUL.fX), SkScalarCeilToScalar(devRadiiUL.fY)}; |
280 | 0 | newRadii[1] = {SkScalarCeilToScalar(devRadiiUR.fX), SkScalarCeilToScalar(devRadiiUR.fY)}; |
281 | 0 | newRadii[2] = {SkScalarCeilToScalar(devRadiiLR.fX), SkScalarCeilToScalar(devRadiiLR.fY)}; |
282 | 0 | newRadii[3] = {SkScalarCeilToScalar(devRadiiLL.fX), SkScalarCeilToScalar(devRadiiLL.fY)}; |
283 | |
|
284 | 0 | SkRRect rrectToDraw; |
285 | 0 | rrectToDraw.setRectRadii(newRect, newRadii); |
286 | 0 | const SkISize dimensions = |
287 | 0 | SkISize::Make(newRRWidth + 2 * devBlurRadius, newRRHeight + 2 * devBlurRadius); |
288 | 0 | SkBitmap ninePatchBitmap = skgpu::CreateRRectBlurMask(rrectToDraw, dimensions, devSigma); |
289 | 0 | if (ninePatchBitmap.empty()) { |
290 | 0 | return std::nullopt; |
291 | 0 | } |
292 | | |
293 | 0 | sk_sp<TextureProxy> ninePatch = RecorderPriv::CreateCachedProxy(recorder, ninePatchBitmap, |
294 | 0 | "BlurredRRectNinePatch"); |
295 | 0 | if (!ninePatch) { |
296 | 0 | return std::nullopt; |
297 | 0 | } |
298 | | |
299 | 0 | const float blurRadius = 3.0f * SkScalarCeilToScalar(devSigma - 1.0f / 6.0f); |
300 | 0 | const float edgeSize = 2.0f * blurRadius + SkRRectPriv::GetSimpleRadii(devRRect).fX + 0.5f; |
301 | 0 | const Rect shapeData = devRRect.rect().makeOutset(blurRadius, blurRadius); |
302 | | |
303 | | // Determine how much to outset the draw bounds to ensure we hit pixels within 3*sigma. |
304 | 0 | std::optional<Rect> drawBounds = outset_bounds(localToDevice, devSigma, srcRRect.rect()); |
305 | 0 | if (!drawBounds) { |
306 | 0 | return std::nullopt; |
307 | 0 | } |
308 | | |
309 | 0 | constexpr float kUnusedBlurData = 0.0f; |
310 | 0 | return AnalyticBlurMask(*drawBounds, |
311 | 0 | SkM44(), |
312 | 0 | ShapeType::kRRect, |
313 | 0 | shapeData, |
314 | 0 | {edgeSize, kUnusedBlurData}, |
315 | 0 | ninePatch); |
316 | 0 | } |
317 | | |
318 | | } // namespace skgpu::graphite |