xref: /aosp_15_r20/external/skia/src/effects/imagefilters/SkMatrixConvolutionImageFilter.cpp (revision c8dee2aa9b3f27cf6c858bd81872bdeb2c07ed17)
1 /*
2  * Copyright 2012 The Android Open Source Project
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/effects/imagefilters/SkMatrixConvolutionImageFilter.h"
9 
10 #include "include/core/SkAlphaType.h"
11 #include "include/core/SkBitmap.h"
12 #include "include/core/SkColorType.h"
13 #include "include/core/SkFlattenable.h"
14 #include "include/core/SkImage.h"
15 #include "include/core/SkImageFilter.h"
16 #include "include/core/SkImageInfo.h"
17 #include "include/core/SkM44.h"
18 #include "include/core/SkPoint.h"
19 #include "include/core/SkRect.h"
20 #include "include/core/SkRefCnt.h"
21 #include "include/core/SkSamplingOptions.h"
22 #include "include/core/SkScalar.h"
23 #include "include/core/SkShader.h"
24 #include "include/core/SkSize.h"
25 #include "include/core/SkTileMode.h"
26 #include "include/core/SkTypes.h"
27 #include "include/effects/SkImageFilters.h"
28 #include "include/effects/SkRuntimeEffect.h"
29 #include "include/private/base/SkMath.h"
30 #include "include/private/base/SkSpan_impl.h"
31 #include "include/private/base/SkTArray.h"
32 #include "include/private/base/SkTemplates.h"
33 #include "src/base/SkSafeMath.h"
34 #include "src/core/SkImageFilterTypes.h"
35 #include "src/core/SkImageFilter_Base.h"
36 #include "src/core/SkKnownRuntimeEffects.h"
37 #include "src/core/SkPicturePriv.h"
38 #include "src/core/SkReadBuffer.h"
39 #include "src/core/SkRectPriv.h"
40 #include "src/core/SkWriteBuffer.h"
41 
42 #include <cstdint>
43 #include <cstring>
44 #include <optional>
45 #include <utility>
46 
47 using namespace skia_private;
48 using namespace MatrixConvolutionImageFilter;
49 
50 namespace {
51 
52 static_assert(kLargeKernelSize % 4 == 0, "Must be a multiple of 4");
53 static_assert(kSmallKernelSize <= kLargeKernelSize, "Small kernel size must be <= max size");
54 // The uniform array storing the kernel is packed into half4's so that we don't waste space
55 // forcing array elements out to 16-byte alignment when using std140.
56 static_assert(kMaxUniformKernelSize % 4 == 0, "Must be a multiple of 4");
57 
58 SkBitmap create_kernel_bitmap(const SkISize& kernelSize, const float* kernel,
59                               float* innerGain, float* innerBias);
60 
61 class SkMatrixConvolutionImageFilter final : public SkImageFilter_Base {
62 public:
SkMatrixConvolutionImageFilter(const SkISize & kernelSize,const SkScalar * kernel,SkScalar gain,SkScalar bias,const SkIPoint & kernelOffset,bool convolveAlpha,sk_sp<SkImageFilter> const * input)63     SkMatrixConvolutionImageFilter(const SkISize& kernelSize, const SkScalar* kernel,
64                                    SkScalar gain, SkScalar bias, const SkIPoint& kernelOffset,
65                                    bool convolveAlpha, sk_sp<SkImageFilter> const* input)
66             : SkImageFilter_Base(input, 1)
67             , fKernel(kernel, kernelSize.width() * kernelSize.height())
68             , fKernelSize(kernelSize)
69             , fKernelOffset({kernelOffset.fX, kernelOffset.fY})
70             , fGain(gain)
71             , fBias(bias)
72             , fConvolveAlpha(convolveAlpha) {
73         // The public factory should have ensured these before creating this object.
74         SkASSERT(SkSafeMath::Mul(kernelSize.fWidth, kernelSize.fHeight) <= kLargeKernelSize);
75         SkASSERT(kernelSize.fWidth >= 1 && kernelSize.fHeight >= 1);
76         SkASSERT(kernelOffset.fX >= 0 && kernelOffset.fX < kernelSize.fWidth);
77         SkASSERT(kernelOffset.fY >= 0 && kernelOffset.fY < kernelSize.fHeight);
78 
79         // Does nothing for small kernels, otherwise encodes kernel into an A8 image.
80         fKernelBitmap = create_kernel_bitmap(kernelSize, kernel, &fInnerGain, &fInnerBias);
81     }
82 
83     SkRect computeFastBounds(const SkRect& bounds) const override;
84 
85 protected:
86     void flatten(SkWriteBuffer&) const override;
87 
88 private:
89     friend void ::SkRegisterMatrixConvolutionImageFilterFlattenable();
SK_FLATTENABLE_HOOKS(SkMatrixConvolutionImageFilter)90     SK_FLATTENABLE_HOOKS(SkMatrixConvolutionImageFilter)
91 
92     bool onAffectsTransparentBlack() const override {
93         // affectsTransparentBlack() is conflated with "canComputeFastBounds" and MatrixConvolution
94         // is unique in that it might not produce unbounded output, but we can't calculate the
95         // fast bounds because the kernel is applied in device space and no transform is provided
96         // with that API.
97         // TODO(skbug.com/14617): Accept a matrix in computeFastBounds() so that we can handle the
98         // layer-space kernel case.
99 
100         // That issue aside, a matrix convolution can affect transparent black when it has a
101         // non-zero bias and convolves alpha (if it doesn't convolve the alpha channel then the bias
102         // applied to RGB doesn't matter for transparent black pixels).
103         // NOTE: The crop image filters that wrap the matrix convolution to apply tile modes will
104         // reset this property when possible.
105         return true;
106     }
107 
108     skif::FilterResult onFilterImage(const skif::Context& context) const override;
109 
110     skif::LayerSpace<SkIRect> onGetInputLayerBounds(
111             const skif::Mapping& mapping,
112             const skif::LayerSpace<SkIRect>& desiredOutput,
113             std::optional<skif::LayerSpace<SkIRect>> contentBounds) const override;
114 
115     std::optional<skif::LayerSpace<SkIRect>> onGetOutputLayerBounds(
116             const skif::Mapping& mapping,
117             std::optional<skif::LayerSpace<SkIRect>> contentBounds) const override;
118 
119     // Helper functions to adjust 'bounds' by the kernel size and offset, either for what would be
120     // sampled when covering 'bounds', or what could produce values when applied to 'bounds'.
121     skif::LayerSpace<SkIRect> boundsSampledByKernel(const skif::LayerSpace<SkIRect>& bounds) const;
122     skif::LayerSpace<SkIRect> boundsAffectedByKernel(const skif::LayerSpace<SkIRect>& bounds) const;
123 
124     sk_sp<SkShader> createShader(const skif::Context& ctx, sk_sp<SkShader> input) const;
125 
126     // Original kernel data, preserved for serialization even if it was encoded into fKernelBitmap
127     TArray<float> fKernel;
128 
129     // Unlike the majority of image filters, the kernel is applied as-is to the layer-space pixels.
130     // This means that the kernel size and offset are always in the layer coordinate system.
131     skif::LayerSpace<SkISize>  fKernelSize;
132     skif::LayerSpace<skif::IVector> fKernelOffset;
133 
134     float fGain;
135     float fBias; // NOTE: This is assumed to be in [0-255] for historical reasons
136     bool  fConvolveAlpha;
137 
138     // Derived from fKernel when larger than what we will upload as uniforms; fInnerBias and
139     // fInnerGain  reconstruct the original coefficient from unorm8 data as (a+innerBias)*innerGain
140     // Since these are derived, they are not serialized.
141     SkBitmap fKernelBitmap;
142     float fInnerBias;
143     float fInnerGain;
144 };
145 
146 // LayerSpace doesn't have a clean type to represent 4 separate edge deltas, but the result
147 // is a valid layer-space rectangle, so just go back to the underlying SkIRect temporarily.
adjust(const skif::LayerSpace<SkIRect> & rect,int dl,int dt,int dr,int db)148 skif::LayerSpace<SkIRect> adjust(const skif::LayerSpace<SkIRect>& rect,
149                                  int dl, int dt, int dr, int db) {
150     SkIRect adjusted = SkIRect(rect);
151     adjusted.adjust(dl, dt, dr, db);
152     return skif::LayerSpace<SkIRect>(adjusted);
153 }
154 
quantize_by_kernel_size(int kernelSize)155 std::pair<int, SkKnownRuntimeEffects::StableKey> quantize_by_kernel_size(int kernelSize) {
156     if (kernelSize < kMaxUniformKernelSize) {
157         return { kMaxUniformKernelSize, SkKnownRuntimeEffects::StableKey::kMatrixConvUniforms };
158     } else if (kernelSize <= kSmallKernelSize) {
159         return { kSmallKernelSize, SkKnownRuntimeEffects::StableKey::kMatrixConvTexSm };
160     }
161 
162     return { kLargeKernelSize, SkKnownRuntimeEffects::StableKey::kMatrixConvTexLg };
163 }
164 
create_kernel_bitmap(const SkISize & kernelSize,const float * kernel,float * innerGain,float * innerBias)165 SkBitmap create_kernel_bitmap(const SkISize& kernelSize, const float* kernel,
166                               float* innerGain, float* innerBias) {
167     int length = kernelSize.fWidth * kernelSize.fHeight;
168     auto [quantizedKernelSize, key] = quantize_by_kernel_size(length);
169     if (key == SkKnownRuntimeEffects::StableKey::kMatrixConvUniforms) {
170         // No bitmap is needed to store the kernel on the GPU
171         *innerGain = 1.f;
172         *innerBias = 0.f;
173         return {};
174     }
175 
176 
177     // The convolution kernel is "big". The SVG spec has no upper limit on what's supported so
178     // store the kernel in a SkBitmap that will be uploaded to a data texture. We could
179     // implement a more straight forward evaluation loop for the CPU backend, but kernels of
180     // this size are already going to be very slow so we accept the extra indirection to
181     // keep the code paths consolidated.
182     //
183     // We store the data in A8 for universal support, but this requires normalizing the values
184     // and adding an extra inner bias operation to the shader. We could store values in A16 or
185     // A32 for improved accuracy but that would require querying GPU capabilities, which
186     // prevents creating the bitmap once during initialization. Even on the GPU, kernels larger
187     // than 5x5 quickly exceed realtime capabilities, so the loss of precision isn't a great
188     // concern either.
189     float min = kernel[0];
190     float max = kernel[0];
191     for (int i = 1; i < length; ++i) {
192         if (kernel[i] < min) {
193             min = kernel[i];
194         }
195         if (kernel[i] > max) {
196             max = kernel[i];
197         }
198     }
199 
200     *innerGain = max - min;
201     *innerBias = min;
202     // Treat a near-0 gain (i.e. box blur) as 1 and let innerBias move everything to final value.
203     if (SkScalarNearlyZero(*innerGain)) {
204         *innerGain = 1.f;
205     }
206 
207     SkBitmap kernelBM;
208     if (!kernelBM.tryAllocPixels(SkImageInfo::Make({ quantizedKernelSize, 1 },
209                                                    kAlpha_8_SkColorType,
210                                                    kPremul_SkAlphaType))) {
211         // OOM so return an empty bitmap, which will be detected later on in onFilterImage().
212         return {};
213     }
214 
215     for (int i = 0; i < length; ++i) {
216         *kernelBM.getAddr8(i, 0) = SkScalarRoundToInt(255 * (kernel[i] - min) / *innerGain);
217     }
218     for (int i = length; i < quantizedKernelSize; ++i) {
219         *kernelBM.getAddr8(i, 0) = 0;
220     }
221 
222     kernelBM.setImmutable();
223     return kernelBM;
224 }
225 
226 } // anonymous namespace
227 
MatrixConvolution(const SkISize & kernelSize,const SkScalar kernel[],SkScalar gain,SkScalar bias,const SkIPoint & kernelOffset,SkTileMode tileMode,bool convolveAlpha,sk_sp<SkImageFilter> input,const CropRect & cropRect)228 sk_sp<SkImageFilter> SkImageFilters::MatrixConvolution(const SkISize& kernelSize,
229                                                        const SkScalar kernel[],
230                                                        SkScalar gain,
231                                                        SkScalar bias,
232                                                        const SkIPoint& kernelOffset,
233                                                        SkTileMode tileMode,
234                                                        bool convolveAlpha,
235                                                        sk_sp<SkImageFilter> input,
236                                                        const CropRect& cropRect) {
237     if (kernelSize.width() < 1 || kernelSize.height() < 1) {
238         return nullptr;
239     }
240     if (SkSafeMath::Mul(kernelSize.width(), kernelSize.height()) > kLargeKernelSize) {
241         return nullptr;
242     }
243     if (!kernel) {
244         return nullptr;
245     }
246     if ((kernelOffset.fX < 0) || (kernelOffset.fX >= kernelSize.fWidth) ||
247         (kernelOffset.fY < 0) || (kernelOffset.fY >= kernelSize.fHeight)) {
248         return nullptr;
249     }
250 
251     // The 'tileMode' behavior is not well-defined if there is no crop, so we only apply it if
252     // there is a provided 'cropRect'.
253     sk_sp<SkImageFilter> filter = std::move(input);
254     if (cropRect && tileMode != SkTileMode::kDecal) {
255         // Historically the input image was restricted to the cropRect when tiling was not kDecal
256         // so that the kernel evaluated the tiled edge conditions, while a kDecal crop only affected
257         // the output.
258         filter = SkImageFilters::Crop(*cropRect, tileMode, std::move(filter));
259     }
260     filter = sk_sp<SkImageFilter>(new SkMatrixConvolutionImageFilter(
261             kernelSize, kernel, gain, bias, kernelOffset, convolveAlpha, &filter));
262     if (cropRect) {
263         // But regardless of the tileMode, the output is decal cropped.
264         filter = SkImageFilters::Crop(*cropRect, SkTileMode::kDecal, std::move(filter));
265     }
266     return filter;
267 }
268 
SkRegisterMatrixConvolutionImageFilterFlattenable()269 void SkRegisterMatrixConvolutionImageFilterFlattenable() {
270     SK_REGISTER_FLATTENABLE(SkMatrixConvolutionImageFilter);
271     // TODO (michaelludwig) - Remove after grace period for SKPs to stop using old name
272     SkFlattenable::Register("SkMatrixConvolutionImageFilterImpl",
273                             SkMatrixConvolutionImageFilter::CreateProc);
274 }
275 
CreateProc(SkReadBuffer & buffer)276 sk_sp<SkFlattenable> SkMatrixConvolutionImageFilter::CreateProc(SkReadBuffer& buffer) {
277     SK_IMAGEFILTER_UNFLATTEN_COMMON(common, 1);
278 
279     SkISize kernelSize;
280     kernelSize.fWidth = buffer.readInt();
281     kernelSize.fHeight = buffer.readInt();
282     const int count = buffer.getArrayCount();
283 
284     const int64_t kernelArea = sk_64_mul(kernelSize.width(), kernelSize.height());
285     if (!buffer.validate(kernelArea == count)) {
286         return nullptr;
287     }
288     if (!buffer.validateCanReadN<SkScalar>(count)) {
289         return nullptr;
290     }
291     AutoSTArray<16, SkScalar> kernel(count);
292     if (!buffer.readScalarArray(kernel.get(), count)) {
293         return nullptr;
294     }
295     SkScalar gain = buffer.readScalar();
296     SkScalar bias = buffer.readScalar();
297     SkIPoint kernelOffset;
298     kernelOffset.fX = buffer.readInt();
299     kernelOffset.fY = buffer.readInt();
300 
301     SkTileMode tileMode = SkTileMode::kDecal;
302     if (buffer.isVersionLT(SkPicturePriv::kConvolutionImageFilterTilingUpdate)) {
303         tileMode = buffer.read32LE(SkTileMode::kLastTileMode);
304     } // else SkCropImageFilter handles the tile mode (if any)
305 
306     bool convolveAlpha = buffer.readBool();
307 
308     if (!buffer.isValid()) {
309         return nullptr;
310     }
311     // NOTE: For SKPs with version >= kConvolutionImageFilterTilingUpdate, tileMode will be kDecal
312     // and common.cropRect() will be null (so the factory also ignores tileMode). Any
313     // cropping/tiling will have been handled by the deserialized input/output Crop image filters.
314     return SkImageFilters::MatrixConvolution(
315                 kernelSize, kernel.get(), gain, bias, kernelOffset, tileMode,
316                 convolveAlpha, common.getInput(0), common.cropRect());
317 }
318 
flatten(SkWriteBuffer & buffer) const319 void SkMatrixConvolutionImageFilter::flatten(SkWriteBuffer& buffer) const {
320     this->SkImageFilter_Base::flatten(buffer);
321     buffer.writeInt(fKernelSize.width());
322     buffer.writeInt(fKernelSize.height());
323     buffer.writeScalarArray(fKernel.data(), fKernel.size());
324     buffer.writeScalar(fGain);
325     buffer.writeScalar(fBias);
326     buffer.writeInt(fKernelOffset.x());
327     buffer.writeInt(fKernelOffset.y());
328     buffer.writeBool(fConvolveAlpha);
329 }
330 
331 ///////////////////////////////////////////////////////////////////////////////////////////////////
332 
boundsSampledByKernel(const skif::LayerSpace<SkIRect> & bounds) const333 skif::LayerSpace<SkIRect> SkMatrixConvolutionImageFilter::boundsSampledByKernel(
334         const skif::LayerSpace<SkIRect>& bounds) const {
335     return adjust(bounds,
336                   -fKernelOffset.x(),
337                   -fKernelOffset.y(),
338                   fKernelSize.width() - fKernelOffset.x() - 1,
339                   fKernelSize.height() - fKernelOffset.y() - 1);
340 }
341 
boundsAffectedByKernel(const skif::LayerSpace<SkIRect> & bounds) const342 skif::LayerSpace<SkIRect> SkMatrixConvolutionImageFilter::boundsAffectedByKernel(
343         const skif::LayerSpace<SkIRect>& bounds) const {
344     return adjust(bounds,
345                   fKernelOffset.x() - fKernelSize.width() + 1,
346                   fKernelOffset.y() - fKernelSize.height() + 1,
347                   fKernelOffset.x(),
348                   fKernelOffset.y());
349 }
350 
351 
352 
createShader(const skif::Context & ctx,sk_sp<SkShader> input) const353 sk_sp<SkShader> SkMatrixConvolutionImageFilter::createShader(const skif::Context& ctx,
354                                                              sk_sp<SkShader> input) const {
355     const int kernelLength = fKernelSize.width() * fKernelSize.height();
356     auto [_, key] = quantize_by_kernel_size(kernelLength);
357     const bool useTextureShader = (key != SkKnownRuntimeEffects::StableKey::kMatrixConvUniforms);
358     if (useTextureShader && fKernelBitmap.empty()) {
359         return nullptr; // No actual kernel data to work with from a prior OOM
360     }
361 
362     const SkRuntimeEffect* matrixConvEffect = GetKnownRuntimeEffect(key);
363 
364     SkRuntimeShaderBuilder builder(sk_ref_sp(matrixConvEffect));
365     builder.child("child") = std::move(input);
366 
367     if (useTextureShader) {
368         sk_sp<SkImage> cachedKernel = ctx.backend()->getCachedBitmap(fKernelBitmap);
369         if (!cachedKernel) {
370             return nullptr;
371         }
372         builder.child("kernel") = cachedKernel->makeRawShader(SkFilterMode::kNearest);
373         builder.uniform("innerGainAndBias") = SkV2{fInnerGain, fInnerBias};
374     } else {
375         float paddedKernel[kMaxUniformKernelSize];
376         memcpy(paddedKernel, fKernel.data(), kernelLength*sizeof(float));
377         memset(paddedKernel+kernelLength, 0, (kMaxUniformKernelSize - kernelLength)*sizeof(float));
378 
379         builder.uniform("kernel").set(paddedKernel, kMaxUniformKernelSize);
380     }
381 
382     builder.uniform("size") = SkISize(fKernelSize);
383     builder.uniform("offset") = skif::IVector(fKernelOffset);
384     // Scale the user-provided bias by 1/255 to match the [0,1] color channel range
385     builder.uniform("gainAndBias") = SkV2{fGain, fBias / 255.f};
386     builder.uniform("convolveAlpha") = fConvolveAlpha ? 1 : 0;
387 
388     return builder.makeShader();
389 }
390 
onFilterImage(const skif::Context & context) const391 skif::FilterResult SkMatrixConvolutionImageFilter::onFilterImage(
392         const skif::Context& context) const {
393     using ShaderFlags = skif::FilterResult::ShaderFlags;
394 
395     skif::LayerSpace<SkIRect> requiredInput = this->boundsSampledByKernel(context.desiredOutput());
396     skif::FilterResult childOutput =
397             this->getChildOutput(0, context.withNewDesiredOutput(requiredInput));
398 
399     skif::LayerSpace<SkIRect> outputBounds;
400     if (fConvolveAlpha && fBias != 0.f) {
401         // The convolution will produce a non-trivial value for every pixel so fill desired output.
402         outputBounds = context.desiredOutput();
403     } else {
404         // Calculate the possible extent of the convolution given what was actually produced by the
405         // child filter and then intersect that with the desired output.
406         outputBounds = this->boundsAffectedByKernel(childOutput.layerBounds());
407         if (!outputBounds.intersect(context.desiredOutput())) {
408             return {};
409         }
410     }
411 
412     skif::FilterResult::Builder builder{context};
413     builder.add(childOutput,
414                 this->boundsSampledByKernel(outputBounds),
415                 ShaderFlags::kSampledRepeatedly);
416     return builder.eval([&](SkSpan<sk_sp<SkShader>> inputs) {
417         return this->createShader(context, inputs[0]);
418     }, outputBounds);
419 }
420 
onGetInputLayerBounds(const skif::Mapping & mapping,const skif::LayerSpace<SkIRect> & desiredOutput,std::optional<skif::LayerSpace<SkIRect>> contentBounds) const421 skif::LayerSpace<SkIRect> SkMatrixConvolutionImageFilter::onGetInputLayerBounds(
422         const skif::Mapping& mapping,
423         const skif::LayerSpace<SkIRect>& desiredOutput,
424         std::optional<skif::LayerSpace<SkIRect>> contentBounds) const {
425     // Adjust the desired output bounds by the kernel size to avoid evaluating edge conditions, and
426     // then recurse to the child filter.
427     skif::LayerSpace<SkIRect> requiredInput = this->boundsSampledByKernel(desiredOutput);
428     return this->getChildInputLayerBounds(0, mapping, requiredInput, contentBounds);
429 }
430 
onGetOutputLayerBounds(const skif::Mapping & mapping,std::optional<skif::LayerSpace<SkIRect>> contentBounds) const431 std::optional<skif::LayerSpace<SkIRect>> SkMatrixConvolutionImageFilter::onGetOutputLayerBounds(
432         const skif::Mapping& mapping,
433         std::optional<skif::LayerSpace<SkIRect>> contentBounds) const {
434     if (fConvolveAlpha && fBias != 0.f) {
435         // Applying the kernel as a convolution to fully transparent black will result in 0 for
436         // each channel, unless the bias itself shifts this "zero-point". However, when the alpha
437         // channel is not convolved, the original a=0 is preserved and producing a premul color
438         // discards the non-zero bias. Convolving the alpha channel and a non-zero bias can mean
439         // the transparent black pixels outside of any input image become non-transparent black.
440         return skif::LayerSpace<SkIRect>::Unbounded();
441     }
442 
443     // Otherwise apply the kernel to the output bounds of the child filter.
444     auto outputBounds = this->getChildOutputLayerBounds(0, mapping, contentBounds);
445     if (outputBounds) {
446         return this->boundsAffectedByKernel(*outputBounds);
447     } else {
448         return skif::LayerSpace<SkIRect>::Unbounded();
449     }
450 }
451 
computeFastBounds(const SkRect & bounds) const452 SkRect SkMatrixConvolutionImageFilter::computeFastBounds(const SkRect& bounds) const {
453     // See onAffectsTransparentBlack(), but without knowing the local-to-device transform, we don't
454     // know how many pixels will be sampled by the kernel. Return unbounded to match the
455     // expectations of an image filter that "affects" transparent black.
456     return SkRectPriv::MakeLargeS32();
457 }
458