Source code

Revision control

Copy as Markdown

Other Tools

/*
* Copyright 2012 The Android Open Source Project
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "include/effects/SkImageFilters.h"
#include "include/core/SkAlphaType.h"
#include "include/core/SkBitmap.h"
#include "include/core/SkColorType.h"
#include "include/core/SkFlattenable.h"
#include "include/core/SkImage.h"
#include "include/core/SkImageFilter.h"
#include "include/core/SkImageInfo.h"
#include "include/core/SkM44.h"
#include "include/core/SkPoint.h"
#include "include/core/SkRect.h"
#include "include/core/SkRefCnt.h"
#include "include/core/SkSamplingOptions.h"
#include "include/core/SkScalar.h"
#include "include/core/SkShader.h"
#include "include/core/SkSize.h"
#include "include/core/SkString.h"
#include "include/core/SkTileMode.h"
#include "include/core/SkTypes.h"
#include "include/effects/SkRuntimeEffect.h"
#include "include/private/base/SkMath.h"
#include "include/private/base/SkMutex.h"
#include "include/private/base/SkSpan_impl.h"
#include "include/private/base/SkTArray.h"
#include "include/private/base/SkTemplates.h"
#include "include/private/base/SkThreadAnnotations.h"
#include "src/base/SkMathPriv.h"
#include "src/base/SkSafeMath.h"
#include "src/core/SkImageFilterTypes.h"
#include "src/core/SkImageFilter_Base.h"
#include "src/core/SkLRUCache.h"
#include "src/core/SkPicturePriv.h"
#include "src/core/SkReadBuffer.h"
#include "src/core/SkRectPriv.h"
#include "src/core/SkRuntimeEffectPriv.h"
#include "src/core/SkWriteBuffer.h"
#include <cstdint>
#include <cstring>
#include <optional>
#include <utility>
using namespace skia_private;
namespace {
// The matrix convolution image filter applies the convolution naively, it does not use any DFT to
// convert the input images into the frequency domain. As such, kernels can quickly become too
// slow to run in a reasonable amount of time (and anyone using a giant kernel should not be
// relying on Skia to perform the calculations). 256 as a limit on the kernel size is somewhat
// arbitrary but should, hopefully, not cause existing clients/websites to fail when historically
// there was no upper limit.
// Note: SkSL balks (w/ a "program is too large" error) whenever the number of kernel values
// is >= 2048 (e.g., 8x256, 16x128, ...) so that should be a pretty good upper limit for what
// is being seen in the wild.
static constexpr int kMaxKernelSize = 256;
// The uniform-based kernel shader can store 28 values in any order layout (28x1, 1x25, 5x5, and
// smaller orders like 3x3 or 5x4, etc.), but must be a multiple of 4 for better packing in std140.
static constexpr int kMaxUniformKernelSize = 28;
SkBitmap create_kernel_bitmap(const SkISize& kernelSize, const float* kernel,
float* innerGain, float* innerBias);
class SkMatrixConvolutionImageFilter final : public SkImageFilter_Base {
public:
SkMatrixConvolutionImageFilter(const SkISize& kernelSize, const SkScalar* kernel,
SkScalar gain, SkScalar bias, const SkIPoint& kernelOffset,
bool convolveAlpha, sk_sp<SkImageFilter> const* input)
: SkImageFilter_Base(input, 1)
, fKernel(kernel, kernelSize.width() * kernelSize.height())
, fKernelSize(kernelSize)
, fKernelOffset({kernelOffset.fX, kernelOffset.fY})
, fGain(gain)
, fBias(bias)
, fConvolveAlpha(convolveAlpha) {
// The public factory should have ensured these before creating this object.
SkASSERT(SkSafeMath::Mul(kernelSize.fWidth, kernelSize.fHeight) <= kMaxKernelSize);
SkASSERT(kernelSize.fWidth >= 1 && kernelSize.fHeight >= 1);
SkASSERT(kernelOffset.fX >= 0 && kernelOffset.fX < kernelSize.fWidth);
SkASSERT(kernelOffset.fY >= 0 && kernelOffset.fY < kernelSize.fHeight);
// Does nothing for small kernels, otherwise encodes kernel into an A8 image.
fKernelBitmap = create_kernel_bitmap(kernelSize, kernel, &fInnerGain, &fInnerBias);
}
SkRect computeFastBounds(const SkRect& bounds) const override;
protected:
void flatten(SkWriteBuffer&) const override;
private:
friend void ::SkRegisterMatrixConvolutionImageFilterFlattenable();
SK_FLATTENABLE_HOOKS(SkMatrixConvolutionImageFilter)
bool onAffectsTransparentBlack() const override {
// affectsTransparentBlack() is conflated with "canComputeFastBounds" and MatrixConvolution
// is unique in that it might not produce unbounded output, but we can't calculate the
// fast bounds because the kernel is applied in device space and no transform is provided
// with that API.
// TODO(skbug.com/14617): Accept a matrix in computeFastBounds() so that we can handle the
// layer-space kernel case.
// That issue aside, a matrix convolution can affect transparent black when it has a
// non-zero bias and convolves alpha (if it doesn't convolve the alpha channel then the bias
// applied to RGB doesn't matter for transparent black pixels).
// NOTE: The crop image filters that wrap the matrix convolution to apply tile modes will
// reset this property when possible.
return true;
}
skif::FilterResult onFilterImage(const skif::Context& context) const override;
skif::LayerSpace<SkIRect> onGetInputLayerBounds(
const skif::Mapping& mapping,
const skif::LayerSpace<SkIRect>& desiredOutput,
std::optional<skif::LayerSpace<SkIRect>> contentBounds) const override;
std::optional<skif::LayerSpace<SkIRect>> onGetOutputLayerBounds(
const skif::Mapping& mapping,
std::optional<skif::LayerSpace<SkIRect>> contentBounds) const override;
// Helper functions to adjust 'bounds' by the kernel size and offset, either for what would be
// sampled when covering 'bounds', or what could produce values when applied to 'bounds'.
skif::LayerSpace<SkIRect> boundsSampledByKernel(const skif::LayerSpace<SkIRect>& bounds) const;
skif::LayerSpace<SkIRect> boundsAffectedByKernel(const skif::LayerSpace<SkIRect>& bounds) const;
sk_sp<SkShader> createShader(const skif::Context& ctx, sk_sp<SkShader> input) const;
// Original kernel data, preserved for serialization even if it was encoded into fKernelBitmap
TArray<float> fKernel;
// Unlike the majority of image filters, the kernel is applied as-is to the layer-space pixels.
// This means that the kernel size and offset are always in the layer coordinate system.
skif::LayerSpace<SkISize> fKernelSize;
skif::LayerSpace<skif::IVector> fKernelOffset;
float fGain;
float fBias; // NOTE: This is assumed to be in [0-255] for historical reasons
bool fConvolveAlpha;
// Derived from fKernel when larger than what we will upload as uniforms; fInnerBias and
// fInnerGain reconstruct the original coefficient from unorm8 data as (a+innerBias)*innerGain
// Since these are derived, they are not serialized.
SkBitmap fKernelBitmap;
float fInnerBias;
float fInnerGain;
};
// LayerSpace doesn't have a clean type to represent 4 separate edge deltas, but the result
// is a valid layer-space rectangle, so just go back to the underlying SkIRect temporarily.
skif::LayerSpace<SkIRect> adjust(const skif::LayerSpace<SkIRect>& rect,
int dl, int dt, int dr, int db) {
SkIRect adjusted = SkIRect(rect);
adjusted.adjust(dl, dt, dr, db);
return skif::LayerSpace<SkIRect>(adjusted);
}
SkBitmap create_kernel_bitmap(const SkISize& kernelSize, const float* kernel,
float* innerGain, float* innerBias) {
int length = kernelSize.fWidth * kernelSize.fHeight;
if (length <= kMaxUniformKernelSize) {
// No bitmap is needed to store the kernel on the GPU
*innerGain = 1.f;
*innerBias = 0.f;
return {};
}
// The convolution kernel is "big". The SVG spec has no upper limit on what's supported so
// store the kernel in a SkBitmap that will be uploaded to a data texture. We could
// implement a more straight forward evaluation loop for the CPU backend, but kernels of
// this size are already going to be very slow so we accept the extra indirection to
// keep the code paths consolidated.
//
// We store the data in A8 for universal support, but this requires normalizing the values
// and adding an extra inner bias operation to the shader. We could store values in A16 or
// A32 for improved accuracy but that would require querying GPU capabilities, which
// prevents creating the bitmap once during initialization. Even on the GPU, kernels larger
// than 5x5 quickly exceed realtime capabilities, so the loss of precision isn't a great
// concern either.
float min = kernel[0];
float max = kernel[0];
for (int i = 1; i < length; ++i) {
if (kernel[i] < min) {
min = kernel[i];
}
if (kernel[i] > max) {
max = kernel[i];
}
}
*innerGain = max - min;
*innerBias = min;
// Treat a near-0 gain (i.e. box blur) as 1 and let innerBias move everything to final value.
if (SkScalarNearlyZero(*innerGain)) {
*innerGain = 1.f;
}
SkBitmap kernelBM;
if (!kernelBM.tryAllocPixels(SkImageInfo::Make(kernelSize,
kAlpha_8_SkColorType,
kPremul_SkAlphaType))) {
// OOM so return an empty bitmap, which will be detected later on in onFilterImage().
return {};
}
for (int y = 0; y < kernelSize.fHeight; ++y) {
for (int x = 0; x < kernelSize.fWidth; ++x) {
int i = y * kernelSize.fWidth + x;
*kernelBM.getAddr8(x, y) = SkScalarRoundToInt(255 * (kernel[i] - min) / *innerGain);
}
}
kernelBM.setImmutable();
return kernelBM;
}
} // end namespace
sk_sp<SkImageFilter> SkImageFilters::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) {
if (kernelSize.width() < 1 || kernelSize.height() < 1) {
return nullptr;
}
if (SkSafeMath::Mul(kernelSize.width(), kernelSize.height()) > kMaxKernelSize) {
return nullptr;
}
if (!kernel) {
return nullptr;
}
if ((kernelOffset.fX < 0) || (kernelOffset.fX >= kernelSize.fWidth) ||
(kernelOffset.fY < 0) || (kernelOffset.fY >= kernelSize.fHeight)) {
return nullptr;
}
// The 'tileMode' behavior is not well-defined if there is no crop, so we only apply it if
// there is a provided 'cropRect'.
sk_sp<SkImageFilter> filter = std::move(input);
if (cropRect && tileMode != SkTileMode::kDecal) {
// Historically the input image was restricted to the cropRect when tiling was not kDecal
// so that the kernel evaluated the tiled edge conditions, while a kDecal crop only affected
// the output.
filter = SkImageFilters::Crop(*cropRect, tileMode, std::move(filter));
}
filter = sk_sp<SkImageFilter>(new SkMatrixConvolutionImageFilter(
kernelSize, kernel, gain, bias, kernelOffset, convolveAlpha, &filter));
if (cropRect) {
// But regardless of the tileMode, the output is decal cropped.
filter = SkImageFilters::Crop(*cropRect, SkTileMode::kDecal, std::move(filter));
}
return filter;
}
void SkRegisterMatrixConvolutionImageFilterFlattenable() {
SK_REGISTER_FLATTENABLE(SkMatrixConvolutionImageFilter);
// TODO (michaelludwig) - Remove after grace period for SKPs to stop using old name
SkFlattenable::Register("SkMatrixConvolutionImageFilterImpl",
SkMatrixConvolutionImageFilter::CreateProc);
}
sk_sp<SkFlattenable> SkMatrixConvolutionImageFilter::CreateProc(SkReadBuffer& buffer) {
SK_IMAGEFILTER_UNFLATTEN_COMMON(common, 1);
SkISize kernelSize;
kernelSize.fWidth = buffer.readInt();
kernelSize.fHeight = buffer.readInt();
const int count = buffer.getArrayCount();
const int64_t kernelArea = sk_64_mul(kernelSize.width(), kernelSize.height());
if (!buffer.validate(kernelArea == count)) {
return nullptr;
}
if (!buffer.validateCanReadN<SkScalar>(count)) {
return nullptr;
}
AutoSTArray<16, SkScalar> kernel(count);
if (!buffer.readScalarArray(kernel.get(), count)) {
return nullptr;
}
SkScalar gain = buffer.readScalar();
SkScalar bias = buffer.readScalar();
SkIPoint kernelOffset;
kernelOffset.fX = buffer.readInt();
kernelOffset.fY = buffer.readInt();
SkTileMode tileMode = SkTileMode::kDecal;
if (buffer.isVersionLT(SkPicturePriv::kConvolutionImageFilterTilingUpdate)) {
tileMode = buffer.read32LE(SkTileMode::kLastTileMode);
} // else SkCropImageFilter handles the tile mode (if any)
bool convolveAlpha = buffer.readBool();
if (!buffer.isValid()) {
return nullptr;
}
// NOTE: For SKPs with version >= kConvolutionImageFilterTilingUpdate, tileMode will be kDecal
// and common.cropRect() will be null (so the factory also ignores tileMode). Any
// cropping/tiling will have been handled by the deserialized input/output Crop image filters.
return SkImageFilters::MatrixConvolution(
kernelSize, kernel.get(), gain, bias, kernelOffset, tileMode,
convolveAlpha, common.getInput(0), common.cropRect());
}
void SkMatrixConvolutionImageFilter::flatten(SkWriteBuffer& buffer) const {
this->SkImageFilter_Base::flatten(buffer);
buffer.writeInt(fKernelSize.width());
buffer.writeInt(fKernelSize.height());
buffer.writeScalarArray(fKernel.data(), fKernel.size());
buffer.writeScalar(fGain);
buffer.writeScalar(fBias);
buffer.writeInt(fKernelOffset.x());
buffer.writeInt(fKernelOffset.y());
buffer.writeBool(fConvolveAlpha);
}
///////////////////////////////////////////////////////////////////////////////////////////////////
skif::LayerSpace<SkIRect> SkMatrixConvolutionImageFilter::boundsSampledByKernel(
const skif::LayerSpace<SkIRect>& bounds) const {
return adjust(bounds,
-fKernelOffset.x(),
-fKernelOffset.y(),
fKernelSize.width() - fKernelOffset.x() - 1,
fKernelSize.height() - fKernelOffset.y() - 1);
}
skif::LayerSpace<SkIRect> SkMatrixConvolutionImageFilter::boundsAffectedByKernel(
const skif::LayerSpace<SkIRect>& bounds) const {
return adjust(bounds,
fKernelOffset.x() - fKernelSize.width() + 1,
fKernelOffset.y() - fKernelSize.height() + 1,
fKernelOffset.x(),
fKernelOffset.y());
}
// There are two shader variants: a small kernel version that stores the matrix in uniforms
// and iterates in 1D (selected when texWidth==0 & texHeight==0); and a large kernel version
// that stores the matrix in a texture. The 2D texture kernel shader still uses constant-length
// for loops (up to the texWidth and texHeight passed in); the actual kernel size is uploaded
// as a uniform, allowing the shaders to be quantized.
static sk_sp<SkRuntimeEffect> get_runtime_effect(int texWidth, int texHeight) {
// While the loop structure and uniforms are different, pieces of the algorithm are common and
// defined statically for re-use in the two shaders:
static const char* kHeaderSkSL =
"uniform int2 size;"
"uniform int2 offset;"
"uniform half2 gainAndBias;"
"uniform int convolveAlpha;" // FIXME not a full int?
"uniform shader child;"
"half4 main(float2 coord) {"
"half4 sum = half4(0);"
"half origAlpha = 0;";
// Used in the inner loop to accumulate convolution sum
static const char* kAccumulateSkSL =
"half4 c = child.eval(coord + half2(kernelPos) - half2(offset));"
"if (convolveAlpha == 0) {"
// When not convolving alpha, remember the original alpha for actual sample
// coord, and perform accumulation on unpremul colors.
"if (kernelPos == offset) {"
"origAlpha = c.a;"
"}"
"c = unpremul(c);"
"}"
"sum += c*k;";
// Used after the loop to calculate final color
static const char* kFooterSkSL =
"half4 color = sum*gainAndBias.x + gainAndBias.y;"
"if (convolveAlpha == 0) {"
// Reset the alpha to the original and convert to premul RGB
"color = half4(color.rgb*origAlpha, origAlpha);"
"} else {"
// Ensure convolved alpha is within [0, 1]
"color.a = saturate(color.a);"
"}"
// Make RGB valid premul w/ respect to the alpha (either original or convolved)
"color.rgb = clamp(color.rgb, 0, color.a);"
"return color;"
"}";
// The uniform array storing the kernel is packed into half4's so that we don't waste space
// forcing array elements out to 16-byte alignment when using std140.
static_assert(kMaxUniformKernelSize % 4 == 0, "Must be a multiple of 4");
static SkRuntimeEffect* uniformEffect = SkMakeRuntimeEffect(SkRuntimeEffect::MakeForShader,
SkStringPrintf("const int kMaxUniformKernelSize = %d / 4;"
"uniform half4 kernel[kMaxUniformKernelSize];"
"%s" // kHeaderSkSL
"int2 kernelPos = int2(0);"
"for (int i = 0; i < kMaxUniformKernelSize; ++i) {"
"if (kernelPos.y >= size.y) { break; }"
"half4 k4 = kernel[i];"
"for (int j = 0; j < 4; ++j) {"
"if (kernelPos.y >= size.y) { break; }"
"half k = k4[j];"
"%s" // kAccumulateSkSL
// The 1D index has to be "constant", so reconstruct 2D coords
// instead of a more conventional double for-loop and i=y*w+x
"kernelPos.x += 1;"
"if (kernelPos.x >= size.x) {"
"kernelPos.x = 0;"
"kernelPos.y += 1;"
"}"
"}"
"}"
"%s", // kFooterSkSL
kMaxUniformKernelSize, kHeaderSkSL, kAccumulateSkSL, kFooterSkSL).c_str());
// The texture-backed kernel creates shaders with quantized upper bounds on the kernel size and
// then stored in a thread-safe LRU cache.
static SkMutex cacheLock;
static SkLRUCache<SkISize, sk_sp<SkRuntimeEffect>>
textureShaderCache SK_GUARDED_BY(cacheLock) {/*maxCount=*/5};
static const auto makeTextureEffect = [](SkISize maxKernelSize) {
return SkMakeRuntimeEffect(SkRuntimeEffect::MakeForShader,
SkStringPrintf("const int kMaxKernelWidth = %d;"
"const int kMaxKernelHeight = %d;"
"uniform shader kernel;"
"uniform half2 innerGainAndBias;"
"%s" // kHeaderSkSL
"for (int y = 0; y < kMaxKernelHeight; ++y) {"
"if (y >= size.y) { break; }"
"for (int x = 0; x < kMaxKernelWidth; ++x) {"
"if (x >= size.x) { break; }"
"int2 kernelPos = int2(x,y);"
"half k = kernel.eval(half2(kernelPos) + 0.5).a;"
"k = k * innerGainAndBias.x + innerGainAndBias.y;"
"%s" // kAccumulateSkSL
"}"
"}"
"%s", // kFooterSkSL
maxKernelSize.fWidth, maxKernelSize.fHeight,
kHeaderSkSL, kAccumulateSkSL, kFooterSkSL).c_str());
};
if (texWidth == 0 && texHeight == 0) {
return sk_ref_sp(uniformEffect);
} else {
static_assert(kMaxKernelSize % 4 == 0, "Must be a multiple of 4");
SkASSERT(SkSafeMath::Mul(texWidth, texHeight) <= kMaxKernelSize);
const SkISize key = {SkNextPow2(texWidth), SkNextPow2(texHeight)};
SkAutoMutexExclusive acquire{cacheLock};
sk_sp<SkRuntimeEffect>* effect = textureShaderCache.find(key);
if (!effect) {
// Adopt the raw pointer returned by makeTextureEffect so that it will be deleted if
// it's removed from the LRU cache.
sk_sp<SkRuntimeEffect> newEffect{makeTextureEffect(key)};
effect = textureShaderCache.insert(key, std::move(newEffect));
}
return *effect;
}
}
sk_sp<SkShader> SkMatrixConvolutionImageFilter::createShader(const skif::Context& ctx,
sk_sp<SkShader> input) const {
const int kernelLength = fKernelSize.width() * fKernelSize.height();
const bool useTextureShader = kernelLength > kMaxUniformKernelSize;
if (useTextureShader && fKernelBitmap.empty()) {
return nullptr; // No actual kernel data to work with from a prior OOM
}
auto effect = get_runtime_effect(useTextureShader ? fKernelSize.width() : 0,
useTextureShader ? fKernelSize.height() : 0);
SkRuntimeShaderBuilder builder(std::move(effect));
builder.child("child") = std::move(input);
if (useTextureShader) {
sk_sp<SkImage> cachedKernel = ctx.backend()->getCachedBitmap(fKernelBitmap);
if (!cachedKernel) {
return nullptr;
}
builder.child("kernel") = cachedKernel->makeRawShader(SkFilterMode::kNearest);
builder.uniform("innerGainAndBias") = SkV2{fInnerGain, fInnerBias};
} else {
float paddedKernel[kMaxUniformKernelSize];
memcpy(paddedKernel, fKernel.data(), kernelLength*sizeof(float));
memset(paddedKernel+kernelLength, 0, (kMaxUniformKernelSize - kernelLength)*sizeof(float));
builder.uniform("kernel").set(paddedKernel, kMaxUniformKernelSize);
}
builder.uniform("size") = SkISize(fKernelSize);
builder.uniform("offset") = skif::IVector(fKernelOffset);
// Scale the user-provided bias by 1/255 to match the [0,1] color channel range
builder.uniform("gainAndBias") = SkV2{fGain, fBias / 255.f};
builder.uniform("convolveAlpha") = fConvolveAlpha ? 1 : 0;
return builder.makeShader();
}
skif::FilterResult SkMatrixConvolutionImageFilter::onFilterImage(
const skif::Context& context) const {
using ShaderFlags = skif::FilterResult::ShaderFlags;
skif::LayerSpace<SkIRect> requiredInput = this->boundsSampledByKernel(context.desiredOutput());
skif::FilterResult childOutput =
this->getChildOutput(0, context.withNewDesiredOutput(requiredInput));
skif::LayerSpace<SkIRect> outputBounds;
if (fConvolveAlpha && fBias != 0.f) {
// The convolution will produce a non-trivial value for every pixel so fill desired output.
outputBounds = context.desiredOutput();
} else {
// Calculate the possible extent of the convolution given what was actually produced by the
// child filter and then intersect that with the desired output.
outputBounds = this->boundsAffectedByKernel(childOutput.layerBounds());
if (!outputBounds.intersect(context.desiredOutput())) {
return {};
}
}
skif::FilterResult::Builder builder{context};
builder.add(childOutput,
this->boundsSampledByKernel(outputBounds),
ShaderFlags::kSampledRepeatedly);
return builder.eval([&](SkSpan<sk_sp<SkShader>> inputs) {
return this->createShader(context, inputs[0]);
}, outputBounds);
}
skif::LayerSpace<SkIRect> SkMatrixConvolutionImageFilter::onGetInputLayerBounds(
const skif::Mapping& mapping,
const skif::LayerSpace<SkIRect>& desiredOutput,
std::optional<skif::LayerSpace<SkIRect>> contentBounds) const {
// Adjust the desired output bounds by the kernel size to avoid evaluating edge conditions, and
// then recurse to the child filter.
skif::LayerSpace<SkIRect> requiredInput = this->boundsSampledByKernel(desiredOutput);
return this->getChildInputLayerBounds(0, mapping, requiredInput, contentBounds);
}
std::optional<skif::LayerSpace<SkIRect>> SkMatrixConvolutionImageFilter::onGetOutputLayerBounds(
const skif::Mapping& mapping,
std::optional<skif::LayerSpace<SkIRect>> contentBounds) const {
if (fConvolveAlpha && fBias != 0.f) {
// Applying the kernel as a convolution to fully transparent black will result in 0 for
// each channel, unless the bias itself shifts this "zero-point". However, when the alpha
// channel is not convolved, the original a=0 is preserved and producing a premul color
// discards the non-zero bias. Convolving the alpha channel and a non-zero bias can mean
// the transparent black pixels outside of any input image become non-transparent black.
return skif::LayerSpace<SkIRect>::Unbounded();
}
// Otherwise apply the kernel to the output bounds of the child filter.
auto outputBounds = this->getChildOutputLayerBounds(0, mapping, contentBounds);
if (outputBounds) {
return this->boundsAffectedByKernel(*outputBounds);
} else {
return skif::LayerSpace<SkIRect>::Unbounded();
}
}
SkRect SkMatrixConvolutionImageFilter::computeFastBounds(const SkRect& bounds) const {
// See onAffectsTransparentBlack(), but without knowing the local-to-device transform, we don't
// know how many pixels will be sampled by the kernel. Return unbounded to match the
// expectations of an image filter that "affects" transparent black.
return SkRectPriv::MakeLargeS32();
}