Flutter Engine
The Flutter Engine
Loading...
Searching...
No Matches
ClipStack_graphite.cpp
Go to the documentation of this file.
1/*
2 * Copyright 2022 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
13#include "src/base/SkTLazy.h"
14#include "src/core/SkPathPriv.h"
16#include "src/core/SkRectPriv.h"
21
22namespace skgpu::graphite {
23
24namespace {
25
26Rect subtract(const Rect& a, const Rect& b, bool exact) {
27 SkRect diff;
28 if (SkRectPriv::Subtract(a.asSkRect(), b.asSkRect(), &diff) || !exact) {
29 // Either A-B is exactly the rectangle stored in diff, or we don't need an exact answer
30 // and can settle for the subrect of A excluded from B (which is also 'diff')
31 return Rect{diff};
32 } else {
33 // For our purposes, we want the original A when A-B cannot be exactly represented
34 return a;
35 }
36}
37
38bool oriented_bbox_intersection(const Rect& a, const Transform& aXform,
39 const Rect& b, const Transform& bXform) {
40 // NOTE: We intentionally exclude projected bounds for two reasons:
41 // 1. We can skip the division by w and worring about clipping to w = 0.
42 // 2. W/o the projective case, the separating axes are simpler to compute (see below).
43 SkASSERT(aXform.type() != Transform::Type::kPerspective &&
44 bXform.type() != Transform::Type::kPerspective);
45 SkV4 quadA[4], quadB[4];
46
47 aXform.mapPoints(a, quadA);
48 bXform.mapPoints(b, quadB);
49
50 // There are 4 separating axes, defined by the two normals from quadA and from quadB, but
51 // since they were produced by transforming a rectangle by an affine transform, we know the
52 // normals are orthoganal to the basis vectors of upper 2x2 of their two transforms.
53 auto axesX = skvx::float4(-aXform.matrix().rc(1,0), -aXform.matrix().rc(1,1),
54 -bXform.matrix().rc(1,0), -bXform.matrix().rc(1,1));
55 auto axesY = skvx::float4(aXform.matrix().rc(0,0), aXform.matrix().rc(0,1),
56 bXform.matrix().rc(0,0), bXform.matrix().rc(0,1));
57
58 // Projections of the 4 corners of each quadrilateral vs. the 4 axes. For orthonormal
59 // transforms, the projections of a quad's corners to its own normal axes should work out
60 // to the original dimensions of the rectangle, but this code handles skew and scale factors
61 // without branching.
62 auto aProj0 = quadA[0].x * axesX + quadA[0].y * axesY;
63 auto aProj1 = quadA[1].x * axesX + quadA[1].y * axesY;
64 auto aProj2 = quadA[2].x * axesX + quadA[2].y * axesY;
65 auto aProj3 = quadA[3].x * axesX + quadA[3].y * axesY;
66
67 auto bProj0 = quadB[0].x * axesX + quadB[0].y * axesY;
68 auto bProj1 = quadB[1].x * axesX + quadB[1].y * axesY;
69 auto bProj2 = quadB[2].x * axesX + quadB[2].y * axesY;
70 auto bProj3 = quadB[3].x * axesX + quadB[3].y * axesY;
71
72 // Minimum and maximum projected values against the 4 axes, for both quadA and quadB, which
73 // gives us four pairs of intervals to test for separation.
74 auto minA = min(min(aProj0, aProj1), min(aProj2, aProj3));
75 auto maxA = max(max(aProj0, aProj1), max(aProj2, aProj3));
76 auto minB = min(min(bProj0, bProj1), min(bProj2, bProj3));
77 auto maxB = max(max(bProj0, bProj1), max(bProj2, bProj3));
78
79 auto overlaps = (minB <= maxA) & (minA <= maxB);
80 return all(overlaps); // any non-overlapping interval would imply no intersection
81}
82
83static constexpr Transform kIdentity = Transform::Identity();
84
85} // anonymous namespace
86
87///////////////////////////////////////////////////////////////////////////////
88// ClipStack::TransformedShape
89
90// A flyweight object describing geometry, subject to a local-to-device transform.
91// This can be used by SaveRecords, Elements, and draws to determine how two shape operations
92// interact with each other, without needing to share a base class, friend each other, or have a
93// template for each combination of two types.
96 const Shape& fShape;
99
101
102 // contains() performs a fair amount of work to be as accurate as possible since it can mean
103 // greatly simplifying the clip stack. However, in some contexts this isn't worth doing because
104 // the actual shape is only an approximation (save records), or there's no current way to take
105 // advantage of knowing this shape contains another (draws containing a clip hypothetically
106 // could replace their geometry to draw the clip directly, but that isn't implemented now).
108
109 bool intersects(const TransformedShape&) const;
110 bool contains(const TransformedShape&) const;
111};
112
115 return false;
116 }
117
120 // The two shape's coordinate spaces are different but both rect-stays-rect or simpler.
121 // This means, though, that their outer bounds approximations are tight to their transormed
122 // shape bounds. There's no point to do further tests given that and that we already found
123 // that these outer bounds *do* intersect.
124 return true;
125 } else if (fLocalToDevice == o.fLocalToDevice) {
126 // Since the two shape's local coordinate spaces are the same, we can compare shape
127 // bounds directly for a more accurate intersection test. We intentionally do not go
128 // further and do shape-specific intersection tests since these could have unknown
129 // complexity (for paths) and limited utility (e.g. two round rects that are disjoint
130 // solely from their corner curves).
131 return fShape.bounds().intersects(o.fShape.bounds());
134 // The shapes don't share the same coordinate system, and their approximate 'outer'
135 // bounds in device space could have substantial outsetting to contain the transformed
136 // shape (e.g. 45 degree rotation). Perform a more detailed check on their oriented
137 // bounding boxes.
138 return oriented_bbox_intersection(fShape.bounds(), fLocalToDevice,
140 }
141 // Else multiple perspective transforms are involved, so assume intersection and allow the
142 // rasterizer to handle perspective clipping.
143 return true;
144}
145
147 if (fInnerBounds.contains(o.fOuterBounds)) {
148 return true;
149 }
150 // Skip more expensive contains() checks if configured not to, or if the extent of 'o' exceeds
151 // this shape's outer bounds. When that happens there must be some part of 'o' that cannot be
152 // contained in this shape.
153 if (fContainsChecksOnlyBounds || !fOuterBounds.contains(o.fOuterBounds)) {
154 return false;
155 }
156
157 if (fContainsChecksOnlyBounds) {
158 return false; // don't do any more work
159 }
160
161 if (fLocalToDevice == o.fLocalToDevice) {
162 // Test the shapes directly against each other, with a special check for a rrect+rrect
163 // containment (a intersect b == a implies b contains a) and paths (same gen ID, or same
164 // path for small paths means they contain each other).
165 static constexpr int kMaxPathComparePoints = 16;
166 if (fShape.isRRect() && o.fShape.isRRect()) {
167 return SkRRectPriv::ConservativeIntersect(fShape.rrect(), o.fShape.rrect())
168 == o.fShape.rrect();
169 } else if (fShape.isPath() && o.fShape.isPath()) {
170 // TODO: Is this worth doing still if clips only cost as much as a single draw?
171 return (fShape.path().getGenerationID() == o.fShape.path().getGenerationID()) ||
172 (fShape.path().countPoints() <= kMaxPathComparePoints &&
173 fShape.path() == o.fShape.path());
174 } else {
175 return fShape.conservativeContains(o.fShape.bounds());
176 }
177 } else if (fLocalToDevice.type() <= Transform::Type::kRectStaysRect &&
179 // Optimize the common case where o's bounds can be mapped tightly into this coordinate
180 // space and then tested against our shape.
181 Rect localBounds = fLocalToDevice.inverseMapRect(
183 return fShape.conservativeContains(localBounds);
184 } else if (fShape.convex()) {
185 // Since this shape is convex, if all four corners of o's bounding box are inside it
186 // then the entirety of o is also guaranteed to be inside it.
187 SkV4 deviceQuad[4];
188 o.fLocalToDevice.mapPoints(o.fShape.bounds(), deviceQuad);
189 SkV4 localQuad[4];
190 fLocalToDevice.inverseMapPoints(deviceQuad, localQuad, 4);
191 for (int i = 0; i < 4; ++i) {
192 // TODO: Would be nice to make this consistent with how the GPU clips NDC w.
193 if (deviceQuad[i].w < SkPathPriv::kW0PlaneDistance ||
194 localQuad[i].w < SkPathPriv::kW0PlaneDistance) {
195 // Something in O actually projects behind the W = 0 plane and would be clipped
196 // to infinity, so it's extremely unlikely that this contains O.
197 return false;
198 }
199 if (!fShape.conservativeContains(skvx::float2::Load(localQuad + i) / localQuad[i].w)) {
200 return false;
201 }
202 }
203 return true;
204 }
205
206 // Else not an easily comparable pair of shapes so assume this doesn't contain O
207 return false;
208}
209
210ClipStack::SimplifyResult ClipStack::Simplify(const TransformedShape& a,
211 const TransformedShape& b) {
212 enum class ClipCombo {
213 kDD = 0b00,
214 kDI = 0b01,
215 kID = 0b10,
216 kII = 0b11
217 };
218
219 switch(static_cast<ClipCombo>(((int) a.fOp << 1) | (int) b.fOp)) {
220 case ClipCombo::kII:
221 // Intersect (A) + Intersect (B)
222 if (!a.intersects(b)) {
223 // Regions with non-zero coverage are disjoint, so intersection = empty
224 return SimplifyResult::kEmpty;
225 } else if (b.contains(a)) {
226 // B's full coverage region contains entirety of A, so intersection = A
227 return SimplifyResult::kAOnly;
228 } else if (a.contains(b)) {
229 // A's full coverage region contains entirety of B, so intersection = B
230 return SimplifyResult::kBOnly;
231 } else {
232 // The shapes intersect in some non-trivial manner
233 return SimplifyResult::kBoth;
234 }
235 case ClipCombo::kID:
236 // Intersect (A) + Difference (B)
237 if (!a.intersects(b)) {
238 // A only intersects B's full coverage region, so intersection = A
239 return SimplifyResult::kAOnly;
240 } else if (b.contains(a)) {
241 // B's zero coverage region completely contains A, so intersection = empty
242 return SimplifyResult::kEmpty;
243 } else {
244 // Intersection cannot be simplified. Note that the combination of a intersect
245 // and difference op in this order cannot produce kBOnly
246 return SimplifyResult::kBoth;
247 }
248 case ClipCombo::kDI:
249 // Difference (A) + Intersect (B) - the mirror of Intersect(A) + Difference(B),
250 // but combining is commutative so this is equivalent barring naming.
251 if (!b.intersects(a)) {
252 // B only intersects A's full coverage region, so intersection = B
253 return SimplifyResult::kBOnly;
254 } else if (a.contains(b)) {
255 // A's zero coverage region completely contains B, so intersection = empty
256 return SimplifyResult::kEmpty;
257 } else {
258 // Cannot be simplified
259 return SimplifyResult::kBoth;
260 }
261 case ClipCombo::kDD:
262 // Difference (A) + Difference (B)
263 if (a.contains(b)) {
264 // A's zero coverage region contains B, so B doesn't remove any extra
265 // coverage from their intersection.
266 return SimplifyResult::kAOnly;
267 } else if (b.contains(a)) {
268 // Mirror of the above case, intersection = B instead
269 return SimplifyResult::kBOnly;
270 } else {
271 // Intersection of the two differences cannot be simplified. Note that for
272 // this op combination it is not possible to produce kEmpty.
273 return SimplifyResult::kBoth;
274 }
275 }
277}
278
279///////////////////////////////////////////////////////////////////////////////
280// ClipStack::Element
281
282ClipStack::RawElement::RawElement(const Rect& deviceBounds,
283 const Transform& localToDevice,
284 const Shape& shape,
285 SkClipOp op)
286 : Element{shape, localToDevice, op}
287 , fUsageBounds{Rect::InfiniteInverted()}
288 , fOrder(DrawOrder::kNoIntersection)
289 , fMaxZ(DrawOrder::kClearDepth)
290 , fInvalidatedByIndex(-1) {
291 // Discard shapes that don't have any area (including when a transform can't be inverted, since
292 // it means the two dimensions are collapsed to 0 or 1 dimension in device space).
293 if (fShape.isLine() || !localToDevice.valid()) {
294 fShape.reset();
295 }
296 // Make sure the shape is not inverted. An inverted shape is equivalent to a non-inverted shape
297 // with the clip op toggled.
298 if (fShape.inverted()) {
300 }
301
304
305 // Apply rect-stays-rect transforms to rects and round rects to reduce the number of unique
306 // local coordinate systems that are in play.
309 if (fShape.isRect()) {
310 // The actual geometry can be updated to the device-intersected bounds and we know the
311 // inner bounds are equal to the outer.
315 } else if (fShape.isRRect()) {
316 // Can't transform in place and must still check transform result since some very
317 // ill-formed scale+translate matrices can cause invalid rrect radii.
318 SkRRect xformed;
319 if (fShape.rrect().transform(fLocalToDevice, &xformed)) {
320 fShape.setRRect(xformed);
322 // Refresh outer bounds to match the transformed round rect in case
323 // SkRRect::transform produces slightly different results from Transform::mapRect.
324 fOuterBounds = fShape.bounds().makeIntersect(deviceBounds);
325 fInnerBounds = Rect{SkRRectPriv::InnerBounds(xformed)}.makeIntersect(fOuterBounds);
326 }
327 }
328 }
329
331 // Either was already an empty shape or a non-empty shape is offscreen, so treat it as such.
332 fShape.reset();
334 }
335
336 // Now that fOp and fShape are canonical, set the shape's fill type to match how it needs to be
337 // drawn as a depth-only shape everywhere that is clipped out (intersect is thus inverse-filled)
339
340 // Post-conditions on inner and outer bounds
341 SkASSERT(fShape.isEmpty() || deviceBounds.contains(fOuterBounds));
342 this->validate();
343}
344
345ClipStack::RawElement::operator ClipStack::TransformedShape() const {
346 return {fLocalToDevice, fShape, fOuterBounds, fInnerBounds, fOp};
347}
348
349void ClipStack::RawElement::drawClip(Device* device) {
350 this->validate();
351
352 // Skip elements that have not affected any draws
353 if (!this->hasPendingDraw()) {
354 SkASSERT(fUsageBounds.isEmptyNegativeOrNaN());
355 return;
356 }
357
358 SkASSERT(!fUsageBounds.isEmptyNegativeOrNaN());
359 // For clip draws, the usage bounds is the scissor.
360 Rect scissor = fUsageBounds.makeRoundOut();
361 Rect drawBounds = fOuterBounds.makeIntersect(scissor);
362 if (!drawBounds.isEmptyNegativeOrNaN()) {
363 // Although we are recording this clip draw after all the draws it affects, 'fOrder' was
364 // determined at the first usage, so after sorting by DrawOrder the clip draw will be in the
365 // right place. Unlike regular draws that use their own "Z", by writing (1 + max Z this clip
366 // affects), it will cause those draws to fail either GREATER and GEQUAL depth tests where
367 // they need to be clipped.
368 DrawOrder order{fMaxZ.next(), fOrder};
369 // An element's clip op is encoded in the shape's fill type. Inverse fills are intersect ops
370 // and regular fills are difference ops. This means fShape is already in the right state to
371 // draw directly.
372 SkASSERT((fOp == SkClipOp::kDifference && !fShape.inverted()) ||
373 (fOp == SkClipOp::kIntersect && fShape.inverted()));
374 device->drawClipShape(fLocalToDevice,
375 fShape,
376 Clip{drawBounds, drawBounds, scissor.asSkIRect(), nullptr},
377 order);
378 }
379
380 // After the clip shape is drawn, reset its state. If the clip element is being popped off the
381 // stack or overwritten because a new clip invalidated it, this won't matter. But if the clips
382 // were drawn because the Device had to flush pending work while the clip stack was not empty,
383 // subsequent draws will still need to be clipped to the elements. In this case, the usage
384 // accumulation process will begin again and automatically use the Device's post-flush Z values
385 // and BoundsManager state.
386 fUsageBounds = Rect::InfiniteInverted();
389}
390
391void ClipStack::RawElement::validate() const {
392 // If the shape type isn't empty, the outer bounds shouldn't be empty; if the inner bounds are
393 // not empty, they must be contained in outer.
394 SkASSERT((fShape.isEmpty() || !fOuterBounds.isEmptyNegativeOrNaN()) &&
395 (fInnerBounds.isEmptyNegativeOrNaN() || fOuterBounds.contains(fInnerBounds)));
396 SkASSERT((fOp == SkClipOp::kDifference && !fShape.inverted()) ||
397 (fOp == SkClipOp::kIntersect && fShape.inverted()));
398 SkASSERT(!this->hasPendingDraw() || !fUsageBounds.isEmptyNegativeOrNaN());
399}
400
401void ClipStack::RawElement::markInvalid(const SaveRecord& current) {
402 SkASSERT(!this->isInvalid());
403 fInvalidatedByIndex = current.firstActiveElementIndex();
404 // NOTE: We don't draw the accumulated clip usage when the element is marked invalid. Some
405 // invalidated elements are part of earlier save records so can become re-active after a restore
406 // in which case they should continue to accumulate. Invalidated elements that are part of the
407 // active save record are removed at the end of the stack modification, which is when they are
408 // explicitly drawn.
409}
410
411void ClipStack::RawElement::restoreValid(const SaveRecord& current) {
412 if (current.firstActiveElementIndex() < fInvalidatedByIndex) {
413 fInvalidatedByIndex = -1;
414 }
415}
416
417bool ClipStack::RawElement::combine(const RawElement& other, const SaveRecord& current) {
418 // Don't combine elements that have collected draw usage, since that changes their geometry.
419 if (this->hasPendingDraw() || other.hasPendingDraw()) {
420 return false;
421 }
422 // To reduce the number of possibilities, only consider intersect+intersect. Difference and
423 // mixed op cases could be analyzed to simplify one of the shapes, but that is a rare
424 // occurrence and the math is much more complicated.
425 if (other.fOp != SkClipOp::kIntersect || fOp != SkClipOp::kIntersect) {
426 return false;
427 }
428
429 // At the moment, only rect+rect or rrect+rrect are supported (although rect+rrect is
430 // treated as a degenerate case of rrect+rrect).
431 bool shapeUpdated = false;
432 if (fShape.isRect() && other.fShape.isRect()) {
433 if (fLocalToDevice == other.fLocalToDevice) {
434 Rect intersection = fShape.rect().makeIntersect(other.fShape.rect());
435 // Simplify() should have caught this case
436 SkASSERT(!intersection.isEmptyNegativeOrNaN());
437 fShape.setRect(intersection);
438 shapeUpdated = true;
439 }
440 } else if ((fShape.isRect() || fShape.isRRect()) &&
441 (other.fShape.isRect() || other.fShape.isRRect())) {
442 if (fLocalToDevice == other.fLocalToDevice) {
443 // Treat rrect+rect intersections as rrect+rrect
444 SkRRect a = fShape.isRect() ? SkRRect::MakeRect(fShape.rect().asSkRect())
445 : fShape.rrect();
446 SkRRect b = other.fShape.isRect() ? SkRRect::MakeRect(other.fShape.rect().asSkRect())
447 : other.fShape.rrect();
448
450 if (!joined.isEmpty()) {
451 // Can reduce to a single element
452 if (joined.isRect()) {
453 // And with a simplified type
454 fShape.setRect(joined.rect());
455 } else {
456 fShape.setRRect(joined);
457 }
458 shapeUpdated = true;
459 }
460 // else the intersection isn't representable as a rrect, or doesn't actually intersect.
461 // ConservativeIntersect doesn't disambiguate those two cases, and just testing bounding
462 // boxes for non-intersection would have already been caught by Simplify(), so
463 // just don't combine the two elements and let rasterization resolve the combination.
464 }
465 }
466
467 if (shapeUpdated) {
468 // This logic works under the assumption that both combined elements were intersect.
469 SkASSERT(fOp == SkClipOp::kIntersect && other.fOp == SkClipOp::kIntersect);
470 fOuterBounds.intersect(other.fOuterBounds);
471 fInnerBounds.intersect(other.fInnerBounds);
472 // Inner bounds can become empty, but outer bounds should not be able to.
473 SkASSERT(!fOuterBounds.isEmptyNegativeOrNaN());
474 fShape.setInverted(true); // the setR[R]ect operations reset to non-inverse
475 this->validate();
476 return true;
477 } else {
478 return false;
479 }
480}
481
482void ClipStack::RawElement::updateForElement(RawElement* added, const SaveRecord& current) {
483 if (this->isInvalid()) {
484 // Already doesn't do anything, so skip this element
485 return;
486 }
487
488 // 'A' refers to this element, 'B' refers to 'added'.
489 switch (Simplify(*this, *added)) {
490 case SimplifyResult::kEmpty:
491 // Mark both elements as invalid to signal that the clip is fully empty
492 this->markInvalid(current);
493 added->markInvalid(current);
494 break;
495
496 case SimplifyResult::kAOnly:
497 // This element already clips more than 'added', so mark 'added' is invalid to skip it
498 added->markInvalid(current);
499 break;
500
501 case SimplifyResult::kBOnly:
502 // 'added' clips more than this element, so mark this as invalid
503 this->markInvalid(current);
504 break;
505
506 case SimplifyResult::kBoth:
507 // Else the bounds checks think we need to keep both, but depending on the combination
508 // of the ops and shape kinds, we may be able to do better.
509 if (added->combine(*this, current)) {
510 // 'added' now fully represents the combination of the two elements
511 this->markInvalid(current);
512 }
513 break;
514 }
515}
516
517ClipStack::RawElement::DrawInfluence
518ClipStack::RawElement::testForDraw(const TransformedShape& draw) const {
519 if (this->isInvalid()) {
520 // Cannot affect the draw
521 return DrawInfluence::kNone;
522 }
523
524 // For this analysis, A refers to the Element and B refers to the draw
525 switch(Simplify(*this, draw)) {
526 case SimplifyResult::kEmpty:
527 // The more detailed per-element checks have determined the draw is clipped out.
528 return DrawInfluence::kClipOut;
529
530 case SimplifyResult::kBOnly:
531 // This element does not affect the draw
532 return DrawInfluence::kNone;
533
534 case SimplifyResult::kAOnly:
535 // If this were the only element, we could replace the draw's geometry but that only
536 // gives us a win if we know that the clip element would only be used by this draw.
537 // For now, just fall through to regular clip handling.
538 [[fallthrough]];
539
540 case SimplifyResult::kBoth:
541 return DrawInfluence::kIntersect;
542 }
543
545}
546
547CompressedPaintersOrder ClipStack::RawElement::updateForDraw(const BoundsManager* boundsManager,
548 const Rect& drawBounds,
549 PaintersDepth drawZ) {
550 SkASSERT(!this->isInvalid());
551 SkASSERT(!drawBounds.isEmptyNegativeOrNaN());
552
553 if (!this->hasPendingDraw()) {
554 // No usage yet so we need an order that we will use when drawing to just the depth
555 // attachment. It is sufficient to use the next CompressedPaintersOrder after the
556 // most recent draw under this clip's outer bounds. It is necessary to use the
557 // entire clip's outer bounds because the order has to be determined before the
558 // final usage bounds are known and a subsequent draw could require a completely
559 // different portion of the clip than this triggering draw.
560 //
561 // Lazily determining the order has several benefits to computing it when the clip
562 // element was first created:
563 // - Elements that are invalidated by nested clips before draws are made do not
564 // waste time in the BoundsManager.
565 // - Elements that never actually modify a draw (e.g. a defensive clip) do not
566 // waste time in the BoundsManager.
567 // - A draw that triggers clip usage on multiple elements will more likely assign
568 // the same order to those elements, meaning their depth-only draws are more
569 // likely to batch in the final DrawPass.
570 //
571 // However, it does mean that clip elements can have the same order as each other,
572 // or as later draws (e.g. after the clip has been popped off the stack). Any
573 // overlap between clips or draws is addressed when the clip is drawn by selecting
574 // an appropriate DisjointStencilIndex value. Stencil-aside, this order assignment
575 // logic, max Z tracking, and the depth test during rasterization are able to
576 // resolve everything correctly even if clips have the same order value.
577 // See go/clip-stack-order for a detailed analysis of why this works.
578 fOrder = boundsManager->getMostRecentDraw(fOuterBounds).next();
579 fUsageBounds = drawBounds;
580 fMaxZ = drawZ;
581 } else {
582 // Earlier draws have already used this element so we cannot change where the
583 // depth-only draw will be sorted to, but we need to ensure we cover the new draw's
584 // bounds and use a Z value that will clip out its pixels as appropriate.
585 fUsageBounds.join(drawBounds);
586 if (drawZ > fMaxZ) {
587 fMaxZ = drawZ;
588 }
589 }
590
591 return fOrder;
592}
593
594ClipStack::ClipState ClipStack::RawElement::clipType() const {
595 // Map from the internal shape kind to the clip state enum
596 switch (fShape.type()) {
598 return ClipState::kEmpty;
599
601 return fOp == SkClipOp::kIntersect &&
602 fLocalToDevice.type() == Transform::Type::kIdentity
604
606 return fOp == SkClipOp::kIntersect &&
607 fLocalToDevice.type() == Transform::Type::kIdentity
609
611 // These types should never become RawElements, but call them kComplex in release builds
612 SkASSERT(false);
613 [[fallthrough]];
614
616 return ClipState::kComplex;
617 }
619}
620
621///////////////////////////////////////////////////////////////////////////////
622// ClipStack::SaveRecord
623
624ClipStack::SaveRecord::SaveRecord(const Rect& deviceBounds)
625 : fInnerBounds(deviceBounds)
626 , fOuterBounds(deviceBounds)
627 , fShader(nullptr)
628 , fStartingElementIndex(0)
629 , fOldestValidIndex(0)
630 , fDeferredSaveCount(0)
631 , fStackOp(SkClipOp::kIntersect)
632 , fState(ClipState::kWideOpen) {}
633
634ClipStack::SaveRecord::SaveRecord(const SaveRecord& prior,
635 int startingElementIndex)
636 : fInnerBounds(prior.fInnerBounds)
637 , fOuterBounds(prior.fOuterBounds)
638 , fShader(prior.fShader)
639 , fStartingElementIndex(startingElementIndex)
640 , fOldestValidIndex(prior.fOldestValidIndex)
641 , fDeferredSaveCount(0)
642 , fStackOp(prior.fStackOp)
643 , fState(prior.fState) {
644 // If the prior record added an element, this one will insert into the same index
645 // (that's okay since we'll remove it when this record is popped off the stack).
646 SkASSERT(startingElementIndex >= prior.fStartingElementIndex);
647}
648
649ClipStack::ClipState ClipStack::SaveRecord::state() const {
650 if (fShader && fState != ClipState::kEmpty) {
651 return ClipState::kComplex;
652 } else {
653 return fState;
654 }
655}
656
657Rect ClipStack::SaveRecord::scissor(const Rect& deviceBounds, const Rect& drawBounds) const {
658 // This should only be called when the clip stack actually has something non-trivial to evaluate
659 // It is effectively a reduced version of Simplify() dealing only with device-space bounds and
660 // returning the intersection results.
662 SkASSERT(deviceBounds.contains(drawBounds)); // This should have already been handled.
663
664 if (fStackOp == SkClipOp::kDifference) {
665 // kDifference nominally uses the draw's bounds minus the save record's inner bounds as the
666 // scissor. However, if the draw doesn't intersect the clip at all then it doesn't have any
667 // visual effect and we can switch to the device bounds as the canonical scissor.
668 if (!fOuterBounds.intersects(drawBounds)) {
669 return deviceBounds;
670 } else {
671 // This automatically detects the case where the draw is contained in inner bounds and
672 // would be entirely clipped out.
673 return subtract(drawBounds, fInnerBounds, /*exact=*/true);
674 }
675 } else {
676 // kIntersect nominally uses the save record's outer bounds as the scissor. However, if the
677 // draw is contained entirely within those bounds, it doesn't have any visual effect so
678 // switch to using the device bounds as the canonical scissor to minimize state changes.
679 if (fOuterBounds.contains(drawBounds)) {
680 return deviceBounds;
681 } else {
682 // This automatically detects the case where the draw does not intersect the clip.
683 return fOuterBounds;
684 }
685 }
686}
687
688void ClipStack::SaveRecord::removeElements(RawElement::Stack* elements, Device* device) {
689 while (elements->count() > fStartingElementIndex) {
690 // Since the element is being deleted now, it won't be in the ClipStack when the Device
691 // calls recordDeferredClipDraws(). Record the clip's draw now (if it needs it).
692 elements->back().drawClip(device);
693 elements->pop_back();
694 }
695}
696
697void ClipStack::SaveRecord::restoreElements(RawElement::Stack* elements) {
698 // Presumably this SaveRecord is the new top of the stack, and so it owns the elements
699 // from its starting index to restoreCount - 1. Elements from the old save record have
700 // been destroyed already, so their indices would have been >= restoreCount, and any
701 // still-present element can be un-invalidated based on that.
702 int i = elements->count() - 1;
703 for (RawElement& e : elements->ritems()) {
704 if (i < fOldestValidIndex) {
705 break;
706 }
707 e.restoreValid(*this);
708 --i;
709 }
710}
711
712void ClipStack::SaveRecord::addShader(sk_sp<SkShader> shader) {
713 SkASSERT(shader);
714 SkASSERT(this->canBeUpdated());
715 if (!fShader) {
716 fShader = std::move(shader);
717 } else {
718 // The total coverage is computed by multiplying the coverage from each element (shape or
719 // shader), but since multiplication is associative, we can use kSrcIn blending to make
720 // a new shader that represents 'shader' * 'fShader'
721 fShader = SkShaders::Blend(SkBlendMode::kSrcIn, std::move(shader), fShader);
722 }
723}
724
725bool ClipStack::SaveRecord::addElement(RawElement&& toAdd,
726 RawElement::Stack* elements,
727 Device* device) {
728 // Validity check the element's state first
729 toAdd.validate();
730
731 // And we shouldn't be adding an element if we have a deferred save
732 SkASSERT(this->canBeUpdated());
733
734 if (fState == ClipState::kEmpty) {
735 // The clip is already empty, and we only shrink, so there's no need to record this element.
736 return false;
737 } else if (toAdd.shape().isEmpty()) {
738 // An empty difference op should have been detected earlier, since it's a no-op
739 SkASSERT(toAdd.op() == SkClipOp::kIntersect);
740 fState = ClipState::kEmpty;
741 this->removeElements(elements, device);
742 return true;
743 }
744
745 // Here we treat the SaveRecord as a "TransformedShape" with the identity transform, and a shape
746 // equal to its outer bounds. This lets us get accurate intersection tests against the new
747 // element, but we pass true to skip more detailed contains checks because the SaveRecord's
748 // shape is potentially very different from its aggregate outer bounds.
749 Shape outerSaveBounds{fOuterBounds};
750 TransformedShape save{kIdentity, outerSaveBounds, fOuterBounds, fInnerBounds, fStackOp,
751 /*containsChecksOnlyBounds=*/true};
752
753 // In this invocation, 'A' refers to the existing stack's bounds and 'B' refers to the new
754 // element.
755 switch (Simplify(save, toAdd)) {
756 case SimplifyResult::kEmpty:
757 // The combination results in an empty clip
758 fState = ClipState::kEmpty;
759 this->removeElements(elements, device);
760 return true;
761
762 case SimplifyResult::kAOnly:
763 // The combination would not be any different than the existing clip
764 return false;
765
766 case SimplifyResult::kBOnly:
767 // The combination would invalidate the entire existing stack and can be replaced with
768 // just the new element.
769 this->replaceWithElement(std::move(toAdd), elements, device);
770 return true;
771
772 case SimplifyResult::kBoth:
773 // The new element combines in a complex manner, so update the stack's bounds based on
774 // the combination of its and the new element's ops (handled below)
775 break;
776 }
777
778 if (fState == ClipState::kWideOpen) {
779 // When the stack was wide open and the clip effect was kBoth, the "complex" manner is
780 // simply to keep the element and update the stack bounds to be the element's intersected
781 // with the device.
782 this->replaceWithElement(std::move(toAdd), elements, device);
783 return true;
784 }
785
786 // Some form of actual clip element(s) to combine with.
787 if (fStackOp == SkClipOp::kIntersect) {
788 if (toAdd.op() == SkClipOp::kIntersect) {
789 // Intersect (stack) + Intersect (toAdd)
790 // - Bounds updates is simply the paired intersections of outer and inner.
791 fOuterBounds.intersect(toAdd.outerBounds());
792 fInnerBounds.intersect(toAdd.innerBounds());
793 // Outer should not have become empty, but is allowed to if there's no intersection.
794 SkASSERT(!fOuterBounds.isEmptyNegativeOrNaN());
795 } else {
796 // Intersect (stack) + Difference (toAdd)
797 // - Shrink the stack's outer bounds if the difference op's inner bounds completely
798 // cuts off an edge.
799 // - Shrink the stack's inner bounds to completely exclude the op's outer bounds.
800 fOuterBounds = subtract(fOuterBounds, toAdd.innerBounds(), /* exact */ true);
801 fInnerBounds = subtract(fInnerBounds, toAdd.outerBounds(), /* exact */ false);
802 }
803 } else {
804 if (toAdd.op() == SkClipOp::kIntersect) {
805 // Difference (stack) + Intersect (toAdd)
806 // - Bounds updates are just the mirror of Intersect(stack) + Difference(toAdd)
807 Rect oldOuter = fOuterBounds;
808 fOuterBounds = subtract(toAdd.outerBounds(), fInnerBounds, /* exact */ true);
809 fInnerBounds = subtract(toAdd.innerBounds(), oldOuter, /* exact */ false);
810 } else {
811 // Difference (stack) + Difference (toAdd)
812 // - The updated outer bounds is the union of outer bounds and the inner becomes the
813 // largest of the two possible inner bounds
814 fOuterBounds.join(toAdd.outerBounds());
815 if (toAdd.innerBounds().area() > fInnerBounds.area()) {
816 fInnerBounds = toAdd.innerBounds();
817 }
818 }
819 }
820
821 // If we get here, we're keeping the new element and the stack's bounds have been updated.
822 // We ought to have caught the cases where the stack bounds resemble an empty or wide open
823 // clip, so assert that's the case.
824 SkASSERT(!fOuterBounds.isEmptyNegativeOrNaN() &&
825 (fInnerBounds.isEmptyNegativeOrNaN() || fOuterBounds.contains(fInnerBounds)));
826
827 return this->appendElement(std::move(toAdd), elements, device);
828}
829
830bool ClipStack::SaveRecord::appendElement(RawElement&& toAdd,
831 RawElement::Stack* elements,
832 Device* device) {
833 // Update past elements to account for the new element
834 int i = elements->count() - 1;
835
836 // After the loop, elements between [max(youngestValid, startingIndex)+1, count-1] can be
837 // removed from the stack (these are the active elements that have been invalidated by the
838 // newest element; since it's the active part of the stack, no restore() can bring them back).
839 int youngestValid = fStartingElementIndex - 1;
840 // After the loop, elements between [0, oldestValid-1] are all invalid. The value of oldestValid
841 // becomes the save record's new fLastValidIndex value.
842 int oldestValid = elements->count();
843 // After the loop, this is the earliest active element that was invalidated. It may be
844 // older in the stack than earliestValid, so cannot be popped off, but can be used to store
845 // the new element instead of allocating more.
846 RawElement* oldestActiveInvalid = nullptr;
847 int oldestActiveInvalidIndex = elements->count();
848
849 for (RawElement& existing : elements->ritems()) {
850 if (i < fOldestValidIndex) {
851 break;
852 }
853 // We don't need to pass the actual index that toAdd will be saved to; just the minimum
854 // index of this save record, since that will result in the same restoration behavior later.
855 existing.updateForElement(&toAdd, *this);
856
857 if (toAdd.isInvalid()) {
858 if (existing.isInvalid()) {
859 // Both new and old invalid implies the entire clip becomes empty
860 fState = ClipState::kEmpty;
861 return true;
862 } else {
863 // The new element doesn't change the clip beyond what the old element already does
864 return false;
865 }
866 } else if (existing.isInvalid()) {
867 // The new element cancels out the old element. The new element may have been modified
868 // to account for the old element's geometry.
869 if (i >= fStartingElementIndex) {
870 // Still active, so the invalidated index could be used to store the new element
871 oldestActiveInvalid = &existing;
872 oldestActiveInvalidIndex = i;
873 }
874 } else {
875 // Keep both new and old elements
876 oldestValid = i;
877 if (i > youngestValid) {
878 youngestValid = i;
879 }
880 }
881
882 --i;
883 }
884
885 // Post-iteration validity check
886 SkASSERT(oldestValid == elements->count() ||
887 (oldestValid >= fOldestValidIndex && oldestValid < elements->count()));
888 SkASSERT(youngestValid == fStartingElementIndex - 1 ||
889 (youngestValid >= fStartingElementIndex && youngestValid < elements->count()));
890 SkASSERT((oldestActiveInvalid && oldestActiveInvalidIndex >= fStartingElementIndex &&
891 oldestActiveInvalidIndex < elements->count()) || !oldestActiveInvalid);
892
893 // Update final state
894 SkASSERT(oldestValid >= fOldestValidIndex);
895 fOldestValidIndex = std::min(oldestValid, oldestActiveInvalidIndex);
896 fState = oldestValid == elements->count() ? toAdd.clipType() : ClipState::kComplex;
897 if (fStackOp == SkClipOp::kDifference && toAdd.op() == SkClipOp::kIntersect) {
898 // The stack remains in difference mode only as long as all elements are difference
899 fStackOp = SkClipOp::kIntersect;
900 }
901
902 int targetCount = youngestValid + 1;
903 if (!oldestActiveInvalid || oldestActiveInvalidIndex >= targetCount) {
904 // toAdd will be stored right after youngestValid
905 targetCount++;
906 oldestActiveInvalid = nullptr;
907 }
908 while (elements->count() > targetCount) {
909 SkASSERT(oldestActiveInvalid != &elements->back()); // shouldn't delete what we'll reuse
910 elements->back().drawClip(device);
911 elements->pop_back();
912 }
913 if (oldestActiveInvalid) {
914 oldestActiveInvalid->drawClip(device);
915 *oldestActiveInvalid = std::move(toAdd);
916 } else if (elements->count() < targetCount) {
917 elements->push_back(std::move(toAdd));
918 } else {
919 elements->back().drawClip(device);
920 elements->back() = std::move(toAdd);
921 }
922
923 return true;
924}
925
926void ClipStack::SaveRecord::replaceWithElement(RawElement&& toAdd,
927 RawElement::Stack* elements,
928 Device* device) {
929 // The aggregate state of the save record mirrors the element
930 fInnerBounds = toAdd.innerBounds();
931 fOuterBounds = toAdd.outerBounds();
932 fStackOp = toAdd.op();
933 fState = toAdd.clipType();
934
935 // All prior active element can be removed from the stack: [startingIndex, count - 1]
936 int targetCount = fStartingElementIndex + 1;
937 while (elements->count() > targetCount) {
938 elements->back().drawClip(device);
939 elements->pop_back();
940 }
941 if (elements->count() < targetCount) {
942 elements->push_back(std::move(toAdd));
943 } else {
944 elements->back().drawClip(device);
945 elements->back() = std::move(toAdd);
946 }
947
948 SkASSERT(elements->count() == fStartingElementIndex + 1);
949
950 // This invalidates all older elements that are owned by save records lower in the clip stack.
951 fOldestValidIndex = fStartingElementIndex;
952}
953
954///////////////////////////////////////////////////////////////////////////////
955// ClipStack
956
957// NOTE: Based on draw calls in all GMs, SKPs, and SVGs as of 08/20, 98% use a clip stack with
958// one Element and up to two SaveRecords, thus the inline size for RawElement::Stack and
959// SaveRecord::Stack (this conveniently keeps the size of ClipStack manageable). The max
960// encountered element stack depth was 5 and the max save depth was 6. Using an increment of 8 for
961// these stacks means that clip management will incur a single allocation for the remaining 2%
962// of the draws, with extra head room for more complex clips encountered in the wild.
963static constexpr int kElementStackIncrement = 8;
964static constexpr int kSaveStackIncrement = 8;
965
967 : fElements(kElementStackIncrement)
968 , fSaves(kSaveStackIncrement)
969 , fDevice(owningDevice) {
970 // Start with a save record that is wide open
971 fSaves.emplace_back(this->deviceBounds());
972}
973
974ClipStack::~ClipStack() = default;
975
977 SkASSERT(!fSaves.empty());
978 fSaves.back().pushSave();
979}
980
982 SkASSERT(!fSaves.empty());
983 SaveRecord& current = fSaves.back();
984 if (current.popSave()) {
985 // This was just a deferred save being undone, so the record doesn't need to be removed yet
986 return;
987 }
988
989 // When we remove a save record, we delete all elements >= its starting index and any masks
990 // that were rasterized for it.
991 current.removeElements(&fElements, fDevice);
992
993 fSaves.pop_back();
994 // Restore any remaining elements that were only invalidated by the now-removed save record.
995 fSaves.back().restoreElements(&fElements);
996}
997
998Rect ClipStack::deviceBounds() const {
999 return Rect::WH(fDevice->width(), fDevice->height());
1000}
1001
1003 const SaveRecord& current = this->currentSaveRecord();
1004 if (current.state() == ClipState::kEmpty) {
1005 return Rect::InfiniteInverted();
1006 } else if (current.state() == ClipState::kWideOpen) {
1007 return this->deviceBounds();
1008 } else {
1009 if (current.op() == SkClipOp::kDifference) {
1010 // The outer/inner bounds represent what's cut out, so full bounds remains the device
1011 // bounds, minus any fully clipped content that spans the device edge.
1012 return subtract(this->deviceBounds(), current.innerBounds(), /* exact */ true);
1013 } else {
1014 SkASSERT(this->deviceBounds().contains(current.outerBounds()));
1015 return current.outerBounds();
1016 }
1017 }
1018}
1019
1020ClipStack::SaveRecord& ClipStack::writableSaveRecord(bool* wasDeferred) {
1021 SaveRecord& current = fSaves.back();
1022 if (current.canBeUpdated()) {
1023 // Current record is still open, so it can be modified directly
1024 *wasDeferred = false;
1025 return current;
1026 } else {
1027 // Must undefer the save to get a new record.
1028 SkAssertResult(current.popSave());
1029 *wasDeferred = true;
1030 return fSaves.emplace_back(current, fElements.count());
1031 }
1032}
1033
1035 // Shaders can't bring additional coverage
1036 if (this->currentSaveRecord().state() == ClipState::kEmpty) {
1037 return;
1038 }
1039
1040 bool wasDeferred;
1041 this->writableSaveRecord(&wasDeferred).addShader(std::move(shader));
1042 // Geometry elements are not invalidated by updating the clip shader
1043 // TODO(b/238763003): Integrating clipShader into graphite needs more thought, particularly how
1044 // to handle the shader explosion and where to put the effects in the GraphicsPipelineDesc.
1045 // One idea is to use sample locations and draw the clipShader into the depth buffer.
1046 // Another is resolve the clip shader into an alpha mask image that is sampled by the draw.
1047}
1048
1049void ClipStack::clipShape(const Transform& localToDevice,
1050 const Shape& shape,
1051 SkClipOp op) {
1052 if (this->currentSaveRecord().state() == ClipState::kEmpty) {
1053 return;
1054 }
1055
1056 // This will apply the transform if it's shape-type preserving, and clip the element's bounds
1057 // to the device bounds (NOT the conservative clip bounds, since those are based on the net
1058 // effect of all elements while device bounds clipping happens implicitly. During addElement,
1059 // we may still be able to invalidate some older elements).
1060 // NOTE: Does not try to simplify the shape type by inspecting the SkPath.
1061 RawElement element{this->deviceBounds(), localToDevice, shape, op};
1062
1063 // An empty op means do nothing (for difference), or close the save record, so we try and detect
1064 // that early before doing additional unnecessary save record allocation.
1065 if (element.shape().isEmpty()) {
1066 if (element.op() == SkClipOp::kDifference) {
1067 // If the shape is empty and we're subtracting, this has no effect on the clip
1068 return;
1069 }
1070 // else we will make the clip empty, but we need a new save record to record that change
1071 // in the clip state; fall through to below and updateForElement() will handle it.
1072 }
1073
1074 bool wasDeferred;
1075 SaveRecord& save = this->writableSaveRecord(&wasDeferred);
1076 SkDEBUGCODE(int elementCount = fElements.count();)
1077 if (!save.addElement(std::move(element), &fElements, fDevice)) {
1078 if (wasDeferred) {
1079 // We made a new save record, but ended up not adding an element to the stack.
1080 // So instead of keeping an empty save record around, pop it off and restore the counter
1081 SkASSERT(elementCount == fElements.count());
1082 fSaves.pop_back();
1083 fSaves.back().pushSave();
1084 }
1085 }
1086}
1087
1089 const Geometry& geometry,
1090 const SkStrokeRec& style,
1091 bool outsetBoundsForAA,
1092 ClipStack::ElementList* outEffectiveElements) const {
1093 static const Clip kClippedOut = {
1095
1096 const SaveRecord& cs = this->currentSaveRecord();
1097 if (cs.state() == ClipState::kEmpty) {
1098 // We know the draw is clipped out so don't bother computing the base draw bounds.
1099 return kClippedOut;
1100 }
1101 // Compute draw bounds, clipped only to our device bounds since we need to return that even if
1102 // the clip stack is known to be wide-open.
1103 const Rect deviceBounds = this->deviceBounds();
1104
1105 // When 'style' isn't fill, 'shape' describes the pre-stroke shape so we can't use it to check
1106 // against clip elements and so 'styledShape' will be set to the bounds post-stroking.
1107 SkTCopyOnFirstWrite<Shape> styledShape;
1108 if (geometry.isShape()) {
1109 styledShape.init(geometry.shape());
1110 } else {
1111 // The geometry is something special like text or vertices, in which case it's definitely
1112 // not a shape that could simplify cleanly with the clip stack.
1113 styledShape.initIfNeeded(geometry.bounds());
1114 }
1115
1116 auto origSize = geometry.bounds().size();
1117 if (!SkIsFinite(origSize.x(), origSize.y())) {
1118 // Discard all non-finite geometry as if it were clipped out
1119 return kClippedOut;
1120 }
1121
1122 // Inverse-filled shapes always fill the entire device (restricted to the clip).
1123 // Query the invertedness of the shape before any of the `setRect` calls below, which can
1124 // modify it.
1125 bool infiniteBounds = styledShape->inverted();
1126
1127 // Discard fills and strokes that cannot produce any coverage: an empty fill, or a
1128 // zero-length stroke that has butt caps. Otherwise the stroke style applies to a vertical
1129 // or horizontal line (making it non-empty), or it's a zero-length path segment that
1130 // must produce round or square caps (making it non-empty):
1131 // https://www.w3.org/TR/SVG11/implnote.html#PathElementImplementationNotes
1132 if (!infiniteBounds && (styledShape->isLine() || any(origSize == 0.f))) {
1133 if (style.isFillStyle() || (style.getCap() == SkPaint::kButt_Cap && all(origSize == 0.f))) {
1134 return kClippedOut;
1135 }
1136 }
1137
1138 Rect transformedShapeBounds;
1139 bool shapeInDeviceSpace = false;
1140
1141 // Some renderers make the drawn area larger than the geometry for anti-aliasing
1142 float rendererOutset = outsetBoundsForAA ? localToDevice.localAARadius(styledShape->bounds())
1143 : 0.f;
1144 if (!SkIsFinite(rendererOutset)) {
1145 transformedShapeBounds = deviceBounds;
1146 infiniteBounds = true;
1147 } else {
1148 // Will be in device space once style/AA outsets and the localToDevice transform are
1149 // applied.
1150 transformedShapeBounds = styledShape->bounds();
1151
1152 // Regular filled shapes and strokes get larger based on style and transform
1153 if (!style.isHairlineStyle() || rendererOutset != 0.0f) {
1154 float localStyleOutset = style.getInflationRadius() + rendererOutset;
1155 transformedShapeBounds.outset(localStyleOutset);
1156
1157 if (!style.isFillStyle() || rendererOutset != 0.0f) {
1158 // While this loses any shape type, the bounds remain local so hopefully tests are
1159 // fairly accurate.
1160 styledShape.writable()->setRect(transformedShapeBounds);
1161 }
1162 }
1163
1164 transformedShapeBounds = localToDevice.mapRect(transformedShapeBounds);
1165
1166 // Hairlines get an extra pixel *after* transforming to device space, unless the renderer
1167 // has already defined an outset
1168 if (style.isHairlineStyle() && rendererOutset == 0.0f) {
1169 transformedShapeBounds.outset(0.5f);
1170 // and the associated transform must be kIdentity since the bounds have been mapped by
1171 // localToDevice already.
1172 styledShape.writable()->setRect(transformedShapeBounds);
1173 shapeInDeviceSpace = true;
1174 }
1175
1176 // Restrict bounds to the device limits.
1177 transformedShapeBounds.intersect(deviceBounds);
1178 }
1179
1180 Rect drawBounds; // defined in device space
1181 if (infiniteBounds) {
1182 drawBounds = deviceBounds;
1183 styledShape.writable()->setRect(drawBounds);
1184 shapeInDeviceSpace = true;
1185 } else {
1186 drawBounds = transformedShapeBounds;
1187 }
1188
1189 if (drawBounds.isEmptyNegativeOrNaN() || cs.state() == ClipState::kWideOpen) {
1190 // Either the draw is off screen, so it's clipped out regardless of the state of the
1191 // SaveRecord, or there are no elements to apply to the draw. In both cases, 'drawBounds'
1192 // has the correct value, the scissor is the device bounds (ignored if clipped-out).
1193 return Clip(drawBounds, transformedShapeBounds, deviceBounds.asSkIRect(), cs.shader());
1194 }
1195
1196 // We don't evaluate Simplify() on the SaveRecord and the draw because a reduced version of
1197 // Simplify is effectively performed in computing the scissor rect.
1198 // Given that, we can skip iterating over the clip elements when:
1199 // - the draw's *scissored* bounds are empty, which happens when the draw was clipped out.
1200 // - the scissored bounds are contained in our inner bounds, which happens if all we need to
1201 // apply to the draw is the computed scissor rect.
1202 // TODO: The Clip's scissor is defined in terms of integer pixel coords, but if we move to
1203 // clip plane distances in the vertex shader, it can be defined in terms of the original float
1204 // coordinates.
1205 Rect scissor = cs.scissor(deviceBounds, drawBounds).makeRoundOut();
1206 drawBounds.intersect(scissor);
1207 transformedShapeBounds.intersect(scissor);
1208 if (drawBounds.isEmptyNegativeOrNaN() || cs.innerBounds().contains(drawBounds)) {
1209 // Like above, in both cases drawBounds holds the right value.
1210 return Clip(drawBounds, transformedShapeBounds, scissor.asSkIRect(), cs.shader());
1211 }
1212
1213 // If we made it here, the clip stack affects the draw in a complex way so iterate each element.
1214 // A draw is a transformed shape that "intersects" the clip. We use empty inner bounds because
1215 // there's currently no way to re-write the draw as the clip's geometry, so there's no need to
1216 // check if the draw contains the clip (vice versa is still checked and represents an unclipped
1217 // draw so is very useful to identify).
1218 TransformedShape draw{shapeInDeviceSpace ? kIdentity : localToDevice,
1219 *styledShape,
1220 /*outerBounds=*/drawBounds,
1221 /*innerBounds=*/Rect::InfiniteInverted(),
1222 /*op=*/SkClipOp::kIntersect,
1223 /*containsChecksOnlyBounds=*/true};
1224
1225 SkASSERT(outEffectiveElements);
1226 SkASSERT(outEffectiveElements->empty());
1227 int i = fElements.count();
1228 for (const RawElement& e : fElements.ritems()) {
1229 --i;
1230 if (i < cs.oldestElementIndex()) {
1231 // All earlier elements have been invalidated by elements already processed so the draw
1232 // can't be affected by them and cannot contribute to their usage bounds.
1233 break;
1234 }
1235
1236 auto influence = e.testForDraw(draw);
1237 if (influence == RawElement::DrawInfluence::kClipOut) {
1238 outEffectiveElements->clear();
1239 return kClippedOut;
1240 }
1241 if (influence == RawElement::DrawInfluence::kIntersect) {
1242 outEffectiveElements->push_back(&e);
1243 }
1244 }
1245
1246 return Clip(drawBounds, transformedShapeBounds, scissor.asSkIRect(), cs.shader());
1247}
1248
1250 const ElementList& effectiveElements,
1251 const BoundsManager* boundsManager,
1252 PaintersDepth z) {
1253 if (clip.isClippedOut()) {
1255 }
1256
1257 SkDEBUGCODE(const SaveRecord& cs = this->currentSaveRecord();)
1258 SkASSERT(cs.state() != ClipState::kEmpty);
1259
1261 for (int i = 0; i < effectiveElements.size(); ++i) {
1262 // ClipStack owns the elements in the `clipState` so it's OK to downcast and cast away
1263 // const.
1264 // TODO: Enforce the ownership? In debug builds we could invalidate a `ClipStateForDraw` if
1265 // its element pointers become dangling and assert validity here.
1266 const RawElement* e = static_cast<const RawElement*>(effectiveElements[i]);
1268 const_cast<RawElement*>(e)->updateForDraw(boundsManager, clip.drawBounds(), z);
1269 maxClipOrder = std::max(order, maxClipOrder);
1270 }
1271
1272 return maxClipOrder;
1273}
1274
1276 for (auto& e : fElements.items()) {
1277 // When a Device requires all clip elements to be recorded, we have to iterate all elements,
1278 // and will draw clip shapes for elements that are still marked as invalid from the clip
1279 // stack, including those that are older than the current save record's oldest valid index,
1280 // because they could have accumulated draw usage prior to being invalidated, but weren't
1281 // flushed when they were invalidated because of an intervening save.
1282 e.drawClip(fDevice);
1283 }
1284}
1285
1286} // namespace skgpu::graphite
int count
#define SkAssertResult(cond)
Definition SkAssert.h:123
#define SkUNREACHABLE
Definition SkAssert.h:135
#define SkASSERT(cond)
Definition SkAssert.h:116
@ kSrcIn
r = s * da
SkClipOp
Definition SkClipOp.h:13
#define SkDEBUGCODE(...)
Definition SkDebug.h:23
static bool SkIsFinite(T x, Pack... values)
static bool contains(const SkRect &r, SkPoint p)
static SkPath clip(const SkPath &path, const SkHalfPlane &plane)
Definition SkPath.cpp:3824
static bool subtract(const R &a, const R &b, R *out)
Definition SkRect.cpp:177
static void draw(SkCanvas *canvas, SkRect &target, int x, int y)
Definition aaclip.cpp:27
Shape
int height() const
Definition SkDevice.h:120
int width() const
Definition SkDevice.h:119
@ kButt_Cap
no stroke extension
Definition SkPaint.h:334
static constexpr SkScalar kW0PlaneDistance
Definition SkPathPriv.h:40
uint32_t getGenerationID() const
Definition SkPath.cpp:356
static SkRect InnerBounds(const SkRRect &rr)
Definition SkRRect.cpp:750
static SkRRect ConservativeIntersect(const SkRRect &a, const SkRRect &b)
Definition SkRRect.cpp:812
const SkRect & rect() const
Definition SkRRect.h:264
bool transform(const SkMatrix &matrix, SkRRect *dst) const
Definition SkRRect.cpp:436
static SkRRect MakeRect(const SkRect &r)
Definition SkRRect.h:149
bool isRect() const
Definition SkRRect.h:84
bool isEmpty() const
Definition SkRRect.h:83
static bool Subtract(const SkRect &a, const SkRect &b, SkRect *out)
Definition SkRect.cpp:252
SkScalar getInflationRadius() const
bool isHairlineStyle() const
Definition SkStrokeRec.h:47
SkPaint::Cap getCap() const
Definition SkStrokeRec.h:44
bool isFillStyle() const
Definition SkStrokeRec.h:51
int count() const
T & emplace_back(Args &&... args)
bool empty() const
void initIfNeeded(Args &&... args)
Definition SkTLazy.h:172
void init(const T &initial)
Definition SkTLazy.h:164
void clipShader(sk_sp< SkShader > shader)
ClipStack(Device *owningDevice)
CompressedPaintersOrder updateClipStateForDraw(const Clip &clip, const ElementList &effectiveElements, const BoundsManager *, PaintersDepth z)
Clip visitClipStackForDraw(const Transform &, const Geometry &, const SkStrokeRec &, bool outsetBoundsForAA, ElementList *outEffectiveElements) const
void clipShape(const Transform &localToDevice, const Shape &shape, SkClipOp op)
static constexpr PaintersDepth kClearDepth
Definition DrawOrder.h:109
static constexpr CompressedPaintersOrder kNoIntersection
Definition DrawOrder.h:113
const Shape & shape() const
Definition Geometry.h:106
static AI Rect InfiniteInverted()
Definition Rect.h:64
AI Rect makeIntersect(Rect rect) const
Definition Rect.h:140
AI bool isEmptyNegativeOrNaN() const
Definition Rect.h:102
static AI Rect WH(float w, float h)
Definition Rect.h:46
AI float2 size() const
Definition Rect.h:107
AI bool intersects(ComplementRect comp) const
Definition Rect.h:126
AI Rect & intersect(Rect rect)
Definition Rect.h:151
AI SkIRect asSkIRect() const
Definition Rect.h:96
AI Rect makeRoundOut() const
Definition Rect.h:133
AI Rect & outset(float outset)
Definition Rect.h:147
const SkPath & path() const
Definition Shape.h:107
Rect bounds() const
Definition Shape.cpp:69
bool inverted() const
Definition Shape.h:65
void setRRect(const SkRRect &rrect)
Definition Shape.h:131
bool isRect() const
Definition Shape.h:61
bool isEmpty() const
Definition Shape.h:59
bool isLine() const
Definition Shape.h:60
const SkRRect & rrect() const
Definition Shape.h:106
bool isRRect() const
Definition Shape.h:62
void setInverted(bool inverted)
Definition Shape.h:70
void setRect(const SkRect &rect)
Definition Shape.h:125
bool isPath() const
Definition Shape.h:63
void mapPoints(const Rect &localRect, SkV4 deviceOut[4]) const
float localAARadius(const Rect &bounds) const
Rect mapRect(const Rect &rect) const
static constexpr Transform Identity()
bool empty() const
Definition SkTArray.h:194
int size() const
Definition SkTArray.h:416
VkDevice device
Definition main.cc:53
static bool b
struct MyStruct a[10]
AtkStateType state
static float max(float r, float g, float b)
Definition hsl.cpp:49
static float min(float r, float g, float b)
Definition hsl.cpp:48
SkRRect rrect
Definition SkRecords.h:232
constexpr std::array< float, 9 > kIdentity
TRect< Scalar > Rect
Definition rect.h:746
static constexpr int kElementStackIncrement
MonotonicValue< PaintersDepthSequence > PaintersDepth
Definition DrawOrder.h:86
static constexpr int kSaveStackIncrement
MonotonicValue< CompressedPaintersOrderSequence > CompressedPaintersOrder
Definition DrawOrder.h:58
skgpu::graphite::Transform Transform
Vec< 4, float > float4
Definition SkVx.h:1146
SIT bool all(const Vec< 1, T > &x)
Definition SkVx.h:582
SkScalar w
static constexpr SkIRect MakeEmpty()
Definition SkRect.h:45
Definition SkM44.h:98
float w
Definition SkM44.h:99
float y
Definition SkM44.h:99
float x
Definition SkM44.h:99
bool intersects(const TransformedShape &) const
bool contains(const TransformedShape &) const
static SKVX_ALWAYS_INLINE Vec Load(const void *ptr)
Definition SkVx.h:109