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 "include/effects/SkImageFilters.h"
9
10 #include "include/core/SkFlattenable.h"
11 #include "include/core/SkImageFilter.h"
12 #include "include/core/SkM44.h"
13 #include "include/core/SkRect.h"
14 #include "include/core/SkRefCnt.h"
15 #include "include/core/SkScalar.h"
16 #include "include/core/SkShader.h"
17 #include "include/core/SkSize.h"
18 #include "include/core/SkTypes.h"
19 #include "include/effects/SkRuntimeEffect.h"
20 #include "include/private/base/SkSpan_impl.h"
21 #include "src/core/SkImageFilterTypes.h"
22 #include "src/core/SkImageFilter_Base.h"
23 #include "src/core/SkKnownRuntimeEffects.h"
24 #include "src/core/SkReadBuffer.h"
25 #include "src/core/SkWriteBuffer.h"
26
27 #include <algorithm>
28 #include <cstdint>
29 #include <optional>
30 #include <utility>
31
32 namespace {
33
34 enum class MorphType {
35 kErode,
36 kDilate,
37 kLastType = kDilate
38 };
39
40 enum class MorphDirection { kX, kY };
41
42 class SkMorphologyImageFilter final : public SkImageFilter_Base {
43 public:
SkMorphologyImageFilter(MorphType type,SkSize radii,sk_sp<SkImageFilter> input)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
52 protected:
53 void flatten(SkWriteBuffer&) const override;
54
55 private:
56 friend void ::SkRegisterMorphologyImageFilterFlattenables();
57 SK_FLATTENABLE_HOOKS(SkMorphologyImageFilter)
58
59 skif::FilterResult onFilterImage(const skif::Context&) const override;
60
61 skif::LayerSpace<SkIRect> onGetInputLayerBounds(
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
radii(const skif::Mapping & mapping) const70 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
requiredInput(const skif::Mapping & mapping,skif::LayerSpace<SkIRect> bounds) const80 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
kernelOutputBounds(const skif::Mapping & mapping,skif::LayerSpace<SkIRect> bounds) const87 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;
103 skif::ParameterSpace<SkSize> fRadii;
104 };
105
make_morphology(MorphType type,SkSize radii,sk_sp<SkImageFilter> input,const SkImageFilters::CropRect & cropRect)106 sk_sp<SkImageFilter> make_morphology(MorphType type,
107 SkSize radii,
108 sk_sp<SkImageFilter> input,
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.
131 static constexpr int kMaxLinearRadius = 14; // KEEP IN SYNC WITH CONSTANT IN `sksl_rt_shader.sksl`
make_linear_morphology(sk_sp<SkShader> input,MorphType type,MorphDirection direction,int radius)132 sk_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.
make_sparse_morphology(sk_sp<SkShader> input,MorphType type,MorphDirection direction,int radius)154 sk_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
morphology_pass(const skif::Context & ctx,const skif::FilterResult & input,MorphType type,MorphDirection dir,int radius)171 skif::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) {
176 return skif::LayerSpace<SkISize>({
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
206 skif::FilterResult::Builder builder{stepCtx};
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
Dilate(SkScalar radiusX,SkScalar radiusY,sk_sp<SkImageFilter> input,const CropRect & cropRect)227 sk_sp<SkImageFilter> SkImageFilters::Dilate(SkScalar radiusX, SkScalar radiusY,
228 sk_sp<SkImageFilter> input,
229 const CropRect& cropRect) {
230 return make_morphology(MorphType::kDilate, {radiusX, radiusY}, std::move(input), cropRect);
231 }
232
Erode(SkScalar radiusX,SkScalar radiusY,sk_sp<SkImageFilter> input,const CropRect & cropRect)233 sk_sp<SkImageFilter> SkImageFilters::Erode(SkScalar radiusX, SkScalar radiusY,
234 sk_sp<SkImageFilter> input,
235 const CropRect& cropRect) {
236 return make_morphology(MorphType::kErode, {radiusX, radiusY}, std::move(input), cropRect);
237 }
238
SkRegisterMorphologyImageFilterFlattenables()239 void SkRegisterMorphologyImageFilterFlattenables() {
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
CreateProc(SkReadBuffer & buffer)245 sk_sp<SkFlattenable> SkMorphologyImageFilter::CreateProc(SkReadBuffer& buffer) {
246 SK_IMAGEFILTER_UNFLATTEN_COMMON(common, 1);
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
flatten(SkWriteBuffer & buffer) const261 void 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
onFilterImage(const skif::Context & ctx) const270 skif::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
onGetInputLayerBounds(const skif::Mapping & mapping,const skif::LayerSpace<SkIRect> & desiredOutput,std::optional<skif::LayerSpace<SkIRect>> contentBounds) const296 skif::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
onGetOutputLayerBounds(const skif::Mapping & mapping,std::optional<skif::LayerSpace<SkIRect>> contentBounds) const304 std::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 {
311 return skif::LayerSpace<SkIRect>::Unbounded();
312 }
313 }
314
computeFastBounds(const SkRect & src) const315 SkRect 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 }
325