| /* |
| * Copyright 2023 Google LLC |
| * |
| * Use of this source code is governed by a BSD-style license that can be |
| * found in the LICENSE file. |
| */ |
| |
| #ifndef SkBlurEngine_DEFINED |
| #define SkBlurEngine_DEFINED |
| |
| #include "include/core/SkM44.h" // IWYU pragma: keep |
| #include "include/core/SkRefCnt.h" |
| #include "include/core/SkSize.h" |
| #include "include/core/SkSpan.h" |
| #include "include/private/base/SkFloatingPoint.h" |
| |
| #include <array> |
| |
| class SkColorSpace; |
| class SkDevice; |
| class SkRuntimeEffect; |
| class SkShader; |
| class SkSpecialImage; |
| struct SkImageInfo; |
| struct SkIRect; |
| |
| enum class SkTileMode; |
| enum SkColorType : int; |
| |
| /** |
| * SkBlurEngine is a backend-agnostic provider of blur algorithms. Each Skia backend defines a blur |
| * engine with a set of supported algorithms and/or implementations. A given implementation may be |
| * optimized for a particular color type, sigma range, or available hardware. Each engine and its |
| * algorithms are assumed to operate only on SkImages corresponding to its Skia backend, and will |
| * produce output SkImages of the same type. |
| * |
| * Algorithms are allowed to specify a maximum supported sigma. If the desired sigma is higher than |
| * this, the input image and output region must be downscaled by the caller before invoking the |
| * algorithm. This is to provide the most flexibility for input representation (e.g. directly |
| * rasterize at half resolution or apply deferred filter effects during the first downsample pass). |
| * |
| * skif::FilterResult::Builder::blur() is a convenient wrapper around the blur engine and |
| * automatically handles resizing. |
| */ |
| class SkBlurEngine { |
| public: |
| class Algorithm; |
| |
| virtual ~SkBlurEngine() = default; |
| |
| // Returns an Algorithm ideal for the requested 'sigma' that will support sampling an image of |
| // the given 'colorType'. If the engine does not support the requested configuration, it returns |
| // null. The engine maintains the lifetime of its algorithms, so the returned non-null |
| // Algorithms live as long as the engine does. |
| virtual const Algorithm* findAlgorithm(SkSize sigma, |
| SkColorType colorType) const = 0; |
| |
| // TODO: Consolidate common utility functions from SkBlurMask.h into this header. |
| |
| // Any sigmas smaller than this are effectively an identity blur so can skip convolution at a |
| // higher level. The value was chosen because it corresponds roughly to a radius of 1/10px, and |
| // because 2*sigma^2 is slightly greater than SK_ScalarNearlyZero. |
| static constexpr bool IsEffectivelyIdentity(float sigma) { return sigma <= 0.03f; } |
| |
| // Convert from a sigma Gaussian standard deviation to a pixel radius such that pixels outside |
| // the radius would have an insignificant contribution to the final blurred value. |
| static int SigmaToRadius(float sigma) { |
| // sk_float_ceil2int is not constexpr |
| return IsEffectivelyIdentity(sigma) ? 0 : sk_float_ceil2int(3.f * sigma); |
| } |
| }; |
| |
| class SkBlurEngine::Algorithm { |
| public: |
| virtual ~Algorithm() = default; |
| |
| // The maximum sigma that can be passed to blur() in the X and/or Y sigma values. Larger |
| // requested sigmas must manually downscale the input image and upscale the output image. |
| virtual float maxSigma() const = 0; |
| |
| // Whether or not the SkTileMode can be passed to blur() must be SkTileMode::kDecal, or if any |
| // tile mode is supported. If only kDecal is supported, then callers must manually apply the |
| // tilemode and account for that in the src and dst bounds passed into blur(). If this returns |
| // false, then the algorithm supports all SkTileModes. |
| // TODO: Once CPU blurs support all tile modes, this API can go away. |
| virtual bool supportsOnlyDecalTiling() const = 0; |
| |
| // Produce a blurred image that fills 'dstRect' (their dimensions will match). 'dstRect's top |
| // left corner defines the output's location relative to the 'src' image. 'srcRect' restricts |
| // the pixels that are included in the blur and is also relative to 'src'. The 'tileMode' |
| // applies to the boundary of 'srcRect', which must be contained within 'src's dimensions. |
| // |
| // 'srcRect' and 'dstRect' may be different sizes and even be disjoint. |
| // |
| // The returned SkImage will have the same color type and colorspace as the input image. It will |
| // be an SkImage type matching the underlying Skia backend. If the 'src' SkImage is not a |
| // compatible SkImage type, null is returned. |
| // TODO(b/299474380): This only takes SkSpecialImage to work with skif::FilterResult and |
| // SkDevice::snapSpecial(); SkImage would be ideal. |
| virtual sk_sp<SkSpecialImage> blur(SkSize sigma, |
| sk_sp<SkSpecialImage> src, |
| const SkIRect& srcRect, |
| SkTileMode tileMode, |
| const SkIRect& dstRect) const = 0; |
| }; |
| |
| /** |
| * The default blur implementation uses internal runtime effects to evaluate either a single 2D |
| * kernel within a shader, or performs two 1D blur passes. This algorithm is backend agnostic but |
| * must be subclassed per backend to define the SkDevice creation function. |
| */ |
| class SkShaderBlurAlgorithm : public SkBlurEngine::Algorithm { |
| public: |
| float maxSigma() const override { return kMaxLinearSigma; } |
| bool supportsOnlyDecalTiling() const override { return false; } |
| |
| sk_sp<SkSpecialImage> blur(SkSize sigma, |
| sk_sp<SkSpecialImage> src, |
| const SkIRect& srcRect, |
| SkTileMode tileMode, |
| const SkIRect& dstRect) const override; |
| |
| private: |
| // Create a new surface, which can be approx-fit and have undefined contents. |
| virtual sk_sp<SkDevice> makeDevice(const SkImageInfo&) const = 0; |
| |
| sk_sp<SkSpecialImage> renderBlur(sk_sp<SkShader> blurEffect, |
| const SkIRect& dstRect, |
| SkColorType colorType, |
| sk_sp<SkColorSpace> colorSpace) const; |
| sk_sp<SkSpecialImage> evalBlur2D(SkSize sigma, |
| SkISize radii, |
| sk_sp<SkSpecialImage> input, |
| const SkIRect& srcRect, |
| SkTileMode tileMode, |
| const SkIRect& dstRect) const; |
| sk_sp<SkSpecialImage> evalBlur1D(float sigma, |
| int radius, |
| SkV2 dir, |
| sk_sp<SkSpecialImage> input, |
| SkIRect srcRect, |
| SkTileMode tileMode, |
| SkIRect dstRect) const; |
| |
| // TODO: These are internal details of the blur shaders, but are public for now because multiple |
| // backends invoke the blur shaders directly. Once everything just goes through this class, these |
| // can be hidden. |
| public: |
| |
| // The kernel width of a Gaussian blur of the given pixel radius, when all pixels are sampled. |
| static constexpr int KernelWidth(int radius) { return 2 * radius + 1; } |
| |
| // The kernel width of a Gaussian blur of the given pixel radius, that relies on HW bilinear |
| // filtering to combine adjacent pixels. |
| static constexpr int LinearKernelWidth(int radius) { return radius + 1; } |
| |
| // The maximum sigma that can be computed without downscaling is based on the number of uniforms |
| // and texture samples the effects will make in a single pass. For 1D passes, the number of |
| // samples is equal to `LinearKernelWidth`; for 2D passes, it is equal to |
| // `KernelWidth(radiusX)*KernelWidth(radiusY)`. This maps back to different maximum sigmas |
| // depending on the approach used, as well as the ratio between the sigmas for the X and Y axes |
| // if a 2D blur is performed. |
| static constexpr int kMaxSamples = 28; |
| |
| // TODO(b/297393474): Update max linear sigma to 9; it had been 4 when a full 1D kernel was |
| // used, but never updated after the linear filtering optimization reduced the number of |
| // sample() calls required. Keep it at 4 for now to better isolate performance changes due to |
| // switching to a runtime effect and constant loop structure. |
| static constexpr float kMaxLinearSigma = 4.f; // -> radius = 27 -> linear kernel width = 28 |
| // NOTE: There is no defined kMaxBlurSigma for direct 2D blurs since it is entirely dependent on |
| // the ratio between the two axes' sigmas, but generally it will be small on the order of a |
| // 5x5 kernel. |
| |
| // Return a runtime effect that applies a 2D Gaussian blur in a single pass. The returned effect |
| // can perform arbitrarily sized blur kernels so long as the kernel area is less than |
| // kMaxSamples. An SkRuntimeEffect is returned to give flexibility for callers to convert it to |
| // an SkShader or a GrFragmentProcessor. Callers are responsible for providing the uniform |
| // values (using the appropriate API of the target effect type). The effect declares the |
| // following uniforms: |
| // |
| // uniform half4 kernel[7]; |
| // uniform half4 offsets[14]; |
| // uniform shader child; |
| // |
| // 'kernel' should be set to the output of Compute2DBlurKernel(). 'offsets' should be set to the |
| // output of Compute2DBlurOffsets() with the same 'radii' passed to this function. 'child' |
| // should be bound to whatever input is intended to be blurred, and can use nearest-neighbor |
| // sampling (assuming it's an image). |
| static const SkRuntimeEffect* GetBlur2DEffect(const SkISize& radii); |
| |
| // Return a runtime effect that applies a 1D Gaussian blur, taking advantage of HW linear |
| // interpolation to accumulate adjacent pixels with fewer samples. The returned effect can be |
| // used for both X and Y axes by changing the 'dir' uniform value (see below). It can be used |
| // for all 1D blurs such that BlurLinearKernelWidth(radius) is less than or equal to |
| // kMaxSamples. Like GetBlur2DEffect(), the caller is free to convert this to an SkShader or a |
| // GrFragmentProcessor and is responsible for assigning uniforms with the appropriate API. Its |
| // uniforms are declared as: |
| // |
| // uniform half4 offsetsAndKernel[14]; |
| // uniform half2 dir; |
| // uniform int radius; |
| // uniform shader child; |
| // |
| // 'offsetsAndKernel' should be set to the output of Compute1DBlurLinearKernel(). 'radius' |
| // should match the radius passed to that function. 'dir' should either be the vector {1,0} or |
| // {0,1} for X and Y axis passes, respectively. 'child' should be bound to whatever input is |
| // intended to be blurred and must use linear sampling in order for the outer blur effect to |
| // function correctly. |
| static const SkRuntimeEffect* GetLinearBlur1DEffect(int radius); |
| |
| // Calculates a set of weights for a 2D Gaussian blur of the given sigma and radius. It is |
| // assumed that the radius was from prior calls to BlurSigmaRadius(sigma.width()|height()) and |
| // is passed in to avoid redundant calculations. |
| // |
| // The provided span is fully written. The kernel is stored in row-major order based on the |
| // provided radius. Any remaining indices in the span are zero initialized. The span must have |
| // at least KernelWidth(radius.width())*KernelWidth(radius.height()) elements. |
| // |
| // NOTE: These take spans because it can be useful to compute full kernels that are larger than |
| // what is supported in the GPU effects. |
| static void Compute2DBlurKernel(SkSize sigma, |
| SkISize radius, |
| SkSpan<float> kernel); |
| |
| // A convenience function that packs the kMaxBlurSample scalars into SkV4's to match the |
| // required type of the uniforms in GetBlur2DEffect(). |
| static void Compute2DBlurKernel(SkSize sigma, |
| SkISize radius, |
| std::array<SkV4, kMaxSamples/4>& kernel); |
| |
| // A convenience for the 2D case where one dimension has a sigma of 0. |
| static void Compute1DBlurKernel(float sigma, int radius, SkSpan<float> kernel) { |
| Compute2DBlurKernel(SkSize{sigma, 0.f}, SkISize{radius, 0}, kernel); |
| } |
| |
| // Utility function to fill in 'offsets' for the effect returned by GetBlur2DEffect(). It |
| // automatically fills in the elements beyond the kernel size with the last real offset to |
| // maximize texture cache hits. Each offset is really an SkV2 but are packed into SkV4's to |
| // match the uniform declaration, and are otherwise ordered row-major. |
| static void Compute2DBlurOffsets(SkISize radius, std::array<SkV4, kMaxSamples/2>& offsets); |
| |
| // Calculates a set of weights and sampling offsets for a 1D blur that uses GPU hardware to |
| // linearly combine two logical source pixel values. This assumes that 'radius' was from a prior |
| // call to BlurSigmaRadius() and is passed in to avoid redundant calculations. To match std140 |
| // uniform packing, the offset and kernel weight for adjacent samples are packed into a single |
| // SkV4 as {offset[2*i], kernel[2*i], offset[2*i+1], kernel[2*i+1]} |
| // |
| // The provided array is fully written to. The calculated values are written to indices 0 |
| // through LinearKernelWidth(radius), with any remaining indices zero initialized. |
| // |
| // NOTE: This takes an array of a constrained size because its main use is calculating uniforms |
| // for an effect with a matching constraint. Knowing the size of the linear kernel means the |
| // full kernel can be stored on the stack internally. |
| static void Compute1DBlurLinearKernel(float sigma, |
| int radius, |
| std::array<SkV4, kMaxSamples/2>& offsetsAndKernel); |
| |
| }; |
| |
| #endif // SkBlurEngine_DEFINED |