Flutter Engine
The Flutter Engine
Loading...
Searching...
No Matches
SkImageFilterTypes.cpp
Go to the documentation of this file.
1/*
2 * Copyright 2019 Google LLC
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
19#include "include/core/SkM44.h"
21#include "include/core/SkPicture.h" // IWYU pragma: keep
26#include "src/base/SkMathPriv.h"
27#include "src/base/SkVx.h"
32#include "src/core/SkDevice.h"
37#include "src/core/SkRectPriv.h"
40
41#include <algorithm>
42#include <cmath>
43
44namespace skif {
45
46namespace {
47
48// This exists to cover up issues where infinite precision would produce integers but float
49// math produces values just larger/smaller than an int and roundOut/In on bounds would produce
50// nearly a full pixel error. One such case is crbug.com/1313579 where the caller has produced
51// near integer CTM and uses integer crop rects that would grab an extra row/column of the
52// input image when using a strict roundOut.
53static constexpr float kRoundEpsilon = 1e-3f;
54
55std::pair<bool, bool> are_axes_nearly_integer_aligned(const LayerSpace<SkMatrix>& m,
56 LayerSpace<SkIPoint>* out=nullptr) {
57 float invW = sk_ieee_float_divide(1.f, m.rc(2,2));
58 float tx = SkScalarRoundToScalar(m.rc(0,2)*invW);
59 float ty = SkScalarRoundToScalar(m.rc(1,2)*invW);
60 // expected = [1 0 tx] after normalizing perspective (divide by m[2,2])
61 // [0 1 ty]
62 // [0 0 1]
63 bool affine = SkScalarNearlyEqual(m.rc(2,0)*invW, 0.f, kRoundEpsilon) &&
64 SkScalarNearlyEqual(m.rc(2,1)*invW, 0.f, kRoundEpsilon);
65 if (!affine) {
66 return {false, false};
67 }
68
69 bool xAxis = SkScalarNearlyEqual(1.f, m.rc(0,0)*invW, kRoundEpsilon) &&
70 SkScalarNearlyEqual(0.f, m.rc(0,1)*invW, kRoundEpsilon) &&
71 SkScalarNearlyEqual(tx, m.rc(0,2)*invW, kRoundEpsilon);
72 bool yAxis = SkScalarNearlyEqual(0.f, m.rc(1,0)*invW, kRoundEpsilon) &&
73 SkScalarNearlyEqual(1.f, m.rc(1,1)*invW, kRoundEpsilon) &&
74 SkScalarNearlyEqual(ty, m.rc(1,2)*invW, kRoundEpsilon);
75 if (out && xAxis && yAxis) {
76 *out = LayerSpace<SkIPoint>({(int) tx, (int) ty});
77 }
78 return {xAxis, yAxis};
79}
80
81// If m is epsilon within the form [1 0 tx], this returns true and sets out to [tx, ty]
82// [0 1 ty]
83// [0 0 1 ]
84// TODO: Use this in decomposeCTM() (and possibly extend it to support is_nearly_scale_translate)
85// to be a little more forgiving on matrix types during layer configuration.
86bool is_nearly_integer_translation(const LayerSpace<SkMatrix>& m,
87 LayerSpace<SkIPoint>* out=nullptr) {
88 auto [axisX, axisY] = are_axes_nearly_integer_aligned(m, out);
89 return axisX && axisY;
90}
91
92void decompose_transform(const SkMatrix& transform, SkPoint representativePoint,
93 SkMatrix* postScaling, SkMatrix* scaling) {
95 if (transform.decomposeScale(&scale, postScaling)) {
96 *scaling = SkMatrix::Scale(scale.fWidth, scale.fHeight);
97 } else {
98 // Perspective, which has a non-uniform scaling effect on the filter. Pick a single scale
99 // factor that best matches where the filter will be evaluated.
100 SkScalar approxScale = SkMatrixPriv::DifferentialAreaScale(transform, representativePoint);
101 if (SkIsFinite(approxScale) && !SkScalarNearlyZero(approxScale)) {
102 // Now take the sqrt to go from an area scale factor to a scaling per X and Y
103 approxScale = SkScalarSqrt(approxScale);
104 } else {
105 // The point was behind the W = 0 plane, so don't factor out any scale.
106 approxScale = 1.f;
107 }
108 *postScaling = transform;
109 postScaling->preScale(SkScalarInvert(approxScale), SkScalarInvert(approxScale));
110 *scaling = SkMatrix::Scale(approxScale, approxScale);
111 }
112}
113
114std::optional<LayerSpace<SkMatrix>> periodic_axis_transform(
115 SkTileMode tileMode,
116 const LayerSpace<SkIRect>& crop,
117 const LayerSpace<SkIRect>& output) {
118 if (tileMode == SkTileMode::kClamp || tileMode == SkTileMode::kDecal) {
119 // Not periodic
120 return {};
121 }
122
123 // Lift crop dimensions into 64 bit so that we can combine with 'output' without worrying about
124 // overflowing 32 bits.
125 double cropL = (double) crop.left();
126 double cropT = (double) crop.top();
127 double cropWidth = crop.right() - cropL;
128 double cropHeight = crop.bottom() - cropT;
129
130 // Calculate normalized periodic coordinates of 'output' relative to the 'crop' being tiled.
131 int periodL = sk_double_floor2int((output.left() - cropL) / cropWidth);
132 int periodT = sk_double_floor2int((output.top() - cropT) / cropHeight);
133 int periodR = sk_double_ceil2int((output.right() - cropL) / cropWidth);
134 int periodB = sk_double_ceil2int((output.bottom() - cropT) / cropHeight);
135
136 if (periodR - periodL <= 1 && periodB - periodT <= 1) {
137 // The tiling pattern won't be visible, so we can draw the image without tiling and an
138 // adjusted transform. We calculate the final translation in double to be exact and then
139 // verify that it can round-trip as a float.
140 float sx = 1.f;
141 float sy = 1.f;
142 double tx = -cropL;
143 double ty = -cropT;
144
145 if (tileMode == SkTileMode::kMirror) {
146 // Flip image when in odd periods on each axis.
147 if (periodL % 2 != 0) {
148 sx = -1.f;
149 tx = cropWidth - tx;
150 }
151 if (periodT % 2 != 0) {
152 sy = -1.f;
153 ty = cropHeight - ty;
154 }
155 }
156 // Now translate by periods and make relative to crop's top left again. Given 32-bit inputs,
157 // the period * dimension shouldn't overflow 64-bits.
158 tx += periodL * cropWidth + cropL;
159 ty += periodT * cropHeight + cropT;
160
161 // Representing the periodic tiling as a float SkMatrix would lose the pixel precision
162 // required to represent it, so don't apply this optimization.
163 if (sk_double_saturate2int(tx) != (float) tx ||
164 sk_double_saturate2int(ty) != (float) ty) {
165 return {};
166 }
167
168 SkMatrix periodicTransform;
169 periodicTransform.setScaleTranslate(sx, sy, (float) tx, (float) ty);
170 return LayerSpace<SkMatrix>(periodicTransform);
171 } else {
172 // Both low and high edges of the crop would be visible in 'output', or a mirrored
173 // boundary is visible in 'output'. Just keep the periodic tiling.
174 return {};
175 }
176}
177
178class RasterBackend : public Backend {
179public:
180
181 RasterBackend(const SkSurfaceProps& surfaceProps, SkColorType colorType)
182 : Backend(SkImageFilterCache::Get(), surfaceProps, colorType) {}
183
184 sk_sp<SkDevice> makeDevice(SkISize size,
185 sk_sp<SkColorSpace> colorSpace,
186 const SkSurfaceProps* props) const override {
187 SkImageInfo imageInfo = SkImageInfo::Make(size,
188 this->colorType(),
190 std::move(colorSpace));
191 return SkBitmapDevice::Create(imageInfo, props ? *props : this->surfaceProps());
192 }
193
194 sk_sp<SkSpecialImage> makeImage(const SkIRect& subset, sk_sp<SkImage> image) const override {
195 return SkSpecialImages::MakeFromRaster(subset, image, this->surfaceProps());
196 }
197
198 sk_sp<SkImage> getCachedBitmap(const SkBitmap& data) const override {
199 return SkImages::RasterFromBitmap(data);
200 }
201
202 const SkBlurEngine* getBlurEngine() const override { return nullptr; }
203};
204
205} // anonymous namespace
206
207///////////////////////////////////////////////////////////////////////////////////////////////////
208
210 const SkSurfaceProps& surfaceProps,
212 : fCache(std::move(cache))
213 , fSurfaceProps(surfaceProps)
215
216Backend::~Backend() = default;
217
219 // TODO (skbug:14286): Remove this forcing to 8888. Many legacy image filters only support
220 // N32 on CPU, but once they are implemented in terms of draws and SkSL they will support
221 // all color types, like the GPU backends.
222 colorType = kN32_SkColorType;
223
224 return sk_make_sp<RasterBackend>(surfaceProps, colorType);
225}
226
227void Stats::dumpStats() const {
228 SkDebugf("ImageFilter Stats:\n"
229 " # visited filters: %d\n"
230 " # cache hits: %d\n"
231 " # offscreen surfaces: %d\n"
232 " # shader-clamped draws: %d\n"
233 " # shader-tiled draws: %d\n",
239}
240
241void Stats::reportStats() const {
242 TRACE_EVENT_INSTANT2("skia", "ImageFilter Graph Size", TRACE_EVENT_SCOPE_THREAD,
243 "count", fNumVisitedImageFilters, "cache hits", fNumCacheHits);
244 TRACE_EVENT_INSTANT1("skia", "ImageFilter Surfaces", TRACE_EVENT_SCOPE_THREAD,
245 "count", fNumOffscreenSurfaces);
246 TRACE_EVENT_INSTANT2("skia", "ImageFilter Shader Tiling", TRACE_EVENT_SCOPE_THREAD,
248}
249
250///////////////////////////////////////////////////////////////////////////////////////////////////
251// Mapping
252
253SkIRect RoundOut(SkRect r) { return r.makeInset(kRoundEpsilon, kRoundEpsilon).roundOut(); }
254
255SkIRect RoundIn(SkRect r) { return r.makeOutset(kRoundEpsilon, kRoundEpsilon).roundIn(); }
256
258 const skif::ParameterSpace<SkPoint>& representativePt) {
259 SkMatrix remainder, layer;
260 if (capability == MatrixCapability::kTranslate) {
261 // Apply the entire CTM post-filtering
262 remainder = ctm;
263 layer = SkMatrix::I();
264 } else if (ctm.isScaleTranslate() || capability == MatrixCapability::kComplex) {
265 // Either layer space can be anything (kComplex) - or - it can be scale+translate, and the
266 // ctm is. In both cases, the layer space can be equivalent to device space.
267 remainder = SkMatrix::I();
268 layer = ctm;
269 } else {
270 // This case implies some amount of sampling post-filtering, either due to skew or rotation
271 // in the original matrix. As such, keep the layer matrix as simple as possible.
272 decompose_transform(ctm, SkPoint(representativePt), &remainder, &layer);
273 }
274
275 SkMatrix invRemainder;
276 if (!remainder.invert(&invRemainder)) {
277 // Under floating point arithmetic, it's possible to decompose an invertible matrix into
278 // a scaling matrix and a remainder and have the remainder be non-invertible. Generally
279 // when this happens the scale factors are so large and the matrix so ill-conditioned that
280 // it's unlikely that any drawing would be reasonable, so failing to make a layer is okay.
281 return false;
282 } else {
283 fParamToLayerMatrix = layer;
284 fLayerToDevMatrix = remainder;
285 fDevToLayerMatrix = invRemainder;
286 return true;
287 }
288}
289
291 const SkImageFilter* filter,
292 const skif::ParameterSpace<SkPoint>& representativePt) {
293 return this->decomposeCTM(
294 ctm,
295 filter ? as_IFB(filter)->getCTMCapability() : MatrixCapability::kComplex,
296 representativePt);
297}
298
300 SkMatrix invLayer;
301 if (!layer.invert(&invLayer)) {
302 return false;
303 }
304 fParamToLayerMatrix.postConcat(layer);
305 fDevToLayerMatrix.postConcat(layer);
306 fLayerToDevMatrix.preConcat(invLayer);
307 return true;
308}
309
310// Instantiate map specializations for the 6 geometric types used during filtering
311template<>
312SkRect Mapping::map<SkRect>(const SkRect& geom, const SkMatrix& matrix) {
313 return geom.isEmpty() ? SkRect::MakeEmpty() : matrix.mapRect(geom);
314}
315
316template<>
317SkIRect Mapping::map<SkIRect>(const SkIRect& geom, const SkMatrix& matrix) {
318 if (geom.isEmpty()) {
319 return SkIRect::MakeEmpty();
320 }
321 // Unfortunately, there is a range of integer values such that we have 1px precision as an int,
322 // but less precision as a float. This can lead to non-empty SkIRects becoming empty simply
323 // because of float casting. If we're already dealing with a float rect or having a float
324 // output, that's what we're stuck with; but if we are starting form an irect and desiring an
325 // SkIRect output, we go through efforts to preserve the 1px precision for simple transforms.
326 if (matrix.isScaleTranslate()) {
327 double l = (double)matrix.getScaleX()*geom.fLeft + (double)matrix.getTranslateX();
328 double r = (double)matrix.getScaleX()*geom.fRight + (double)matrix.getTranslateX();
329 double t = (double)matrix.getScaleY()*geom.fTop + (double)matrix.getTranslateY();
330 double b = (double)matrix.getScaleY()*geom.fBottom + (double)matrix.getTranslateY();
331 return {sk_double_saturate2int(std::floor(std::min(l, r) + kRoundEpsilon)),
332 sk_double_saturate2int(std::floor(std::min(t, b) + kRoundEpsilon)),
333 sk_double_saturate2int(std::ceil(std::max(l, r) - kRoundEpsilon)),
334 sk_double_saturate2int(std::ceil(std::max(t, b) - kRoundEpsilon))};
335 } else {
336 return RoundOut(matrix.mapRect(SkRect::Make(geom)));
337 }
338}
339
340template<>
341SkIPoint Mapping::map<SkIPoint>(const SkIPoint& geom, const SkMatrix& matrix) {
343 matrix.mapPoints(&p, 1);
345}
346
347template<>
348SkPoint Mapping::map<SkPoint>(const SkPoint& geom, const SkMatrix& matrix) {
349 SkPoint p;
350 matrix.mapPoints(&p, &geom, 1);
351 return p;
352}
353
354template<>
355Vector Mapping::map<Vector>(const Vector& geom, const SkMatrix& matrix) {
356 SkVector v = SkVector::Make(geom.fX, geom.fY);
357 matrix.mapVectors(&v, 1);
358 return Vector{v};
359}
360
361template<>
362IVector Mapping::map<IVector>(const IVector& geom, const SkMatrix& matrix) {
363 SkVector v = SkVector::Make(SkIntToScalar(geom.fX), SkIntToScalar(geom.fY));
364 matrix.mapVectors(&v, 1);
365 return IVector(SkScalarRoundToInt(v.fX), SkScalarRoundToInt(v.fY));
366}
367
368// Sizes are also treated as non-positioned values (although this assumption breaks down if there's
369// perspective). Unlike vectors, we treat input sizes as specifying lengths of the local X and Y
370// axes and return the lengths of those mapped axes.
371template<>
372SkSize Mapping::map<SkSize>(const SkSize& geom, const SkMatrix& matrix) {
373 if (matrix.isScaleTranslate()) {
374 // This is equivalent to mapping the two basis vectors and calculating their lengths.
375 SkVector sizes = matrix.mapVector(geom.fWidth, geom.fHeight);
376 return {SkScalarAbs(sizes.fX), SkScalarAbs(sizes.fY)};
377 }
378
379 SkVector xAxis = matrix.mapVector(geom.fWidth, 0.f);
380 SkVector yAxis = matrix.mapVector(0.f, geom.fHeight);
381 return {xAxis.length(), yAxis.length()};
382}
383
384template<>
385SkISize Mapping::map<SkISize>(const SkISize& geom, const SkMatrix& matrix) {
386 SkSize size = map(SkSize::Make(geom), matrix);
387 return SkISize::Make(SkScalarCeilToInt(size.fWidth - kRoundEpsilon),
388 SkScalarCeilToInt(size.fHeight - kRoundEpsilon));
389}
390
391template<>
392SkMatrix Mapping::map<SkMatrix>(const SkMatrix& m, const SkMatrix& matrix) {
393 // If 'matrix' maps from the C1 coord space to the C2 coord space, and 'm' is a transform that
394 // operates on, and outputs to, the C1 coord space, we want to return a new matrix that is
395 // equivalent to 'm' that operates on and outputs to C2. This is the same as mapping the input
396 // from C2 to C1 (matrix^-1), then transforming by 'm', and then mapping from C1 to C2 (matrix).
398 SkAssertResult(matrix.invert(&inv));
399 inv.postConcat(m);
400 inv.postConcat(matrix);
401 return inv;
402}
403
404///////////////////////////////////////////////////////////////////////////////////////////////////
405// LayerSpace<T>
406
408 SkTileMode tileMode) const {
409 LayerSpace<SkIRect> fittedSrc = *this;
410 if (tileMode == SkTileMode::kDecal || tileMode == SkTileMode::kClamp) {
411 // For both decal/clamp, we only care about the region that is in dstRect, unless we are
412 // clamping and have to preserve edge pixels when there's no overlap.
413 if (!fittedSrc.intersect(dstRect)) {
414 if (tileMode == SkTileMode::kDecal) {
415 // The dstRect would be filled with transparent black.
416 fittedSrc = LayerSpace<SkIRect>::Empty();
417 } else {
418 // We just need the closest row/column/corner of this rect to dstRect.
419 auto edge = SkRectPriv::ClosestDisjointEdge(SkIRect(fittedSrc), SkIRect(dstRect));
420 fittedSrc = LayerSpace<SkIRect>(edge);
421 }
422 }
423 } // else assume the entire source is needed for periodic tile modes, so leave fittedSrc alone
424
425 return fittedSrc;
426}
427
428// Match rounding tolerances of SkRects to SkIRects
430 return LayerSpace<SkISize>(fData.toRound());
431}
433 return LayerSpace<SkISize>({SkScalarCeilToInt(fData.fWidth - kRoundEpsilon),
434 SkScalarCeilToInt(fData.fHeight - kRoundEpsilon)});
435}
437 return LayerSpace<SkISize>({SkScalarFloorToInt(fData.fWidth + kRoundEpsilon),
438 SkScalarFloorToInt(fData.fHeight + kRoundEpsilon)});
439}
440
442 return LayerSpace<SkRect>(Mapping::map(SkRect(r), fData));
443}
444
445// Effectively mapRect(SkRect).roundOut() but more accurate when the underlying matrix or
446// SkIRect has large floating point values.
448 return LayerSpace<SkIRect>(Mapping::map(SkIRect(r), fData));
449}
450
454
456 return LayerSpace<Vector>(Mapping::map(Vector(v), fData));
457}
458
462
464 LayerSpace<SkRect>* out) const {
465 SkRect mapped;
466 if (r.isEmpty()) {
467 // An empty input always inverse maps to an empty rect "successfully"
469 return true;
470 } else if (SkMatrixPriv::InverseMapRect(fData, &mapped, SkRect(r))) {
471 *out = LayerSpace<SkRect>(mapped);
472 return true;
473 } else {
474 return false;
475 }
476}
477
479 LayerSpace<SkIRect>* out) const {
480 if (rect.isEmpty()) {
481 // An empty input always inverse maps to an empty rect "successfully"
483 return true;
484 } else if (fData.isScaleTranslate()) { // Specialized inverse of 1px-preserving map<SkIRect>
485 // A scale-translate matrix with a 0 scale factor is not invertible.
486 if (fData.getScaleX() == 0.f || fData.getScaleY() == 0.f) {
487 return false;
488 }
489 double l = (rect.left() - (double)fData.getTranslateX()) / (double)fData.getScaleX();
490 double r = (rect.right() - (double)fData.getTranslateX()) / (double)fData.getScaleX();
491 double t = (rect.top() - (double)fData.getTranslateY()) / (double)fData.getScaleY();
492 double b = (rect.bottom() - (double)fData.getTranslateY()) / (double)fData.getScaleY();
493
494 SkIRect mapped{sk_double_saturate2int(std::floor(std::min(l, r) + kRoundEpsilon)),
495 sk_double_saturate2int(std::floor(std::min(t, b) + kRoundEpsilon)),
496 sk_double_saturate2int(std::ceil(std::max(l, r) - kRoundEpsilon)),
497 sk_double_saturate2int(std::ceil(std::max(t, b) - kRoundEpsilon))};
498 *out = LayerSpace<SkIRect>(mapped);
499 return true;
500 } else {
501 SkRect mapped;
502 if (SkMatrixPriv::InverseMapRect(fData, &mapped, SkRect::Make(SkIRect(rect)))) {
503 *out = LayerSpace<SkRect>(mapped).roundOut();
504 return true;
505 }
506 }
507
508 return false;
509}
510
511///////////////////////////////////////////////////////////////////////////////////////////////////
512// FilterResult::AutoSurface
513//
514// AutoSurface manages an SkCanvas and device state to draw to a layer-space bounding box,
515// and then snap it into a FilterResult. It provides operators to be used directly as an SkDevice,
516// assuming surface creation succeeded. It can also be viewed as an SkCanvas (for when an operation
517// is unavailable on SkDevice). A given AutoSurface should only rely on one access API.
518// Usage:
519//
520// AutoSurface surface{ctx, dstBounds, renderInParameterSpace}; // if true, concats layer matrix
521// if (surface) {
522// surface->drawFoo(...);
523// }
524// return surface.snap(); // Automatically handles failed allocations
526public:
528 const LayerSpace<SkIRect>& dstBounds,
529 [[maybe_unused]] PixelBoundary boundary,
530 bool renderInParameterSpace,
531 const SkSurfaceProps* props = nullptr)
532 : fDstBounds(dstBounds)
533#if defined(SK_DONT_PAD_LAYER_IMAGES)
534 , fBoundary(PixelBoundary::kUnknown) {
535#else
536 , fBoundary(boundary) {
537#endif
538 // We don't intersect by ctx.desiredOutput() and only use the Context to make the surface.
539 // It is assumed the caller has already accounted for the desired output, or it's a
540 // situation where the desired output shouldn't apply (e.g. this surface will be transformed
541 // to align with the actual desired output via FilterResult metadata).
542 sk_sp<SkDevice> device = nullptr;
543 if (!dstBounds.isEmpty()) {
544 fDstBounds.outset(LayerSpace<SkISize>({this->padding(), this->padding()}));
545 device = ctx.backend()->makeDevice(SkISize(fDstBounds.size()),
546 ctx.refColorSpace(),
547 props);
548 }
549
550 if (!device) {
551 return;
552 }
553
554 // Wrap the device in a canvas and use that to configure its origin and clip. This ensures
555 // the device and the canvas are in sync regardless of how the AutoSurface user intends
556 // to render.
557 ctx.markNewSurface();
558 fCanvas.emplace(std::move(device));
559 fCanvas->translate(-fDstBounds.left(), -fDstBounds.top());
560 fCanvas->clear(SkColors::kTransparent);
561 if (fBoundary == PixelBoundary::kTransparent) {
562 // Clip to the original un-padded dst bounds, ensuring that the border pixels remain
563 // fully transparent.
564 fCanvas->clipIRect(SkIRect(dstBounds));
565 } else {
566 // Otherwise clip to the possibly padded fDstBounds, if the backend made an approx-fit
567 // surface. If the bounds were padded for PixelBoundary::kInitialized, this will allow
568 // the border pixels to be rendered naturally.
569 fCanvas->clipIRect(SkIRect(fDstBounds));
570 }
571
572 if (renderInParameterSpace) {
573 fCanvas->concat(SkMatrix(ctx.mapping().layerMatrix()));
574 }
575 }
576
577 explicit operator bool() const { return fCanvas.has_value(); }
578
579 SkDevice* device() { SkASSERT(fCanvas.has_value()); return SkCanvasPriv::TopDevice(&*fCanvas); }
580 SkCanvas* operator->() { SkASSERT(fCanvas.has_value()); return &*fCanvas; }
581
583 if (fCanvas.has_value()) {
584 // Finish everything and mark the device as immutable so that snapSpecial() can avoid
585 // copying data.
586 fCanvas->restoreToCount(0);
587 this->device()->setImmutable();
588
589 // Snap a subset of the device with the padded dst bounds
590 SkIRect subset = SkIRect::MakeWH(fDstBounds.width(), fDstBounds.height());
592 fCanvas.reset(); // Only use the AutoSurface once
593
594 if (image && fBoundary != PixelBoundary::kUnknown) {
595 // Inset subset relative to 'image' reported size
596 const int padding = this->padding();
597 subset = SkIRect::MakeSize(image->dimensions()).makeInset(padding, padding);
598 LayerSpace<SkIPoint> origin{{fDstBounds.left() + padding,
599 fDstBounds.top() + padding}};
600 return {image->makeSubset(subset), origin, fBoundary};
601 } else {
602 // No adjustment to make
603 return {image, fDstBounds.topLeft(), PixelBoundary::kUnknown};
604 }
605 } else {
606 return {};
607 }
608 }
609
610private:
611 int padding() const { return fBoundary == PixelBoundary::kUnknown ? 0 : 1; }
612
613 std::optional<SkCanvas> fCanvas;
614 LayerSpace<SkIRect> fDstBounds; // includes padding, if any
615 PixelBoundary fBoundary;
616};
617
618///////////////////////////////////////////////////////////////////////////////////////////////////
619// FilterResult
620
622 auto [image, origin] = this->imageAndOffset(ctx);
623 *offset = SkIPoint(origin);
624 return image;
625}
626
627std::pair<sk_sp<SkSpecialImage>, LayerSpace<SkIPoint>>FilterResult::imageAndOffset(
628 const Context& ctx) const {
629 FilterResult resolved = this->resolve(ctx, ctx.desiredOutput());
630 return {resolved.fImage, resolved.layerBounds().topLeft()};
631}
632
634 if (!fImage) {
635 return {};
636 }
637
638 // SkCanvas processing should have prepared a decal-tiled image before calling this.
639 SkASSERT(fTileMode == SkTileMode::kDecal);
640
641 // PixelBoundary tracking assumes the special image's subset does not include the padding, so
642 // inset by a single pixel.
643 FilterResult inset = this->insetByPixel();
644 // Trust that SkCanvas configured the layer's SkDevice to ensure the padding remained
645 // transparent. Upgrading this pixel boundary knowledge allows the source image to use the
646 // simpler clamp math (vs. decal math) when used in a shader context.
647 SkASSERT(inset.fBoundary == PixelBoundary::kInitialized &&
648 inset.fTileMode == SkTileMode::kDecal);
649 inset.fBoundary = PixelBoundary::kTransparent;
650 return inset;
651}
652
653FilterResult FilterResult::insetByPixel() const {
654 // This assumes that the image is pixel aligned with its layer bounds, which is validated in
655 // the call to subset().
656 auto insetBounds = fLayerBounds;
657 insetBounds.inset(LayerSpace<SkISize>({1, 1}));
658 // Shouldn't be calling this except in situations where padding was explicitly added before.
659 SkASSERT(!insetBounds.isEmpty());
660 return this->subset(fLayerBounds.topLeft(), insetBounds);
661}
662
663SkEnumBitMask<FilterResult::BoundsAnalysis> FilterResult::analyzeBounds(
664 const SkMatrix& xtraTransform,
665 const SkIRect& dstBounds,
666 BoundsScope scope) const {
667 static constexpr SkSamplingOptions kNearestNeighbor = {};
668 static constexpr float kHalfPixel = 0.5f;
669 static constexpr float kCubicRadius = 1.5f;
670
671 SkEnumBitMask<BoundsAnalysis> analysis = BoundsAnalysis::kSimple;
672 const bool fillsLayerBounds = fTileMode != SkTileMode::kDecal ||
673 (fColorFilter && as_CFB(fColorFilter)->affectsTransparentBlack());
674
675 // 1. Is the layer geometry visible in the dstBounds (ignoring whether or not there are shading
676 // effects that highlight that boundary).
677 SkRect pixelCenterBounds = SkRect::Make(dstBounds);
678 if (!SkRectPriv::QuadContainsRect(xtraTransform,
679 SkIRect(fLayerBounds),
680 dstBounds,
681 kRoundEpsilon)) {
682 // 1a. If an effect doesn't fill out to the layer bounds, is the image content itself
683 // clipped by the layer bounds?
684 bool requireLayerCrop = fillsLayerBounds;
685 if (!fillsLayerBounds) {
686 LayerSpace<SkIRect> imageBounds =
687 fTransform.mapRect(LayerSpace<SkIRect>{fImage->dimensions()});
688 requireLayerCrop = !fLayerBounds.contains(imageBounds);
689 }
690
691 if (requireLayerCrop) {
692 analysis |= BoundsAnalysis::kRequiresLayerCrop;
693 // And since the layer crop will have to be applied externally, we can restrict the
694 // sample bounds to the intersection of dstBounds and layerBounds
695 SkIRect layerBoundsInDst = Mapping::map(SkIRect(fLayerBounds), xtraTransform);
696 // In some cases these won't intersect, usually in a complex graph where the input is
697 // a bitmap or the dynamic source, in which case it hasn't been clipped or dropped by
698 // earlier image filter processing for that particular node. We could return a flag here
699 // to signal that the operation should be treated as transparent black, but that would
700 // create more shader combinations and image sampling will still do the right thing by
701 // leaving 'pixelCenterBounds' as the original 'dstBounds'.
702 (void) pixelCenterBounds.intersect(SkRect::Make(layerBoundsInDst));
703 }
704 // else this is a decal-tiled, non-transparent affecting FilterResult that doesn't have
705 // its pixel data clipped by the layer bounds, so the layer crop doesn't have to be applied
706 // separately. But this means that the image will be sampled over all of 'dstBounds'.
707 }
708 // else the layer bounds geometry isn't visible, so 'dstBounds' is already a tighter bounding
709 // box for how the image will be sampled.
710
711 // 2. Are the tiling and deferred color filter effects visible in the sampled bounds
712 SkRect imageBounds = SkRect::Make(fImage->dimensions());
713 LayerSpace<SkMatrix> netTransform = fTransform;
714 netTransform.postConcat(LayerSpace<SkMatrix>(xtraTransform));
715 SkM44 netM44{SkMatrix(netTransform)};
716
717 const auto [xAxisAligned, yAxisAligned] = are_axes_nearly_integer_aligned(netTransform);
718 const bool isPixelAligned = xAxisAligned && yAxisAligned;
719 // When decal sampling, we use an inset image bounds for checking if the dst is covered. If not,
720 // an image that exactly filled the dst bounds could still sample transparent black, in which
721 // case the transform's scale factor needs to be taken into account.
722 const bool decalLeaks = fTileMode == SkTileMode::kDecal &&
723 fSamplingOptions != kNearestNeighbor &&
724 !isPixelAligned;
725
726 const float sampleRadius = fSamplingOptions.useCubic ? kCubicRadius : kHalfPixel;
727 SkRect safeImageBounds = imageBounds.makeInset(sampleRadius, sampleRadius);
728 if (fSamplingOptions == kDefaultSampling && !isPixelAligned) {
729 // When using default sampling, integer translations are eventually downgraded to nearest
730 // neighbor, so the 1/2px inset clamping is sufficient to safely access within the subset.
731 // When staying with linear filtering, a sample at 1/2px inset exactly will end up accessing
732 // one external pixel with a weight of 0 (but MSAN will complain and not all GPUs actually
733 // seem to get that correct). To be safe we have to clamp to epsilon inside the 1/2px.
734 safeImageBounds.inset(xAxisAligned ? 0.f : kRoundEpsilon,
735 yAxisAligned ? 0.f : kRoundEpsilon);
736 }
737 bool hasPixelPadding = fBoundary != PixelBoundary::kUnknown;
738
740 decalLeaks ? safeImageBounds : imageBounds,
741 pixelCenterBounds,
742 kRoundEpsilon)) {
743 analysis |= BoundsAnalysis::kDstBoundsNotCovered;
744 if (fillsLayerBounds) {
745 analysis |= BoundsAnalysis::kHasLayerFillingEffect;
746 }
747 if (decalLeaks) {
748 // Some amount of decal tiling will be visible in the output so check the relative size
749 // of the decal interpolation from texel to dst space; if it's not close to 1 it needs
750 // to be handled specially to keep rendering methods visually consistent.
751 float scaleFactors[2];
752 if (!(SkMatrix(netTransform).getMinMaxScales(scaleFactors) &&
753 SkScalarNearlyEqual(scaleFactors[0], 1.f, 0.2f) &&
754 SkScalarNearlyEqual(scaleFactors[1], 1.f, 0.2f))) {
755 analysis |= BoundsAnalysis::kRequiresDecalInLayerSpace;
756 if (fBoundary == PixelBoundary::kTransparent) {
757 // Turn off considering the transparent padding as safe to prevent that
758 // transparency from multiplying with the layer-space decal effect.
759 hasPixelPadding = false;
760 }
761 }
762 }
763 }
764
765 if (scope == BoundsScope::kDeferred) {
766 return analysis; // skip sampling analysis
767 } else if (scope == BoundsScope::kCanDrawDirectly &&
768 !(analysis & BoundsAnalysis::kHasLayerFillingEffect)) {
769 // When drawing the image directly, the geometry is limited to the image. If the texels
770 // are pixel aligned, then it is safe to skip shader-based tiling.
771 const bool nnOrBilerp = fSamplingOptions == kDefaultSampling ||
772 fSamplingOptions == kNearestNeighbor;
773 if (nnOrBilerp && (hasPixelPadding || isPixelAligned)) {
774 return analysis;
775 }
776 }
777
778 // 3. Would image pixels outside of its subset be sampled if shader-clamping is skipped?
779
780 // Include the padding for sampling analysis and inset the dst by 1/2 px to represent where the
781 // sampling is evaluated at.
782 if (hasPixelPadding) {
783 safeImageBounds.outset(1.f, 1.f);
784 }
785 pixelCenterBounds.inset(kHalfPixel, kHalfPixel);
786
787 // True if all corners of 'pixelCenterBounds' are on the inside of each edge of
788 // 'safeImageBounds', ordered T,R,B,L.
790 safeImageBounds,
791 pixelCenterBounds,
792 kRoundEpsilon);
793 if (!all(edgeMask)) {
794 // Sampling outside the image subset occurs, but if the edges that are exceeded are HW
795 // edges, then we can avoid using shader-based tiling.
796 skvx::int4 hwEdge{fImage->subset().fTop == 0,
797 fImage->subset().fRight == fImage->backingStoreDimensions().fWidth,
798 fImage->subset().fBottom == fImage->backingStoreDimensions().fHeight,
799 fImage->subset().fLeft == 0};
800 if (fTileMode == SkTileMode::kRepeat || fTileMode == SkTileMode::kMirror) {
801 // For periodic tile modes, we require both edges on an axis to be HW edges
802 hwEdge = hwEdge & skvx::shuffle<2,3,0,1>(hwEdge); // TRBL & BLTR
803 }
804 if (!all(edgeMask | hwEdge)) {
805 analysis |= BoundsAnalysis::kRequiresShaderTiling;
806 }
807 }
808
809 return analysis;
810}
811
812void FilterResult::updateTileMode(const Context& ctx, SkTileMode tileMode) {
813 if (fImage) {
814 fTileMode = tileMode;
816 fLayerBounds = ctx.desiredOutput();
817 }
818 }
819}
820
822 const LayerSpace<SkIRect>& crop,
823 SkTileMode tileMode) const {
824 static const LayerSpace<SkMatrix> kIdentity{SkMatrix::I()};
825
826 if (crop.isEmpty() || ctx.desiredOutput().isEmpty()) {
827 // An empty crop cannot be anything other than fully transparent
828 return {};
829 }
830
831 // First, determine how this image's layer bounds interact with the crop rect, which determines
832 // the portion of 'crop' that could have non-transparent content.
833 LayerSpace<SkIRect> cropContent = crop;
834 if (!fImage ||
835 !cropContent.intersect(fLayerBounds)) {
836 // The pixels within 'crop' would be fully transparent, and tiling won't change that.
837 return {};
838 }
839
840 // Second, determine the subset of 'crop' that is relevant to ctx.desiredOutput().
841 LayerSpace<SkIRect> fittedCrop = crop.relevantSubset(ctx.desiredOutput(), tileMode);
842
843 // Third, check if there's overlap with the known non-transparent cropped content and what's
844 // used to tile the desired output. If not, the image is known to be empty. This modifies
845 // 'cropContent' and not 'fittedCrop' so that any transparent padding remains if we have to
846 // apply repeat/mirror tiling to the original geometry.
847 if (!cropContent.intersect(fittedCrop)) {
848 return {};
849 }
850
851 // Fourth, a periodic tiling that covers the output with a single instance of the image can be
852 // simplified to just a transform.
853 auto periodicTransform = periodic_axis_transform(tileMode, fittedCrop, ctx.desiredOutput());
854 if (periodicTransform) {
855 return this->applyTransform(ctx, *periodicTransform, FilterResult::kDefaultSampling);
856 }
857
858 bool preserveTransparencyInCrop = false;
860 // We can reduce the crop dimensions to what's non-transparent
861 fittedCrop = cropContent;
862 } else if (fittedCrop.contains(ctx.desiredOutput())) {
864 fittedCrop = ctx.desiredOutput();
865 } else if (!cropContent.contains(fittedCrop)) {
866 // There is transparency in fittedCrop that must be resolved in order to maintain the new
867 // tiling geometry.
868 preserveTransparencyInCrop = true;
869 if (fTileMode == SkTileMode::kDecal && tileMode == SkTileMode::kClamp) {
870 // include 1px buffer for transparency from original kDecal tiling
871 cropContent.outset(skif::LayerSpace<SkISize>({1, 1}));
872 SkAssertResult(fittedCrop.intersect(cropContent));
873 }
874 } // Otherwise cropContent == fittedCrop
875
876 // Fifth, when the transform is an integer translation, any prior tiling and the new tiling
877 // can sometimes be addressed analytically without producing a new image. Moving the crop into
878 // the image dimensions allows future operations like applying a transform or color filter to
879 // be composed without rendering a new image since there will not be an intervening crop.
880 const bool doubleClamp = fTileMode == SkTileMode::kClamp && tileMode == SkTileMode::kClamp;
882 if (!preserveTransparencyInCrop &&
883 is_nearly_integer_translation(fTransform, &origin) &&
884 (doubleClamp ||
885 !(this->analyzeBounds(fittedCrop) & BoundsAnalysis::kHasLayerFillingEffect))) {
886 // Since the transform is axis-aligned, the tile mode can be applied to the original
887 // image pre-transformation and still be consistent with the 'crop' geometry. When the
888 // original tile mode is decal, extract_subset is always valid. When the original mode is
889 // mirror/repeat, !kHasLayerFillingEffect ensures that 'fittedCrop' is contained within
890 // the base image bounds, so extract_subset is valid. When the original mode is clamp
891 // and the new mode is not clamp, that is also the case. When both modes are clamp, we have
892 // to consider how 'fittedCrop' intersects (or doesn't) with the base image bounds.
893 FilterResult restrictedOutput = this->subset(origin, fittedCrop, doubleClamp);
894 restrictedOutput.updateTileMode(ctx, tileMode);
895 if (restrictedOutput.fBoundary == PixelBoundary::kInitialized ||
897 // Discard kInitialized since a crop is a strict constraint on sampling outside of it.
898 // But preserve (kTransparent+kDecal) if this is a no-op crop.
899 restrictedOutput.fBoundary = PixelBoundary::kUnknown;
900 }
901 return restrictedOutput;
902 } else if (tileMode == SkTileMode::kDecal) {
903 // A decal crop can always be applied as the final operation by adjusting layer bounds, and
904 // does not modify any prior tile mode.
905 SkASSERT(!preserveTransparencyInCrop);
906 FilterResult restrictedOutput = *this;
907 restrictedOutput.fLayerBounds = fittedCrop;
908 return restrictedOutput;
909 } else {
910 // There is a non-trivial transform to the image data that must be applied before the
911 // non-decal tilemode is meant to be applied to the axis-aligned 'crop'.
912 FilterResult tiled = this->resolve(ctx, fittedCrop, /*preserveDstBounds=*/true);
913 tiled.updateTileMode(ctx, tileMode);
914 return tiled;
915 }
916}
917
920 // A null filter is the identity, so it should have been caught during image filter DAG creation
922
923 if (ctx.desiredOutput().isEmpty()) {
924 return {};
925 }
926
927 // Color filters are applied after the transform and image sampling, but before the fLayerBounds
928 // crop. We can compose 'colorFilter' with any previously applied color filter regardless
929 // of the transform/sample state, so long as it respects the effect of the current crop.
930 LayerSpace<SkIRect> newLayerBounds = fLayerBounds;
931 if (as_CFB(colorFilter)->affectsTransparentBlack()) {
932 if (!fImage || !newLayerBounds.intersect(ctx.desiredOutput())) {
933 // The current image's intersection with the desired output is fully transparent, but
934 // the new color filter converts that into a non-transparent color. The desired output
935 // is filled with this color, but use a 1x1 surface and clamp tiling.
938 ctx.desiredOutput().top(),
939 1, 1)},
940 PixelBoundary::kInitialized,
941 /*renderInParameterSpace=*/false};
942 if (surface) {
944 paint.setColor4f(SkColors::kTransparent, /*colorSpace=*/nullptr);
945 paint.setColorFilter(std::move(colorFilter));
946 surface->drawPaint(paint);
947 }
948 FilterResult solidColor = surface.snap();
949 solidColor.updateTileMode(ctx, SkTileMode::kClamp);
950 return solidColor;
951 }
952
953 if (this->analyzeBounds(ctx.desiredOutput()) & BoundsAnalysis::kRequiresLayerCrop) {
954 // Since 'colorFilter' modifies transparent black, the new result's layer bounds must
955 // be the desired output. But if the current image is cropped we need to resolve the
956 // image to avoid losing the effect of the current 'fLayerBounds'.
957 newLayerBounds.outset(LayerSpace<SkISize>({1, 1}));
958 SkAssertResult(newLayerBounds.intersect(ctx.desiredOutput()));
959 FilterResult filtered = this->resolve(ctx, newLayerBounds,
960 /*preserveDstBounds=*/true);
961 filtered.fColorFilter = std::move(colorFilter);
962 filtered.updateTileMode(ctx, SkTileMode::kClamp);
963 return filtered;
964 }
965
966 // otherwise we can fill out to the desired output without worrying about losing the crop.
967 newLayerBounds = ctx.desiredOutput();
968 } else {
969 if (!fImage || !LayerSpace<SkIRect>::Intersects(newLayerBounds, ctx.desiredOutput())) {
970 // The color filter does not modify transparent black, so it remains transparent
971 return {};
972 }
973 // otherwise a non-transparent affecting color filter can always be lifted before any crop
974 // because it does not change the "shape" of the prior FilterResult.
975 }
976
977 // If we got here we can compose the new color filter with the previous filter and the prior
978 // layer bounds are either soft-cropped to the desired output, or we fill out the desired output
979 // when the new color filter affects transparent black. We don't check if the entire composed
980 // filter affects transparent black because earlier floods are restricted by the layer bounds.
981 FilterResult filtered = *this;
982 filtered.fLayerBounds = newLayerBounds;
983 filtered.fColorFilter = SkColorFilters::Compose(std::move(colorFilter), fColorFilter);
984 return filtered;
985}
986
987static bool compatible_sampling(const SkSamplingOptions& currentSampling,
988 bool currentXformWontAffectNearest,
989 SkSamplingOptions* nextSampling,
990 bool nextXformWontAffectNearest) {
991 // Both transforms could perform non-trivial sampling, but if they are similar enough we
992 // assume performing one non-trivial sampling operation with the concatenated transform will
993 // not be visually distinguishable from sampling twice.
994 // TODO(michaelludwig): For now ignore mipmap policy, SkSpecialImages are not supposed to be
995 // drawn with mipmapping, and the majority of filter steps produce images that are at the
996 // proper scale and do not define mip levels. The main exception is the ::Image() filter
997 // leaf but that doesn't use this system yet.
998 if (currentSampling.isAniso() && nextSampling->isAniso()) {
999 // Assume we can get away with one sampling at the highest anisotropy level
1000 *nextSampling = SkSamplingOptions::Aniso(std::max(currentSampling.maxAniso,
1001 nextSampling->maxAniso));
1002 return true;
1003 } else if (currentSampling.isAniso() && nextSampling->filter == SkFilterMode::kLinear) {
1004 // Assume we can get away with the current anisotropic filter since the next is linear
1005 *nextSampling = currentSampling;
1006 return true;
1007 } else if (nextSampling->isAniso() && currentSampling.filter == SkFilterMode::kLinear) {
1008 // Mirror of the above, assume we can just get away with next's anisotropic filter
1009 return true;
1010 } else if (currentSampling.useCubic && (nextSampling->filter == SkFilterMode::kLinear ||
1011 (nextSampling->useCubic &&
1012 currentSampling.cubic.B == nextSampling->cubic.B &&
1013 currentSampling.cubic.C == nextSampling->cubic.C))) {
1014 // Assume we can get away with the current bicubic filter, since the next is the same
1015 // or a bilerp that can be upgraded.
1016 *nextSampling = currentSampling;
1017 return true;
1018 } else if (nextSampling->useCubic && currentSampling.filter == SkFilterMode::kLinear) {
1019 // Mirror of the above, assume we can just get away with next's cubic resampler
1020 return true;
1021 } else if (currentSampling.filter == SkFilterMode::kLinear &&
1022 nextSampling->filter == SkFilterMode::kLinear) {
1023 // Assume we can get away with a single bilerp vs. the two
1024 return true;
1025 } else if (nextSampling->filter == SkFilterMode::kNearest && currentXformWontAffectNearest) {
1026 // The next transform and nearest-neighbor filtering isn't impacted by the current transform
1027 SkASSERT(currentSampling.filter == SkFilterMode::kLinear);
1028 return true;
1029 } else if (currentSampling.filter == SkFilterMode::kNearest && nextXformWontAffectNearest) {
1030 // The next transform doesn't change the nearest-neighbor filtering of the current transform
1031 SkASSERT(nextSampling->filter == SkFilterMode::kLinear);
1032 *nextSampling = currentSampling;
1033 return true;
1034 } else {
1035 // The current or next sampling is nearest neighbor, and will produce visible texels
1036 // oriented with the current transform; assume this is a desired effect and preserve it.
1037 return false;
1038 }
1039}
1040
1043 const SkSamplingOptions &sampling) const {
1044 if (!fImage || ctx.desiredOutput().isEmpty()) {
1045 // Transformed transparent black remains transparent black.
1046 SkASSERT(!fColorFilter);
1047 return {};
1048 }
1049
1050 // Extract the sampling options that matter based on the current and next transforms.
1051 // We make sure the new sampling is bilerp (default) if the new transform doesn't matter
1052 // (and assert that the current is bilerp if its transform didn't matter). Bilerp can be
1053 // maximally combined, so simplifies the logic in compatible_sampling().
1054 const bool currentXformIsInteger = is_nearly_integer_translation(fTransform);
1055 const bool nextXformIsInteger = is_nearly_integer_translation(transform);
1056
1057 SkASSERT(!currentXformIsInteger || fSamplingOptions == kDefaultSampling);
1058 SkSamplingOptions nextSampling = nextXformIsInteger ? kDefaultSampling : sampling;
1059
1060 // Determine if the image is being visibly cropped by the layer bounds, in which case we can't
1061 // merge this transform with any previous transform (unless the new transform is an integer
1062 // translation in which case any visible edge is aligned with the desired output and can be
1063 // resolved by intersecting the transformed layer bounds and the output bounds).
1064 bool isCropped = !nextXformIsInteger &&
1065 (this->analyzeBounds(SkMatrix(transform), SkIRect(ctx.desiredOutput()))
1066 & BoundsAnalysis::kRequiresLayerCrop);
1067
1068 FilterResult transformed;
1069 if (!isCropped && compatible_sampling(fSamplingOptions, currentXformIsInteger,
1070 &nextSampling, nextXformIsInteger)) {
1071 // We can concat transforms and 'nextSampling' will be either fSamplingOptions,
1072 // sampling, or a merged combination depending on the two transforms in play.
1073 transformed = *this;
1074 } else {
1075 // We'll have to resolve this FilterResult first before 'transform' and 'sampling' can be
1076 // correctly evaluated. 'nextSampling' will always be 'sampling'.
1077 LayerSpace<SkIRect> tightBounds;
1078 if (transform.inverseMapRect(ctx.desiredOutput(), &tightBounds)) {
1079 transformed = this->resolve(ctx, tightBounds);
1080 }
1081
1082 if (!transformed.fImage) {
1083 // Transform not invertible or resolve failed to create an image
1084 return {};
1085 }
1086 }
1087
1088 transformed.fSamplingOptions = nextSampling;
1089 transformed.fTransform.postConcat(transform);
1090 // Rebuild the layer bounds and then restrict to the current desired output. The original value
1091 // of fLayerBounds includes the image mapped by the original fTransform as well as any
1092 // accumulated soft crops from desired outputs of prior stages. To prevent discarding that info,
1093 // we map fLayerBounds by the additional transform, instead of re-mapping the image bounds.
1094 transformed.fLayerBounds = transform.mapRect(transformed.fLayerBounds);
1095 if (!LayerSpace<SkIRect>::Intersects(transformed.fLayerBounds, ctx.desiredOutput())) {
1096 // The transformed output doesn't touch the desired, so it would just be transparent black.
1097 return {};
1098 }
1099
1100 return transformed;
1101}
1102
1103FilterResult FilterResult::resolve(const Context& ctx,
1104 LayerSpace<SkIRect> dstBounds,
1105 bool preserveDstBounds) const {
1106 // The layer bounds is the final clip, so it can always be used to restrict 'dstBounds'. Even
1107 // if there's a non-decal tile mode or transparent-black affecting color filter, those floods
1108 // are restricted to fLayerBounds.
1109 if (!fImage || (!preserveDstBounds && !dstBounds.intersect(fLayerBounds))) {
1110 return {nullptr, {}};
1111 }
1112
1113 // If we have any extra effect to apply, there's no point in trying to extract a subset.
1114 const bool subsetCompatible = !fColorFilter &&
1115 fTileMode == SkTileMode::kDecal &&
1116 !preserveDstBounds;
1117
1118 // TODO(michaelludwig): If we get to the point where all filter results track bounds in
1119 // floating point, then we can extend this case to any S+T transform.
1120 LayerSpace<SkIPoint> origin;
1121 if (subsetCompatible && is_nearly_integer_translation(fTransform, &origin)) {
1122 return this->subset(origin, dstBounds);
1123 } // else fall through and attempt a draw
1124
1125 // Don't use context properties to avoid DMSAA on internal stages of filter evaluation.
1126 SkSurfaceProps props = {};
1127 PixelBoundary boundary = preserveDstBounds ? PixelBoundary::kUnknown
1128 : PixelBoundary::kTransparent;
1129 AutoSurface surface{ctx, dstBounds, boundary, /*renderInParameterSpace=*/false, &props};
1130 if (surface) {
1131 this->draw(ctx, surface.device(), /*preserveDeviceState=*/false);
1132 }
1133 return surface.snap();
1134}
1135
1136FilterResult FilterResult::subset(const LayerSpace<SkIPoint>& knownOrigin,
1137 const LayerSpace<SkIRect>& subsetBounds,
1138 bool clampSrcIfDisjoint) const {
1139 SkDEBUGCODE(LayerSpace<SkIPoint> actualOrigin;)
1140 SkASSERT(is_nearly_integer_translation(fTransform, &actualOrigin) &&
1141 SkIPoint(actualOrigin) == SkIPoint(knownOrigin));
1142
1143
1144 LayerSpace<SkIRect> imageBounds(SkIRect::MakeXYWH(knownOrigin.x(), knownOrigin.y(),
1145 fImage->width(), fImage->height()));
1146 imageBounds = imageBounds.relevantSubset(subsetBounds, clampSrcIfDisjoint ? SkTileMode::kClamp
1147 : SkTileMode::kDecal);
1148 if (imageBounds.isEmpty()) {
1149 return {};
1150 }
1151
1152 // Offset the image subset directly to avoid issues negating (origin). With the prior
1153 // intersection (bounds - origin) will be >= 0, but (bounds + (-origin)) may not, (e.g.
1154 // origin is INT_MIN).
1155 SkIRect subset = { imageBounds.left() - knownOrigin.x(),
1156 imageBounds.top() - knownOrigin.y(),
1157 imageBounds.right() - knownOrigin.x(),
1158 imageBounds.bottom() - knownOrigin.y() };
1159 SkASSERT(subset.fLeft >= 0 && subset.fTop >= 0 &&
1160 subset.fRight <= fImage->width() && subset.fBottom <= fImage->height());
1161
1162 FilterResult result{fImage->makeSubset(subset), imageBounds.topLeft()};
1163 result.fColorFilter = fColorFilter;
1164
1165 // Update what's known about PixelBoundary based on how the subset aligns.
1166 SkASSERT(result.fBoundary == PixelBoundary::kUnknown);
1167 // If the pixel bounds didn't change, preserve the original boundary value
1168 if (fImage->subset() == result.fImage->subset()) {
1169 result.fBoundary = fBoundary;
1170 } else {
1171 // If the new pixel bounds are bordered by valid data, upgrade to kInitialized
1172 SkIRect safeSubset = fImage->subset();
1173 if (fBoundary == PixelBoundary::kUnknown) {
1174 safeSubset.inset(1, 1);
1175 }
1176 if (safeSubset.contains(result.fImage->subset())) {
1177 result.fBoundary = PixelBoundary::kInitialized;
1178 }
1179 }
1180 return result;
1181}
1182
1183void FilterResult::draw(const Context& ctx, SkDevice* target, const SkBlender* blender) const {
1185 this->draw(ctx, target, /*preserveDeviceState=*/true, blender);
1186}
1187
1188void FilterResult::draw(const Context& ctx,
1190 bool preserveDeviceState,
1191 const SkBlender* blender) const {
1192 const bool blendAffectsTransparentBlack = blender && as_BB(blender)->affectsTransparentBlack();
1193 if (!fImage) {
1194 // The image is transparent black, this is a no-op unless we need to apply the blend mode
1195 if (blendAffectsTransparentBlack) {
1196 SkPaint clear;
1198 clear.setBlender(sk_ref_sp(blender));
1199 device->drawPaint(clear);
1200 }
1201 return;
1202 }
1203
1204 BoundsScope scope = blendAffectsTransparentBlack ? BoundsScope::kShaderOnly
1205 : BoundsScope::kCanDrawDirectly;
1206 SkEnumBitMask<BoundsAnalysis> analysis = this->analyzeBounds(device->localToDevice(),
1208 scope);
1209
1210 if (analysis & BoundsAnalysis::kRequiresLayerCrop) {
1211 if (blendAffectsTransparentBlack) {
1212 // This is similar to the resolve() path in applyColorFilter() when the filter affects
1213 // transparent black but must be applied after the prior visible layer bounds clip.
1214 // NOTE: We map devClipBounds() by the local-to-device matrix instead of the Context
1215 // mapping because that works for both use cases: drawing to the final device (where
1216 // the transforms are the same), or drawing to intermediate layer images (where they
1217 // are not the same).
1218 LayerSpace<SkIRect> dstBounds;
1219 if (!LayerSpace<SkMatrix>(device->localToDevice()).inverseMapRect(
1220 LayerSpace<SkIRect>(device->devClipBounds()), &dstBounds)) {
1221 return;
1222 }
1223 // Regardless of the scenario, the end result is that it's in layer space.
1224 FilterResult clipped = this->resolve(ctx, dstBounds);
1225 clipped.draw(ctx, device, preserveDeviceState, blender);
1226 return;
1227 }
1228 // Otherwise we can apply the layer bounds as a clip to avoid an intermediate render pass
1229 if (preserveDeviceState) {
1231 }
1232 device->clipRect(SkRect::Make(SkIRect(fLayerBounds)), SkClipOp::kIntersect, /*aa=*/true);
1233 }
1234
1235 // If we are an integer translate, the default bilinear sampling *should* be equivalent to
1236 // nearest-neighbor. Going through the direct image-drawing path tends to detect this
1237 // and reduce sampling automatically. When we have to use an image shader, this isn't
1238 // detected and some GPUs' linear filtering doesn't exactly match nearest-neighbor and can
1239 // lead to leaks beyond the image's subset. Detect and reduce sampling explicitly.
1240 const bool pixelAligned =
1241 is_nearly_integer_translation(fTransform) &&
1242 is_nearly_integer_translation(skif::LayerSpace<SkMatrix>(device->localToDevice()));
1243 SkSamplingOptions sampling = fSamplingOptions;
1244 if (sampling == kDefaultSampling && pixelAligned) {
1245 sampling = {};
1246 }
1247
1248 if (analysis & BoundsAnalysis::kHasLayerFillingEffect ||
1249 (blendAffectsTransparentBlack && (analysis & BoundsAnalysis::kDstBoundsNotCovered))) {
1250 // Fill the canvas with the shader, so that the pixels beyond the image dimensions are still
1251 // covered by the draw and either resolve tiling into the image, color filter transparent
1252 // black, apply the blend mode to the dst, or any combination thereof.
1253 SkPaint paint;
1254 paint.setBlender(sk_ref_sp(blender));
1255 paint.setShader(this->getAnalyzedShaderView(ctx, sampling, analysis));
1257 } else {
1258 this->drawAnalyzedImage(ctx, device, sampling, analysis, blender);
1259 }
1260
1261 if (preserveDeviceState && (analysis & BoundsAnalysis::kRequiresLayerCrop)) {
1263 }
1264}
1265
1266void FilterResult::drawAnalyzedImage(const Context& ctx,
1268 const SkSamplingOptions& finalSampling,
1270 const SkBlender* blender) const {
1271 SkASSERT(!(analysis & BoundsAnalysis::kHasLayerFillingEffect));
1272
1273 SkPaint paint;
1274 paint.setBlender(sk_ref_sp(blender));
1275 paint.setColorFilter(fColorFilter);
1276
1277 // src's origin is embedded in fTransform. For historical reasons, drawSpecial() does
1278 // not automatically use the device's current local-to-device matrix, but that's what preps
1279 // it to match the expected layer coordinate system.
1280 SkMatrix netTransform = SkMatrix::Concat(device->localToDevice(), SkMatrix(fTransform));
1281
1282 // Check fSamplingOptions for linear filtering, not 'finalSampling' since it may have been
1283 // reduced to nearest neighbor.
1284 if (this->canClampToTransparentBoundary(analysis) && fSamplingOptions == kDefaultSampling) {
1285 SkASSERT(!(analysis & BoundsAnalysis::kRequiresShaderTiling));
1286 // Draw non-AA with a 1px outset image so that the transparent boundary filtering is
1287 // not multiplied with the AA (which creates a harsher AA transition).
1288 netTransform.preTranslate(-1.f, -1.f);
1289 device->drawSpecial(fImage->makePixelOutset().get(), netTransform, finalSampling, paint,
1291 } else {
1292 paint.setAntiAlias(true);
1294 if (analysis & BoundsAnalysis::kRequiresShaderTiling) {
1296 ctx.markShaderBasedTilingRequired(SkTileMode::kClamp);
1297 }
1298 device->drawSpecial(fImage.get(), netTransform, finalSampling, paint, constraint);
1299 }
1300}
1301
1302sk_sp<SkShader> FilterResult::asShader(const Context& ctx,
1303 const SkSamplingOptions& xtraSampling,
1305 const LayerSpace<SkIRect>& sampleBounds) const {
1306 if (!fImage) {
1307 return nullptr;
1308 }
1309 // Even if flags don't force resolving the filter result to an axis-aligned image, if the
1310 // extra sampling to be applied is not compatible with the accumulated transform and sampling,
1311 // or if the logical image is cropped by the layer bounds, the FilterResult will need to be
1312 // resolved to an image before we wrap it as an SkShader. When checking if cropped, we use the
1313 // FilterResult's layer bounds instead of the context's desired output, assuming that the layer
1314 // bounds reflect the bounds of the coords a parent shader will pass to eval().
1315 const bool currentXformIsInteger = is_nearly_integer_translation(fTransform);
1316 const bool nextXformIsInteger = !(flags & ShaderFlags::kNonTrivialSampling);
1317
1318 SkBlendMode colorFilterMode;
1319 SkEnumBitMask<BoundsAnalysis> analysis = this->analyzeBounds(sampleBounds,
1320 BoundsScope::kShaderOnly);
1321
1322 SkSamplingOptions sampling = xtraSampling;
1323 const bool needsResolve =
1324 // Deferred calculations on the input would be repeated with each sample, but we allow
1325 // simple color filters to skip resolving since their repeated math should be cheap.
1327 ((fColorFilter && (!fColorFilter->asAColorMode(nullptr, &colorFilterMode) ||
1328 colorFilterMode > SkBlendMode::kLastCoeffMode)) ||
1329 !SkColorSpace::Equals(fImage->getColorSpace(), ctx.colorSpace()))) ||
1330 // The deferred sampling options can't be merged with the one requested
1331 !compatible_sampling(fSamplingOptions, currentXformIsInteger,
1332 &sampling, nextXformIsInteger) ||
1333 // The deferred edge of the layer bounds is visible to sampling
1334 (analysis & BoundsAnalysis::kRequiresLayerCrop);
1335
1336 // Downgrade to nearest-neighbor if the sequence of sampling doesn't do anything
1337 if (sampling == kDefaultSampling && nextXformIsInteger &&
1338 (needsResolve || currentXformIsInteger)) {
1339 sampling = {};
1340 }
1341
1342 sk_sp<SkShader> shader;
1343 if (needsResolve) {
1344 // The resolve takes care of fTransform (sans origin), fTileMode, fColorFilter, and
1345 // fLayerBounds.
1346 FilterResult resolved = this->resolve(ctx, sampleBounds);
1347 if (resolved) {
1348 // Redo the analysis, however, because it's hard to predict HW edge tiling. Since the
1349 // original layer crop was visible, that implies that the now-resolved image won't cover
1350 // dst bounds. Since we are using this as a shader to fill the dst bounds, we may have
1351 // to still do shader-clamping (to a transparent boundary) if the resolved image doesn't
1352 // have HW-tileable boundaries.
1353 [[maybe_unused]] static constexpr SkEnumBitMask<BoundsAnalysis> kExpectedAnalysis =
1354 BoundsAnalysis::kDstBoundsNotCovered | BoundsAnalysis::kRequiresShaderTiling;
1355 analysis = resolved.analyzeBounds(sampleBounds, BoundsScope::kShaderOnly);
1356 SkASSERT(!(analysis & ~kExpectedAnalysis));
1357 return resolved.getAnalyzedShaderView(ctx, sampling, analysis);
1358 }
1359 } else {
1360 shader = this->getAnalyzedShaderView(ctx, sampling, analysis);
1361 }
1362
1363 return shader;
1364}
1365
1366sk_sp<SkShader> FilterResult::getAnalyzedShaderView(
1367 const Context& ctx,
1368 const SkSamplingOptions& finalSampling,
1369 SkEnumBitMask<BoundsAnalysis> analysis) const {
1370 const SkMatrix& localMatrix(fTransform);
1371 const SkRect imageBounds = SkRect::Make(fImage->dimensions());
1372 // We need to apply the decal in a coordinate space that matches the resolution of the layer
1373 // space. If the transform preserves rectangles, map the image bounds by the transform so we
1374 // can apply it before we evaluate the shader. Otherwise decompose the transform into a
1375 // non-scaling post-decal transform and a scaling pre-decal transform.
1376 SkMatrix postDecal, preDecal;
1377 if (localMatrix.rectStaysRect() ||
1378 !(analysis & BoundsAnalysis::kRequiresDecalInLayerSpace)) {
1379 postDecal = SkMatrix::I();
1380 preDecal = localMatrix;
1381 } else {
1382 decompose_transform(localMatrix, imageBounds.center(), &postDecal, &preDecal);
1383 }
1384
1385 // If the image covers the dst bounds, then its tiling won't be visible, so we can switch
1386 // to the faster kClamp for either HW or shader-based tiling. If we are applying the decal
1387 // in layer space, then that extra shader implements the tiling, so we can switch to clamp
1388 // for the image shader itself.
1389 SkTileMode effectiveTileMode = fTileMode;
1390 const bool decalClampToTransparent = this->canClampToTransparentBoundary(analysis);
1391 const bool strict = SkToBool(analysis & BoundsAnalysis::kRequiresShaderTiling);
1392
1393 sk_sp<SkShader> imageShader;
1394 if (strict && decalClampToTransparent) {
1395 // Make the image shader apply to the 1px outset so that the strict subset includes the
1396 // transparent pixels.
1397 preDecal.preTranslate(-1.f, -1.f);
1398 imageShader = fImage->makePixelOutset()->asShader(SkTileMode::kClamp, finalSampling,
1399 preDecal, strict);
1400 effectiveTileMode = SkTileMode::kClamp;
1401 } else {
1402 if (!(analysis & BoundsAnalysis::kDstBoundsNotCovered) ||
1403 (analysis & BoundsAnalysis::kRequiresDecalInLayerSpace)) {
1404 effectiveTileMode = SkTileMode::kClamp;
1405 }
1406 imageShader = fImage->asShader(effectiveTileMode, finalSampling, preDecal, strict);
1407 }
1408 if (strict) {
1409 ctx.markShaderBasedTilingRequired(effectiveTileMode);
1410 }
1411
1412 if (analysis & BoundsAnalysis::kRequiresDecalInLayerSpace) {
1413 SkASSERT(fTileMode == SkTileMode::kDecal);
1414 // TODO(skbug:12784) - As part of fully supporting subsets in image shaders, it probably
1415 // makes sense to share the subset tiling logic that's in GrTextureEffect as dedicated
1416 // SkShaders. Graphite can then add those to its program as-needed vs. always doing
1417 // shader-based tiling, and CPU can have raster-pipeline tiling applied more flexibly than
1418 // at the bitmap level. At that point, this effect is redundant and can be replaced with the
1419 // decal-subset shader.
1420 const SkRuntimeEffect* decalEffect =
1421 GetKnownRuntimeEffect(SkKnownRuntimeEffects::StableKey::kDecal);
1422
1424 builder.child("image") = std::move(imageShader);
1425 builder.uniform("decalBounds") = preDecal.mapRect(imageBounds);
1426
1427 imageShader = builder.makeShader();
1428 }
1429
1430 if (imageShader && (analysis & BoundsAnalysis::kRequiresDecalInLayerSpace)) {
1431 imageShader = imageShader->makeWithLocalMatrix(postDecal);
1432 }
1433
1434 if (imageShader && fColorFilter) {
1435 imageShader = imageShader->makeWithColorFilter(fColorFilter);
1436 }
1437
1438 // Shader now includes the image, the sampling, the tile mode, the transform, and the color
1439 // filter, skipping deferred effects that aren't present or aren't visible given 'analysis'.
1440 // The last "effect", layer bounds cropping, must be handled externally by either resolving
1441 // the image before hand or clipping the device that's drawing the returned shader.
1442 return imageShader;
1443}
1444
1445static int downscale_step_count(float netScaleFactor) {
1446 int steps = SkNextLog2(sk_float_ceil2int(1.f / netScaleFactor));
1447 // There are (steps-1) 1/2x steps and then one step that will be between 1/2-1x. If the
1448 // final step is practically the identity scale, we can save a render pass and not incur too
1449 // much sampling error by reducing the step count and using a final scale that's slightly less
1450 // than 1/2.
1451 if (steps > 0) {
1452 // For a multipass rescale, we allow for a lot of tolerance when deciding to collapse the
1453 // final step. If there's only a single pass, we require the scale factor to be very close
1454 // to the identity since it causes the step count to go to 0.
1455 static constexpr float kMultiPassLimit = 0.8f;
1456 static constexpr float kNearIdentityLimit = 1.f - kRoundEpsilon; // 1px error in 1000px img
1457
1458 float finalStepScale = netScaleFactor * (1 << (steps - 1));
1459 float limit = steps == 1 ? kNearIdentityLimit : kMultiPassLimit;
1460 if (finalStepScale >= limit) {
1461 steps--;
1462 }
1463 }
1464
1465 return steps;
1466}
1467
1468// The following code uses "PixelSpace" as an alias to refer to the LayerSpace of the low-res
1469// input image and blurred output to differentiate values for the original and final layer space
1470template <typename T>
1472
1473FilterResult FilterResult::rescale(const Context& ctx,
1475 bool enforceDecal) const {
1476 LayerSpace<SkIRect> visibleLayerBounds = fLayerBounds;
1477 if (!fImage || !visibleLayerBounds.intersect(ctx.desiredOutput()) ||
1478 scale.width() <= 0.f || scale.height() <= 0.f) {
1479 return {};
1480 }
1481
1482 int xSteps = downscale_step_count(scale.width());
1483 int ySteps = downscale_step_count(scale.height());
1484
1485 // NOTE: For the first pass, PixelSpace and LayerSpace are equivalent
1486 PixelSpace<SkIPoint> origin;
1487 const bool pixelAligned = is_nearly_integer_translation(fTransform, &origin);
1488 SkEnumBitMask<BoundsAnalysis> analysis = this->analyzeBounds(ctx.desiredOutput(),
1489 BoundsScope::kShaderOnly);
1490
1491 // If there's no actual scaling, and no other effects that have to be resolved for blur(),
1492 // then just extract the necessary subset. Otherwise fall through and apply the effects with
1493 // scale factor (possibly identity).
1494 const bool canDeferTiling =
1495 pixelAligned &&
1496 !(analysis & BoundsAnalysis::kRequiresLayerCrop) &&
1497 !(enforceDecal && (analysis & BoundsAnalysis::kHasLayerFillingEffect));
1498
1499 const bool hasEffectsToApply =
1500 !canDeferTiling ||
1501 SkToBool(fColorFilter) ||
1502 fImage->colorType() != ctx.backend()->colorType() ||
1503 !SkColorSpace::Equals(fImage->getColorSpace(), ctx.colorSpace());
1504
1505 if (xSteps == 0 && ySteps == 0 && !hasEffectsToApply) {
1506 if (analysis & BoundsAnalysis::kHasLayerFillingEffect) {
1507 // At this point, the only effects that could be visible is a non-decal mode, so just
1508 // return the image with adjusted layer bounds to match desired output.
1509 FilterResult noop = *this;
1510 noop.fLayerBounds = visibleLayerBounds;
1511 return noop;
1512 } else {
1513 // The visible layer bounds represents a tighter bounds than the image itself
1514 return this->subset(origin, visibleLayerBounds);
1515 }
1516 }
1517
1518 PixelSpace<SkIRect> srcRect;
1520 if (canDeferTiling && (analysis & BoundsAnalysis::kHasLayerFillingEffect)) {
1521 // When we can defer tiling, and said tiling is visible, rescaling the original image
1522 // uses smaller textures.
1523 srcRect = LayerSpace<SkIRect>(SkIRect::MakeXYWH(origin.x(), origin.y(),
1524 fImage->width(), fImage->height()));
1525 tileMode = fTileMode;
1526 } else {
1527 // Otherwise we either have to rescale the layer-bounds-sized image (!canDeferTiling)
1528 // or the tiling isn't visible so the layer bounds reprenents a smaller effective
1529 // image than the original image data.
1530 srcRect = visibleLayerBounds;
1532 }
1533
1534 srcRect = srcRect.relevantSubset(ctx.desiredOutput(), tileMode);
1535 if (srcRect.isEmpty()) {
1536 return {};
1537 }
1538
1539 // To avoid incurring error from rounding up the dimensions at every step, the logical size of
1540 // the image is tracked in floats through the whole process; rounding to integers is only done
1541 // to produce a conservative pixel buffer and clamp-tiling is used so that partially covered
1542 // pixels are filled with the un-weighted color.
1543 PixelSpace<SkRect> stepBoundsF{srcRect};
1544 // stepPixelBounds is used to calculate how much padding needs to be added. Adding 1px outset
1545 // keeps the math consistent for first iteration vs. later iterations, and logically represents
1546 // the first downscale triggering the tilemode vs. later steps sampling the preserved tiling
1547 // in the padded pixels.
1548 PixelSpace<SkIRect> stepPixelBounds{srcRect};
1549 stepPixelBounds.outset(PixelSpace<SkISize>({1, 1}));
1550
1551 // If we made it here, at least one iteration is required, even if xSteps and ySteps are 0.
1552 sk_sp<SkSpecialImage> image = nullptr;
1553 while(!image || xSteps > 0 || ySteps > 0) {
1554 float sx = 1.f;
1555 if (xSteps > 0) {
1556 sx = xSteps > 1 ? 0.5f : srcRect.width()*scale.width() / stepBoundsF.width();
1557 xSteps--;
1558 }
1559
1560 float sy = 1.f;
1561 if (ySteps > 0) {
1562 sy = ySteps > 1 ? 0.5f : srcRect.height()*scale.height() / stepBoundsF.height();
1563 ySteps--;
1564 }
1565
1566 PixelSpace<SkRect> dstBoundsF{SkRect::MakeWH(stepBoundsF.width() * sx,
1567 stepBoundsF.height() * sy)};
1568 PixelSpace<SkIRect> dstPixelBounds = dstBoundsF.roundOut();
1570 // To sample beyond the padded src texel, we need
1571 // dstFracX + px - 1/2 > sx*(srcFracX - 1/2)
1572 // px=1 always satisfies this for sx=1/2 on intermediate steps, but for 0.5 < sx < 1
1573 // the fractional bounds and rounding can require an additional padded pixel.
1574 // We calculate from the right edge because we keep the left edge pixel aligned.
1575 float srcFracX = stepPixelBounds.right() - stepBoundsF.right() - 0.5f;
1576 float dstFracX = dstPixelBounds.right() - dstBoundsF.right() - 0.5f;
1577 int px = std::max(1, sk_float_ceil2int((sx*srcFracX - dstFracX)));
1578
1579 float srcFracY = stepPixelBounds.bottom() - stepBoundsF.bottom() - 0.5f;
1580 float dstFracY = dstPixelBounds.bottom() - dstBoundsF.bottom() - 0.5f;
1581 int py = std::max(1, sk_float_ceil2int((sy*srcFracY - dstFracY)));
1582
1583 dstPixelBounds.outset(PixelSpace<SkISize>({px, py}));
1584
1585 // If the axis scale factor was identity, the dst pixel bounds *after* padding will
1586 // match the step pixel bounds. We have to add re-add the padding on identity iterations
1587 // because the initial dst bounds is based on the un-padded stepBoundsF.
1588 SkASSERT(sx != 1.f || dstPixelBounds.width() == stepPixelBounds.width());
1589 SkASSERT(sy != 1.f || dstPixelBounds.height() == stepPixelBounds.height());
1590 }
1591
1592 // TODO(b/323886180): Take advantage of pixel boundary tracking here, passing in kUnknown
1593 // preserves the surface dimensions exactly for now.
1594 AutoSurface surface{ctx, dstPixelBounds, PixelBoundary::kUnknown,
1595 /*renderInParameterSpace=*/false};
1596 if (surface) {
1597 // Fill all of surface (to include any padded edge pixels) with 'scaleXform' as the CTM.
1598 const auto scaleXform = PixelSpace<SkMatrix>::RectToRect(stepBoundsF, dstBoundsF);
1599 surface->concat(SkMatrix(scaleXform));
1600
1601 SkPaint paint;
1602 if (!image) {
1603 // Redo analysis with the actual scale transform and padded low res bounds, but
1604 // remove kRequiresDecalInLayerSpace because it will always trigger with the scale
1605 // factor and can be automatically applied at the end when upscaling.
1606 analysis = this->analyzeBounds(SkMatrix(scaleXform), SkIRect(dstPixelBounds),
1607 BoundsScope::kShaderOnly);
1608 analysis &= ~BoundsAnalysis::kRequiresDecalInLayerSpace;
1609 paint.setShader(this->getAnalyzedShaderView(ctx, fSamplingOptions, analysis));
1610 } else {
1611 // Otherwise just bilinearly downsample the origin-aligned prior step's image.
1613 SkMatrix::Translate(origin.x(), origin.y())));
1614 if (!image->isExactFit()) {
1616 }
1617 }
1618
1619 surface->drawPaint(paint);
1620 } else {
1621 // Rescaling can't complete, no sense in downscaling non-existent data
1622 return {};
1623 }
1624
1626 // Now we have incorporated a 1px transparent border, so next image can use clamping.
1627 // OR we have incorporated the transparency-affecting color filter's result to the
1628 // 1px transparent border so the next image can still use clamping.
1630 } // else we are non-decal deferred so use repeat/mirror/clamp all the way down.
1631
1632 // TODO(b/323886180): Once rescale() is updated to use smarter padding and PixelBoundary,
1633 // this can stay as a FilterResult.
1634 FilterResult snapped = surface.snap();
1635 image = snapped.fImage;
1636 origin = snapped.fLayerBounds.topLeft();
1637 stepBoundsF = dstBoundsF;
1638 stepPixelBounds = dstPixelBounds;
1639 }
1640
1641 // Rebuild the downscaled image as a FilterResult, including a transform back to the original
1642 // layer-space resolution, restoring the layer bounds it should fill, and setting tile mode.
1643 FilterResult result{std::move(image), origin};
1644 result.fTransform.postConcat(
1646 result.fLayerBounds = visibleLayerBounds;
1647
1648 // TODO(b/323886180): Set the pixel boundary to kInitialized or kTransparent since rescale()
1649 // does add padding to the image.
1650 if (enforceDecal) {
1651 // Since we weren't deferring the tiling, the original tile mode should have been resolved
1652 // in the first iteration. However, as part of the decimation, we included transparent
1653 // padding and switched to clamp. Switching back to "decal" in this case has no visual
1654 // effect but keeps downstream legacy blur algorithms happy.
1655 SkASSERT(!canDeferTiling && tileMode == SkTileMode::kClamp);
1656 result.fTileMode = SkTileMode::kDecal;
1657 } else {
1658 result.fTileMode = tileMode;
1659 }
1660 return result;
1661}
1662
1664 sk_sp<SkPicture> pic,
1665 ParameterSpace<SkRect> cullRect) {
1666 SkASSERT(pic);
1667 LayerSpace<SkIRect> dstBounds = ctx.mapping().paramToLayer(cullRect).roundOut();
1668 if (!dstBounds.intersect(ctx.desiredOutput())) {
1669 return {};
1670 }
1671
1672 // Given the standard usage of the picture image filter (i.e., to render content at a fixed
1673 // resolution that, most likely, differs from the screen's) disable LCD text by removing any
1674 // knowledge of the pixel geometry.
1675 // TODO: Should we just generally do this for layers with image filters? Or can we preserve it
1676 // for layers that are still axis-aligned?
1677 SkSurfaceProps props = ctx.backend()->surfaceProps()
1679 AutoSurface surface{ctx, dstBounds, PixelBoundary::kTransparent,
1680 /*renderInParameterSpace=*/true, &props};
1681 if (surface) {
1682 surface->clipRect(SkRect(cullRect));
1683 surface->drawPicture(std::move(pic));
1684 }
1685 return surface.snap();
1686}
1687
1689 sk_sp<SkShader> shader,
1690 bool dither) {
1691 SkASSERT(shader);
1692 AutoSurface surface{ctx, ctx.desiredOutput(), PixelBoundary::kTransparent,
1693 /*renderInParameterSpace=*/true};
1694 if (surface) {
1695 SkPaint paint;
1696 paint.setShader(shader);
1697 paint.setDither(dither);
1698 surface->drawPaint(paint);
1699 }
1700 return surface.snap();
1701}
1702
1705 SkRect srcRect,
1706 ParameterSpace<SkRect> dstRect,
1707 const SkSamplingOptions& sampling) {
1708 SkASSERT(image);
1709
1710 SkRect imageBounds = SkRect::Make(image->dimensions());
1711 if (!imageBounds.contains(srcRect)) {
1712 SkMatrix srcToDst = SkMatrix::RectToRect(srcRect, SkRect(dstRect));
1713 if (!srcRect.intersect(imageBounds)) {
1714 return {}; // No overlap, so return an empty/transparent image
1715 }
1716 // Adjust dstRect to match the updated srcRect
1717 dstRect = ParameterSpace<SkRect>{srcToDst.mapRect(srcRect)};
1718 }
1719
1720 if (SkRect(dstRect).isEmpty()) {
1721 return {}; // Output collapses to empty
1722 }
1723
1724 // Check for direct conversion to an SkSpecialImage and then FilterResult. Eventually this
1725 // whole function should be replaceable with:
1726 // FilterResult(fImage, fSrcRect, fDstRect).applyTransform(mapping.layerMatrix(), fSampling);
1727 SkIRect srcSubset = RoundOut(srcRect);
1728 if (SkRect::Make(srcSubset) == srcRect) {
1729 // Construct an SkSpecialImage from the subset directly instead of drawing.
1730 sk_sp<SkSpecialImage> specialImage = ctx.backend()->makeImage(srcSubset, std::move(image));
1731
1732 // Treat the srcRect's top left as "layer" space since we are folding the src->dst transform
1733 // and the param->layer transform into a single transform step. We don't override the
1734 // PixelBoundary from kUnknown even if srcRect is contained within the 'image' because the
1735 // client could be doing their own external approximate-fit texturing.
1736 skif::FilterResult subset{std::move(specialImage),
1737 skif::LayerSpace<SkIPoint>(srcSubset.topLeft())};
1739 SkMatrix::RectToRect(srcRect, SkRect(dstRect)));
1740 return subset.applyTransform(ctx, skif::LayerSpace<SkMatrix>(transform), sampling);
1741 }
1742
1743 // For now, draw the src->dst subset of image into a new image.
1744 LayerSpace<SkIRect> dstBounds = ctx.mapping().paramToLayer(dstRect).roundOut();
1745 if (!dstBounds.intersect(ctx.desiredOutput())) {
1746 return {};
1747 }
1748
1749 AutoSurface surface{ctx, dstBounds, PixelBoundary::kTransparent,
1750 /*renderInParameterSpace=*/true};
1751 if (surface) {
1752 SkPaint paint;
1753 paint.setAntiAlias(true);
1754 surface->drawImageRect(std::move(image), srcRect, SkRect(dstRect), sampling, &paint,
1756 }
1757 return surface.snap();
1758}
1759
1760///////////////////////////////////////////////////////////////////////////////////////////////////
1761// FilterResult::Builder
1762
1765
1766SkSpan<sk_sp<SkShader>> FilterResult::Builder::createInputShaders(
1767 const LayerSpace<SkIRect>& outputBounds,
1768 bool evaluateInParameterSpace) {
1770 SkMatrix layerToParam;
1771 if (evaluateInParameterSpace) {
1772 // The FilterResult is meant to be sampled in layer space, but the shader this is feeding
1773 // into is being sampled in parameter space. Add the inverse of the layerMatrix() (i.e.
1774 // layer to parameter space) as a local matrix to convert from the parameter-space coords
1775 // of the outer shader to the layer-space coords of the FilterResult).
1776 SkAssertResult(fContext.mapping().layerMatrix().invert(&layerToParam));
1777 // Automatically add nonTrivial sampling if the layer-to-parameter space mapping isn't
1778 // also pixel aligned.
1779 if (!is_nearly_integer_translation(LayerSpace<SkMatrix>(layerToParam))) {
1781 }
1782 }
1783
1784 fInputShaders.reserve(fInputs.size());
1785 for (const SampledFilterResult& input : fInputs) {
1786 // Assume the input shader will be evaluated once per pixel in the output unless otherwise
1787 // specified when the FilterResult was added to the builder.
1788 auto sampleBounds = input.fSampleBounds ? *input.fSampleBounds : outputBounds;
1789 auto shader = input.fImage.asShader(fContext,
1790 input.fSampling,
1791 input.fFlags | xtraFlags,
1792 sampleBounds);
1793 if (evaluateInParameterSpace && shader) {
1794 shader = shader->makeWithLocalMatrix(layerToParam);
1795 }
1796 fInputShaders.push_back(std::move(shader));
1797 }
1798 return SkSpan<sk_sp<SkShader>>(fInputShaders);
1799}
1800
1801LayerSpace<SkIRect> FilterResult::Builder::outputBounds(
1802 std::optional<LayerSpace<SkIRect>> explicitOutput) const {
1803 // Pessimistically assume output fills the full desired bounds
1804 LayerSpace<SkIRect> output = fContext.desiredOutput();
1805 if (explicitOutput.has_value()) {
1806 // Intersect with the provided explicit bounds
1807 if (!output.intersect(*explicitOutput)) {
1808 return LayerSpace<SkIRect>::Empty();
1809 }
1810 }
1811 return output;
1812}
1813
1814FilterResult FilterResult::Builder::drawShader(sk_sp<SkShader> shader,
1815 const LayerSpace<SkIRect>& outputBounds,
1816 bool evaluateInParameterSpace) const {
1817 SkASSERT(!outputBounds.isEmpty()); // Should have been rejected before we created shaders
1818 if (!shader) {
1819 return {};
1820 }
1821
1822 AutoSurface surface{fContext, outputBounds, PixelBoundary::kTransparent,
1823 evaluateInParameterSpace};
1824 if (surface) {
1825 SkPaint paint;
1826 paint.setShader(std::move(shader));
1827 surface->drawPaint(paint);
1828 }
1829 return surface.snap();
1830}
1831
1833 // merge() could return an empty image on 0 added inputs, but this should have been caught
1834 // earlier and routed to SkImageFilters::Empty() instead.
1835 SkASSERT(!fInputs.empty());
1836 if (fInputs.size() == 1) {
1837 SkASSERT(!fInputs[0].fSampleBounds.has_value() &&
1838 fInputs[0].fSampling == kDefaultSampling &&
1839 fInputs[0].fFlags == ShaderFlags::kNone);
1840 return fInputs[0].fImage;
1841 }
1842
1843 const auto mergedBounds = LayerSpace<SkIRect>::Union(
1844 (int) fInputs.size(),
1845 [this](int i) { return fInputs[i].fImage.layerBounds(); });
1846 const auto outputBounds = this->outputBounds(mergedBounds);
1847
1848 AutoSurface surface{fContext, outputBounds, PixelBoundary::kTransparent,
1849 /*renderInParameterSpace=*/false};
1850 if (surface) {
1851 for (const SampledFilterResult& input : fInputs) {
1852 SkASSERT(!input.fSampleBounds.has_value() &&
1853 input.fSampling == kDefaultSampling &&
1854 input.fFlags == ShaderFlags::kNone);
1855 input.fImage.draw(fContext, surface.device(), /*preserveDeviceState=*/true);
1856 }
1857 }
1858 return surface.snap();
1859}
1860
1862 SkASSERT(fInputs.size() == 1);
1863
1864 // TODO: The blur functor is only supported for GPU contexts; SkBlurImageFilter should have
1865 // detected this.
1866 const SkBlurEngine* blurEngine = fContext.backend()->getBlurEngine();
1867 SkASSERT(blurEngine);
1868
1869 // TODO: All tilemodes are applied right now in resolve() so query with just kDecal
1870 const SkBlurEngine::Algorithm* algorithm = blurEngine->findAlgorithm(
1871 SkSize(sigma), fContext.backend()->colorType());
1872 if (!algorithm) {
1873 return {};
1874 }
1875
1876 // TODO: Move resizing logic out of GrBlurUtils into this function
1877 SkASSERT(sigma.width() <= algorithm->maxSigma() && sigma.height() <= algorithm->maxSigma());
1878
1879 // TODO: De-duplicate this logic between SkBlurImageFilter, here, and skgpu::BlurUtils.
1881 LayerSpace<SkSize>({3.f*sigma.width(), 3.f*sigma.height()}).ceil();
1882 auto maxOutput = fInputs[0].fImage.layerBounds();
1883 maxOutput.outset(radii);
1884
1885 // TODO: If the input image is periodic, the output that's calculated can be the original image
1886 // size and then have the layer bounds and tilemode of the output image apply the tile again.
1887 // Similarly, a clamped blur can be restricted to a radius-outset buffer of the image bounds
1888 // (vs. layer bounds) and rendered with clamp tiling.
1889 const auto outputBounds = this->outputBounds(maxOutput);
1890 if (outputBounds.isEmpty()) {
1891 return {};
1892 }
1893
1894 // These are the source pixels that will be read from the input image, which can be calculated
1895 // internally because the blur's access pattern is well defined (vs. needing it to be provided
1896 // in Builder::add()).
1897 auto sampleBounds = outputBounds;
1898 sampleBounds.outset(radii);
1899
1900 // TODO: If the blur implementation requires downsampling, we should incorporate any deferred
1901 // transform and colorfilter to the first rescale step instead of generating a full resolution
1902 // simple image first.
1903 // TODO: The presence of a non-decal tilemode should not force resolving to a simple image; it
1904 // should be incorporated into the image that's sampled by the blur effect (modulo biasing edge
1905 // pixels somehow for very large clamp blurs).
1906 // TODO: resolve() doesn't actually guarantee that the returned image has the same color space
1907 // as the Context, but probably should since the blur algorithm operates in the color space of
1908 // the input image.
1909 FilterResult resolved = fInputs[0].fImage.resolve(fContext, sampleBounds);
1910 if (!resolved) {
1911 return {};
1912 }
1913
1914 // TODO: Can blur() take advantage of AutoSurface? Right now the GPU functions are responsible
1915 // for creating their own target surfaces.
1916 auto srcRelativeOutput = outputBounds;
1917 srcRelativeOutput.offset(-resolved.layerBounds().topLeft());
1918 resolved = {algorithm->blur(SkSize(sigma),
1919 resolved.fImage,
1920 SkIRect::MakeSize(resolved.fImage->dimensions()),
1922 SkIRect(srcRelativeOutput)),
1923 outputBounds.topLeft()};
1924 // TODO: Allow the blur functor to provide an upscaling transform that is applied to the
1925 // FilterResult so that a render pass can possibly be elided if this is the final operation.
1926 return resolved;
1927}
1928
1929} // end namespace skif
static SkM44 inv(const SkM44 &m)
Definition 3d.cpp:26
SkColorType fColorType
@ kPremul_SkAlphaType
pixel components are premultiplied by alpha
Definition SkAlphaType.h:29
#define SkAssertResult(cond)
Definition SkAssert.h:123
#define SkASSERT(cond)
Definition SkAssert.h:116
SkBlendMode
Definition SkBlendMode.h:38
@ kLastCoeffMode
last porter duff blend mode
SkBlenderBase * as_BB(SkBlender *blend)
static SkColorFilterBase * as_CFB(SkColorFilter *filter)
SkColorType
Definition SkColorType.h:19
void SK_SPI SkDebugf(const char format[],...) SK_PRINTF_LIKE(1
#define SkDEBUGCODE(...)
Definition SkDebug.h:23
#define sk_float_ceil2int(x)
#define sk_double_floor2int(x)
static bool SkIsFinite(T x, Pack... values)
#define sk_double_ceil2int(x)
static constexpr int sk_double_saturate2int(double x)
static constexpr float sk_ieee_float_divide(float numer, float denom)
static SkImageFilter_Base * as_IFB(SkImageFilter *filter)
static SkColorType colorType(AImageDecoder *decoder, const AImageDecoderHeaderInfo *headerInfo)
static int SkNextLog2(uint32_t value)
Definition SkMathPriv.h:238
sk_sp< T > sk_ref_sp(T *obj)
Definition SkRefCnt.h:381
const Context & fContext
#define SkScalarInvert(x)
Definition SkScalar.h:73
static bool SkScalarNearlyZero(SkScalar x, SkScalar tolerance=SK_ScalarNearlyZero)
Definition SkScalar.h:101
static bool SkScalarNearlyEqual(SkScalar x, SkScalar y, SkScalar tolerance=SK_ScalarNearlyZero)
Definition SkScalar.h:107
#define SkScalarRoundToInt(x)
Definition SkScalar.h:37
#define SkScalarCeilToInt(x)
Definition SkScalar.h:36
#define SkIntToScalar(x)
Definition SkScalar.h:57
#define SkScalarRoundToScalar(x)
Definition SkScalar.h:32
#define SkScalarSqrt(x)
Definition SkScalar.h:42
#define SkScalarFloorToInt(x)
Definition SkScalar.h:35
#define SkScalarAbs(x)
Definition SkScalar.h:39
@ kUnknown_SkPixelGeometry
SkTileMode
Definition SkTileMode.h:13
static constexpr bool SkToBool(const T &x)
Definition SkTo.h:35
#define TRACE_EVENT_SCOPE_THREAD
Type::kYUV Type::kRGBA() int(0.7 *637)
static sk_sp< SkBitmapDevice > Create(const SkImageInfo &, const SkSurfaceProps &, SkRasterHandleAllocator *=nullptr)
bool affectsTransparentBlack() const
virtual float maxSigma() const =0
virtual sk_sp< SkSpecialImage > blur(SkSize sigma, sk_sp< SkSpecialImage > src, const SkIRect &srcRect, SkTileMode tileMode, const SkIRect &dstRect) const =0
virtual const Algorithm * findAlgorithm(SkSize sigma, SkColorType colorType) const =0
static SkDevice * TopDevice(const SkCanvas *canvas)
SrcRectConstraint
Definition SkCanvas.h:1541
@ kStrict_SrcRectConstraint
sample only inside bounds; slower
Definition SkCanvas.h:1542
@ kFast_SrcRectConstraint
sample outside bounds; faster
Definition SkCanvas.h:1543
bool affectsTransparentBlack() const
static sk_sp< SkColorFilter > Compose(const sk_sp< SkColorFilter > &outer, sk_sp< SkColorFilter > inner)
static bool Equals(const SkColorSpace *, const SkColorSpace *)
virtual void setImmutable()
Definition SkDevice.h:293
virtual void popClipStack()=0
virtual void pushClipStack()=0
const SkMatrix & localToDevice() const
Definition SkDevice.h:179
virtual sk_sp< SkSpecialImage > snapSpecial(const SkIRect &subset, bool forceCopy=false)
Definition SkDevice.cpp:316
virtual void drawSpecial(SkSpecialImage *, const SkMatrix &localToDevice, const SkSamplingOptions &, const SkPaint &, SkCanvas::SrcRectConstraint constraint=SkCanvas::kStrict_SrcRectConstraint)
Definition SkDevice.cpp:305
virtual SkIRect devClipBounds() const =0
virtual void clipRect(const SkRect &rect, SkClipOp op, bool aa)=0
virtual void drawPaint(const SkPaint &paint)=0
Definition SkM44.h:150
SkM44 & postConcat(const SkM44 &m)
Definition SkM44.h:355
static SkScalar DifferentialAreaScale(const SkMatrix &m, const SkPoint &p)
static bool InverseMapRect(const SkMatrix &mx, SkRect *dst, const SkRect &src)
static SkMatrix Scale(SkScalar sx, SkScalar sy)
Definition SkMatrix.h:75
static SkMatrix RectToRect(const SkRect &src, const SkRect &dst, ScaleToFit mode=kFill_ScaleToFit)
Definition SkMatrix.h:157
SkMatrix & postConcat(const SkMatrix &other)
Definition SkMatrix.cpp:683
static SkMatrix Translate(SkScalar dx, SkScalar dy)
Definition SkMatrix.h:91
void setScaleTranslate(SkScalar sx, SkScalar sy, SkScalar tx, SkScalar ty)
Definition SkMatrix.h:1803
static SkMatrix Concat(const SkMatrix &a, const SkMatrix &b)
Definition SkMatrix.h:1775
bool invert(SkMatrix *inverse) const
Definition SkMatrix.h:1206
static const SkMatrix & I()
SkMatrix & preTranslate(SkScalar dx, SkScalar dy)
Definition SkMatrix.cpp:263
SkMatrix & preConcat(const SkMatrix &other)
Definition SkMatrix.cpp:674
bool isScaleTranslate() const
Definition SkMatrix.h:236
SkMatrix & preScale(SkScalar sx, SkScalar sy, SkScalar px, SkScalar py)
Definition SkMatrix.cpp:315
bool mapRect(SkRect *dst, const SkRect &src, SkApplyPerspectiveClip pc=SkApplyPerspectiveClip::kYes) const
void setColor4f(const SkColor4f &color, SkColorSpace *colorSpace=nullptr)
Definition SkPaint.h:253
void setBlender(sk_sp< SkBlender > blender)
Definition SkPaint.cpp:155
static skvx::int4 QuadContainsRectMask(const SkM44 &m, const SkRect &a, const SkRect &b, float tol=0.f)
Definition SkRect.cpp:272
static SkIRect ClosestDisjointEdge(const SkIRect &src, const SkIRect &dst)
Definition SkRect.cpp:323
static bool QuadContainsRect(const SkMatrix &m, const SkIRect &a, const SkIRect &b, float tol=0.f)
Definition SkRect.cpp:261
sk_sp< SkSpecialImage > makeSubset(const SkIRect &subset) const
bool isExactFit() const
SkISize dimensions() const
virtual sk_sp< SkShader > asShader(SkTileMode, const SkSamplingOptions &, const SkMatrix &lm, bool strict=true) const
SkSurfaceProps cloneWithPixelGeometry(SkPixelGeometry newPixelGeometry) const
const SkSurfaceProps & surfaceProps() const
~Backend() override
SkColorType colorType() const
virtual sk_sp< SkSpecialImage > makeImage(const SkIRect &subset, sk_sp< SkImage > image) const =0
Backend(sk_sp< SkImageFilterCache > cache, const SkSurfaceProps &surfaceProps, const SkColorType colorType)
virtual sk_sp< SkDevice > makeDevice(SkISize size, sk_sp< SkColorSpace >, const SkSurfaceProps *props=nullptr) const =0
const Backend * backend() const
void markNewSurface() const
void markShaderBasedTilingRequired(SkTileMode tileMode) const
SkColorSpace * colorSpace() const
sk_sp< SkColorSpace > refColorSpace() const
const LayerSpace< SkIRect > & desiredOutput() const
const Mapping & mapping() const
AutoSurface(const Context &ctx, const LayerSpace< SkIRect > &dstBounds, PixelBoundary boundary, bool renderInParameterSpace, const SkSurfaceProps *props=nullptr)
Builder(const Context &context)
FilterResult blur(const LayerSpace< SkSize > &sigma)
FilterResult applyColorFilter(const Context &ctx, sk_sp< SkColorFilter > colorFilter) const
FilterResult applyCrop(const Context &ctx, const LayerSpace< SkIRect > &crop, SkTileMode tileMode=SkTileMode::kDecal) const
FilterResult insetForSaveLayer() const
static FilterResult MakeFromPicture(const Context &ctx, sk_sp< SkPicture > pic, ParameterSpace< SkRect > cullRect)
void draw(const Context &ctx, SkDevice *target, const SkBlender *blender) const
sk_sp< SkSpecialImage > imageAndOffset(const Context &ctx, SkIPoint *offset) const
static FilterResult MakeFromImage(const Context &ctx, sk_sp< SkImage > image, SkRect srcRect, ParameterSpace< SkRect > dstRect, const SkSamplingOptions &sampling)
const SkSpecialImage * image() const
const SkColorFilter * colorFilter() const
SkSamplingOptions sampling() const
LayerSpace< SkIRect > layerBounds() const
static constexpr SkSamplingOptions kDefaultSampling
SkTileMode tileMode() const
static FilterResult MakeFromShader(const Context &ctx, sk_sp< SkShader > shader, bool dither)
FilterResult applyTransform(const Context &ctx, const LayerSpace< SkMatrix > &transform, const SkSamplingOptions &sampling) const
LayerSpace< SkIPoint > topLeft() const
void outset(const LayerSpace< SkISize > &delta)
LayerSpace< SkISize > size() const
const SkMatrix & layerToDevice() const
bool decomposeCTM(const SkMatrix &ctm, const SkImageFilter *filter, const skif::ParameterSpace< SkPoint > &representativePt)
const SkMatrix & layerMatrix() const
LayerSpace< T > paramToLayer(const ParameterSpace< T > &paramGeometry) const
bool adjustLayerSpace(const SkMatrix &layer)
const Paint & paint
VkSurfaceKHR surface
Definition main.cc:49
sk_sp< SkImage > image
Definition examples.cpp:29
float SkScalar
Definition extension.cpp:12
static bool b
struct MyStruct s
FlutterSemanticsFlag flags
if(end==-1)
GAsyncResult * result
uint32_t * target
const GrXPFactory * Get(SkBlendMode mode)
constexpr SkColor4f kTransparent
Definition SkColor.h:434
SK_API sk_sp< SkImage > RasterFromBitmap(const SkBitmap &bitmap)
const SkRuntimeEffect * GetKnownRuntimeEffect(StableKey stableKey)
unsigned useCenter Optional< SkMatrix > matrix
Definition SkRecords.h:258
it will be possible to load the file into Perfetto s trace viewer disable asset Prevents usage of any non test fonts unless they were explicitly Loaded via prefetched default font Indicates whether the embedding started a prefetch of the default font manager before creating the engine run In non interactive keep the shell running after the Dart script has completed enable serial On low power devices with low core running concurrent GC tasks on threads can cause them to contend with the UI thread which could potentially lead to jank This option turns off all concurrent GC activities domain network JSON encoded network policy per domain This overrides the DisallowInsecureConnections switch Embedder can specify whether to allow or disallow insecure connections at a domain level old gen heap size
Definition switches.h:259
SkIRect RoundOut(SkRect r)
sk_sp< Backend > MakeRasterBackend(const SkSurfaceProps &surfaceProps, SkColorType colorType)
static int downscale_step_count(float netScaleFactor)
static bool compatible_sampling(const SkSamplingOptions &currentSampling, bool currentXformWontAffectNearest, SkSamplingOptions *nextSampling, bool nextXformWontAffectNearest)
SkIRect RoundIn(SkRect r)
SIT bool all(const Vec< 1, T > &x)
Definition SkVx.h:582
Definition ref_ptr.h:256
static SkColor4f transform(SkColor4f c, SkColorSpace *src, SkColorSpace *dst)
Definition p3.cpp:47
static SkRect inset(const SkRect &r)
const Scalar scale
Point offset
int32_t fX
x-axis value
int32_t fY
y-axis value
static constexpr SkIPoint Make(int32_t x, int32_t y)
void inset(int32_t dx, int32_t dy)
Definition SkRect.h:411
int32_t fBottom
larger y-axis bounds
Definition SkRect.h:36
int32_t fTop
smaller y-axis bounds
Definition SkRect.h:34
static constexpr SkIRect MakeSize(const SkISize &size)
Definition SkRect.h:66
static constexpr SkIRect MakeEmpty()
Definition SkRect.h:45
static constexpr SkIRect MakeWH(int32_t w, int32_t h)
Definition SkRect.h:56
bool isEmpty() const
Definition SkRect.h:202
constexpr SkIPoint topLeft() const
Definition SkRect.h:151
static constexpr SkIRect MakeXYWH(int32_t x, int32_t y, int32_t w, int32_t h)
Definition SkRect.h:104
SkIRect makeInset(int32_t dx, int32_t dy) const
Definition SkRect.h:332
int32_t fLeft
smaller x-axis bounds
Definition SkRect.h:33
bool contains(int32_t x, int32_t y) const
Definition SkRect.h:463
int32_t fRight
larger x-axis bounds
Definition SkRect.h:35
static constexpr SkISize Make(int32_t w, int32_t h)
Definition SkSize.h:20
static SkImageInfo Make(int width, int height, SkColorType ct, SkAlphaType at)
float fX
x-axis value
static constexpr SkPoint Make(float x, float y)
float length() const
float fY
y-axis value
static SkRect Make(const SkISize &size)
Definition SkRect.h:669
static constexpr SkRect MakeEmpty()
Definition SkRect.h:595
constexpr float left() const
Definition SkRect.h:734
void inset(float dx, float dy)
Definition SkRect.h:1060
constexpr float top() const
Definition SkRect.h:741
void roundIn(SkIRect *dst) const
Definition SkRect.h:1266
bool intersect(const SkRect &r)
Definition SkRect.cpp:114
void outset(float dx, float dy)
Definition SkRect.h:1077
SkRect makeOutset(float dx, float dy) const
Definition SkRect.h:1002
SkRect makeInset(float dx, float dy) const
Definition SkRect.h:987
bool contains(SkScalar x, SkScalar y) const
Definition extension.cpp:19
void roundOut(SkIRect *dst) const
Definition SkRect.h:1241
constexpr float right() const
Definition SkRect.h:748
bool isEmpty() const
Definition SkRect.h:693
constexpr SkPoint center() const
Definition SkRect.h:792
static constexpr SkRect MakeWH(float w, float h)
Definition SkRect.h:609
constexpr float bottom() const
Definition SkRect.h:755
static constexpr SkSamplingOptions Aniso(int maxAniso)
const SkCubicResampler cubic
const SkFilterMode filter
static constexpr SkSize Make(SkScalar w, SkScalar h)
Definition SkSize.h:56
SkScalar fHeight
Definition SkSize.h:54
SkScalar fWidth
Definition SkSize.h:53
void reportStats() const
void dumpStats() const
#define TRACE_EVENT_INSTANT2(category_group, name, arg1_name, arg1_val, arg2_name, arg2_val)
#define TRACE_EVENT_INSTANT1(category_group, name, arg1_name, arg1_val)