blob: 654440aba85224bc34e97cbaed8f84370a46f988 [file] [log] [blame]
/*
* Copyright 2022 Google LLC
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "src/gpu/graphite/ClipStack_graphite.h"
#include "include/core/SkMatrix.h"
#include "include/core/SkShader.h"
#include "include/core/SkStrokeRec.h"
#include "src/core/SkMatrixProvider.h"
#include "src/core/SkPathPriv.h"
#include "src/core/SkRRectPriv.h"
#include "src/core/SkRectPriv.h"
#include "src/core/SkTLazy.h"
#include "src/gpu/graphite/Device.h"
#include "src/gpu/graphite/DrawParams.h"
#include "src/gpu/graphite/geom/BoundsManager.h"
#include "src/gpu/graphite/geom/Geometry.h"
namespace skgpu::graphite {
namespace {
Rect subtract(const Rect& a, const Rect& b, bool exact) {
SkRect diff;
if (SkRectPriv::Subtract(a.asSkRect(), b.asSkRect(), &diff) || !exact) {
// Either A-B is exactly the rectangle stored in diff, or we don't need an exact answer
// and can settle for the subrect of A excluded from B (which is also 'diff')
return Rect{diff};
} else {
// For our purposes, we want the original A when A-B cannot be exactly represented
return a;
}
}
bool oriented_bbox_intersection(const Rect& a, const Transform& aXform,
const Rect& b, const Transform& bXform) {
// NOTE: We intentionally exclude projected bounds for two reasons:
// 1. We can skip the division by w and worring about clipping to w = 0.
// 2. W/o the projective case, the separating axes are simpler to compute (see below).
SkASSERT(aXform.type() != Transform::Type::kProjection &&
bXform.type() != Transform::Type::kProjection);
SkV4 quadA[4], quadB[4];
aXform.mapPoints(a, quadA);
bXform.mapPoints(b, quadB);
// There are 4 separating axes, defined by the two normals from quadA and from quadB, but
// since they were produced by transforming a rectangle by an affine transform, we know the
// normals are orthoganal to the basis vectors of upper 2x2 of their two transforms.
auto axesX = skvx::float4(-aXform.matrix().rc(1,0), -aXform.matrix().rc(1,1),
-bXform.matrix().rc(1,0), -bXform.matrix().rc(1,1));
auto axesY = skvx::float4(aXform.matrix().rc(0,0), aXform.matrix().rc(0,1),
bXform.matrix().rc(0,0), bXform.matrix().rc(0,1));
// Projections of the 4 corners of each quadrilateral vs. the 4 axes. For orthonormal
// transforms, the projections of a quad's corners to its own normal axes should work out
// to the original dimensions of the rectangle, but this code handles skew and scale factors
// without branching.
auto aProj0 = quadA[0].x * axesX + quadA[0].y * axesY;
auto aProj1 = quadA[1].x * axesX + quadA[1].y * axesY;
auto aProj2 = quadA[2].x * axesX + quadA[2].y * axesY;
auto aProj3 = quadA[3].x * axesX + quadA[3].y * axesY;
auto bProj0 = quadB[0].x * axesX + quadB[0].y * axesY;
auto bProj1 = quadB[1].x * axesX + quadB[1].y * axesY;
auto bProj2 = quadB[2].x * axesX + quadB[2].y * axesY;
auto bProj3 = quadB[3].x * axesX + quadB[3].y * axesY;
// Minimum and maximum projected values against the 4 axes, for both quadA and quadB, which
// gives us four pairs of intervals to test for separation.
auto minA = min(min(aProj0, aProj1), min(aProj2, aProj3));
auto maxA = max(max(aProj0, aProj1), max(aProj2, aProj3));
auto minB = min(min(bProj0, bProj1), min(bProj2, bProj3));
auto maxB = max(max(bProj0, bProj1), max(bProj2, bProj3));
auto overlaps = (minB <= maxA) & (minA <= maxB);
return all(overlaps); // any non-overlapping interval would imply no intersection
}
static const Transform kIdentity{SkM44()};
} // anonymous namespace
///////////////////////////////////////////////////////////////////////////////
// ClipStack::TransformedShape
// A flyweight object describing geometry, subject to a local-to-device transform.
// This can be used by SaveRecords, Elements, and draws to determine how two shape operations
// interact with each other, without needing to share a base class, friend each other, or have a
// template for each combination of two types.
struct ClipStack::TransformedShape {
const Transform& fLocalToDevice;
const Shape& fShape;
const Rect& fOuterBounds;
const Rect& fInnerBounds;
SkClipOp fOp;
// contains() performs a fair amount of work to be as accurate as possible since it can mean
// greatly simplifying the clip stack. However, in some contexts this isn't worth doing because
// the actual shape is only an approximation (save records), or there's no current way to take
// advantage of knowing this shape contains another (draws containing a clip hypothetically
// could replace their geometry to draw the clip directly, but that isn't implemented now).
bool fContainsChecksOnlyBounds = false;
bool intersects(const TransformedShape&) const;
bool contains(const TransformedShape&) const;
};
bool ClipStack::TransformedShape::intersects(const TransformedShape& o) const {
if (!fOuterBounds.intersects(o.fOuterBounds)) {
return false;
}
if (fLocalToDevice.type() <= Transform::Type::kRectStaysRect &&
o.fLocalToDevice.type() <= Transform::Type::kRectStaysRect) {
// The two shape's coordinate spaces are different but both rect-stays-rect or simpler.
// This means, though, that their outer bounds approximations are tight to their transormed
// shape bounds. There's no point to do further tests given that and that we already found
// that these outer bounds *do* intersect.
return true;
} else if (fLocalToDevice == o.fLocalToDevice) {
// Since the two shape's local coordinate spaces are the same, we can compare shape
// bounds directly for a more accurate intersection test. We intentionally do not go
// further and do shape-specific intersection tests since these could have unknown
// complexity (for paths) and limited utility (e.g. two round rects that are disjoint
// solely from their corner curves).
return fShape.bounds().intersects(o.fShape.bounds());
} else if (fLocalToDevice.type() != Transform::Type::kProjection &&
o.fLocalToDevice.type() != Transform::Type::kProjection) {
// The shapes don't share the same coordinate system, and their approximate 'outer'
// bounds in device space could have substantial outsetting to contain the transformed
// shape (e.g. 45 degree rotation). Perform a more detailed check on their oriented
// bounding boxes.
return oriented_bbox_intersection(fShape.bounds(), fLocalToDevice,
o.fShape.bounds(), o.fLocalToDevice);
}
// Else multiple perspective transforms are involved, so assume intersection and allow the
// rasterizer to handle perspective clipping.
return true;
}
bool ClipStack::TransformedShape::contains(const TransformedShape& o) const {
if (fInnerBounds.contains(o.fOuterBounds)) {
return true;
}
// Skip more expensive contains() checks if configured not to, or if the extent of 'o' exceeds
// this shape's outer bounds. When that happens there must be some part of 'o' that cannot be
// contained in this shape.
if (fContainsChecksOnlyBounds || !fOuterBounds.contains(o.fOuterBounds)) {
return false;
}
if (fContainsChecksOnlyBounds) {
return false; // don't do any more work
}
if (fLocalToDevice == o.fLocalToDevice) {
// Test the shapes directly against each other, with a special check for a rrect+rrect
// containment (a intersect b == a implies b contains a) and paths (same gen ID, or same
// path for small paths means they contain each other).
static constexpr int kMaxPathComparePoints = 16;
if (fShape.isRRect() && o.fShape.isRRect()) {
return SkRRectPriv::ConservativeIntersect(fShape.rrect(), o.fShape.rrect())
== o.fShape.rrect();
} else if (fShape.isPath() && o.fShape.isPath()) {
// TODO: Is this worth doing still if clips only cost as much as a single draw?
return (fShape.path().getGenerationID() == o.fShape.path().getGenerationID()) ||
(fShape.path().countPoints() <= kMaxPathComparePoints &&
fShape.path() == o.fShape.path());
} else {
return fShape.conservativeContains(o.fShape.bounds());
}
} else if (fLocalToDevice.type() <= Transform::Type::kRectStaysRect &&
o.fLocalToDevice.type() <= Transform::Type::kRectStaysRect) {
// Optimize the common case where o's bounds can be mapped tightly into this coordinate
// space and then tested against our shape.
Rect localBounds = fLocalToDevice.inverseMapRect(
o.fLocalToDevice.mapRect(o.fShape.bounds()));
return fShape.conservativeContains(localBounds);
} else if (fShape.convex()) {
// Since this shape is convex, if all four corners of o's bounding box are inside it
// then the entirety of o is also guaranteed to be inside it.
SkV4 deviceQuad[4];
o.fLocalToDevice.mapPoints(o.fShape.bounds(), deviceQuad);
SkV4 localQuad[4];
fLocalToDevice.inverseMapPoints(deviceQuad, localQuad, 4);
for (int i = 0; i < 4; ++i) {
// TODO: Would be nice to make this consistent with how the GPU clips NDC w.
if (deviceQuad[i].w < SkPathPriv::kW0PlaneDistance ||
localQuad[i].w < SkPathPriv::kW0PlaneDistance) {
// Something in O actually projects behind the W = 0 plane and would be clipped
// to infinity, so it's extremely unlikely that this contains O.
return false;
}
if (!fShape.conservativeContains(skvx::float2::Load(localQuad + i) / localQuad[i].w)) {
return false;
}
}
return true;
}
// Else not an easily comparable pair of shapes so assume this doesn't contain O
return false;
}
ClipStack::SimplifyResult ClipStack::Simplify(const TransformedShape& a,
const TransformedShape& b) {
enum class ClipCombo {
kDD = 0b00,
kDI = 0b01,
kID = 0b10,
kII = 0b11
};
switch(static_cast<ClipCombo>(((int) a.fOp << 1) | (int) b.fOp)) {
case ClipCombo::kII:
// Intersect (A) + Intersect (B)
if (!a.intersects(b)) {
// Regions with non-zero coverage are disjoint, so intersection = empty
return SimplifyResult::kEmpty;
} else if (b.contains(a)) {
// B's full coverage region contains entirety of A, so intersection = A
return SimplifyResult::kAOnly;
} else if (a.contains(b)) {
// A's full coverage region contains entirety of B, so intersection = B
return SimplifyResult::kBOnly;
} else {
// The shapes intersect in some non-trivial manner
return SimplifyResult::kBoth;
}
case ClipCombo::kID:
// Intersect (A) + Difference (B)
if (!a.intersects(b)) {
// A only intersects B's full coverage region, so intersection = A
return SimplifyResult::kAOnly;
} else if (b.contains(a)) {
// B's zero coverage region completely contains A, so intersection = empty
return SimplifyResult::kEmpty;
} else {
// Intersection cannot be simplified. Note that the combination of a intersect
// and difference op in this order cannot produce kBOnly
return SimplifyResult::kBoth;
}
case ClipCombo::kDI:
// Difference (A) + Intersect (B) - the mirror of Intersect(A) + Difference(B),
// but combining is commutative so this is equivalent barring naming.
if (!b.intersects(a)) {
// B only intersects A's full coverage region, so intersection = B
return SimplifyResult::kBOnly;
} else if (a.contains(b)) {
// A's zero coverage region completely contains B, so intersection = empty
return SimplifyResult::kEmpty;
} else {
// Cannot be simplified
return SimplifyResult::kBoth;
}
case ClipCombo::kDD:
// Difference (A) + Difference (B)
if (a.contains(b)) {
// A's zero coverage region contains B, so B doesn't remove any extra
// coverage from their intersection.
return SimplifyResult::kAOnly;
} else if (b.contains(a)) {
// Mirror of the above case, intersection = B instead
return SimplifyResult::kBOnly;
} else {
// Intersection of the two differences cannot be simplified. Note that for
// this op combination it is not possible to produce kEmpty.
return SimplifyResult::kBoth;
}
}
}
///////////////////////////////////////////////////////////////////////////////
// ClipStack::Element
ClipStack::RawElement::RawElement(const Rect& deviceBounds,
const Transform& localToDevice,
const Shape& shape,
SkClipOp op)
: Element{shape, localToDevice, op}
, fUsageBounds{Rect::InfiniteInverted()}
, fOrder(DrawOrder::kNoIntersection)
, fMaxZ(DrawOrder::kClearDepth)
, fInvalidatedByIndex(-1) {
// Discard shapes that don't have any area (including when a transform can't be inverted, since
// it means the two dimensions are collapsed to 0 or 1 dimension in device space).
if (fShape.isLine() || !localToDevice.valid()) {
fShape.reset();
}
// Make sure the shape is not inverted. An inverted shape is equivalent to a non-inverted shape
// with the clip op toggled.
if (fShape.inverted()) {
fOp = (fOp == SkClipOp::kIntersect) ? SkClipOp::kDifference : SkClipOp::kIntersect;
}
fOuterBounds = fLocalToDevice.mapRect(fShape.bounds()).makeIntersect(deviceBounds);
fInnerBounds = Rect::InfiniteInverted();
// Apply rect-stays-rect transforms to rects and round rects to reduce the number of unique
// local coordinate systems that are in play.
if (!fOuterBounds.isEmptyNegativeOrNaN() &&
fLocalToDevice.type() <= Transform::Type::kRectStaysRect) {
if (fShape.isRect()) {
// The actual geometry can be updated to the device-intersected bounds and we know the
// inner bounds are equal to the outer.
fShape.setRect(fOuterBounds);
fLocalToDevice = kIdentity;
fInnerBounds = fOuterBounds;
} else if (fShape.isRRect()) {
// Can't transform in place and must still check transform result since some very
// ill-formed scale+translate matrices can cause invalid rrect radii.
SkRRect xformed;
if (fShape.rrect().transform(fLocalToDevice, &xformed)) {
fShape.setRRect(xformed);
fLocalToDevice = kIdentity;
// Refresh outer bounds to match the transformed round rect in case
// SkRRect::transform produces slightly different results from Transform::mapRect.
fOuterBounds = fShape.bounds().makeIntersect(deviceBounds);
fInnerBounds = Rect{SkRRectPriv::InnerBounds(xformed)}.makeIntersect(fOuterBounds);
}
}
}
if (fOuterBounds.isEmptyNegativeOrNaN()) {
// Either was already an empty shape or a non-empty shape is offscreen, so treat it as such.
fShape.reset();
fInnerBounds = Rect::InfiniteInverted();
}
// Now that fOp and fShape are canonical, set the shape's fill type to match how it needs to be
// drawn as a depth-only shape everywhere that is clipped out (intersect is thus inverse-filled)
fShape.setInverted(fOp == SkClipOp::kIntersect);
// Post-conditions on inner and outer bounds
SkASSERT(fShape.isEmpty() || deviceBounds.contains(fOuterBounds));
this->validate();
}
ClipStack::RawElement::operator TransformedShape() const {
return {fLocalToDevice, fShape, fOuterBounds, fInnerBounds, fOp};
}
void ClipStack::RawElement::drawClip(Device* device) {
this->validate();
// Skip elements that have not affected any draws
if (!this->hasPendingDraw()) {
SkASSERT(fUsageBounds.isEmptyNegativeOrNaN());
return;
}
SkASSERT(!fUsageBounds.isEmptyNegativeOrNaN());
// For clip draws, the usage bounds is the scissor.
Rect scissor = fUsageBounds.makeRoundOut();
Rect drawBounds = fOuterBounds.makeIntersect(scissor);
if (!drawBounds.isEmptyNegativeOrNaN()) {
// Although we are recording this clip draw after all the draws it affects, 'fOrder' was
// determined at the first usage, so after sorting by DrawOrder the clip draw will be in the
// right place. Unlike regular draws that use their own "Z", by writing (1 + max Z this clip
// affects), it will cause those draws to fail either GREATER and GEQUAL depth tests where
// they need to be clipped.
DrawOrder order{fMaxZ.next(), fOrder};
// An element's clip op is encoded in the shape's fill type. Inverse fills are intersect ops
// and regular fills are difference ops. This means fShape is already in the right state to
// draw directly.
SkASSERT((fOp == SkClipOp::kDifference && !fShape.inverted()) ||
(fOp == SkClipOp::kIntersect && fShape.inverted()));
device->drawClipShape(fLocalToDevice, fShape, Clip{drawBounds, scissor.asSkIRect()}, order);
}
// After the clip shape is drawn, reset its state. If the clip element is being popped off the
// stack or overwritten because a new clip invalidated it, this won't matter. But if the clips
// were drawn because the Device had to flush pending work while the clip stack was not empty,
// subsequent draws will still need to be clipped to the elements. In this case, the usage
// accumulation process will begin again and automatically use the Device's post-flush Z values
// and BoundsManager state.
fUsageBounds = Rect::InfiniteInverted();
fOrder = DrawOrder::kNoIntersection;
fMaxZ = DrawOrder::kClearDepth;
}
void ClipStack::RawElement::validate() const {
// If the shape type isn't empty, the outer bounds shouldn't be empty; if the inner bounds are
// not empty, they must be contained in outer.
SkASSERT((fShape.isEmpty() || !fOuterBounds.isEmptyNegativeOrNaN()) &&
(fInnerBounds.isEmptyNegativeOrNaN() || fOuterBounds.contains(fInnerBounds)));
SkASSERT((fOp == SkClipOp::kDifference && !fShape.inverted()) ||
(fOp == SkClipOp::kIntersect && fShape.inverted()));
SkASSERT(!this->hasPendingDraw() || !fUsageBounds.isEmptyNegativeOrNaN());
}
void ClipStack::RawElement::markInvalid(const SaveRecord& current) {
SkASSERT(!this->isInvalid());
fInvalidatedByIndex = current.firstActiveElementIndex();
// NOTE: We don't draw the accumulated clip usage when the element is marked invalid. Some
// invalidated elements are part of earlier save records so can become re-active after a restore
// in which case they should continue to accumulate. Invalidated elements that are part of the
// active save record are removed at the end of the stack modification, which is when they are
// explicitly drawn.
}
void ClipStack::RawElement::restoreValid(const SaveRecord& current) {
if (current.firstActiveElementIndex() < fInvalidatedByIndex) {
fInvalidatedByIndex = -1;
}
}
bool ClipStack::RawElement::combine(const RawElement& other, const SaveRecord& current) {
// Don't combine elements that have collected draw usage, since that changes their geometry.
if (this->hasPendingDraw() || other.hasPendingDraw()) {
return false;
}
// To reduce the number of possibilities, only consider intersect+intersect. Difference and
// mixed op cases could be analyzed to simplify one of the shapes, but that is a rare
// occurrence and the math is much more complicated.
if (other.fOp != SkClipOp::kIntersect || fOp != SkClipOp::kIntersect) {
return false;
}
// At the moment, only rect+rect or rrect+rrect are supported (although rect+rrect is
// treated as a degenerate case of rrect+rrect).
bool shapeUpdated = false;
if (fShape.isRect() && other.fShape.isRect()) {
if (fLocalToDevice == other.fLocalToDevice) {
Rect intersection = fShape.rect().makeIntersect(other.fShape.rect());
// Simplify() should have caught this case
SkASSERT(!intersection.isEmptyNegativeOrNaN());
fShape.setRect(intersection);
shapeUpdated = true;
}
} else if ((fShape.isRect() || fShape.isRRect()) &&
(other.fShape.isRect() || other.fShape.isRRect())) {
if (fLocalToDevice == other.fLocalToDevice) {
// Treat rrect+rect intersections as rrect+rrect
SkRRect a = fShape.isRect() ? SkRRect::MakeRect(fShape.rect().asSkRect())
: fShape.rrect();
SkRRect b = other.fShape.isRect() ? SkRRect::MakeRect(other.fShape.rect().asSkRect())
: other.fShape.rrect();
SkRRect joined = SkRRectPriv::ConservativeIntersect(a, b);
if (!joined.isEmpty()) {
// Can reduce to a single element
if (joined.isRect()) {
// And with a simplified type
fShape.setRect(joined.rect());
} else {
fShape.setRRect(joined);
}
shapeUpdated = true;
}
// else the intersection isn't representable as a rrect, or doesn't actually intersect.
// ConservativeIntersect doesn't disambiguate those two cases, and just testing bounding
// boxes for non-intersection would have already been caught by Simplify(), so
// just don't combine the two elements and let rasterization resolve the combination.
}
}
if (shapeUpdated) {
// This logic works under the assumption that both combined elements were intersect.
SkASSERT(fOp == SkClipOp::kIntersect && other.fOp == SkClipOp::kIntersect);
fOuterBounds.intersect(other.fOuterBounds);
fInnerBounds.intersect(other.fInnerBounds);
// Inner bounds can become empty, but outer bounds should not be able to.
SkASSERT(!fOuterBounds.isEmptyNegativeOrNaN());
fShape.setInverted(true); // the setR[R]ect operations reset to non-inverse
this->validate();
return true;
} else {
return false;
}
}
void ClipStack::RawElement::updateForElement(RawElement* added, const SaveRecord& current) {
if (this->isInvalid()) {
// Already doesn't do anything, so skip this element
return;
}
// 'A' refers to this element, 'B' refers to 'added'.
switch (Simplify(*this, *added)) {
case SimplifyResult::kEmpty:
// Mark both elements as invalid to signal that the clip is fully empty
this->markInvalid(current);
added->markInvalid(current);
break;
case SimplifyResult::kAOnly:
// This element already clips more than 'added', so mark 'added' is invalid to skip it
added->markInvalid(current);
break;
case SimplifyResult::kBOnly:
// 'added' clips more than this element, so mark this as invalid
this->markInvalid(current);
break;
case SimplifyResult::kBoth:
// Else the bounds checks think we need to keep both, but depending on the combination
// of the ops and shape kinds, we may be able to do better.
if (added->combine(*this, current)) {
// 'added' now fully represents the combination of the two elements
this->markInvalid(current);
}
break;
}
}
std::pair<bool, CompressedPaintersOrder>
ClipStack::RawElement::updateForDraw(const BoundsManager* boundsManager,
const TransformedShape& draw,
PaintersDepth drawZ) {
if (this->isInvalid()) {
// Cannot affect the draw
return {/*clippedOut=*/false, DrawOrder::kNoIntersection};
}
// For this analysis, A refers to the Element and B refers to the draw
switch(Simplify(*this, draw)) {
case SimplifyResult::kEmpty:
// The more detailed per-element checks have determined the draw is clipped out.
return {/*clippedOut=*/true, DrawOrder::kNoIntersection};
case SimplifyResult::kBOnly:
// This element does not affect the draw
return {/*clippedOut=*/false, DrawOrder::kNoIntersection};
case SimplifyResult::kAOnly:
// If this were the only element, we could replace the draw's geometry but that only
// gives us a win if we know that the clip element would only be used by this draw.
// For now, just fall through to regular clip handling.
[[fallthrough]];
case SimplifyResult::kBoth:
if (!this->hasPendingDraw()) {
// No usage yet so we need an order that we will use when drawing to just the depth
// attachment. It is sufficient to use the next CompressedPaintersOrder after the
// most recent draw under this clip's outer bounds. It is necessary to use the
// entire clip's outer bounds because the order has to be determined before the
// final usage bounds are known and a subsequent draw could require a completely
// different portion of the clip than this triggering draw.
//
// Lazily determining the order has several benefits to computing it when the clip
// element was first created:
// - Elements that are invalidated by nested clips before draws are made do not
// waste time in the BoundsManager.
// - Elements that never actually modify a draw (e.g. a defensive clip) do not
// waste time in the BoundsManager.
// - A draw that triggers clip usage on multiple elements will more likely assign
// the same order to those elements, meaning their depth-only draws are more
// likely to batch in the final DrawPass.
//
// However, it does mean that clip elements can have the same order as each other,
// or as later draws (e.g. after the clip has been popped off the stack). Any
// overlap between clips or draws is addressed when the clip is drawn by selecting
// an appropriate DisjointStencilIndex value. Stencil-aside, this order assignment
// logic, max Z tracking, and the depth test during rasterization are able to
// resolve everything correctly even if clips have the same order value.
// See go/clip-stack-order for a detailed analysis of why this works.
fOrder = boundsManager->getMostRecentDraw(fOuterBounds).next();
fUsageBounds = draw.fOuterBounds;
fMaxZ = drawZ;
} else {
// Earlier draws have already used this element so we cannot change where the
// depth-only draw will be sorted to, but we need to ensure we cover the new draw's
// bounds and use a Z value that will clip out its pixels as appropriate.
fUsageBounds.join(draw.fOuterBounds);
if (drawZ > fMaxZ) {
fMaxZ = drawZ;
}
}
return {/*clippedOut=*/false, fOrder};
}
SkUNREACHABLE;
}
ClipStack::ClipState ClipStack::RawElement::clipType() const {
// Map from the internal shape kind to the clip state enum
switch (fShape.type()) {
case Shape::Type::kEmpty:
return ClipState::kEmpty;
case Shape::Type::kRect:
return fOp == SkClipOp::kIntersect &&
fLocalToDevice.type() == Transform::Type::kIdentity
? ClipState::kDeviceRect : ClipState::kComplex;
case Shape::Type::kRRect:
return fOp == SkClipOp::kIntersect &&
fLocalToDevice.type() == Transform::Type::kIdentity
? ClipState::kDeviceRRect : ClipState::kComplex;
case Shape::Type::kLine:
// These types should never become RawElements, but call them kComplex in release builds
SkASSERT(false);
[[fallthrough]];
case Shape::Type::kPath:
return ClipState::kComplex;
}
SkUNREACHABLE;
}
///////////////////////////////////////////////////////////////////////////////
// ClipStack::SaveRecord
ClipStack::SaveRecord::SaveRecord(const Rect& deviceBounds)
: fInnerBounds(deviceBounds)
, fOuterBounds(deviceBounds)
, fShader(nullptr)
, fStartingElementIndex(0)
, fOldestValidIndex(0)
, fDeferredSaveCount(0)
, fStackOp(SkClipOp::kIntersect)
, fState(ClipState::kWideOpen) {}
ClipStack::SaveRecord::SaveRecord(const SaveRecord& prior,
int startingElementIndex)
: fInnerBounds(prior.fInnerBounds)
, fOuterBounds(prior.fOuterBounds)
, fShader(prior.fShader)
, fStartingElementIndex(startingElementIndex)
, fOldestValidIndex(prior.fOldestValidIndex)
, fDeferredSaveCount(0)
, fStackOp(prior.fStackOp)
, fState(prior.fState) {
// If the prior record added an element, this one will insert into the same index
// (that's okay since we'll remove it when this record is popped off the stack).
SkASSERT(startingElementIndex >= prior.fStartingElementIndex);
}
ClipStack::ClipState ClipStack::SaveRecord::state() const {
if (fShader && fState != ClipState::kEmpty) {
return ClipState::kComplex;
} else {
return fState;
}
}
Rect ClipStack::SaveRecord::scissor(const Rect& deviceBounds, const Rect& drawBounds) const {
// This should only be called when the clip stack actually has something non-trivial to evaluate
// It is effectively a reduced version of Simplify() dealing only with device-space bounds and
// returning the intersection results.
SkASSERT(this->state() != ClipState::kEmpty && this->state() != ClipState::kWideOpen);
SkASSERT(deviceBounds.contains(drawBounds)); // This should have already been handled.
if (fStackOp == SkClipOp::kDifference) {
// kDifference nominally uses the draw's bounds minus the save record's inner bounds as the
// scissor. However, if the draw doesn't intersect the clip at all then it doesn't have any
// visual effect and we can switch to the device bounds as the canonical scissor.
if (!fOuterBounds.intersects(drawBounds)) {
return deviceBounds;
} else {
// This automatically detects the case where the draw is contained in inner bounds and
// would be entirely clipped out.
return subtract(drawBounds, fInnerBounds, /*exact=*/true);
}
} else {
// kIntersect nominally uses the save record's outer bounds as the scissor. However, if the
// draw is contained entirely within those bounds, it doesn't have any visual effect so
// switch to using the device bounds as the canonical scissor to minimize state changes.
if (fOuterBounds.contains(drawBounds)) {
return deviceBounds;
} else {
// This automatically detects the case where the draw does not intersect the clip.
return fOuterBounds;
}
}
}
void ClipStack::SaveRecord::removeElements(RawElement::Stack* elements, Device* device) {
while (elements->count() > fStartingElementIndex) {
// Since the element is being deleted now, it won't be in the ClipStack when the Device
// calls recordDeferredClipDraws(). Record the clip's draw now (if it needs it).
elements->back().drawClip(device);
elements->pop_back();
}
}
void ClipStack::SaveRecord::restoreElements(RawElement::Stack* elements) {
// Presumably this SaveRecord is the new top of the stack, and so it owns the elements
// from its starting index to restoreCount - 1. Elements from the old save record have
// been destroyed already, so their indices would have been >= restoreCount, and any
// still-present element can be un-invalidated based on that.
int i = elements->count() - 1;
for (RawElement& e : elements->ritems()) {
if (i < fOldestValidIndex) {
break;
}
e.restoreValid(*this);
--i;
}
}
void ClipStack::SaveRecord::addShader(sk_sp<SkShader> shader) {
SkASSERT(shader);
SkASSERT(this->canBeUpdated());
if (!fShader) {
fShader = std::move(shader);
} else {
// The total coverage is computed by multiplying the coverage from each element (shape or
// shader), but since multiplication is associative, we can use kSrcIn blending to make
// a new shader that represents 'shader' * 'fShader'
fShader = SkShaders::Blend(SkBlendMode::kSrcIn, std::move(shader), fShader);
}
}
bool ClipStack::SaveRecord::addElement(RawElement&& toAdd,
RawElement::Stack* elements,
Device* device) {
// Validity check the element's state first
toAdd.validate();
// And we shouldn't be adding an element if we have a deferred save
SkASSERT(this->canBeUpdated());
if (fState == ClipState::kEmpty) {
// The clip is already empty, and we only shrink, so there's no need to record this element.
return false;
} else if (toAdd.shape().isEmpty()) {
// An empty difference op should have been detected earlier, since it's a no-op
SkASSERT(toAdd.op() == SkClipOp::kIntersect);
fState = ClipState::kEmpty;
this->removeElements(elements, device);
return true;
}
// Here we treat the SaveRecord as a "TransformedShape" with the identity transform, and a shape
// equal to its outer bounds. This lets us get accurate intersection tests against the new
// element, but we pass true to skip more detailed contains checks because the SaveRecord's
// shape is potentially very different from its aggregate outer bounds.
Shape outerSaveBounds{fOuterBounds};
TransformedShape save{kIdentity, outerSaveBounds, fOuterBounds, fInnerBounds, fStackOp,
/*containsChecksOnlyBounds=*/true};
// In this invocation, 'A' refers to the existing stack's bounds and 'B' refers to the new
// element.
switch (Simplify(save, toAdd)) {
case SimplifyResult::kEmpty:
// The combination results in an empty clip
fState = ClipState::kEmpty;
this->removeElements(elements, device);
return true;
case SimplifyResult::kAOnly:
// The combination would not be any different than the existing clip
return false;
case SimplifyResult::kBOnly:
// The combination would invalidate the entire existing stack and can be replaced with
// just the new element.
this->replaceWithElement(std::move(toAdd), elements, device);
return true;
case SimplifyResult::kBoth:
// The new element combines in a complex manner, so update the stack's bounds based on
// the combination of its and the new element's ops (handled below)
break;
}
if (fState == ClipState::kWideOpen) {
// When the stack was wide open and the clip effect was kBoth, the "complex" manner is
// simply to keep the element and update the stack bounds to be the element's intersected
// with the device.
this->replaceWithElement(std::move(toAdd), elements, device);
return true;
}
// Some form of actual clip element(s) to combine with.
if (fStackOp == SkClipOp::kIntersect) {
if (toAdd.op() == SkClipOp::kIntersect) {
// Intersect (stack) + Intersect (toAdd)
// - Bounds updates is simply the paired intersections of outer and inner.
fOuterBounds.intersect(toAdd.outerBounds());
fInnerBounds.intersect(toAdd.innerBounds());
// Outer should not have become empty, but is allowed to if there's no intersection.
SkASSERT(!fOuterBounds.isEmptyNegativeOrNaN());
} else {
// Intersect (stack) + Difference (toAdd)
// - Shrink the stack's outer bounds if the difference op's inner bounds completely
// cuts off an edge.
// - Shrink the stack's inner bounds to completely exclude the op's outer bounds.
fOuterBounds = subtract(fOuterBounds, toAdd.innerBounds(), /* exact */ true);
fInnerBounds = subtract(fInnerBounds, toAdd.outerBounds(), /* exact */ false);
}
} else {
if (toAdd.op() == SkClipOp::kIntersect) {
// Difference (stack) + Intersect (toAdd)
// - Bounds updates are just the mirror of Intersect(stack) + Difference(toAdd)
Rect oldOuter = fOuterBounds;
fOuterBounds = subtract(toAdd.outerBounds(), fInnerBounds, /* exact */ true);
fInnerBounds = subtract(toAdd.innerBounds(), oldOuter, /* exact */ false);
} else {
// Difference (stack) + Difference (toAdd)
// - The updated outer bounds is the union of outer bounds and the inner becomes the
// largest of the two possible inner bounds
fOuterBounds.join(toAdd.outerBounds());
if (toAdd.innerBounds().area() > fInnerBounds.area()) {
fInnerBounds = toAdd.innerBounds();
}
}
}
// If we get here, we're keeping the new element and the stack's bounds have been updated.
// We ought to have caught the cases where the stack bounds resemble an empty or wide open
// clip, so assert that's the case.
SkASSERT(!fOuterBounds.isEmptyNegativeOrNaN() &&
(fInnerBounds.isEmptyNegativeOrNaN() || fOuterBounds.contains(fInnerBounds)));
return this->appendElement(std::move(toAdd), elements, device);
}
bool ClipStack::SaveRecord::appendElement(RawElement&& toAdd,
RawElement::Stack* elements,
Device* device) {
// Update past elements to account for the new element
int i = elements->count() - 1;
// After the loop, elements between [max(youngestValid, startingIndex)+1, count-1] can be
// removed from the stack (these are the active elements that have been invalidated by the
// newest element; since it's the active part of the stack, no restore() can bring them back).
int youngestValid = fStartingElementIndex - 1;
// After the loop, elements between [0, oldestValid-1] are all invalid. The value of oldestValid
// becomes the save record's new fLastValidIndex value.
int oldestValid = elements->count();
// After the loop, this is the earliest active element that was invalidated. It may be
// older in the stack than earliestValid, so cannot be popped off, but can be used to store
// the new element instead of allocating more.
RawElement* oldestActiveInvalid = nullptr;
int oldestActiveInvalidIndex = elements->count();
for (RawElement& existing : elements->ritems()) {
if (i < fOldestValidIndex) {
break;
}
// We don't need to pass the actual index that toAdd will be saved to; just the minimum
// index of this save record, since that will result in the same restoration behavior later.
existing.updateForElement(&toAdd, *this);
if (toAdd.isInvalid()) {
if (existing.isInvalid()) {
// Both new and old invalid implies the entire clip becomes empty
fState = ClipState::kEmpty;
return true;
} else {
// The new element doesn't change the clip beyond what the old element already does
return false;
}
} else if (existing.isInvalid()) {
// The new element cancels out the old element. The new element may have been modified
// to account for the old element's geometry.
if (i >= fStartingElementIndex) {
// Still active, so the invalidated index could be used to store the new element
oldestActiveInvalid = &existing;
oldestActiveInvalidIndex = i;
}
} else {
// Keep both new and old elements
oldestValid = i;
if (i > youngestValid) {
youngestValid = i;
}
}
--i;
}
// Post-iteration validity check
SkASSERT(oldestValid == elements->count() ||
(oldestValid >= fOldestValidIndex && oldestValid < elements->count()));
SkASSERT(youngestValid == fStartingElementIndex - 1 ||
(youngestValid >= fStartingElementIndex && youngestValid < elements->count()));
SkASSERT((oldestActiveInvalid && oldestActiveInvalidIndex >= fStartingElementIndex &&
oldestActiveInvalidIndex < elements->count()) || !oldestActiveInvalid);
// Update final state
SkASSERT(oldestValid >= fOldestValidIndex);
fOldestValidIndex = std::min(oldestValid, oldestActiveInvalidIndex);
fState = oldestValid == elements->count() ? toAdd.clipType() : ClipState::kComplex;
if (fStackOp == SkClipOp::kDifference && toAdd.op() == SkClipOp::kIntersect) {
// The stack remains in difference mode only as long as all elements are difference
fStackOp = SkClipOp::kIntersect;
}
int targetCount = youngestValid + 1;
if (!oldestActiveInvalid || oldestActiveInvalidIndex >= targetCount) {
// toAdd will be stored right after youngestValid
targetCount++;
oldestActiveInvalid = nullptr;
}
while (elements->count() > targetCount) {
SkASSERT(oldestActiveInvalid != &elements->back()); // shouldn't delete what we'll reuse
elements->back().drawClip(device);
elements->pop_back();
}
if (oldestActiveInvalid) {
oldestActiveInvalid->drawClip(device);
*oldestActiveInvalid = std::move(toAdd);
} else if (elements->count() < targetCount) {
elements->push_back(std::move(toAdd));
} else {
elements->back().drawClip(device);
elements->back() = std::move(toAdd);
}
return true;
}
void ClipStack::SaveRecord::replaceWithElement(RawElement&& toAdd,
RawElement::Stack* elements,
Device* device) {
// The aggregate state of the save record mirrors the element
fInnerBounds = toAdd.innerBounds();
fOuterBounds = toAdd.outerBounds();
fStackOp = toAdd.op();
fState = toAdd.clipType();
// All prior active element can be removed from the stack: [startingIndex, count - 1]
int targetCount = fStartingElementIndex + 1;
while (elements->count() > targetCount) {
elements->back().drawClip(device);
elements->pop_back();
}
if (elements->count() < targetCount) {
elements->push_back(std::move(toAdd));
} else {
elements->back().drawClip(device);
elements->back() = std::move(toAdd);
}
SkASSERT(elements->count() == fStartingElementIndex + 1);
// This invalidates all older elements that are owned by save records lower in the clip stack.
fOldestValidIndex = fStartingElementIndex;
}
///////////////////////////////////////////////////////////////////////////////
// ClipStack
// NOTE: Based on draw calls in all GMs, SKPs, and SVGs as of 08/20, 98% use a clip stack with
// one Element and up to two SaveRecords, thus the inline size for RawElement::Stack and
// SaveRecord::Stack (this conveniently keeps the size of ClipStack manageable). The max
// encountered element stack depth was 5 and the max save depth was 6. Using an increment of 8 for
// these stacks means that clip management will incur a single allocation for the remaining 2%
// of the draws, with extra head room for more complex clips encountered in the wild.
static constexpr int kElementStackIncrement = 8;
static constexpr int kSaveStackIncrement = 8;
ClipStack::ClipStack(Device* owningDevice)
: fElements(kElementStackIncrement)
, fSaves(kSaveStackIncrement)
, fDevice(owningDevice) {
// Start with a save record that is wide open
fSaves.emplace_back(this->deviceBounds());
}
ClipStack::~ClipStack() = default;
void ClipStack::save() {
SkASSERT(!fSaves.empty());
fSaves.back().pushSave();
}
void ClipStack::restore() {
SkASSERT(!fSaves.empty());
SaveRecord& current = fSaves.back();
if (current.popSave()) {
// This was just a deferred save being undone, so the record doesn't need to be removed yet
return;
}
// When we remove a save record, we delete all elements >= its starting index and any masks
// that were rasterized for it.
current.removeElements(&fElements, fDevice);
fSaves.pop_back();
// Restore any remaining elements that were only invalidated by the now-removed save record.
fSaves.back().restoreElements(&fElements);
}
Rect ClipStack::deviceBounds() const {
return Rect::WH(fDevice->width(), fDevice->height());
}
Rect ClipStack::conservativeBounds() const {
const SaveRecord& current = this->currentSaveRecord();
if (current.state() == ClipState::kEmpty) {
return Rect::InfiniteInverted();
} else if (current.state() == ClipState::kWideOpen) {
return this->deviceBounds();
} else {
if (current.op() == SkClipOp::kDifference) {
// The outer/inner bounds represent what's cut out, so full bounds remains the device
// bounds, minus any fully clipped content that spans the device edge.
return subtract(this->deviceBounds(), current.innerBounds(), /* exact */ true);
} else {
SkASSERT(this->deviceBounds().contains(current.outerBounds()));
return current.outerBounds();
}
}
}
ClipStack::SaveRecord& ClipStack::writableSaveRecord(bool* wasDeferred) {
SaveRecord& current = fSaves.back();
if (current.canBeUpdated()) {
// Current record is still open, so it can be modified directly
*wasDeferred = false;
return current;
} else {
// Must undefer the save to get a new record.
SkAssertResult(current.popSave());
*wasDeferred = true;
return fSaves.emplace_back(current, fElements.count());
}
}
void ClipStack::clipShader(sk_sp<SkShader> shader) {
// Shaders can't bring additional coverage
if (this->currentSaveRecord().state() == ClipState::kEmpty) {
return;
}
bool wasDeferred;
this->writableSaveRecord(&wasDeferred).addShader(std::move(shader));
// Geometry elements are not invalidated by updating the clip shader
// TODO: Integrating clipShader into graphite needs more thought, particularly around how to
// handle the shader explosion and where to put the effects in the GraphicsPipelineDesc.
// One idea is to use sample locations and draw the clipShader into the depth buffer.
// Another is resolve the clip shader into an alpha mask image that is sampled by the draw.
}
void ClipStack::clipShape(const Transform& localToDevice,
const Shape& shape,
SkClipOp op) {
if (this->currentSaveRecord().state() == ClipState::kEmpty) {
return;
}
// This will apply the transform if it's shape-type preserving, and clip the element's bounds
// to the device bounds (NOT the conservative clip bounds, since those are based on the net
// effect of all elements while device bounds clipping happens implicitly. During addElement,
// we may still be able to invalidate some older elements).
// NOTE: Does not try to simplify the shape type by inspecting the SkPath.
RawElement element{this->deviceBounds(), localToDevice, shape, op};
// An empty op means do nothing (for difference), or close the save record, so we try and detect
// that early before doing additional unnecessary save record allocation.
if (element.shape().isEmpty()) {
if (element.op() == SkClipOp::kDifference) {
// If the shape is empty and we're subtracting, this has no effect on the clip
return;
}
// else we will make the clip empty, but we need a new save record to record that change
// in the clip state; fall through to below and updateForElement() will handle it.
}
bool wasDeferred;
SaveRecord& save = this->writableSaveRecord(&wasDeferred);
SkDEBUGCODE(int elementCount = fElements.count();)
if (!save.addElement(std::move(element), &fElements, fDevice)) {
if (wasDeferred) {
// We made a new save record, but ended up not adding an element to the stack.
// So instead of keeping an empty save record around, pop it off and restore the counter
SkASSERT(elementCount == fElements.count());
fSaves.pop_back();
fSaves.back().pushSave();
}
}
}
std::pair<Clip, CompressedPaintersOrder> ClipStack::applyClipToDraw(
const BoundsManager* boundsManager,
const Transform& localToDevice,
const Geometry& geometry,
const SkStrokeRec& style,
PaintersDepth z) {
const SaveRecord& cs = this->currentSaveRecord();
if (cs.state() == ClipState::kEmpty) {
// We know the draw is clipped out so don't bother computing the base draw bounds.
return {Clip{Rect::InfiniteInverted(), SkIRect::MakeEmpty()}, DrawOrder::kNoIntersection};
}
// Compute draw bounds, clipped only to our device bounds since we need to return that even if
// the clip stack is known to be wide-open.
const Rect deviceBounds = this->deviceBounds();
// When 'style' isn't fill, 'shape' describes the pre-stroke shape so we can't use it to check
// against clip elements and this will be set to the bounds of the post-stroked shape instead.
SkTCopyOnFirstWrite<Shape> styledShape;
if (geometry.isShape()) {
styledShape.init(geometry.shape());
} else {
// The geometry is something special like text or vertices, in which case it's definitely
// not a shape that could simplify cleanly with the clip stack.
styledShape.initIfNeeded(geometry.bounds());
}
Rect drawBounds; // defined in device space
if (styledShape->inverted()) {
// Inverse-filled shapes always fill the entire device (restricted to the clip).
drawBounds = deviceBounds;
styledShape.writable()->setRect(drawBounds);
} else {
// Regular filled shapes and strokes get larger based on style and transform
drawBounds = styledShape->bounds();
if (!style.isHairlineStyle()) {
float localStyleOutset = style.getInflationRadius();
drawBounds.outset(localStyleOutset);
if (!style.isFillStyle()) {
// While this loses any shape type, the bounds remain local so hopefully tests are
// fairly accurate.
styledShape.writable()->setRect(drawBounds);
}
}
drawBounds = localToDevice.mapRect(drawBounds);
// Hairlines get an extra pixel *after* transforming to device space
if (style.isHairlineStyle()) {
drawBounds.outset(0.5f);
// and the associated transform must be kIdentity since drawBounds has been mapped by
// localToDevice already.
styledShape.writable()->setRect(drawBounds);
}
// Restrict bounds to the device limits
drawBounds.intersect(deviceBounds);
}
if (drawBounds.isEmptyNegativeOrNaN() || cs.state() == ClipState::kWideOpen) {
// Either the draw is off screen, so it's clipped out regardless of the state of the
// SaveRecord, or there are no elements to apply to the draw. In both cases, 'drawBounds'
// has the correct value, the scissor is the device bounds (ignored if clipped-out), and
// we can return kNoIntersection for the painter's order.
return {Clip{drawBounds, deviceBounds.asSkIRect()}, DrawOrder::kNoIntersection};
}
// We don't evaluate Simplify() on the SaveRecord and the draw because a reduced version of
// Simplify is effectively performed in computing the scissor rect.
// Given that, we can skip iterating over the clip elements when:
// - the draw's *scissored* bounds are empty, which happens when the draw was clipped out.
// - the draw's *bounds* are contained in our inner bounds, which happens if all we need to
// apply to the draw is the computed scissor rect.
// TODO: The Clip's scissor is defined in terms of integer pixel coords, but if we move to
// clip plane distances in the vertex shader, it can be defined in terms of the original float
// coordinates.
Rect scissor = cs.scissor(deviceBounds, drawBounds).makeRoundOut();
drawBounds.intersect(scissor);
if (drawBounds.isEmptyNegativeOrNaN() || cs.innerBounds().contains(drawBounds)) {
// Like above, in both cases drawBounds holds the right value and can return kNoIntersection
return {Clip{drawBounds, scissor.asSkIRect()}, DrawOrder::kNoIntersection};
}
// If we made it here, the clip stack affects the draw in a complex way so iterate each element.
// A draw is a transformed shape that "intersects" the clip. We use empty inner bounds because
// there's currently no way to re-write the draw as the clip's geometry, so there's no need to
// check if the draw contains the clip (vice versa is still checked and represents an unclipped
// draw so is very useful to identify).
TransformedShape draw{style.isHairlineStyle() ? kIdentity : localToDevice,
*styledShape,
/*outerBounds=*/drawBounds,
/*innerBounds=*/Rect::InfiniteInverted(),
/*op=*/SkClipOp::kIntersect,
/*containsChecksOnlyBounds=*/true};
CompressedPaintersOrder maxClipOrder = DrawOrder::kNoIntersection;
int i = fElements.count();
for (RawElement& e : fElements.ritems()) {
--i;
if (i < cs.oldestElementIndex()) {
// All earlier elements have been invalidated by elements already processed so the draw
// can't be affected by them and cannot contribute to their usage bounds.
break;
}
auto [clippedOut, order] = e.updateForDraw(boundsManager, draw, z);
if (clippedOut) {
drawBounds = Rect::InfiniteInverted();
break;
} else {
maxClipOrder = std::max(order, maxClipOrder);
}
}
return {Clip{drawBounds, scissor.asSkIRect()}, maxClipOrder};
}
void ClipStack::recordDeferredClipDraws() {
for (auto& e : fElements.items()) {
// When a Device requires all clip elements to be recorded, we have to iterate all elements,
// and will draw clip shapes for elements that are still marked as invalid from the clip
// stack, including those that are older than the current save record's oldest valid index,
// because they could have accumulated draw usage prior to being invalidated, but weren't
// flushed when they were invalidated because of an intervening save.
e.drawClip(fDevice);
}
}
} // namespace skgpu