• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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