Flutter Engine
The Flutter Engine
Loading...
Searching...
No Matches
SkMorphologyImageFilter.cpp
Go to the documentation of this file.
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
9
12#include "include/core/SkM44.h"
13#include "include/core/SkRect.h"
17#include "include/core/SkSize.h"
26
27#include <algorithm>
28#include <cstdint>
29#include <optional>
30#include <utility>
31
32namespace {
33
34enum class MorphType {
35 kErode,
36 kDilate,
38};
39
40enum class MorphDirection { kX, kY };
41
42class SkMorphologyImageFilter final : public SkImageFilter_Base {
43public:
44 SkMorphologyImageFilter(MorphType type, SkSize radii, sk_sp<SkImageFilter> input)
45 : SkImageFilter_Base(&input, 1)
46 , fType(type)
47 , fRadii(radii) {}
48
49 SkRect computeFastBounds(const SkRect& src) const override;
50
51
52protected:
53 void flatten(SkWriteBuffer&) const override;
54
55private:
56 friend void ::SkRegisterMorphologyImageFilterFlattenables();
57 SK_FLATTENABLE_HOOKS(SkMorphologyImageFilter)
58
59 skif::FilterResult onFilterImage(const skif::Context&) const override;
60
62 const skif::Mapping& mapping,
63 const skif::LayerSpace<SkIRect>& desiredOutput,
64 std::optional<skif::LayerSpace<SkIRect>> contentBounds) const override;
65
66 std::optional<skif::LayerSpace<SkIRect>> onGetOutputLayerBounds(
67 const skif::Mapping& mapping,
68 std::optional<skif::LayerSpace<SkIRect>> contentBounds) const override;
69
70 skif::LayerSpace<SkISize> radii(const skif::Mapping& mapping) const {
71 skif::LayerSpace<SkISize> radii = mapping.paramToLayer(fRadii).round();
72 SkASSERT(radii.width() >= 0 && radii.height() >= 0);
73
74 // We limit the radius to something small, to avoid slow draw calls: crbug.com/1123035
75 static constexpr int kMaxRadii = 256;
76 return skif::LayerSpace<SkISize>({std::min(radii.width(), kMaxRadii),
77 std::min(radii.height(), kMaxRadii)});
78 }
79
80 skif::LayerSpace<SkIRect> requiredInput(const skif::Mapping& mapping,
81 skif::LayerSpace<SkIRect> bounds) const {
82 // The input for a morphology filter is always the kernel outset, regardless of morph type.
83 bounds.outset(this->radii(mapping));
84 return bounds;
85 }
86
87 skif::LayerSpace<SkIRect> kernelOutputBounds(const skif::Mapping& mapping,
88 skif::LayerSpace<SkIRect> bounds) const {
89 skif::LayerSpace<SkISize> radii = this->radii(mapping);
90 if (fType == MorphType::kDilate) {
91 // Transparent pixels up to the kernel radius away will be overridden by kDilate's "max"
92 // function and be set to the input's boundary pixel colors, thus expanding the output.
93 bounds.outset(radii);
94 } else {
95 // Pixels closer than the kernel radius to the input image's edges are overridden by
96 // kErode's "min" function and will be set to transparent black, contracting the output.
97 bounds.inset(radii);
98 }
99 return bounds;
100 }
101
102 MorphType fType;
104};
105
106sk_sp<SkImageFilter> make_morphology(MorphType type,
107 SkSize radii,
109 const SkImageFilters::CropRect& cropRect) {
110 if (radii.width() < 0.f || radii.height() < 0.f) {
111 return nullptr; // invalid
112 }
113 sk_sp<SkImageFilter> filter = std::move(input);
114 if (radii.width() > 0.f || radii.height() > 0.f) {
115 filter = sk_sp<SkImageFilter>(new SkMorphologyImageFilter(type, radii, std::move(filter)));
116 }
117 // otherwise both radii are 0, so the kernel is always the identity function, in which case
118 // we just need to apply the 'cropRect' to the 'input'.
119
120 if (cropRect) {
121 filter = SkImageFilters::Crop(*cropRect, std::move(filter));
122 }
123 return filter;
124}
125
126// The linear morphology kernel does (2R+1) texture samples per pixel, which we want to keep less
127// than the maximum fragment samples allowed in DX9SM2 (32), so we choose R=14 to have some head
128// room. The other tradeoff is that for R > kMaxLinearRadius, the sparse morphology kernel only
129// requires 2 samples to double the accumulated kernel size, but at the cost of another render
130// target.
131static constexpr int kMaxLinearRadius = 14; // KEEP IN SYNC W/ SkKnownRuntimeEffects.cpp VERSION
132sk_sp<SkShader> make_linear_morphology(sk_sp<SkShader> input,
133 MorphType type,
134 MorphDirection direction,
135 int radius) {
136 SkASSERT(radius <= kMaxLinearRadius);
137
138 const SkRuntimeEffect* linearMorphologyEffect =
139 GetKnownRuntimeEffect(SkKnownRuntimeEffects::StableKey::kLinearMorphology);
140
141 SkRuntimeShaderBuilder builder(sk_ref_sp(linearMorphologyEffect));
142 builder.child("child") = std::move(input);
143 builder.uniform("offset") = direction == MorphDirection::kX ? SkV2{1.f, 0.f} : SkV2{0.f, 1.f};
144 builder.uniform("flip") = (type == MorphType::kDilate) ? 1.f : -1.f;
145 builder.uniform("radius") = (int32_t) radius;
146
147 return builder.makeShader();
148}
149
150// Assuming 'input' was created by a series of morphology passes, each texel holds the aggregate
151// (min or max depending on type) of (i-R) to (i+R) for some R. If 'radius' <= R, then the returned
152// shader produces a new aggregate at each texel, i, of (i-R-radius) to (i+R+radius) with only two
153// texture samples, which can be used to double the kernel size of the morphology effect.
154sk_sp<SkShader> make_sparse_morphology(sk_sp<SkShader> input,
155 MorphType type,
156 MorphDirection direction,
157 int radius) {
158
159 const SkRuntimeEffect* sparseMorphologyEffect =
160 GetKnownRuntimeEffect(SkKnownRuntimeEffects::StableKey::kSparseMorphology);
161
162 SkRuntimeShaderBuilder builder(sk_ref_sp(sparseMorphologyEffect));
163 builder.child("child") = std::move(input);
164 builder.uniform("offset") = direction == MorphDirection::kX ? SkV2{(float)radius, 0.f}
165 : SkV2{0.f, (float)radius};
166 builder.uniform("flip") = (type == MorphType::kDilate) ? 1.f : -1.f;
167
168 return builder.makeShader();
169}
170
171skif::FilterResult morphology_pass(const skif::Context& ctx, const skif::FilterResult& input,
172 MorphType type, MorphDirection dir, int radius) {
173 using ShaderFlags = skif::FilterResult::ShaderFlags;
174
175 auto axisDelta = [dir](int step) {
177 dir == MorphDirection::kX ? step : 0,
178 dir == MorphDirection::kY ? step : 0});
179 };
180
181 // The first iteration will sample a full kernel outset from the final output.
182 skif::LayerSpace<SkIRect> sampleBounds = ctx.desiredOutput();
183 sampleBounds.outset(axisDelta(radius));
184
185 skif::FilterResult childOutput = input;
186 int appliedRadius = 0;
187 while (radius > appliedRadius) {
188 if (!childOutput) {
189 return {}; // Eroded or dilated transparent black is still transparent black
190 }
191
192 // The first iteration uses up to kMaxLinearRadius with a linear accumulation pass.
193 // After that we double the radius each step until we can finish with the target radius.
194 int stepRadius =
195 appliedRadius == 0 ? std::min(kMaxLinearRadius, radius)
196 : std::min(radius - appliedRadius, appliedRadius);
197
198 skif::Context stepCtx = ctx;
199 if (appliedRadius + stepRadius < radius) {
200 // Intermediate steps need to output what will be sampled on the next iteration
201 auto outputBounds = sampleBounds;
202 outputBounds.inset(axisDelta(stepRadius));
203 stepCtx = ctx.withNewDesiredOutput(outputBounds);
204 } // else the last iteration should output what was originally requested
205
207 builder.add(childOutput, sampleBounds, ShaderFlags::kSampledRepeatedly);
208 childOutput = builder.eval(
209 [&](SkSpan<sk_sp<SkShader>> inputs) {
210 if (appliedRadius == 0) {
211 return make_linear_morphology(inputs[0], type, dir, stepRadius);
212 } else {
213 return make_sparse_morphology(inputs[0], type, dir, stepRadius);
214 }
215 });
216
217 sampleBounds = stepCtx.desiredOutput();
218 appliedRadius += stepRadius;
219 SkASSERT(appliedRadius <= radius); // Our last iteration should hit 'radius' exactly.
220 }
221
222 return childOutput;
223}
224
225} // end namespace
226
229 const CropRect& cropRect) {
230 return make_morphology(MorphType::kDilate, {radiusX, radiusY}, std::move(input), cropRect);
231}
232
235 const CropRect& cropRect) {
236 return make_morphology(MorphType::kErode, {radiusX, radiusY}, std::move(input), cropRect);
237}
238
240 SK_REGISTER_FLATTENABLE(SkMorphologyImageFilter);
241 // TODO (michaelludwig): Remove after grace period for SKPs to stop using old name
242 SkFlattenable::Register("SkMorphologyImageFilterImpl", SkMorphologyImageFilter::CreateProc);
243}
244
245sk_sp<SkFlattenable> SkMorphologyImageFilter::CreateProc(SkReadBuffer& buffer) {
247
248 SkScalar width = buffer.readScalar();
249 SkScalar height = buffer.readScalar();
250 MorphType filterType = buffer.read32LE(MorphType::kLastType);
251
252 if (filterType == MorphType::kDilate) {
253 return SkImageFilters::Dilate(width, height, common.getInput(0), common.cropRect());
254 } else if (filterType == MorphType::kErode) {
255 return SkImageFilters::Erode(width, height, common.getInput(0), common.cropRect());
256 } else {
257 return nullptr;
258 }
259}
260
261void SkMorphologyImageFilter::flatten(SkWriteBuffer& buffer) const {
262 this->SkImageFilter_Base::flatten(buffer);
263 buffer.writeScalar(SkSize(fRadii).width());
264 buffer.writeScalar(SkSize(fRadii).height());
265 buffer.writeInt(static_cast<int>(fType));
266}
267
268///////////////////////////////////////////////////////////////////////////////
269
270skif::FilterResult SkMorphologyImageFilter::onFilterImage(const skif::Context& ctx) const {
271 skif::LayerSpace<SkIRect> requiredInput =
272 this->requiredInput(ctx.mapping(), ctx.desiredOutput());
273 skif::FilterResult childOutput =
274 this->getChildOutput(0, ctx.withNewDesiredOutput(requiredInput));
275
276 // If childOutput completely fulfilled requiredInput, maxOutput will match the context's
277 // desired output, but if the output image is smaller, this will restrict the morphology output
278 // to what is actual produceable.
279 skif::LayerSpace<SkIRect> maxOutput =
280 this->kernelOutputBounds(ctx.mapping(), childOutput.layerBounds());
281 if (!maxOutput.intersect(ctx.desiredOutput())) {
282 return {};
283 }
284
285 // The X pass has to preserve the extra rows to later be consumed by the Y pass.
286 skif::LayerSpace<SkISize> radii = this->radii(ctx.mapping());
287 skif::LayerSpace<SkIRect> maxOutputX = maxOutput;
288 maxOutputX.outset(skif::LayerSpace<SkISize>({0, radii.height()}));
289 childOutput = morphology_pass(ctx.withNewDesiredOutput(maxOutputX), childOutput, fType,
290 MorphDirection::kX, radii.width());
291 childOutput = morphology_pass(ctx.withNewDesiredOutput(maxOutput), childOutput, fType,
292 MorphDirection::kY, radii.height());
293 return childOutput;
294}
295
296skif::LayerSpace<SkIRect> SkMorphologyImageFilter::onGetInputLayerBounds(
297 const skif::Mapping& mapping,
298 const skif::LayerSpace<SkIRect>& desiredOutput,
299 std::optional<skif::LayerSpace<SkIRect>> contentBounds) const {
300 skif::LayerSpace<SkIRect> requiredInput = this->requiredInput(mapping, desiredOutput);
301 return this->getChildInputLayerBounds(0, mapping, requiredInput, contentBounds);
302}
303
304std::optional<skif::LayerSpace<SkIRect>> SkMorphologyImageFilter::onGetOutputLayerBounds(
305 const skif::Mapping& mapping,
306 std::optional<skif::LayerSpace<SkIRect>> contentBounds) const {
307 auto childOutput = this->getChildOutputLayerBounds(0, mapping, contentBounds);
308 if (childOutput) {
309 return this->kernelOutputBounds(mapping, *childOutput);
310 } else {
312 }
313}
314
315SkRect SkMorphologyImageFilter::computeFastBounds(const SkRect& src) const {
316 // See kernelOutputBounds() for rationale
317 SkRect bounds = this->getInput(0) ? this->getInput(0)->computeFastBounds(src) : src;
318 if (fType == MorphType::kDilate) {
319 bounds.outset(SkSize(fRadii).width(), SkSize(fRadii).height());
320 } else {
321 bounds.inset(SkSize(fRadii).width(), SkSize(fRadii).height());
322 }
323 return bounds;
324}
static int step(int x, SkScalar min, SkScalar max)
Definition BlurTest.cpp:215
#define SkASSERT(cond)
Definition SkAssert.h:116
#define SK_FLATTENABLE_HOOKS(type)
#define SK_REGISTER_FLATTENABLE(type)
#define SK_IMAGEFILTER_UNFLATTEN_COMMON(localVar, expectedCount)
void SkRegisterMorphologyImageFilterFlattenables()
sk_sp< T > sk_ref_sp(T *obj)
Definition SkRefCnt.h:381
static void Register(const char name[], Factory)
virtual skif::LayerSpace< SkIRect > onGetInputLayerBounds(const skif::Mapping &mapping, const skif::LayerSpace< SkIRect > &desiredOutput, std::optional< skif::LayerSpace< SkIRect > > contentBounds) const =0
void flatten(SkWriteBuffer &) const override
virtual std::optional< skif::LayerSpace< SkIRect > > onGetOutputLayerBounds(const skif::Mapping &mapping, std::optional< skif::LayerSpace< SkIRect > > contentBounds) const =0
virtual skif::FilterResult onFilterImage(const skif::Context &context) const =0
virtual SkRect computeFastBounds(const SkRect &bounds) const
static sk_sp< SkImageFilter > Erode(SkScalar radiusX, SkScalar radiusY, sk_sp< SkImageFilter > input, const CropRect &cropRect={})
static sk_sp< SkImageFilter > Crop(const SkRect &rect, SkTileMode tileMode, sk_sp< SkImageFilter > input)
static sk_sp< SkImageFilter > Dilate(SkScalar radiusX, SkScalar radiusY, sk_sp< SkImageFilter > input, const CropRect &cropRect={})
const LayerSpace< SkIRect > & desiredOutput() const
Context withNewDesiredOutput(const LayerSpace< SkIRect > &desiredOutput) const
const Mapping & mapping() const
LayerSpace< SkIRect > layerBounds() const
LayerSpace< T > paramToLayer(const ParameterSpace< T > &paramGeometry) const
float SkScalar
Definition extension.cpp:12
static const uint8_t buffer[]
static float min(float r, float g, float b)
Definition hsl.cpp:48
const SkRuntimeEffect * GetKnownRuntimeEffect(StableKey stableKey)
Optional< SkRect > bounds
Definition SkRecords.h:189
DEF_SWITCHES_START aot vmservice shared library Name of the *so containing AOT compiled Dart assets for launching the service isolate vm snapshot The VM snapshot data that will be memory mapped as read only SnapshotAssetPath must be present isolate snapshot The isolate snapshot data that will be memory mapped as read only SnapshotAssetPath must be present cache dir Path to the cache directory This is different from the persistent_cache_path in embedder which is used for Skia shader cache icu native lib Path to the library file that exports the ICU data vm service The hostname IP address on which the Dart VM Service should be served If not defaults to or::depending on whether ipv6 is specified vm service A custom Dart VM Service port The default is to pick a randomly available open port disable vm Disable the Dart VM Service The Dart VM Service is never available in release mode disable vm service Disable mDNS Dart VM Service publication Bind to the IPv6 localhost address for the Dart VM Service Ignored if vm service host is set endless trace Enable an endless trace buffer The default is a ring buffer This is useful when very old events need to viewed For during application launch Memory usage will continue to grow indefinitely however Start app with an specific route defined on the framework flutter assets dir
Definition switches.h:145
Definition ref_ptr.h:256
int32_t height
int32_t width
SkScalar width() const
Definition SkSize.h:76
SkScalar height() const
Definition SkSize.h:77
Definition SkM44.h:19