• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2023 Google LLC
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 #ifndef SkBlurEngine_DEFINED
9 #define SkBlurEngine_DEFINED
10 
11 #include "include/core/SkM44.h"  // IWYU pragma: keep
12 #include "include/core/SkRefCnt.h"
13 #include "include/core/SkSize.h"
14 #include "include/core/SkSpan.h"
15 #include "include/private/base/SkFloatingPoint.h"
16 
17 #include <array>
18 
19 class SkDevice;
20 class SkRuntimeEffect;
21 class SkRuntimeShaderBuilder;
22 class SkSpecialImage;
23 struct SkImageInfo;
24 struct SkIRect;
25 
26 enum class SkFilterMode;
27 enum class SkTileMode;
28 enum SkColorType : int;
29 
30 /**
31  * SkBlurEngine is a backend-agnostic provider of blur algorithms. Each Skia backend defines a blur
32  * engine with a set of supported algorithms and/or implementations. A given implementation may be
33  * optimized for a particular color type, sigma range, or available hardware. Each engine and its
34  * algorithms are assumed to operate only on SkImages corresponding to its Skia backend, and will
35  * produce output SkImages of the same type.
36  *
37  * Algorithms are allowed to specify a maximum supported sigma. If the desired sigma is higher than
38  * this, the input image and output region must be downscaled by the caller before invoking the
39  * algorithm. This is to provide the most flexibility for input representation (e.g. directly
40  * rasterize at half resolution or apply deferred filter effects during the first downsample pass).
41  *
42  * skif::FilterResult::Builder::blur() is a convenient wrapper around the blur engine and
43  * automatically handles resizing.
44 */
45 class SkBlurEngine {
46 public:
47     class Algorithm;
48 
49     virtual ~SkBlurEngine() = default;
50 
51     // Returns an Algorithm ideal for the requested 'sigma' that will support sampling an image of
52     // the given 'colorType'. If the engine does not support the requested configuration, it returns
53     // null. The engine maintains the lifetime of its algorithms, so the returned non-null
54     // Algorithms live as long as the engine does.
55     virtual const Algorithm* findAlgorithm(SkSize sigma,
56                                            SkColorType colorType) const = 0;
57 
58     // TODO: Consolidate common utility functions from SkBlurMask.h into this header.
59 
60     // Any sigmas smaller than this are effectively an identity blur so can skip convolution at a
61     // higher level. The value was chosen because it corresponds roughly to a radius of 1/10px, and
62     // because 2*sigma^2 is slightly greater than SK_ScalarNearlyZero.
IsEffectivelyIdentity(float sigma)63     static constexpr bool IsEffectivelyIdentity(float sigma) { return sigma <= 0.03f; }
64 
65     // Convert from a sigma Gaussian standard deviation to a pixel radius such that pixels outside
66     // the radius would have an insignificant contribution to the final blurred value.
SigmaToRadius(float sigma)67     static int SigmaToRadius(float sigma) {
68         // sk_float_ceil2int is not constexpr
69         return IsEffectivelyIdentity(sigma) ? 0 : sk_float_ceil2int(3.f * sigma);
70     }
71 };
72 
73 class SkBlurEngine::Algorithm {
74 public:
75     virtual ~Algorithm() = default;
76 
77     // The maximum sigma that can be passed to blur() in the X and/or Y sigma values. Larger
78     // requested sigmas must manually downscale the input image and upscale the output image.
79     virtual float maxSigma() const = 0;
80 
81     // Whether or not the SkTileMode can be passed to blur() must be SkTileMode::kDecal, or if any
82     // tile mode is supported. If only kDecal is supported, then callers must manually apply the
83     // tilemode and account for that in the src and dst bounds passed into blur(). If this returns
84     // false, then the algorithm supports all SkTileModes.
85     // TODO: Once CPU blurs support all tile modes, this API can go away.
86     virtual bool supportsOnlyDecalTiling() const = 0;
87 
88     // Produce a blurred image that fills 'dstRect' (their dimensions will match). 'dstRect's top
89     // left corner defines the output's location relative to the 'src' image. 'srcRect' restricts
90     // the pixels that are included in the blur and is also relative to 'src'. The 'tileMode'
91     // applies to the boundary of 'srcRect', which must be contained within 'src's dimensions.
92     //
93     // 'srcRect' and 'dstRect' may be different sizes and even be disjoint.
94     //
95     // The returned SkImage will have the same color type and colorspace as the input image. It will
96     // be an SkImage type matching the underlying Skia backend. If the 'src' SkImage is not a
97     // compatible SkImage type, null is returned.
98     // TODO(b/299474380): This only takes SkSpecialImage to work with skif::FilterResult and
99     // SkDevice::snapSpecial(); SkImage would be ideal.
100     virtual sk_sp<SkSpecialImage> blur(SkSize sigma,
101                                        sk_sp<SkSpecialImage> src,
102                                        const SkIRect& srcRect,
103                                        SkTileMode tileMode,
104                                        const SkIRect& dstRect) const = 0;
105 };
106 
107 /**
108  * The default blur implementation uses internal runtime effects to evaluate either a single 2D
109  * kernel within a shader, or performs two 1D blur passes. This algorithm is backend agnostic but
110  * must be subclassed per backend to define the SkDevice creation function.
111  */
112 class SkShaderBlurAlgorithm : public SkBlurEngine::Algorithm {
113 public:
maxSigma()114     float maxSigma() const override { return kMaxLinearSigma; }
supportsOnlyDecalTiling()115     bool supportsOnlyDecalTiling() const override { return false; }
116 
117     sk_sp<SkSpecialImage> blur(SkSize sigma,
118                                sk_sp<SkSpecialImage> src,
119                                const SkIRect& srcRect,
120                                SkTileMode tileMode,
121                                const SkIRect& dstRect) const override;
122 
123 private:
124     // Create a new surface, which can be approx-fit and have undefined contents.
125     virtual sk_sp<SkDevice> makeDevice(const SkImageInfo&) const = 0;
126 
127     sk_sp<SkSpecialImage> renderBlur(SkRuntimeShaderBuilder* blurEffectBuilder,
128                                      SkFilterMode filter,
129                                      SkISize radii,
130                                      sk_sp<SkSpecialImage> input,
131                                      const SkIRect& srcRect,
132                                      SkTileMode tileMode,
133                                      const SkIRect& dstRect) const;
134     sk_sp<SkSpecialImage> evalBlur2D(SkSize sigma,
135                                      SkISize radii,
136                                      sk_sp<SkSpecialImage> input,
137                                      const SkIRect& srcRect,
138                                      SkTileMode tileMode,
139                                      const SkIRect& dstRect) const;
140     sk_sp<SkSpecialImage> evalBlur1D(float sigma,
141                                      int radius,
142                                      SkV2 dir,
143                                      sk_sp<SkSpecialImage> input,
144                                      SkIRect srcRect,
145                                      SkTileMode tileMode,
146                                      SkIRect dstRect) const;
147 
148 // TODO: These are internal details of the blur shaders, but are public for now because multiple
149 // backends invoke the blur shaders directly. Once everything just goes through this class, these
150 // can be hidden.
151 public:
152 
153     // The kernel width of a Gaussian blur of the given pixel radius, when all pixels are sampled.
KernelWidth(int radius)154     static constexpr int KernelWidth(int radius) { return 2 * radius + 1; }
155 
156     // The kernel width of a Gaussian blur of the given pixel radius, that relies on HW bilinear
157     // filtering to combine adjacent pixels.
LinearKernelWidth(int radius)158     static constexpr int LinearKernelWidth(int radius) { return radius + 1; }
159 
160     // The maximum sigma that can be computed without downscaling is based on the number of uniforms
161     // and texture samples the effects will make in a single pass. For 1D passes, the number of
162     // samples is equal to `LinearKernelWidth`; for 2D passes, it is equal to
163     // `KernelWidth(radiusX)*KernelWidth(radiusY)`. This maps back to different maximum sigmas
164     // depending on the approach used, as well as the ratio between the sigmas for the X and Y axes
165     // if a 2D blur is performed.
166     static constexpr int kMaxSamples = 28;
167 
168     // TODO(b/297393474): Update max linear sigma to 9; it had been 4 when a full 1D kernel was
169     // used, but never updated after the linear filtering optimization reduced the number of
170     // sample() calls required. Keep it at 4 for now to better isolate performance changes due to
171     // switching to a runtime effect and constant loop structure.
172     static constexpr float kMaxLinearSigma = 4.f; // -> radius = 27 -> linear kernel width = 28
173     // NOTE: There is no defined kMaxBlurSigma for direct 2D blurs since it is entirely dependent on
174     // the ratio between the two axes' sigmas, but generally it will be small on the order of a
175     // 5x5 kernel.
176 
177     // Return a runtime effect that applies a 2D Gaussian blur in a single pass. The returned effect
178     // can perform arbitrarily sized blur kernels so long as the kernel area is less than
179     // kMaxSamples. An SkRuntimeEffect is returned to give flexibility for callers to convert it to
180     // an SkShader or a GrFragmentProcessor. Callers are responsible for providing the uniform
181     // values (using the appropriate API of the target effect type). The effect declares the
182     // following uniforms:
183     //
184     //    uniform half4  kernel[7];
185     //    uniform half4  offsets[14];
186     //    uniform shader child;
187     //
188     // 'kernel' should be set to the output of Compute2DBlurKernel(). 'offsets' should be set to the
189     // output of Compute2DBlurOffsets() with the same 'radii' passed to this function. 'child'
190     // should be bound to whatever input is intended to be blurred, and can use nearest-neighbor
191     // sampling (assuming it's an image).
192     static const SkRuntimeEffect* GetBlur2DEffect(const SkISize& radii);
193 
194     // Return a runtime effect that applies a 1D Gaussian blur, taking advantage of HW linear
195     // interpolation to accumulate adjacent pixels with fewer samples. The returned effect can be
196     // used for both X and Y axes by changing the 'dir' uniform value (see below). It can be used
197     // for all 1D blurs such that BlurLinearKernelWidth(radius) is less than or equal to
198     // kMaxSamples. Like GetBlur2DEffect(), the caller is free to convert this to an SkShader or a
199     // GrFragmentProcessor and is responsible for assigning uniforms with the appropriate API. Its
200     // uniforms are declared as:
201     //
202     //     uniform half4  offsetsAndKernel[14];
203     //     uniform half2  dir;
204     //     uniform int    radius;
205     //     uniform shader child;
206     //
207     // 'offsetsAndKernel' should be set to the output of Compute1DBlurLinearKernel(). 'radius'
208     // should match the radius passed to that function. 'dir' should either be the vector {1,0} or
209     // {0,1} for X and Y axis passes, respectively. 'child' should be bound to whatever input is
210     // intended to be blurred and must use linear sampling in order for the outer blur effect to
211     // function correctly.
212     static const SkRuntimeEffect* GetLinearBlur1DEffect(int radius);
213 
214     // Calculates a set of weights for a 2D Gaussian blur of the given sigma and radius. It is
215     // assumed that the radius was from prior calls to BlurSigmaRadius(sigma.width()|height()) and
216     // is passed in to avoid redundant calculations.
217     //
218     // The provided span is fully written. The kernel is stored in row-major order based on the
219     // provided radius. Any remaining indices in the span are zero initialized. The span must have
220     // at least KernelWidth(radius.width())*KernelWidth(radius.height()) elements.
221     //
222     // NOTE: These take spans because it can be useful to compute full kernels that are larger than
223     // what is supported in the GPU effects.
224     static void Compute2DBlurKernel(SkSize sigma,
225                                     SkISize radius,
226                                     SkSpan<float> kernel);
227 
228     // A convenience function that packs the kMaxBlurSample scalars into SkV4's to match the
229     // required type of the uniforms in GetBlur2DEffect().
230     static void Compute2DBlurKernel(SkSize sigma,
231                                     SkISize radius,
232                                     std::array<SkV4, kMaxSamples/4>& kernel);
233 
234     // A convenience for the 2D case where one dimension has a sigma of 0.
Compute1DBlurKernel(float sigma,int radius,SkSpan<float> kernel)235     static  void Compute1DBlurKernel(float sigma, int radius, SkSpan<float> kernel) {
236         Compute2DBlurKernel(SkSize{sigma, 0.f}, SkISize{radius, 0}, kernel);
237     }
238 
239     // Utility function to fill in 'offsets' for the effect returned by GetBlur2DEffect(). It
240     // automatically fills in the elements beyond the kernel size with the last real offset to
241     // maximize texture cache hits. Each offset is really an SkV2 but are packed into SkV4's to
242     // match the uniform declaration, and are otherwise ordered row-major.
243     static void Compute2DBlurOffsets(SkISize radius, std::array<SkV4, kMaxSamples/2>& offsets);
244 
245     // Calculates a set of weights and sampling offsets for a 1D blur that uses GPU hardware to
246     // linearly combine two logical source pixel values. This assumes that 'radius' was from a prior
247     // call to BlurSigmaRadius() and is passed in to avoid redundant calculations. To match std140
248     // uniform packing, the offset and kernel weight for adjacent samples are packed into a single
249     // SkV4 as {offset[2*i], kernel[2*i], offset[2*i+1], kernel[2*i+1]}
250     //
251     // The provided array is fully written to. The calculated values are written to indices 0
252     // through LinearKernelWidth(radius), with any remaining indices zero initialized.
253     //
254     // NOTE: This takes an array of a constrained size because its main use is calculating uniforms
255     // for an effect with a matching constraint. Knowing the size of the linear kernel means the
256     // full kernel can be stored on the stack internally.
257     static void Compute1DBlurLinearKernel(float sigma,
258                                           int radius,
259                                           std::array<SkV4, kMaxSamples/2>& offsetsAndKernel);
260 
261 };
262 
263 #endif // SkBlurEngine_DEFINED
264