1 /*
2 * Copyright 2013 Google Inc.
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/core/SkGpuBlurUtils.h"
9
10 #include "include/core/SkBitmap.h"
11 #include "include/core/SkRect.h"
12
13 #if SK_SUPPORT_GPU
14 #include "include/gpu/GrRecordingContext.h"
15 #include "src/gpu/GrCaps.h"
16 #include "src/gpu/GrRecordingContextPriv.h"
17 #include "src/gpu/SkGr.h"
18 #include "src/gpu/effects/GrGaussianConvolutionFragmentProcessor.h"
19 #include "src/gpu/effects/GrMatrixConvolutionEffect.h"
20
21 using Direction = GrGaussianConvolutionFragmentProcessor::Direction;
22
fill_in_2D_gaussian_kernel(float * kernel,int width,int height,SkScalar sigmaX,SkScalar sigmaY)23 static void fill_in_2D_gaussian_kernel(float* kernel, int width, int height,
24 SkScalar sigmaX, SkScalar sigmaY) {
25 const float twoSigmaSqrdX = 2.0f * SkScalarToFloat(SkScalarSquare(sigmaX));
26 const float twoSigmaSqrdY = 2.0f * SkScalarToFloat(SkScalarSquare(sigmaY));
27
28 // SkGpuBlurUtils::GaussianBlur() should have detected the cases where a 2D blur
29 // degenerates to a 1D on X or Y, or to the identity.
30 SkASSERT(!SkGpuBlurUtils::IsEffectivelyZeroSigma(sigmaX) &&
31 !SkGpuBlurUtils::IsEffectivelyZeroSigma(sigmaY));
32 SkASSERT(!SkScalarNearlyZero(twoSigmaSqrdX) && !SkScalarNearlyZero(twoSigmaSqrdY));
33
34 const float sigmaXDenom = 1.0f / twoSigmaSqrdX;
35 const float sigmaYDenom = 1.0f / twoSigmaSqrdY;
36 const int xRadius = width / 2;
37 const int yRadius = height / 2;
38
39 float sum = 0.0f;
40 for (int x = 0; x < width; x++) {
41 float xTerm = static_cast<float>(x - xRadius);
42 xTerm = xTerm * xTerm * sigmaXDenom;
43 for (int y = 0; y < height; y++) {
44 float yTerm = static_cast<float>(y - yRadius);
45 float xyTerm = sk_float_exp(-(xTerm + yTerm * yTerm * sigmaYDenom));
46 // Note that the constant term (1/(sqrt(2*pi*sigma^2)) of the Gaussian
47 // is dropped here, since we renormalize the kernel below.
48 kernel[y * width + x] = xyTerm;
49 sum += xyTerm;
50 }
51 }
52 // Normalize the kernel
53 float scale = 1.0f / sum;
54 for (int i = 0; i < width * height; ++i) {
55 kernel[i] *= scale;
56 }
57 }
58
59 /**
60 * Draws 'dstRect' into 'surfaceFillContext' evaluating a 1D Gaussian over 'srcView'. The src rect
61 * is 'dstRect' offset by 'dstToSrcOffset'. 'mode' and 'bounds' are applied to the src coords.
62 */
convolve_gaussian_1d(GrSurfaceFillContext * sfc,GrSurfaceProxyView srcView,const SkIRect srcSubset,SkIVector dstToSrcOffset,const SkIRect & dstRect,SkAlphaType srcAlphaType,Direction direction,int radius,float sigma,SkTileMode mode)63 static void convolve_gaussian_1d(GrSurfaceFillContext* sfc,
64 GrSurfaceProxyView srcView,
65 const SkIRect srcSubset,
66 SkIVector dstToSrcOffset,
67 const SkIRect& dstRect,
68 SkAlphaType srcAlphaType,
69 Direction direction,
70 int radius,
71 float sigma,
72 SkTileMode mode) {
73 SkASSERT(radius && !SkGpuBlurUtils::IsEffectivelyZeroSigma(sigma));
74 auto wm = SkTileModeToWrapMode(mode);
75 auto srcRect = dstRect.makeOffset(dstToSrcOffset);
76 // NOTE: This could just be GrMatrixConvolutionEffect with one of the dimensions set to 1
77 // and the appropriate kernel already computed, but there's value in keeping the shader simpler.
78 // TODO(michaelludwig): Is this true? If not, is the shader key simplicity worth it two have
79 // two convolution effects?
80 std::unique_ptr<GrFragmentProcessor> conv =
81 GrGaussianConvolutionFragmentProcessor::Make(std::move(srcView),
82 srcAlphaType,
83 direction,
84 radius,
85 sigma,
86 wm,
87 srcSubset,
88 &srcRect,
89 *sfc->caps());
90 sfc->fillRectToRectWithFP(srcRect, dstRect, std::move(conv));
91 }
92
convolve_gaussian_2d(GrRecordingContext * context,GrSurfaceProxyView srcView,GrColorType srcColorType,const SkIRect & srcBounds,const SkIRect & dstBounds,int radiusX,int radiusY,SkScalar sigmaX,SkScalar sigmaY,SkTileMode mode,sk_sp<SkColorSpace> finalCS,SkBackingFit dstFit)93 static std::unique_ptr<GrSurfaceDrawContext> convolve_gaussian_2d(GrRecordingContext* context,
94 GrSurfaceProxyView srcView,
95 GrColorType srcColorType,
96 const SkIRect& srcBounds,
97 const SkIRect& dstBounds,
98 int radiusX,
99 int radiusY,
100 SkScalar sigmaX,
101 SkScalar sigmaY,
102 SkTileMode mode,
103 sk_sp<SkColorSpace> finalCS,
104 SkBackingFit dstFit) {
105 SkASSERT(radiusX && radiusY);
106 SkASSERT(!SkGpuBlurUtils::IsEffectivelyZeroSigma(sigmaX) &&
107 !SkGpuBlurUtils::IsEffectivelyZeroSigma(sigmaY));
108 // Create the sdc with default SkSurfaceProps. Gaussian blurs will soon use a
109 // GrSurfaceFillContext, at which point the SkSurfaceProps won't exist anymore.
110 auto surfaceDrawContext = GrSurfaceDrawContext::Make(
111 context, srcColorType, std::move(finalCS), dstFit, dstBounds.size(), SkSurfaceProps(),
112 1, GrMipmapped::kNo, srcView.proxy()->isProtected(), srcView.origin());
113 if (!surfaceDrawContext) {
114 return nullptr;
115 }
116
117 SkISize size = SkISize::Make(SkGpuBlurUtils::KernelWidth(radiusX),
118 SkGpuBlurUtils::KernelWidth(radiusY));
119 SkIPoint kernelOffset = SkIPoint::Make(radiusX, radiusY);
120 GrPaint paint;
121 auto wm = SkTileModeToWrapMode(mode);
122
123 // GaussianBlur() should have downsampled the request until we can handle the 2D blur with
124 // just a uniform array.
125 SkASSERT(size.area() <= GrMatrixConvolutionEffect::kMaxUniformSize);
126 float kernel[GrMatrixConvolutionEffect::kMaxUniformSize];
127 fill_in_2D_gaussian_kernel(kernel, size.width(), size.height(), sigmaX, sigmaY);
128 auto conv = GrMatrixConvolutionEffect::Make(context, std::move(srcView), srcBounds,
129 size, kernel, 1.0f, 0.0f, kernelOffset, wm, true,
130 *surfaceDrawContext->caps());
131
132 paint.setColorFragmentProcessor(std::move(conv));
133 paint.setPorterDuffXPFactory(SkBlendMode::kSrc);
134
135 // 'dstBounds' is actually in 'srcView' proxy space. It represents the blurred area from src
136 // space that we want to capture in the new RTC at {0, 0}. Hence, we use its size as the rect to
137 // draw and it directly as the local rect.
138 surfaceDrawContext->fillRectToRect(nullptr, std::move(paint), GrAA::kNo, SkMatrix::I(),
139 SkRect::Make(dstBounds.size()), SkRect::Make(dstBounds));
140
141 return surfaceDrawContext;
142 }
143
convolve_gaussian(GrRecordingContext * context,GrSurfaceProxyView srcView,GrColorType srcColorType,SkAlphaType srcAlphaType,SkIRect srcBounds,SkIRect dstBounds,Direction direction,int radius,float sigma,SkTileMode mode,sk_sp<SkColorSpace> finalCS,SkBackingFit fit)144 static std::unique_ptr<GrSurfaceDrawContext> convolve_gaussian(GrRecordingContext* context,
145 GrSurfaceProxyView srcView,
146 GrColorType srcColorType,
147 SkAlphaType srcAlphaType,
148 SkIRect srcBounds,
149 SkIRect dstBounds,
150 Direction direction,
151 int radius,
152 float sigma,
153 SkTileMode mode,
154 sk_sp<SkColorSpace> finalCS,
155 SkBackingFit fit) {
156 using namespace SkGpuBlurUtils;
157 SkASSERT(radius > 0 && !SkGpuBlurUtils::IsEffectivelyZeroSigma(sigma));
158 // Logically we're creating an infinite blur of 'srcBounds' of 'srcView' with 'mode' tiling
159 // and then capturing the 'dstBounds' portion in a new RTC where the top left of 'dstBounds' is
160 // at {0, 0} in the new RTC.
161 //
162 // Create the sdc with default SkSurfaceProps. Gaussian blurs will soon use a
163 // GrSurfaceFillContext, at which point the SkSurfaceProps won't exist anymore.
164 auto dstRenderTargetContext = GrSurfaceDrawContext::Make(
165 context, srcColorType, std::move(finalCS), fit, dstBounds.size(), SkSurfaceProps(), 1,
166 GrMipmapped::kNo, srcView.proxy()->isProtected(), srcView.origin());
167 if (!dstRenderTargetContext) {
168 return nullptr;
169 }
170 // This represents the translation from 'dstRenderTargetContext' coords to 'srcView' coords.
171 auto rtcToSrcOffset = dstBounds.topLeft();
172
173 auto srcBackingBounds = SkIRect::MakeSize(srcView.proxy()->backingStoreDimensions());
174 // We've implemented splitting the dst bounds up into areas that do and do not need to
175 // use shader based tiling but only for some modes...
176 bool canSplit = mode == SkTileMode::kDecal || mode == SkTileMode::kClamp;
177 // ...but it's not worth doing the splitting if we'll get HW tiling instead of shader tiling.
178 bool canHWTile =
179 srcBounds.contains(srcBackingBounds) &&
180 !context->priv().caps()->reducedShaderMode() && // this mode always uses shader tiling
181 !(mode == SkTileMode::kDecal && !context->priv().caps()->clampToBorderSupport());
182 if (!canSplit || canHWTile) {
183 auto dstRect = SkIRect::MakeSize(dstBounds.size());
184 convolve_gaussian_1d(dstRenderTargetContext.get(), std::move(srcView), srcBounds,
185 rtcToSrcOffset, dstRect, srcAlphaType, direction, radius, sigma, mode);
186 return dstRenderTargetContext;
187 }
188
189 // 'left' and 'right' are the sub rects of 'srcBounds' where 'mode' must be enforced.
190 // 'mid' is the area where we can ignore the mode because the kernel does not reach to the
191 // edge of 'srcBounds'.
192 SkIRect mid, left, right;
193 // 'top' and 'bottom' are areas of 'dstBounds' that are entirely above/below 'srcBounds'.
194 // These are areas that we can simply clear in the dst in kDecal mode. If 'srcBounds'
195 // straddles the top edge of 'dstBounds' then 'top' will be inverted and we will skip
196 // processing for the rect. Similar for 'bottom'. The positional/directional labels above refer
197 // to the Direction::kX case and one should think of these as 'left' and 'right' for
198 // Direction::kY.
199 SkIRect top, bottom;
200 if (Direction::kX == direction) {
201 top = {dstBounds.left(), dstBounds.top() , dstBounds.right(), srcBounds.top() };
202 bottom = {dstBounds.left(), srcBounds.bottom(), dstBounds.right(), dstBounds.bottom()};
203
204 // Inset for sub-rect of 'srcBounds' where the x-dir kernel doesn't reach the edges, clipped
205 // vertically to dstBounds.
206 int midA = std::max(srcBounds.top() , dstBounds.top() );
207 int midB = std::min(srcBounds.bottom(), dstBounds.bottom());
208 mid = {srcBounds.left() + radius, midA, srcBounds.right() - radius, midB};
209 if (mid.isEmpty()) {
210 // There is no middle where the bounds can be ignored. Make the left span the whole
211 // width of dst and we will not draw mid or right.
212 left = {dstBounds.left(), mid.top(), dstBounds.right(), mid.bottom()};
213 } else {
214 left = {dstBounds.left(), mid.top(), mid.left() , mid.bottom()};
215 right = {mid.right(), mid.top(), dstBounds.right(), mid.bottom()};
216 }
217 } else {
218 // This is the same as the x direction code if you turn your head 90 degrees CCW. Swap x and
219 // y and swap top/bottom with left/right.
220 top = {dstBounds.left(), dstBounds.top(), srcBounds.left() , dstBounds.bottom()};
221 bottom = {srcBounds.right(), dstBounds.top(), dstBounds.right(), dstBounds.bottom()};
222
223 int midA = std::max(srcBounds.left() , dstBounds.left() );
224 int midB = std::min(srcBounds.right(), dstBounds.right());
225 mid = {midA, srcBounds.top() + radius, midB, srcBounds.bottom() - radius};
226
227 if (mid.isEmpty()) {
228 left = {mid.left(), dstBounds.top(), mid.right(), dstBounds.bottom()};
229 } else {
230 left = {mid.left(), dstBounds.top(), mid.right(), mid.top() };
231 right = {mid.left(), mid.bottom() , mid.right(), dstBounds.bottom()};
232 }
233 }
234
235 auto convolve = [&](SkIRect rect) {
236 // Transform rect into the render target's coord system.
237 rect.offset(-rtcToSrcOffset);
238 convolve_gaussian_1d(dstRenderTargetContext.get(), srcView, srcBounds, rtcToSrcOffset, rect,
239 srcAlphaType, direction, radius, sigma, mode);
240 };
241 auto clear = [&](SkIRect rect) {
242 // Transform rect into the render target's coord system.
243 rect.offset(-rtcToSrcOffset);
244 dstRenderTargetContext->clearAtLeast(rect, SK_PMColor4fTRANSPARENT);
245 };
246
247 // Doing mid separately will cause two draws to occur (left and right batch together). At
248 // small sizes of mid it is worse to issue more draws than to just execute the slightly
249 // more complicated shader that implements the tile mode across mid. This threshold is
250 // very arbitrary right now. It is believed that a 21x44 mid on a Moto G4 is a significant
251 // regression compared to doing one draw but it has not been locally evaluated or tuned.
252 // The optimal cutoff is likely to vary by GPU.
253 if (!mid.isEmpty() && mid.width()*mid.height() < 256*256) {
254 left.join(mid);
255 left.join(right);
256 mid = SkIRect::MakeEmpty();
257 right = SkIRect::MakeEmpty();
258 // It's unknown whether for kDecal it'd be better to expand the draw rather than a draw and
259 // up to two clears.
260 if (mode == SkTileMode::kClamp) {
261 left.join(top);
262 left.join(bottom);
263 top = SkIRect::MakeEmpty();
264 bottom = SkIRect::MakeEmpty();
265 }
266 }
267
268 if (!top.isEmpty()) {
269 if (mode == SkTileMode::kDecal) {
270 clear(top);
271 } else {
272 convolve(top);
273 }
274 }
275
276 if (!bottom.isEmpty()) {
277 if (mode == SkTileMode::kDecal) {
278 clear(bottom);
279 } else {
280 convolve(bottom);
281 }
282 }
283
284 if (mid.isEmpty()) {
285 convolve(left);
286 } else {
287 convolve(left);
288 convolve(right);
289 convolve(mid);
290 }
291 return dstRenderTargetContext;
292 }
293
294 // Expand the contents of 'srcRenderTargetContext' to fit in 'dstII'. At this point, we are
295 // expanding an intermediate image, so there's no need to account for a proxy offset from the
296 // original input.
reexpand(GrRecordingContext * context,std::unique_ptr<GrSurfaceContext> src,const SkRect & srcBounds,SkISize dstSize,sk_sp<SkColorSpace> colorSpace,SkBackingFit fit)297 static std::unique_ptr<GrSurfaceDrawContext> reexpand(GrRecordingContext* context,
298 std::unique_ptr<GrSurfaceContext> src,
299 const SkRect& srcBounds,
300 SkISize dstSize,
301 sk_sp<SkColorSpace> colorSpace,
302 SkBackingFit fit) {
303 GrSurfaceProxyView srcView = src->readSurfaceView();
304 if (!srcView.asTextureProxy()) {
305 return nullptr;
306 }
307
308 GrColorType srcColorType = src->colorInfo().colorType();
309 SkAlphaType srcAlphaType = src->colorInfo().alphaType();
310
311 src.reset(); // no longer needed
312
313 // Create the sdc with default SkSurfaceProps. Gaussian blurs will soon use a
314 // GrSurfaceFillContext, at which point the SkSurfaceProps won't exist anymore.
315 auto dstRenderTargetContext = GrSurfaceDrawContext::Make(
316 context, srcColorType, std::move(colorSpace), fit, dstSize, SkSurfaceProps(), 1,
317 GrMipmapped::kNo, srcView.proxy()->isProtected(), srcView.origin());
318 if (!dstRenderTargetContext) {
319 return nullptr;
320 }
321
322 GrPaint paint;
323 auto fp = GrTextureEffect::MakeSubset(std::move(srcView), srcAlphaType, SkMatrix::I(),
324 GrSamplerState::Filter::kLinear, srcBounds, srcBounds,
325 *context->priv().caps());
326 paint.setColorFragmentProcessor(std::move(fp));
327 paint.setPorterDuffXPFactory(SkBlendMode::kSrc);
328
329 dstRenderTargetContext->fillRectToRect(nullptr, std::move(paint), GrAA::kNo, SkMatrix::I(),
330 SkRect::Make(dstSize), srcBounds);
331
332 return dstRenderTargetContext;
333 }
334
two_pass_gaussian(GrRecordingContext * context,GrSurfaceProxyView srcView,GrColorType srcColorType,SkAlphaType srcAlphaType,sk_sp<SkColorSpace> colorSpace,SkIRect srcBounds,SkIRect dstBounds,float sigmaX,float sigmaY,int radiusX,int radiusY,SkTileMode mode,SkBackingFit fit)335 static std::unique_ptr<GrSurfaceDrawContext> two_pass_gaussian(GrRecordingContext* context,
336 GrSurfaceProxyView srcView,
337 GrColorType srcColorType,
338 SkAlphaType srcAlphaType,
339 sk_sp<SkColorSpace> colorSpace,
340 SkIRect srcBounds,
341 SkIRect dstBounds,
342 float sigmaX,
343 float sigmaY,
344 int radiusX,
345 int radiusY,
346 SkTileMode mode,
347 SkBackingFit fit) {
348 SkASSERT(radiusX || radiusY);
349 std::unique_ptr<GrSurfaceDrawContext> dstRenderTargetContext;
350 if (radiusX > 0) {
351 SkBackingFit xFit = radiusY > 0 ? SkBackingFit::kApprox : fit;
352 // Expand the dstBounds vertically to produce necessary content for the y-pass. Then we will
353 // clip these in a tile-mode dependent way to ensure the tile-mode gets implemented
354 // correctly. However, if we're not going to do a y-pass then we must use the original
355 // dstBounds without clipping to produce the correct output size.
356 SkIRect xPassDstBounds = dstBounds;
357 if (radiusY) {
358 xPassDstBounds.outset(0, radiusY);
359 if (mode == SkTileMode::kRepeat || mode == SkTileMode::kMirror) {
360 int srcH = srcBounds.height();
361 int srcTop = srcBounds.top();
362 if (mode == SkTileMode::kMirror) {
363 srcTop -= srcH;
364 srcH *= 2;
365 }
366
367 float floatH = srcH;
368 // First row above the dst rect where we should restart the tile mode.
369 int n = sk_float_floor2int_no_saturate((xPassDstBounds.top() - srcTop)/floatH);
370 int topClip = srcTop + n*srcH;
371
372 // First row above below the dst rect where we should restart the tile mode.
373 n = sk_float_ceil2int_no_saturate(
374 (xPassDstBounds.bottom() - srcBounds.bottom())/floatH);
375 int bottomClip = srcBounds.bottom() + n*srcH;
376
377 xPassDstBounds.fTop = std::max(xPassDstBounds.top(), topClip);
378 xPassDstBounds.fBottom = std::min(xPassDstBounds.bottom(), bottomClip);
379 } else {
380 if (xPassDstBounds.fBottom <= srcBounds.top()) {
381 if (mode == SkTileMode::kDecal) {
382 return nullptr;
383 }
384 xPassDstBounds.fTop = srcBounds.top();
385 xPassDstBounds.fBottom = xPassDstBounds.fTop + 1;
386 } else if (xPassDstBounds.fTop >= srcBounds.bottom()) {
387 if (mode == SkTileMode::kDecal) {
388 return nullptr;
389 }
390 xPassDstBounds.fBottom = srcBounds.bottom();
391 xPassDstBounds.fTop = xPassDstBounds.fBottom - 1;
392 } else {
393 xPassDstBounds.fTop = std::max(xPassDstBounds.fTop, srcBounds.top());
394 xPassDstBounds.fBottom = std::min(xPassDstBounds.fBottom, srcBounds.bottom());
395 }
396 int leftSrcEdge = srcBounds.fLeft - radiusX ;
397 int rightSrcEdge = srcBounds.fRight + radiusX;
398 if (mode == SkTileMode::kClamp) {
399 // In clamp the column just outside the src bounds has the same value as the
400 // column just inside, unlike decal.
401 leftSrcEdge += 1;
402 rightSrcEdge -= 1;
403 }
404 if (xPassDstBounds.fRight <= leftSrcEdge) {
405 if (mode == SkTileMode::kDecal) {
406 return nullptr;
407 }
408 xPassDstBounds.fLeft = xPassDstBounds.fRight - 1;
409 } else {
410 xPassDstBounds.fLeft = std::max(xPassDstBounds.fLeft, leftSrcEdge);
411 }
412 if (xPassDstBounds.fLeft >= rightSrcEdge) {
413 if (mode == SkTileMode::kDecal) {
414 return nullptr;
415 }
416 xPassDstBounds.fRight = xPassDstBounds.fLeft + 1;
417 } else {
418 xPassDstBounds.fRight = std::min(xPassDstBounds.fRight, rightSrcEdge);
419 }
420 }
421 }
422 dstRenderTargetContext = convolve_gaussian(
423 context, std::move(srcView), srcColorType, srcAlphaType, srcBounds, xPassDstBounds,
424 Direction::kX, radiusX, sigmaX, mode, colorSpace, xFit);
425 if (!dstRenderTargetContext) {
426 return nullptr;
427 }
428 srcView = dstRenderTargetContext->readSurfaceView();
429 SkIVector newDstBoundsOffset = dstBounds.topLeft() - xPassDstBounds.topLeft();
430 dstBounds = SkIRect::MakeSize(dstBounds.size()).makeOffset(newDstBoundsOffset);
431 srcBounds = SkIRect::MakeSize(xPassDstBounds.size());
432 }
433
434 if (!radiusY) {
435 return dstRenderTargetContext;
436 }
437
438 return convolve_gaussian(context, std::move(srcView), srcColorType, srcAlphaType, srcBounds,
439 dstBounds, Direction::kY, radiusY, sigmaY, mode, colorSpace, fit);
440 }
441
442 namespace SkGpuBlurUtils {
443
GaussianBlur(GrRecordingContext * context,GrSurfaceProxyView srcView,GrColorType srcColorType,SkAlphaType srcAlphaType,sk_sp<SkColorSpace> colorSpace,SkIRect dstBounds,SkIRect srcBounds,float sigmaX,float sigmaY,SkTileMode mode,SkBackingFit fit)444 std::unique_ptr<GrSurfaceDrawContext> GaussianBlur(GrRecordingContext* context,
445 GrSurfaceProxyView srcView,
446 GrColorType srcColorType,
447 SkAlphaType srcAlphaType,
448 sk_sp<SkColorSpace> colorSpace,
449 SkIRect dstBounds,
450 SkIRect srcBounds,
451 float sigmaX,
452 float sigmaY,
453 SkTileMode mode,
454 SkBackingFit fit) {
455 SkASSERT(context);
456 TRACE_EVENT2("skia.gpu", "GaussianBlur", "sigmaX", sigmaX, "sigmaY", sigmaY);
457
458 if (!srcView.asTextureProxy()) {
459 return nullptr;
460 }
461
462 int maxRenderTargetSize = context->priv().caps()->maxRenderTargetSize();
463 if (dstBounds.width() > maxRenderTargetSize || dstBounds.height() > maxRenderTargetSize) {
464 return nullptr;
465 }
466
467 int radiusX = SigmaRadius(sigmaX);
468 int radiusY = SigmaRadius(sigmaY);
469 // Attempt to reduce the srcBounds in order to detect that we can set the sigmas to zero or
470 // to reduce the amount of work to rescale the source if sigmas are large. TODO: Could consider
471 // how to minimize the required source bounds for repeat/mirror modes.
472 if (mode == SkTileMode::kClamp || mode == SkTileMode::kDecal) {
473 SkIRect reach = dstBounds.makeOutset(radiusX, radiusY);
474 SkIRect intersection;
475 if (!intersection.intersect(reach, srcBounds)) {
476 if (mode == SkTileMode::kDecal) {
477 return nullptr;
478 } else {
479 if (reach.fLeft >= srcBounds.fRight) {
480 srcBounds.fLeft = srcBounds.fRight - 1;
481 } else if (reach.fRight <= srcBounds.fLeft) {
482 srcBounds.fRight = srcBounds.fLeft + 1;
483 }
484 if (reach.fTop >= srcBounds.fBottom) {
485 srcBounds.fTop = srcBounds.fBottom - 1;
486 } else if (reach.fBottom <= srcBounds.fTop) {
487 srcBounds.fBottom = srcBounds.fTop + 1;
488 }
489 }
490 } else {
491 srcBounds = intersection;
492 }
493 }
494
495 if (mode != SkTileMode::kDecal) {
496 // All non-decal tile modes are equivalent for one pixel width/height src and amount to a
497 // single color value repeated at each column/row. Applying the normalized kernel to that
498 // column/row yields that same color. So no blurring is necessary.
499 if (srcBounds.width() == 1) {
500 sigmaX = 0.f;
501 radiusX = 0;
502 }
503 if (srcBounds.height() == 1) {
504 sigmaY = 0.f;
505 radiusY = 0;
506 }
507 }
508
509 // If we determined that there is no blurring necessary in either direction then just do a
510 // a draw that applies the tile mode.
511 if (!radiusX && !radiusY) {
512 // Create the sdc with default SkSurfaceProps. Gaussian blurs will soon use a
513 // GrSurfaceFillContext, at which point the SkSurfaceProps won't exist anymore.
514 auto result = GrSurfaceDrawContext::Make(context, srcColorType, std::move(colorSpace), fit,
515 dstBounds.size(), SkSurfaceProps(), 1,
516 GrMipmapped::kNo,
517 srcView.proxy()->isProtected(), srcView.origin());
518 if (!result) {
519 return nullptr;
520 }
521 GrSamplerState sampler(SkTileModeToWrapMode(mode), GrSamplerState::Filter::kNearest);
522 auto fp = GrTextureEffect::MakeSubset(std::move(srcView),
523 srcAlphaType,
524 SkMatrix::I(),
525 sampler,
526 SkRect::Make(srcBounds),
527 SkRect::Make(dstBounds),
528 *context->priv().caps());
529 result->fillRectToRectWithFP(dstBounds, SkIRect::MakeSize(dstBounds.size()), std::move(fp));
530 return result;
531 }
532
533 if (sigmaX <= kMaxSigma && sigmaY <= kMaxSigma) {
534 SkASSERT(radiusX <= GrGaussianConvolutionFragmentProcessor::kMaxKernelRadius);
535 SkASSERT(radiusY <= GrGaussianConvolutionFragmentProcessor::kMaxKernelRadius);
536 // For really small blurs (certainly no wider than 5x5 on desktop GPUs) it is faster to just
537 // launch a single non separable kernel vs two launches.
538 const int kernelSize = (2 * radiusX + 1) * (2 * radiusY + 1);
539 if (radiusX > 0 && radiusY > 0 &&
540 kernelSize <= GrMatrixConvolutionEffect::kMaxUniformSize &&
541 !context->priv().caps()->reducedShaderMode()) {
542 // Apply the proxy offset to src bounds and offset directly
543 return convolve_gaussian_2d(context, std::move(srcView), srcColorType, srcBounds,
544 dstBounds, radiusX, radiusY, sigmaX, sigmaY, mode,
545 std::move(colorSpace), fit);
546 }
547 // This will automatically degenerate into a single pass of X or Y if only one of the
548 // radii are non-zero.
549 return two_pass_gaussian(context, std::move(srcView), srcColorType, srcAlphaType,
550 std::move(colorSpace), srcBounds, dstBounds, sigmaX, sigmaY,
551 radiusX, radiusY, mode, fit);
552 }
553
554 GrColorInfo colorInfo(srcColorType, srcAlphaType, colorSpace);
555 auto srcCtx = GrSurfaceContext::Make(context, srcView, colorInfo);
556 SkASSERT(srcCtx);
557
558 float scaleX = sigmaX > kMaxSigma ? kMaxSigma/sigmaX : 1.f;
559 float scaleY = sigmaY > kMaxSigma ? kMaxSigma/sigmaY : 1.f;
560 // We round down here so that when we recalculate sigmas we know they will be below
561 // kMaxSigma (but clamp to 1 do we don't have an empty texture).
562 SkISize rescaledSize = {std::max(sk_float_floor2int(srcBounds.width() *scaleX), 1),
563 std::max(sk_float_floor2int(srcBounds.height()*scaleY), 1)};
564 // Compute the sigmas using the actual scale factors used once we integerized the
565 // rescaledSize.
566 scaleX = static_cast<float>(rescaledSize.width()) /srcBounds.width();
567 scaleY = static_cast<float>(rescaledSize.height())/srcBounds.height();
568 sigmaX *= scaleX;
569 sigmaY *= scaleY;
570
571 // When we are in clamp mode any artifacts in the edge pixels due to downscaling may be
572 // exacerbated because of the tile mode. The particularly egregious case is when the original
573 // image has transparent black around the edges and the downscaling pulls in some non-zero
574 // values from the interior. Ultimately it'd be better for performance if the calling code could
575 // give us extra context around the blur to account for this. We don't currently have a good way
576 // to communicate this up stack. So we leave a 1 pixel border around the rescaled src bounds.
577 // We populate the top 1 pixel tall row of this border by rescaling the top row of the original
578 // source bounds into it. Because this is only rescaling in x (i.e. rescaling a 1 pixel high
579 // row into a shorter but still 1 pixel high row) we won't read any interior values. And similar
580 // for the other three borders. We'll adjust the source/dest bounds rescaled blur so that this
581 // border of extra pixels is used as the edge pixels for clamp mode but the dest bounds
582 // corresponds only to the pixels inside the border (the normally rescaled pixels inside this
583 // border).
584 // Moreover, if we clamped the rescaled size to 1 column or row then we still have a sigma
585 // that is greater than kMaxSigma. By using a pad and making the src 3 wide/tall instead of
586 // 1 we can recurse again and do another downscale. Since mirror and repeat modes are trivial
587 // for a single col/row we only add padding based on sigma exceeding kMaxSigma for decal.
588 int padX = mode == SkTileMode::kClamp ||
589 (mode == SkTileMode::kDecal && sigmaX > kMaxSigma) ? 1 : 0;
590 int padY = mode == SkTileMode::kClamp ||
591 (mode == SkTileMode::kDecal && sigmaY > kMaxSigma) ? 1 : 0;
592 // Create the sdc with default SkSurfaceProps. Gaussian blurs will soon use a
593 // GrSurfaceFillContext, at which point the SkSurfaceProps won't exist anymore.
594 auto rescaledSDC = GrSurfaceDrawContext::Make(
595 srcCtx->recordingContext(),
596 colorInfo.colorType(),
597 colorInfo.refColorSpace(),
598 SkBackingFit::kApprox,
599 {rescaledSize.width() + 2*padX, rescaledSize.height() + 2*padY},
600 SkSurfaceProps(),
601 1,
602 GrMipmapped::kNo,
603 srcCtx->asSurfaceProxy()->isProtected(),
604 srcCtx->origin());
605 if (!rescaledSDC) {
606 return nullptr;
607 }
608 if ((padX || padY) && mode == SkTileMode::kDecal) {
609 rescaledSDC->clear(SkPMColor4f{0, 0, 0, 0});
610 }
611 if (!srcCtx->rescaleInto(rescaledSDC.get(),
612 SkIRect::MakeSize(rescaledSize).makeOffset(padX, padY),
613 srcBounds,
614 SkSurface::RescaleGamma::kSrc,
615 SkSurface::RescaleMode::kRepeatedLinear)) {
616 return nullptr;
617 }
618 if (mode == SkTileMode::kClamp) {
619 SkASSERT(padX == 1 && padY == 1);
620 // Rather than run a potentially multi-pass rescaler on single rows/columns we just do a
621 // single bilerp draw. If we find this quality unacceptable we should think more about how
622 // to rescale these with better quality but without 4 separate multi-pass downscales.
623 auto cheapDownscale = [&](SkIRect dstRect, SkIRect srcRect) {
624 rescaledSDC->drawTexture(nullptr,
625 srcCtx->readSurfaceView(),
626 srcAlphaType,
627 GrSamplerState::Filter::kLinear,
628 GrSamplerState::MipmapMode::kNone,
629 SkBlendMode::kSrc,
630 SK_PMColor4fWHITE,
631 SkRect::Make(srcRect),
632 SkRect::Make(dstRect),
633 GrAA::kNo,
634 GrQuadAAFlags::kNone,
635 SkCanvas::SrcRectConstraint::kFast_SrcRectConstraint,
636 SkMatrix::I(),
637 nullptr);
638 };
639 auto [dw, dh] = rescaledSize;
640 // The are the src rows and columns from the source that we will scale into the dst padding.
641 float sLCol = srcBounds.left();
642 float sTRow = srcBounds.top();
643 float sRCol = srcBounds.right() - 1;
644 float sBRow = srcBounds.bottom() - 1;
645
646 int sx = srcBounds.left();
647 int sy = srcBounds.top();
648 int sw = srcBounds.width();
649 int sh = srcBounds.height();
650
651 // Downscale the edges from the original source. These draws should batch together (and with
652 // the above interior rescaling when it is a single pass).
653 cheapDownscale(SkIRect::MakeXYWH( 0, 1, 1, dh),
654 SkIRect::MakeXYWH( sLCol, sy, 1, sh));
655 cheapDownscale(SkIRect::MakeXYWH( 1, 0, dw, 1),
656 SkIRect::MakeXYWH( sx, sTRow, sw, 1));
657 cheapDownscale(SkIRect::MakeXYWH(dw + 1, 1, 1, dh),
658 SkIRect::MakeXYWH( sRCol, sy, 1, sh));
659 cheapDownscale(SkIRect::MakeXYWH( 1, dh + 1, dw, 1),
660 SkIRect::MakeXYWH( sx, sBRow, sw, 1));
661
662 // Copy the corners from the original source. These would batch with the edges except that
663 // at time of writing we recognize these can use kNearest and downgrade the filter. So they
664 // batch with each other but not the edge draws.
665 cheapDownscale(SkIRect::MakeXYWH( 0, 0, 1, 1),
666 SkIRect::MakeXYWH(sLCol, sTRow, 1, 1));
667 cheapDownscale(SkIRect::MakeXYWH(dw + 1, 0, 1, 1),
668 SkIRect::MakeXYWH(sRCol, sTRow, 1, 1));
669 cheapDownscale(SkIRect::MakeXYWH(dw + 1,dh + 1, 1, 1),
670 SkIRect::MakeXYWH(sRCol, sBRow, 1, 1));
671 cheapDownscale(SkIRect::MakeXYWH( 0, dh + 1, 1, 1),
672 SkIRect::MakeXYWH(sLCol, sBRow, 1, 1));
673 }
674 srcView = rescaledSDC->readSurfaceView();
675 // Drop the contexts so we don't hold the proxies longer than necessary.
676 rescaledSDC.reset();
677 srcCtx.reset();
678
679 // Compute the dst bounds in the scaled down space. First move the origin to be at the top
680 // left since we trimmed off everything above and to the left of the original src bounds during
681 // the rescale.
682 SkRect scaledDstBounds = SkRect::Make(dstBounds.makeOffset(-srcBounds.topLeft()));
683 scaledDstBounds.fLeft *= scaleX;
684 scaledDstBounds.fTop *= scaleY;
685 scaledDstBounds.fRight *= scaleX;
686 scaledDstBounds.fBottom *= scaleY;
687 // Account for padding in our rescaled src, if any.
688 scaledDstBounds.offset(padX, padY);
689 // Turn the scaled down dst bounds into an integer pixel rect.
690 auto scaledDstBoundsI = scaledDstBounds.roundOut();
691
692 SkIRect scaledSrcBounds = SkIRect::MakeSize(srcView.dimensions());
693 auto sdc = GaussianBlur(context,
694 std::move(srcView),
695 srcColorType,
696 srcAlphaType,
697 colorSpace,
698 scaledDstBoundsI,
699 scaledSrcBounds,
700 sigmaX,
701 sigmaY,
702 mode,
703 fit);
704 if (!sdc) {
705 return nullptr;
706 }
707 // We rounded out the integer scaled dst bounds. Select the fractional dst bounds from the
708 // integer dimension blurred result when we scale back up.
709 scaledDstBounds.offset(-scaledDstBoundsI.left(), -scaledDstBoundsI.top());
710 return reexpand(context, std::move(sdc), scaledDstBounds, dstBounds.size(),
711 std::move(colorSpace), fit);
712 }
713
ComputeBlurredRRectParams(const SkRRect & srcRRect,const SkRRect & devRRect,SkScalar sigma,SkScalar xformedSigma,SkRRect * rrectToDraw,SkISize * widthHeight,SkScalar rectXs[kBlurRRectMaxDivisions],SkScalar rectYs[kBlurRRectMaxDivisions],SkScalar texXs[kBlurRRectMaxDivisions],SkScalar texYs[kBlurRRectMaxDivisions])714 bool ComputeBlurredRRectParams(const SkRRect& srcRRect, const SkRRect& devRRect,
715 SkScalar sigma, SkScalar xformedSigma,
716 SkRRect* rrectToDraw,
717 SkISize* widthHeight,
718 SkScalar rectXs[kBlurRRectMaxDivisions],
719 SkScalar rectYs[kBlurRRectMaxDivisions],
720 SkScalar texXs[kBlurRRectMaxDivisions],
721 SkScalar texYs[kBlurRRectMaxDivisions]) {
722 unsigned int devBlurRadius = 3*SkScalarCeilToInt(xformedSigma-1/6.0f);
723 SkScalar srcBlurRadius = 3.0f * sigma;
724
725 const SkRect& devOrig = devRRect.getBounds();
726 const SkVector& devRadiiUL = devRRect.radii(SkRRect::kUpperLeft_Corner);
727 const SkVector& devRadiiUR = devRRect.radii(SkRRect::kUpperRight_Corner);
728 const SkVector& devRadiiLR = devRRect.radii(SkRRect::kLowerRight_Corner);
729 const SkVector& devRadiiLL = devRRect.radii(SkRRect::kLowerLeft_Corner);
730
731 const int devLeft = SkScalarCeilToInt(std::max<SkScalar>(devRadiiUL.fX, devRadiiLL.fX));
732 const int devTop = SkScalarCeilToInt(std::max<SkScalar>(devRadiiUL.fY, devRadiiUR.fY));
733 const int devRight = SkScalarCeilToInt(std::max<SkScalar>(devRadiiUR.fX, devRadiiLR.fX));
734 const int devBot = SkScalarCeilToInt(std::max<SkScalar>(devRadiiLL.fY, devRadiiLR.fY));
735
736 // This is a conservative check for nine-patchability
737 if (devOrig.fLeft + devLeft + devBlurRadius >= devOrig.fRight - devRight - devBlurRadius ||
738 devOrig.fTop + devTop + devBlurRadius >= devOrig.fBottom - devBot - devBlurRadius) {
739 return false;
740 }
741
742 const SkVector& srcRadiiUL = srcRRect.radii(SkRRect::kUpperLeft_Corner);
743 const SkVector& srcRadiiUR = srcRRect.radii(SkRRect::kUpperRight_Corner);
744 const SkVector& srcRadiiLR = srcRRect.radii(SkRRect::kLowerRight_Corner);
745 const SkVector& srcRadiiLL = srcRRect.radii(SkRRect::kLowerLeft_Corner);
746
747 const SkScalar srcLeft = std::max<SkScalar>(srcRadiiUL.fX, srcRadiiLL.fX);
748 const SkScalar srcTop = std::max<SkScalar>(srcRadiiUL.fY, srcRadiiUR.fY);
749 const SkScalar srcRight = std::max<SkScalar>(srcRadiiUR.fX, srcRadiiLR.fX);
750 const SkScalar srcBot = std::max<SkScalar>(srcRadiiLL.fY, srcRadiiLR.fY);
751
752 int newRRWidth = 2*devBlurRadius + devLeft + devRight + 1;
753 int newRRHeight = 2*devBlurRadius + devTop + devBot + 1;
754 widthHeight->fWidth = newRRWidth + 2 * devBlurRadius;
755 widthHeight->fHeight = newRRHeight + 2 * devBlurRadius;
756
757 const SkRect srcProxyRect = srcRRect.getBounds().makeOutset(srcBlurRadius, srcBlurRadius);
758
759 rectXs[0] = srcProxyRect.fLeft;
760 rectXs[1] = srcProxyRect.fLeft + 2*srcBlurRadius + srcLeft;
761 rectXs[2] = srcProxyRect.fRight - 2*srcBlurRadius - srcRight;
762 rectXs[3] = srcProxyRect.fRight;
763
764 rectYs[0] = srcProxyRect.fTop;
765 rectYs[1] = srcProxyRect.fTop + 2*srcBlurRadius + srcTop;
766 rectYs[2] = srcProxyRect.fBottom - 2*srcBlurRadius - srcBot;
767 rectYs[3] = srcProxyRect.fBottom;
768
769 texXs[0] = 0.0f;
770 texXs[1] = 2.0f*devBlurRadius + devLeft;
771 texXs[2] = 2.0f*devBlurRadius + devLeft + 1;
772 texXs[3] = SkIntToScalar(widthHeight->fWidth);
773
774 texYs[0] = 0.0f;
775 texYs[1] = 2.0f*devBlurRadius + devTop;
776 texYs[2] = 2.0f*devBlurRadius + devTop + 1;
777 texYs[3] = SkIntToScalar(widthHeight->fHeight);
778
779 const SkRect newRect = SkRect::MakeXYWH(SkIntToScalar(devBlurRadius),
780 SkIntToScalar(devBlurRadius),
781 SkIntToScalar(newRRWidth),
782 SkIntToScalar(newRRHeight));
783 SkVector newRadii[4];
784 newRadii[0] = { SkScalarCeilToScalar(devRadiiUL.fX), SkScalarCeilToScalar(devRadiiUL.fY) };
785 newRadii[1] = { SkScalarCeilToScalar(devRadiiUR.fX), SkScalarCeilToScalar(devRadiiUR.fY) };
786 newRadii[2] = { SkScalarCeilToScalar(devRadiiLR.fX), SkScalarCeilToScalar(devRadiiLR.fY) };
787 newRadii[3] = { SkScalarCeilToScalar(devRadiiLL.fX), SkScalarCeilToScalar(devRadiiLL.fY) };
788
789 rrectToDraw->setRectRadii(newRect, newRadii);
790 return true;
791 }
792
793 // TODO: it seems like there should be some synergy with SkBlurMask::ComputeBlurProfile
794 // TODO: maybe cache this on the cpu side?
CreateIntegralTable(float sixSigma,SkBitmap * table)795 int CreateIntegralTable(float sixSigma, SkBitmap* table) {
796 // The texture we're producing represents the integral of a normal distribution over a
797 // six-sigma range centered at zero. We want enough resolution so that the linear
798 // interpolation done in texture lookup doesn't introduce noticeable artifacts. We
799 // conservatively choose to have 2 texels for each dst pixel.
800 int minWidth = 2 * sk_float_ceil2int(sixSigma);
801 // Bin by powers of 2 with a minimum so we get good profile reuse.
802 int width = std::max(SkNextPow2(minWidth), 32);
803
804 if (!table) {
805 return width;
806 }
807
808 if (!table->tryAllocPixels(SkImageInfo::MakeA8(width, 1))) {
809 return 0;
810 }
811 *table->getAddr8(0, 0) = 255;
812 const float invWidth = 1.f / width;
813 for (int i = 1; i < width - 1; ++i) {
814 float x = (i + 0.5f) * invWidth;
815 x = (-6 * x + 3) * SK_ScalarRoot2Over2;
816 float integral = 0.5f * (std::erf(x) + 1.f);
817 *table->getAddr8(i, 0) = SkToU8(sk_float_round2int(255.f * integral));
818 }
819
820 *table->getAddr8(width - 1, 0) = 0;
821 table->setImmutable();
822 return table->width();
823 }
824
825
Compute1DGaussianKernel(float * kernel,float sigma,int radius)826 void Compute1DGaussianKernel(float* kernel, float sigma, int radius) {
827 SkASSERT(radius == SigmaRadius(sigma));
828 if (SkGpuBlurUtils::IsEffectivelyZeroSigma(sigma)) {
829 // Calling SigmaRadius() produces 1, just computing ceil(sigma)*3 produces 3
830 SkASSERT(KernelWidth(radius) == 1);
831 std::fill_n(kernel, 1, 0.f);
832 kernel[0] = 1.f;
833 return;
834 }
835
836 // If this fails, kEffectivelyZeroSigma isn't big enough to prevent precision issues
837 SkASSERT(!SkScalarNearlyZero(2.f * sigma * sigma));
838
839 const float sigmaDenom = 1.0f / (2.f * sigma * sigma);
840 int size = KernelWidth(radius);
841 float sum = 0.0f;
842 for (int i = 0; i < size; ++i) {
843 float term = static_cast<float>(i - radius);
844 // Note that the constant term (1/(sqrt(2*pi*sigma^2)) of the Gaussian
845 // is dropped here, since we renormalize the kernel below.
846 kernel[i] = sk_float_exp(-term * term * sigmaDenom);
847 sum += kernel[i];
848 }
849 // Normalize the kernel
850 float scale = 1.0f / sum;
851 for (int i = 0; i < size; ++i) {
852 kernel[i] *= scale;
853 }
854 }
855
Compute1DLinearGaussianKernel(float * kernel,float * offset,float sigma,int radius)856 void Compute1DLinearGaussianKernel(float* kernel, float* offset, float sigma, int radius) {
857 // Given 2 adjacent gaussian points, they are blended as: Wi * Ci + Wj * Cj.
858 // The GPU will mix Ci and Cj as Ci * (1 - x) + Cj * x during sampling.
859 // Compute W', x such that W' * (Ci * (1 - x) + Cj * x) = Wi * Ci + Wj * Cj.
860 // Solving W' * x = Wj, W' * (1 - x) = Wi:
861 // W' = Wi + Wj
862 // x = Wj / (Wi + Wj)
863 auto get_new_weight = [](float* new_w, float* offset, float wi, float wj) {
864 *new_w = wi + wj;
865 *offset = wj / (wi + wj);
866 };
867
868 // Create a temporary standard kernel.
869 int size = KernelWidth(radius);
870 std::unique_ptr<float[]> temp_kernel(new float[size]);
871 Compute1DGaussianKernel(temp_kernel.get(), sigma, radius);
872
873 // Note that halfsize isn't just size / 2, but radius + 1. This is the size of the output array.
874 int halfsize = LinearKernelWidth(radius);
875 int halfradius = halfsize / 2;
876 int low_index = halfradius - 1;
877
878 // Compute1DGaussianKernel produces a full 2N + 1 kernel. Since the kernel can be mirrored,
879 // compute only the upper half and mirror to the lower half.
880
881 int index = radius;
882 if (radius & 1) {
883 // If N is odd, then use two samples.
884 // The centre texel gets sampled twice, so halve its influence for each sample.
885 // We essentially sample like this:
886 // Texel edges
887 // v v v v
888 // | | | |
889 // \-----^---/ Lower sample
890 // \---^-----/ Upper sample
891 get_new_weight(&kernel[halfradius], &offset[halfradius],
892 temp_kernel[index] * 0.5f, temp_kernel[index + 1]);
893 kernel[low_index] = kernel[halfradius];
894 offset[low_index] = -offset[halfradius];
895 index++;
896 low_index--;
897 } else {
898 // If N is even, then there are an even number of texels on either side of the centre texel.
899 // Sample the centre texel directly.
900 kernel[halfradius] = temp_kernel[index];
901 offset[halfradius] = 0.0f;
902 }
903 index++;
904
905 // Every other pair gets one sample.
906 for (int i = halfradius + 1; i < halfsize; index += 2, i++, low_index--) {
907 get_new_weight(&kernel[i], &offset[i], temp_kernel[index], temp_kernel[index + 1]);
908 offset[i] += static_cast<float>(index - radius);
909
910 // Mirror to lower half.
911 kernel[low_index] = kernel[i];
912 offset[low_index] = -offset[i];
913 }
914 }
915
916 } // namespace SkGpuBlurUtils
917
918 #endif
919