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