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/SkRect.h"
11
12#if SK_SUPPORT_GPU
13#include "include/private/GrRecordingContext.h"
14#include "src/gpu/GrCaps.h"
15#include "src/gpu/GrFixedClip.h"
16#include "src/gpu/GrRecordingContextPriv.h"
17#include "src/gpu/GrRenderTargetContext.h"
18#include "src/gpu/GrRenderTargetContextPriv.h"
19#include "src/gpu/effects/GrGaussianConvolutionFragmentProcessor.h"
20#include "src/gpu/effects/GrMatrixConvolutionEffect.h"
21
22#include "src/gpu/SkGr.h"
23
24#define MAX_BLUR_SIGMA 4.0f
25
26using Direction = GrGaussianConvolutionFragmentProcessor::Direction;
27
28static void scale_irect_roundout(SkIRect* rect, float xScale, float yScale) {
29 rect->fLeft = SkScalarFloorToInt(rect->fLeft * xScale);
30 rect->fTop = SkScalarFloorToInt(rect->fTop * yScale);
31 rect->fRight = SkScalarCeilToInt(rect->fRight * xScale);
32 rect->fBottom = SkScalarCeilToInt(rect->fBottom * yScale);
33}
34
35static void scale_irect(SkIRect* rect, int xScale, int yScale) {
36 rect->fLeft *= xScale;
37 rect->fTop *= yScale;
38 rect->fRight *= xScale;
39 rect->fBottom *= yScale;
40}
41
42#ifdef SK_DEBUG
43static inline int is_even(int x) { return !(x & 1); }
44#endif
45
46static void shrink_irect_by_2(SkIRect* rect, bool xAxis, bool yAxis) {
47 if (xAxis) {
48 SkASSERT(is_even(rect->fLeft) && is_even(rect->fRight));
49 rect->fLeft /= 2;
50 rect->fRight /= 2;
51 }
52 if (yAxis) {
53 SkASSERT(is_even(rect->fTop) && is_even(rect->fBottom));
54 rect->fTop /= 2;
55 rect->fBottom /= 2;
56 }
57}
58
59static float adjust_sigma(float sigma, int maxTextureSize, int *scaleFactor, int *radius) {
60 *scaleFactor = 1;
61 while (sigma > MAX_BLUR_SIGMA) {
62 *scaleFactor *= 2;
63 sigma *= 0.5f;
64 if (*scaleFactor > maxTextureSize) {
65 *scaleFactor = maxTextureSize;
66 sigma = MAX_BLUR_SIGMA;
67 }
68 }
69 *radius = static_cast<int>(ceilf(sigma * 3.0f));
70 SkASSERT(*radius <= GrGaussianConvolutionFragmentProcessor::kMaxKernelRadius);
71 return sigma;
72}
73
74static GrTextureDomain::Mode to_texture_domain_mode(SkTileMode tileMode) {
75 switch (tileMode) {
76 case SkTileMode::kClamp:
77 return GrTextureDomain::kClamp_Mode;
78 case SkTileMode::kDecal:
79 return GrTextureDomain::kDecal_Mode;
80 case SkTileMode::kMirror:
81 // TODO (michaelludwig) - Support mirror mode, treat as repeat for now
82 case SkTileMode::kRepeat:
83 return GrTextureDomain::kRepeat_Mode;
84 default:
85 SK_ABORT("Unsupported tile mode.");
86 }
87}
88
89/**
90 * Draws 'rtcRect' into 'renderTargetContext' evaluating a 1D Gaussian over 'srcView'. The src rect
91 * is 'rtcRect' offset by 'rtcToSrcOffset'. 'mode' and 'bounds' are applied to the src coords.
92 */
93static void convolve_gaussian_1d(GrRenderTargetContext* renderTargetContext,
94 GrSurfaceProxyView srcView,
95 SkIVector rtcToSrcOffset,
96 const SkIRect& rtcRect,
97 SkAlphaType srcAlphaType,
98 Direction direction,
99 int radius,
100 float sigma,
101 SkTileMode mode,
102 int bounds[2]) {
103 GrPaint paint;
104 auto domainMode = to_texture_domain_mode(mode);
105 int realBounds[2];
106 if (bounds) {
107 realBounds[0] = bounds[0]; realBounds[1] = bounds[1];
108 } else {
109 auto proxy = srcView.proxy();
110 realBounds[0] = 0;
111 realBounds[1] = direction == Direction::kX ? proxy->width() : proxy->height();
112 }
113 std::unique_ptr<GrFragmentProcessor> conv(GrGaussianConvolutionFragmentProcessor::Make(
114 std::move(srcView), srcAlphaType, direction, radius, sigma, domainMode, realBounds));
115 paint.addColorFragmentProcessor(std::move(conv));
116 paint.setPorterDuffXPFactory(SkBlendMode::kSrc);
117 auto srcRect = SkRect::Make(rtcRect.makeOffset(rtcToSrcOffset));
118 renderTargetContext->fillRectToRect(GrNoClip(), std::move(paint), GrAA::kNo, SkMatrix::I(),
119 SkRect::Make(rtcRect), srcRect);
120}
121
122static std::unique_ptr<GrRenderTargetContext> convolve_gaussian_2d(GrRecordingContext* context,
123 GrSurfaceProxyView srcView,
124 GrColorType srcColorType,
125 const SkIRect& srcBounds,
126 const SkIRect& dstBounds,
127 int radiusX,
128 int radiusY,
129 SkScalar sigmaX,
130 SkScalar sigmaY,
131 SkTileMode mode,
132 sk_sp<SkColorSpace> finalCS,
133 SkBackingFit dstFit) {
134 auto renderTargetContext = GrRenderTargetContext::Make(
135 context, srcColorType, std::move(finalCS), dstFit, dstBounds.size(), 1,
136 GrMipMapped::kNo, srcView.proxy()->isProtected(), srcView.origin());
137 if (!renderTargetContext) {
138 return nullptr;
139 }
140
141 SkISize size = SkISize::Make(2 * radiusX + 1, 2 * radiusY + 1);
142 SkIPoint kernelOffset = SkIPoint::Make(radiusX, radiusY);
143 GrPaint paint;
144 auto domainMode = to_texture_domain_mode(mode);
145 auto conv = GrMatrixConvolutionEffect::MakeGaussian(std::move(srcView), srcBounds, size,
146 1.0, 0.0, kernelOffset, domainMode, true,
147 sigmaX, sigmaY);
148 paint.addColorFragmentProcessor(std::move(conv));
149 paint.setPorterDuffXPFactory(SkBlendMode::kSrc);
150
151 // 'dstBounds' is actually in 'srcView' proxy space. It represents the blurred area from src
152 // space that we want to capture in the new RTC at {0, 0}. Hence, we use its size as the rect to
153 // draw and it directly as the local rect.
154 renderTargetContext->fillRectToRect(GrNoClip(), std::move(paint), GrAA::kNo, SkMatrix::I(),
155 SkRect::Make(dstBounds.size()), SkRect::Make(dstBounds));
156
157 return renderTargetContext;
158}
159
160static std::unique_ptr<GrRenderTargetContext> convolve_gaussian(GrRecordingContext* context,
161 GrSurfaceProxyView srcView,
162 GrColorType srcColorType,
163 SkAlphaType srcAlphaType,
164 SkIRect* contentRect,
165 SkIRect dstBounds,
166 Direction direction,
167 int radius,
168 float sigma,
169 SkTileMode mode,
170 sk_sp<SkColorSpace> finalCS,
171 SkBackingFit fit) {
172 // Logically we're creating an infinite blur of 'contentRect' of 'srcView' with 'mode' tiling
173 // and then capturing the 'dstBounds' portion in a new RTC where the top left of 'dstBounds' is
174 // at {0, 0} in the new RTC.
175 auto dstRenderTargetContext = GrRenderTargetContext::Make(
176 context, srcColorType, std::move(finalCS), fit, dstBounds.size(), 1, GrMipMapped::kNo,
177 srcView.proxy()->isProtected(), srcView.origin());
178 if (!dstRenderTargetContext) {
179 return nullptr;
180 }
181
182 // This represents the translation from 'dstRenderTargetContext' coords to 'srcView' coords.
183 auto rtcToSrcOffset = dstBounds.topLeft();
184
185 if (SkTileMode::kClamp == mode &&
186 contentRect->contains(SkIRect::MakeSize(srcView.proxy()->backingStoreDimensions()))) {
187 auto dstRect = SkIRect::MakeSize(dstBounds.size());
188 convolve_gaussian_1d(dstRenderTargetContext.get(), std::move(srcView), rtcToSrcOffset,
189 dstRect, srcAlphaType, direction, radius, sigma, SkTileMode::kClamp,
190 nullptr);
191 *contentRect = dstRect;
192 return dstRenderTargetContext;
193 }
194
195 // 'left' and 'right' are the sub rects of 'contentTect' where 'mode' must be enforced.
196 // 'mid' is the area where we can ignore the mode because the kernel does not reach to the
197 // edge of 'contentRect'. The names are derived from the Direction::kX case.
198 // TODO: When mode is kMirror or kRepeat it makes more sense to think of 'contentRect'
199 // as a tile and figure out the collection of mid/left/right rects that cover 'dstBounds'.
200 // Also if 'mid' is small and 'left' or 'right' is non-empty we should probably issue one
201 // draw that implements the mode in the shader rather than break it up in this fashion.
202 SkIRect mid, left, right;
203 // 'top' and 'bottom' are areas of 'dstBounds' that are entirely above/below
204 // 'contentRect'. These are areas that we can simply clear in the dst. If 'contentRect'
205 // straddles the top edge of 'dstBounds' then 'top' will be inverted and we will skip
206 // the clear. Similar for 'bottom'. The positional/directional labels above refer to the
207 // Direction::kX case and one should think of these as 'left' and 'right' for Direction::kY.
208 SkIRect top, bottom;
209 int bounds[2];
210 if (Direction::kX == direction) {
211 bounds[0] = contentRect->left();
212 bounds[1] = contentRect->right();
213
214 top = {dstBounds.left(), dstBounds.top() , dstBounds.right(), contentRect->top()};
215 bottom = {dstBounds.left(), contentRect->bottom(), dstBounds.right(), dstBounds.bottom()};
216
217 // Inset for sub-rect of 'contentRect' where the x-dir kernel doesn't reach the edges.
218 // TODO: Consider clipping mid/left/right to dstBounds to increase likelihood of doing
219 // fewer draws below.
220 mid = contentRect->makeInset(radius, 0);
221
222 left = {dstBounds.left(), mid.top(), mid.left() , mid.bottom()};
223 right = {mid.right(), mid.top(), dstBounds.right(), mid.bottom()};
224
225 // The new 'contentRect' when we're done will be the area between the clears.
226 *contentRect = {dstBounds.left(), top.bottom(), dstBounds.right(), bottom.top()};
227 } else {
228 // This is the same as the x direction code if you turn your head 90 degrees CCW. Swap x and
229 // y and swap top/bottom with left/right.
230 bounds[0] = contentRect->top();
231 bounds[1] = contentRect->bottom();
232
233 top = {dstBounds.left(), dstBounds.top() , contentRect->left(), dstBounds.bottom()};
234 bottom = {contentRect->right(), dstBounds.top() , dstBounds.right() , dstBounds.bottom()};
235
236 mid = contentRect->makeInset(0, radius);
237
238 left = {mid.left(), dstBounds.top(), mid.right(), mid.top() };
239 right = {mid.left(), mid.bottom() , mid.right(), dstBounds.bottom()};
240
241 *contentRect = {top.right(), dstBounds.top(), bottom.left(), dstBounds.bottom()};
242 }
243 // Move all the rects from 'srcView' coord system to 'dstRenderTargetContext' coord system.
244 mid .offset(-rtcToSrcOffset);
245 top .offset(-rtcToSrcOffset);
246 bottom.offset(-rtcToSrcOffset);
247 left .offset(-rtcToSrcOffset);
248 right .offset(-rtcToSrcOffset);
249
250 contentRect->offset(-rtcToSrcOffset);
251
252 if (!top.isEmpty()) {
253 dstRenderTargetContext->clear(&top, SK_PMColor4fTRANSPARENT,
254 GrRenderTargetContext::CanClearFullscreen::kYes);
255 }
256
257 if (!bottom.isEmpty()) {
258 dstRenderTargetContext->clear(&bottom, SK_PMColor4fTRANSPARENT,
259 GrRenderTargetContext::CanClearFullscreen::kYes);
260 }
261
262 if (mid.isEmpty()) {
263 convolve_gaussian_1d(dstRenderTargetContext.get(), std::move(srcView), rtcToSrcOffset,
264 *contentRect, srcAlphaType, direction, radius, sigma, mode, bounds);
265 } else {
266 // Draw right and left margins with bounds; middle without.
267 convolve_gaussian_1d(dstRenderTargetContext.get(), srcView, rtcToSrcOffset, left,
268 srcAlphaType, direction, radius, sigma, mode, bounds);
269 convolve_gaussian_1d(dstRenderTargetContext.get(), srcView, rtcToSrcOffset, right,
270 srcAlphaType, direction, radius, sigma, mode, bounds);
271 convolve_gaussian_1d(dstRenderTargetContext.get(), std::move(srcView), rtcToSrcOffset, mid,
272 srcAlphaType, direction, radius, sigma, SkTileMode::kClamp, nullptr);
273 }
274
275 return dstRenderTargetContext;
276}
277
278// Returns a high quality scaled-down version of src. This is used to create an intermediate,
279// shrunken version of the source image in the event that the requested blur sigma exceeds
280// MAX_BLUR_SIGMA.
281static GrSurfaceProxyView decimate(GrRecordingContext* context,
282 GrSurfaceProxyView srcView,
283 GrColorType srcColorType,
284 SkAlphaType srcAlphaType,
285 SkIPoint srcOffset,
286 SkIRect* contentRect,
287 int scaleFactorX,
288 int scaleFactorY,
289 SkTileMode mode,
290 sk_sp<SkColorSpace> finalCS) {
291 SkASSERT(SkIsPow2(scaleFactorX) && SkIsPow2(scaleFactorY));
292 SkASSERT(scaleFactorX > 1 || scaleFactorY > 1);
293
294 SkIRect srcRect = contentRect->makeOffset(srcOffset);
295
296 scale_irect_roundout(&srcRect, 1.0f / scaleFactorX, 1.0f / scaleFactorY);
297 scale_irect(&srcRect, scaleFactorX, scaleFactorY);
298
299 SkIRect dstRect(srcRect);
300
301 std::unique_ptr<GrRenderTargetContext> dstRenderTargetContext;
302
303 for (int i = 1; i < scaleFactorX || i < scaleFactorY; i *= 2) {
304 shrink_irect_by_2(&dstRect, i < scaleFactorX, i < scaleFactorY);
305
306 dstRenderTargetContext = GrRenderTargetContext::Make(
307 context, srcColorType, finalCS, SkBackingFit::kApprox,
308 {dstRect.fRight, dstRect.fBottom}, 1, GrMipMapped::kNo,
309 srcView.proxy()->isProtected(), srcView.origin());
310 if (!dstRenderTargetContext) {
311 return {};
312 }
313
314 GrPaint paint;
315 std::unique_ptr<GrFragmentProcessor> fp;
316 if (i == 1) {
317 GrSamplerState::WrapMode wrapMode;
318 if (mode == SkTileMode::kClamp) {
319 wrapMode = GrSamplerState::WrapMode::kClamp;
320 } else {
321 // GrTextureEffect does not support WrapMode::k[Mirror]Repeat with
322 // GrSamplerState::Filter::kBilerp. So we use kClampToBorder.
323 wrapMode = GrSamplerState::WrapMode::kClampToBorder;
324 }
325 const auto& caps = *context->priv().caps();
326 GrSamplerState sampler(wrapMode, GrSamplerState::Filter::kBilerp);
327 fp = GrTextureEffect::MakeSubset(std::move(srcView), srcAlphaType, SkMatrix::I(),
328 sampler, SkRect::Make(*contentRect), caps);
329 srcRect.offset(-srcOffset);
330 } else {
331 fp = GrTextureEffect::Make(std::move(srcView), srcAlphaType, SkMatrix::I(),
332 GrSamplerState::Filter::kBilerp);
333 }
334 paint.addColorFragmentProcessor(std::move(fp));
335 paint.setPorterDuffXPFactory(SkBlendMode::kSrc);
336
337 dstRenderTargetContext->fillRectToRect(GrFixedClip::Disabled(), std::move(paint), GrAA::kNo,
338 SkMatrix::I(), SkRect::Make(dstRect),
339 SkRect::Make(srcRect));
340
341 srcView = dstRenderTargetContext->readSurfaceView();
342 if (!srcView.asTextureProxy()) {
343 return {};
344 }
345 srcRect = dstRect;
346 }
347
348 *contentRect = dstRect;
349
350 SkASSERT(dstRenderTargetContext);
351 SkASSERT(srcView == dstRenderTargetContext->readSurfaceView());
352
353 return srcView;
354}
355
356// Expand the contents of 'srcRenderTargetContext' to fit in 'dstII'. At this point, we are
357// expanding an intermediate image, so there's no need to account for a proxy offset from the
358// original input.
359static std::unique_ptr<GrRenderTargetContext> reexpand(GrRecordingContext* context,
360 std::unique_ptr<GrRenderTargetContext> src,
361 const SkIRect& srcBounds,
362 int scaleFactorX,
363 int scaleFactorY,
364 SkISize dstSize,
365 sk_sp<SkColorSpace> colorSpace,
366 SkBackingFit fit) {
367 const SkIRect srcRect = SkIRect::MakeWH(src->width(), src->height());
368
369 GrSurfaceProxyView srcView = src->readSurfaceView();
370 if (!srcView.asTextureProxy()) {
371 return nullptr;
372 }
373
374 GrColorType srcColorType = src->colorInfo().colorType();
375 SkAlphaType srcAlphaType = src->colorInfo().alphaType();
376
377 src.reset(); // no longer needed
378
379 auto dstRenderTargetContext = GrRenderTargetContext::Make(
380 context, srcColorType, std::move(colorSpace), fit, dstSize, 1, GrMipMapped::kNo,
381 srcView.proxy()->isProtected(), srcView.origin());
382 if (!dstRenderTargetContext) {
383 return nullptr;
384 }
385
386 GrPaint paint;
387 const auto& caps = *context->priv().caps();
388 auto fp = GrTextureEffect::MakeSubset(std::move(srcView), srcAlphaType, SkMatrix::I(),
389 GrSamplerState::Filter::kBilerp, SkRect::Make(srcBounds),
390 caps);
391 paint.addColorFragmentProcessor(std::move(fp));
392 paint.setPorterDuffXPFactory(SkBlendMode::kSrc);
393 GrFixedClip clip(SkIRect::MakeSize(dstSize));
394
395 // TODO: using dstII as dstRect results in some image diffs - why?
396 SkIRect dstRect(srcRect);
397 scale_irect(&dstRect, scaleFactorX, scaleFactorY);
398
399 dstRenderTargetContext->fillRectToRect(clip, std::move(paint), GrAA::kNo, SkMatrix::I(),
400 SkRect::Make(dstRect), SkRect::Make(srcRect));
401
402 return dstRenderTargetContext;
403}
404
405static std::unique_ptr<GrRenderTargetContext> two_pass_gaussian(GrRecordingContext* context,
406 GrSurfaceProxyView srcView,
407 GrColorType srcColorType,
408 SkAlphaType srcAlphaType,
409 sk_sp<SkColorSpace> colorSpace,
410 SkIRect* srcBounds,
411 SkIRect dstBounds,
412 float sigmaX,
413 float sigmaY,
414 int radiusX,
415 int radiusY,
416 SkTileMode mode,
417 SkBackingFit fit) {
418 std::unique_ptr<GrRenderTargetContext> dstRenderTargetContext;
419 if (sigmaX > 0.0f) {
420 SkBackingFit xFit = sigmaY > 0 ? SkBackingFit::kApprox : fit;
421 dstRenderTargetContext = convolve_gaussian(
422 context, std::move(srcView), srcColorType, srcAlphaType, srcBounds, dstBounds,
423 Direction::kX, radiusX, sigmaX, mode, colorSpace, xFit);
424 if (!dstRenderTargetContext) {
425 return nullptr;
426 }
427 srcView = dstRenderTargetContext->readSurfaceView();
428 dstBounds = SkIRect::MakeSize(dstBounds.size());
429 }
430
431 if (sigmaY == 0.0f) {
432 return dstRenderTargetContext;
433 }
434
435 return convolve_gaussian(context, std::move(srcView), srcColorType, srcAlphaType, srcBounds,
436 dstBounds, Direction::kY, radiusY, sigmaY, mode, colorSpace, fit);
437}
438
439namespace SkGpuBlurUtils {
440
441std::unique_ptr<GrRenderTargetContext> GaussianBlur(GrRecordingContext* context,
442 GrSurfaceProxyView srcView,
443 GrColorType srcColorType,
444 SkAlphaType srcAlphaType,
445 sk_sp<SkColorSpace> colorSpace,
446 const SkIRect& dstBounds,
447 const SkIRect& srcBounds,
448 float sigmaX,
449 float sigmaY,
450 SkTileMode mode,
451 SkBackingFit fit) {
452 SkASSERT(context);
453 TRACE_EVENT2("skia.gpu", "GaussianBlur", "sigmaX", sigmaX, "sigmaY", sigmaY);
454
455 if (!srcView.asTextureProxy()) {
456 return nullptr;
457 }
458
459 int scaleFactorX, radiusX;
460 int scaleFactorY, radiusY;
461 int maxTextureSize = context->priv().caps()->maxTextureSize();
462 sigmaX = adjust_sigma(sigmaX, maxTextureSize, &scaleFactorX, &radiusX);
463 sigmaY = adjust_sigma(sigmaY, maxTextureSize, &scaleFactorY, &radiusY);
464 SkASSERT(sigmaX || sigmaY);
465
466 auto localSrcBounds = srcBounds;
467
468 if (scaleFactorX == 1 && scaleFactorY == 1) {
469 // For really small blurs (certainly no wider than 5x5 on desktop GPUs) it is faster to just
470 // launch a single non separable kernel vs two launches.
471 if (sigmaX > 0 && sigmaY > 0 && (2 * radiusX + 1) * (2 * radiusY + 1) <= MAX_KERNEL_SIZE) {
472 // Apply the proxy offset to src bounds and offset directly
473 return convolve_gaussian_2d(context, std::move(srcView), srcColorType, srcBounds,
474 dstBounds, radiusX, radiusY, sigmaX, sigmaY, mode,
475 colorSpace, fit);
476 }
477 return two_pass_gaussian(context, std::move(srcView), srcColorType, srcAlphaType,
478 std::move(colorSpace), &localSrcBounds, dstBounds, sigmaX, sigmaY,
479 radiusX, radiusY, mode, fit);
480 }
481
482 auto srcOffset = -dstBounds.topLeft();
483 srcView = decimate(context, std::move(srcView), srcColorType, srcAlphaType, srcOffset,
484 &localSrcBounds, scaleFactorX, scaleFactorY, mode, colorSpace);
485 if (!srcView.proxy()) {
486 return nullptr;
487 }
488 SkASSERT(srcView.asTextureProxy());
489 auto scaledDstBounds = SkIRect::MakeWH(sk_float_ceil(dstBounds.width() / (float)scaleFactorX),
490 sk_float_ceil(dstBounds.height() / (float)scaleFactorY));
491 auto rtc = two_pass_gaussian(context, std::move(srcView), srcColorType, srcAlphaType,
492 colorSpace, &localSrcBounds, scaledDstBounds, sigmaX, sigmaY,
493 radiusX, radiusY, mode, SkBackingFit::kApprox);
494 if (!rtc) {
495 return nullptr;
496 }
497 return reexpand(context, std::move(rtc), localSrcBounds, scaleFactorX, scaleFactorY,
498 dstBounds.size(), std::move(colorSpace), fit);
499}
500
501}
502
503#endif
504