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