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