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