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