blob: 6e495ed61d999a247a88f1095f1de505fcff2221 [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.h"
#include "include/core/SkBlendMode.h"
#include "include/core/SkClipOp.h"
#include "include/core/SkM44.h"
#include "include/core/SkPaint.h"
#include "include/core/SkPath.h"
#include "include/core/SkPoint.h"
#include "include/core/SkRRect.h"
#include "include/core/SkRect.h"
#include "include/core/SkScalar.h"
#include "include/core/SkShader.h"
#include "include/core/SkSpan.h"
#include "include/core/SkStrokeRec.h"
#include "include/gpu/graphite/Recorder.h"
#include "include/private/base/SkDebug.h"
#include "include/private/base/SkFloatingPoint.h"
#include "src/base/SkEnumBitMask.h"
#include "src/base/SkVx.h"
#include "src/core/SkPathPriv.h"
#include "src/core/SkRRectPriv.h"
#include "src/core/SkRectPriv.h"
#include "src/gpu/graphite/AtlasProvider.h"
#include "src/gpu/graphite/ClipAtlasManager.h"
#include "src/gpu/graphite/Device.h"
#include "src/gpu/graphite/DrawParams.h"
#include "src/gpu/graphite/RecorderPriv.h"
#include "src/gpu/graphite/TextureProxy.h"
#include "src/gpu/graphite/geom/BoundsManager.h"
#include "src/gpu/graphite/geom/EdgeAAQuad.h"
#include "src/gpu/graphite/geom/Geometry.h"
#include "src/gpu/graphite/geom/NonMSAAClip.h"
#include <algorithm>
#include <atomic>
#include <utility>
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;
}
}
static constexpr uint32_t kInvalidGenID = 0;
static constexpr uint32_t kEmptyGenID = 1;
static constexpr uint32_t kWideOpenGenID = 2;
uint32_t next_gen_id() {
// 0-2 are reserved for invalid, empty & wide-open
static const uint32_t kFirstUnreservedGenID = 3;
static std::atomic<uint32_t> nextID{kFirstUnreservedGenID};
uint32_t id;
do {
id = nextID.fetch_add(1, std::memory_order_relaxed);
} while (id < kFirstUnreservedGenID);
return id;
}
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::kPerspective &&
bXform.type() != Transform::Type::kPerspective);
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
}
// LTRB are set in returned bitmask if other's LTRB edge is coincident or inside `shape`'s edge.
SkEnumBitMask<EdgeAAQuad::Flags> clipped_edges(const Rect& shape, const Rect& other) {
// Since RB are stored negated in vals(), this works out to
// [other.LT >= shape.LT, other.RB <= shape.RB]
auto insideMask = other.vals() >= shape.vals();
return (insideMask[0] ? EdgeAAQuad::Flags::kLeft : EdgeAAQuad::Flags::kNone) |
(insideMask[1] ? EdgeAAQuad::Flags::kTop : EdgeAAQuad::Flags::kNone) |
(insideMask[2] ? EdgeAAQuad::Flags::kRight : EdgeAAQuad::Flags::kNone) |
(insideMask[3] ? EdgeAAQuad::Flags::kBottom : EdgeAAQuad::Flags::kNone);
}
// Tries to intersect `otherShape` transformed by `otherToDevice` directly into `shape` assuming
// that `shape` is transformed by localToDevice. If possible (true), `shape` represents the exact
// intersection of the two original shapes. Returns true if `shape` is modified, false otherwise.
bool intersect_shape(const Transform& otherToDevice, const Shape& otherShape,
const Transform& localToDevice, Shape* shape,
SkEnumBitMask<EdgeAAQuad::Flags>* edgeFlags) {
// There are only a subset of shape types that we can analytically intersect with each other,
// assuming a simple fill style (always the case for clip shapes):
//
// rects, rrects, flood-fills (empty+inverse-fill)
//
// Flood-fills only appear as part of a draw, so it's only checked for `shape` and not
// `otherShape`. In theory, per-edge AA quads could also be included but they do not appear as
// clip shapes.
//
// Paths and arcs have complex intersection logic, so are skipped under the assumption that
// simple cases have already been mapped to a rect or rrect. Lines are only ever stroked, so
// are incompatible with this function.
//
// EdgeAAQuads that are rectangular can be intersected by being treated as a rect shape and
// adjusting edge flags as non-AA edges are clipped out.
bool shapeIntersectable = shape->isRect() ||
shape->isRRect() ||
shape->isFloodFill();
bool otherIntersectable = otherShape.isRect() || otherShape.isRRect();
// Only clip shapes are used for `otherShape`, so we shouldn't see any flood fills here
SkASSERT(!otherShape.isFloodFill());
// Only rects should have edge flags other than kAll
SkASSERT(*edgeFlags == EdgeAAQuad::Flags::kAll || shape->isRect());
if (!shapeIntersectable || !otherIntersectable) {
// Technically if shapeIntersectable was true for empty+inverse, we could turn the flood
// fill into `otherShape` regardless of its type, but those other types are more expensive
// to render and in the situation where many draws fill against a clip path, we'd want to
// draw the clip a single time vs. drawing the path multiple times.
return false;
}
// In order to combine, otherShape must be able to map into `localToDevice` without changing
// shape class (e.g. to a path when rotated) in order for shading to apply in the same
// coordinate space. This is possible if the relative transform between otherToDevice and
// localToDevice is rectStaysRect.
Transform storage{SkM44::kUninitialized_Constructor};
// We track `local` to `other` and use the `inverseMapRect` functions to map the `otherShape`
// into local space when possible. Using `localToOther` instead of `otherToLocal` allows the
// common case of a device-space clip (otherToDevice == I) and an axis-aligned draw to
// simply use `localToDevice` as `localToOther`.
const Transform* localToOther;
if (otherToDevice == localToDevice) {
// No coordinate space conversion, so set to null to signal identity mapping is skippable.
// NOTE: This case arises in clip-clip combinations when both were axis-aligned and pre-
// transformed to device space.
localToOther = nullptr;
} else if (otherToDevice.type() == Transform::Type::kIdentity &&
localToDevice.type() <= Transform::Type::kRectStaysRect) {
// Relative transform is (otherToDevice)^-1*localToDevice = localToDevice
localToOther = &localToDevice;
} else if (otherToDevice.type() <= Transform::Type::kRectStaysRect &&
localToDevice.type() == Transform::Type::kIdentity) {
// Relative transform is otherToDevice^-1*localToDevice = otherToDevice^-1
// (which may not occur in a common scenario but is harmless). Inverse() is mostly
// shuffling bytes around, not recomputing the inverse.
storage = Transform::Inverse(otherToDevice);
localToOther = &storage;
} else {
// Calculate (otherToDevice)^-1*localToDevice and see if the relative transform is
// of the right type.
storage = Transform(otherToDevice.inverse() * localToDevice);
if (storage.type() <= Transform::Type::kRectStaysRect) {
localToOther = &storage;
} else {
// `otherShape` can't be trivially mapped to the local coordinate space
return false;
}
}
// Since `otherShape` is either a rect or a round rect, bounds() is tight to the linear edges.
Rect localOtherRect = otherShape.bounds();
if (localToOther) {
// In this block, `localOtherRect` is defined in the other coord space and `mapped` is in
// the local coord space. At the end of the block, `localOtherRect` is set to `mapped` so
// that afterwards it is always defined in local space.
Rect mapped = localToOther->inverseMapRect(localOtherRect);
// If we don't have enough precision, the other shape might not map back to the geometry.
// Allow up to 1/1000th of a pixel in tolerance when mapping between coordinate spaces,
// otherwise we'll have to clip the shapes independently.
const float otherTol = 0.001f * otherToDevice.localAARadius(localOtherRect);
if (localOtherRect.isEmptyNegativeOrNaN() ||
!localToOther->mapRect(mapped).nearlyEquals(localOtherRect, otherTol)) {
return false;
}
localOtherRect = mapped;
}
// Remember the edges that get clipped by the intersection
SkEnumBitMask<EdgeAAQuad::Flags> clippedEdges = clipped_edges(shape->bounds(), localOtherRect);
if (!shape->isFloodFill()) {
// And now it's tight to the intersection with `shape`, sans any corner rounding
localOtherRect.intersect(shape->bounds());
}
// Make sure that the intersected shape does not become subpixel in size, since drawing a
// subpixel/hairline shape produces a different result than something that's clipped.
float localAARadius = localToDevice.localAARadius(localOtherRect);
if (!std::isfinite(localAARadius) || any(localOtherRect.size() <= localAARadius)) {
return false;
}
SkRRect localOtherRRect;
if (otherShape.isRect()) {
if (shape->isRect() || shape->isFloodFill()) {
SkASSERT(*edgeFlags == EdgeAAQuad::Flags::kAll || !shape->isFloodFill());
// Assuming that non-AA edges seam with non-AA edges other quads to create a uniform
// coverage field, we turn on the AA edge flag when coincident or clipped. This will
// create a nice AA edge from this draw while the other non-AA quad is discarded.
*edgeFlags |= clippedEdges; // This is a no-op if shape was a flood fill
shape->setRect(localOtherRect);
return true;
} else {
// Fall back to rrect+rrect intersection
localOtherRRect = SkRRect::MakeRect(localOtherRect.asSkRect());
}
} else {
SkASSERT(otherShape.isRRect());
if (localToOther) {
if (auto rr = otherShape.rrect().transform(localToOther->inverse().asM33())) {
localOtherRRect = *rr;
} else {
// Transformation produced invalid geometry
return false;
}
} else {
localOtherRRect = otherShape.rrect();
}
if (shape->isRect() && *edgeFlags != EdgeAAQuad::Flags::kAll) {
// When combining a mixed edge AA quad with a rounded rectangle, we require that all
// non-AA edges be clipped out entirely.
if ((clippedEdges | *edgeFlags) != EdgeAAQuad::Flags::kAll) {
// The intersection shows AA'ed round corners and non-AA'ed edges, which can't be
// represented by just Geometry or Shape.
return false;
}
} else if (shape->isFloodFill()) {
SkASSERT(*edgeFlags == EdgeAAQuad::Flags::kAll);
shape->setRRect(localOtherRRect);
return true;
} // Else continue with rrect+rrect intersection
}
// `shape` can only be rect or rrect at this point, flood fill should already have returned.
// If we've made it this far, we've also determined that the edge flags should be set to kAll
// on a successful rrect+rrect intersection.
SkASSERT(shape->isRect() || shape->isRRect());
SkRRect localRRect = SkRRectPriv::ConservativeIntersect(
localOtherRRect,
shape->isRect() ? SkRRect::MakeRect(shape->rect().asSkRect()) : shape->rrect());
if (localRRect.isRect()) {
// Valid shape that can be simplified to rect
shape->setRect(localRRect.rect());
*edgeFlags = EdgeAAQuad::Flags::kAll;
return true;
} else if (!localRRect.isEmpty()) {
// Intersection is representable as a rrect still
shape->setRRect(localRRect);
*edgeFlags = EdgeAAQuad::Flags::kAll;
return true;
} else {
// Intersection is complex, leave edge flags unmodified
return false;
}
}
Rect snap_scissor(const Rect& a, const Rect& deviceBounds) {
// Snapping to 4 pixel boundaries seems to give a good tradeoff between rasterizing slightly
// more (but being clipped by the depth test), vs. setting a tight scissor that forces a state
// change.
// NOTE: This rounds out to the *next* multiple of 4, so that if the input rectangle happens to
// land on a multiple of 4 we still create some padding to avoid scissoring just AA outsets.
static constexpr int kRes = 4;
Rect snapped = a.makeOutset(kRes - 1.f);
snapped = Rect::FromVals(snapped.vals() * (1.f / kRes)).makeRoundOut();
return Rect::FromVals(snapped.vals() * kRes).makeIntersect(deviceBounds);
}
} // 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::kPerspective &&
o.fLocalToDevice.type() != Transform::Type::kPerspective) {
// 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;
}
}
SkUNREACHABLE;
}
ClipStack::DrawInfluence ClipStack::SimplifyForDraw(const TransformedShape& clip,
const TransformedShape& draw) {
// Given the asserts below, we can just recast the SimplifyResult returned from
// Simplify(A=clip, B=draw):
//
// If the result is kEmpty, the draw is clipped out.
static_assert((int) SimplifyResult::kEmpty == (int) DrawInfluence::kClipsOutDraw);
// If the result is kAOnly, only the clip's shape provides coverage and the draw could be
// replaced with something that just covers the clip bounds.
static_assert((int) SimplifyResult::kAOnly == (int) DrawInfluence::kReplacesDraw);
// If the result is kBOnly, the clip's shape doesn't impact the draw's coverage at all.
static_assert((int) SimplifyResult::kBOnly == (int) DrawInfluence::kNone);
// If the result is kBoth, the clip and the draw combine in a complex manner
static_assert((int) SimplifyResult::kBoth == (int) DrawInfluence::kComplexInteraction);
SimplifyResult result = Simplify(clip, draw);
return static_cast<DrawInfluence>(result);
}
///////////////////////////////////////////////////////////////////////////////
// ClipStack::Element
ClipStack::RawElement::RawElement(const Rect& deviceBounds,
const Transform& localToDevice,
const Shape& shape,
SkClipOp op,
PixelSnapping snapping)
: 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.
if (snapping == PixelSnapping::kYes) {
fOuterBounds.round();
}
fShape.setRect(fOuterBounds);
fLocalToDevice = Transform::Identity();
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.
if (auto xformed = fShape.rrect().transform(fLocalToDevice)) {
if (snapping == PixelSnapping::kYes) {
// The rounded corners will still be anti-aliased, but snap the horizontal and
// vertical edges to pixel values.
xformed->setRectRadii(SkRect::Make(xformed->rect().round()),
xformed->radii().data());
}
fShape.setRRect(*xformed);
fLocalToDevice = Transform::Identity();
// 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 ClipStack::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.
const Rect deviceBounds = Rect::WH(device->width(), device->height());
Rect scissor = fUsageBounds; // all joined usage bounds are pre-snapped
// snappedOuterBounds was the rectangle used in updateForDraw() to query the Z order the clip's
// draw will be inserted at. The scissor must enforce that rendering doesn't happen outside of
// those bounds.
Rect snappedOuterBounds = snap_scissor(fOuterBounds, deviceBounds);
scissor.intersect(snappedOuterBounds);
// But if the overlap is sufficiently large, just rasterize out to the snapped bounds instead of
// adding a tight scissor. A factor of 1/2 is used because that corresponds to the area
// change caused by a 45-degree rotation.
if (0.5f * snappedOuterBounds.area() < scissor.area()) {
scissor = snappedOuterBounds;
}
Rect drawBounds = fOp == SkClipOp::kIntersect ? scissor : 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()));
// NOTE: We use fOuterBounds as the transformed shape bounds as that hasn't been clipped by
// the scissor. It has been clipped by the device bounds, but that shouldn't impact any
// decisions at this point. If that becomes not the case, we can either recompute the
// shape's device-space bounds (fLocalToDevice.mapRect(fShape.bounds())) or store a fully
// unclipped shape bounds on the RawElement.
device->drawClipShape(fLocalToDevice,
fShape,
Clip{drawBounds, fOuterBounds, scissor.asSkIRect(),
/* nonMSAAClip= */ {}, /* shader= */ nullptr},
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;
}
// NOTE: intersect_shape operates on the underlying geometry and ignores the fill rule, which
// because these are intersect clip ops, is the inverse fill. If the shape is updated, the
// resulting geometry is set to a regular fill so it must be re-inverted to represent the
// pixels rasterized for a depth-only clip draw.
SkEnumBitMask<EdgeAAQuad::Flags> edgeFlags = EdgeAAQuad::Flags::kAll;
const bool shapeUpdated = intersect_shape(other.fLocalToDevice, other.fShape,
fLocalToDevice, &fShape, &edgeFlags);
SkASSERT(edgeFlags == EdgeAAQuad::Flags::kAll);
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); // Undo intersect_shape setting it 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;
}
}
ClipStack::DrawInfluence ClipStack::RawElement::testForDraw(const TransformedShape& draw) const {
if (this->isInvalid()) {
// Cannot affect the draw
return DrawInfluence::kNone;
}
return SimplifyForDraw(*this, draw);
}
CompressedPaintersOrder ClipStack::RawElement::updateForDraw(const BoundsManager* boundsManager,
const Rect& deviceBounds,
const Rect& drawBounds,
PaintersDepth drawZ) {
SkASSERT(!this->isInvalid());
SkASSERT(!drawBounds.isEmptyNegativeOrNaN());
// Always record snapped draw bounds to avoid scissor thrashing since these bounds will be used
// to determine the scissor applied to the depth-only draw for the clip element.
Rect snappedDrawBounds = snap_scissor(drawBounds, deviceBounds);
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.
Rect snappedOuterBounds = snap_scissor(fOuterBounds, deviceBounds);
fOrder = boundsManager->getMostRecentDraw(snappedOuterBounds).next();
fUsageBounds = snappedDrawBounds;
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(snappedDrawBounds);
if (drawZ > fMaxZ) {
fMaxZ = drawZ;
}
}
return fOrder;
}
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::kArc:
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)
, fGenID(kInvalidGenID) {}
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)
, fGenID(kInvalidGenID) {
// 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);
}
uint32_t ClipStack::SaveRecord::genID() const {
if (fState == ClipState::kEmpty) {
return kEmptyGenID;
} else if (fState == ClipState::kWideOpen) {
return kWideOpenGenID;
} else {
// The gen ID shouldn't be empty or wide open, since they are reserved for the above
// if-cases. It may be kInvalid if the record hasn't had any elements added to it yet.
SkASSERT(fGenID != kEmptyGenID && fGenID != kWideOpenGenID);
return fGenID;
}
}
ClipStack::ClipState ClipStack::SaveRecord::state() const {
if (fShader && fState != ClipState::kEmpty) {
return ClipState::kComplex;
} else {
return fState;
}
}
ClipStack::DrawInfluence ClipStack::SaveRecord::testForDraw(const TransformedShape& draw) const {
Transform identity = Transform::Identity();
Shape outerSaveBounds{fOuterBounds};
TransformedShape save{identity, outerSaveBounds, fOuterBounds, fInnerBounds, fStackOp,
/*containsChecksOnlyBounds=*/true};
return SimplifyForDraw(save, draw);
}
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};
Transform identity = Transform::Identity();
TransformedShape save{identity, 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);
}
// Changing this will prompt ClipStack to invalidate any masks associated with this record.
fGenID = next_gen_id();
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;
fGenID = next_gen_id();
}
///////////////////////////////////////////////////////////////////////////////
// ClipStack::DrawShape
/**
* DrawShape represents the approximate shape that is being drawn in order to compare it against
* the clip stack's RawElements. It is able to map to a `TransformedShape` to be simplified with
* either the SaveRecord or each element. For non-Shape geometries and stroked shapes, it
* represents the oriented bounding box. For filled Shapes, it preserves the original shape for
* more accurate contains/intersect checks and geometrically combining RawElements into the
* shape.
*/
class ClipStack::DrawShape {
public:
DrawShape(const Transform& localToDevice, const Geometry& geometry)
: fLocalToDevice(&localToDevice)
, fEdgeFlags(EdgeAAQuad::Flags::kAll)
, fScissor(Rect::Infinite())
, fShapeWasModified(false) {
if (geometry.isShape()) {
fShape = geometry.shape();
fShapeMatchesGeometry = true;
} 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, so just track
// its bounds. The exception is EdgeAA quads that are rectangular, in which case we can
// clip its edges and adjust edge flags.
fShape.setRect(geometry.bounds());
if (geometry.isEdgeAAQuad()) {
fEdgeFlags = geometry.edgeAAQuad().edgeFlags();
fShapeMatchesGeometry = geometry.edgeAAQuad().isRect();
} else {
fShapeMatchesGeometry = false;
}
// If geometry is not a shape, it is not inverted.
SkASSERT(!fShape.inverted());
}
fShapeCompatibleWithIntersectShape = fShape.isFloodFill() ||
(!fShape.inverted() && (fShape.isRect() ||
fShape.isRRect()));
}
operator TransformedShape() const {
// A regular draw is a transformed shape that "intersects" the clip. An inverse-filled draw
// is equivalent to "difference". For simple convex shapes we provide an inner bounds
// because we can geometrically intersect clip elements with the draw geometry and not
// really impact the choice of Renderer (given the family of renderers used for simple
// shapes). In theory any convex shape could provide an inner bounds and/or use the detailed
// contains check, but that would cause path rendering draws to potentially change in hard
// to predict ways.
SkClipOp op = fShape.inverted() ? SkClipOp::kDifference : SkClipOp::kIntersect;
return TransformedShape{*fLocalToDevice, fShape, fOuterBounds, fInnerBounds, op,
/*containsChecksOnlyBounds=*/!this->shapeCanBeModified()};
}
bool shapeCanBeModified() const {
return fShapeCompatibleWithIntersectShape && fShapeMatchesGeometry;
}
bool applyStyle(const SkStrokeRec& style, const Rect& deviceBounds);
void applyScissor(const Rect& scissor);
void resetToFloodFill();
bool intersectClipElement(const RawElement& clip);
// Sync any modifications back to `geometry` and return a Clip object encapsulating the
// tracked bounds of the now-clipped draw.
Clip toClip(Geometry* geometry, const NonMSAAClip& analyticClip, const SkShader* clipShader);
private:
const Transform* fLocalToDevice;
// When 'style' isn't fill, the original geometry describes the pre-stroked shape, so 'fShape'
// is updated to include the bounds post-stroking. `fShape` may also include local AA outsets
// under certain circumstances:
// 1. If it's a hairline, the AA outset can be added in local space to preserve a tighter
// oriented bbox compared to device bounds outset by 1px.
// 2. If it's subpixel, the rendered geometry is often treated as a hairline with an adjusted
// coverage ramp.
// Notably, the local AA outset is not included in `styledShape` for other cases to maximize the
// cases where a draw is contained in a clip, or can be clipped geometrically. This assumes that
// rendering an AA'ed non-hairline/subpixel edge produces a 1px feathered edge that's not
// qualitatively different from the 1px feathered edge a clip would enforce.
Shape fShape;
SkEnumBitMask<EdgeAAQuad::Flags> fEdgeFlags;
// Not valid until after applyStyle() is called, although applyScissor() can shrink the inner
// and outer bounds.
Rect fTransformedShapeBounds;
Rect fOuterBounds;
Rect fInnerBounds;
Rect fScissor;
// Whether or not the shape matches the original geometry to draw (with style)
bool fShapeMatchesGeometry;
// Whether or not the clip stack can modify this shape in place (and if it has already done so).
bool fShapeCompatibleWithIntersectShape;
bool fShapeWasModified;
};
bool ClipStack::DrawShape::applyStyle(const SkStrokeRec& style, const Rect& deviceBounds) {
// For overriding fLocalToDevice when the shape is only tracking device-space bounds
static const Transform kIdentity = Transform::Identity();
fTransformedShapeBounds = fShape.bounds(); // not scissor'ed, regular fill rule bounds
auto origSize = fTransformedShapeBounds.size();
if (!SkIsFinite(origSize.x(), origSize.y())) {
// Discard all non-finite geometry as if it were clipped out
return false;
}
// Discard fills and strokes that cannot produce any coverage: an empty fill, or a
// zero-length stroke that has butt caps. Otherwise the stroke style applies to a vertical
// or horizontal line (making it non-empty), or it's a zero-length path segment that
// must produce round or square caps (making it non-empty):
// https://www.w3.org/TR/SVG11/implnote.html#PathElementImplementationNotes
if (!fShape.inverted() && (fShape.isLine() || any(origSize == 0.f))) {
if (style.isFillStyle() || (style.getCap() == SkPaint::kButt_Cap && all(origSize == 0.f))) {
return false;
}
}
// Anti-aliasing makes shapes larger than their original coordinates, but we only care about
// that for local clip checks in certain cases (see above).
// NOTE: After this if-else block, `transformedShapeBounds` will be in device space.
float localAAOutset = fLocalToDevice->localAARadius(fTransformedShapeBounds);
if (!SkIsFinite(localAAOutset)) SK_UNLIKELY {
// We cannot calculate an accurate local shape bounds, and transformedShapeBounds is meant
// to be unclipped. This is to maximize atlas reuse for mostly unclipped draws and to detect
// when a scissor state change is required. Setting transformedShapeBounds to deviceBounds
// is harmless in this case as these benefits are unlikely to apply for this transform.
fTransformedShapeBounds = deviceBounds;
fShape.setRect(deviceBounds);
fLocalToDevice = &kIdentity;
fShapeMatchesGeometry = false;
} else {
// SkStrokeRec::GetInflationRadius() returns a device-space inflation for hairlines.
float localOutset = 0.f;
if (!style.isFillStyle() && !style.isHairlineStyle()) {
// Rectangles, rounded rectangles, and lines do not produce miters so don't count the
// pessimistic limit against their draw bounds.
const float effectiveMiterLimit = fShape.isPath() ? style.getMiter() : 1.f;
// Rectangles and rounded rectangles don't have caps, so don't count that against their
// draw bounds (if we could efficiently know a path was a closed contour, it could
// be included here too).
const SkPaint::Cap effectiveCap = fShape.isRect() || fShape.isRRect()
? SkPaint::kButt_Cap : style.getCap();
localOutset = SkStrokeRec::GetInflationRadius(style.getJoin(),
effectiveMiterLimit,
effectiveCap,
style.getWidth());
}
if (style.isHairlineStyle() ||
(!style.isFillStyle() && style.getWidth() < localAAOutset) ||
(style.isFillStyle() && !fShape.inverted() && any(origSize < localAAOutset))) {
// The geometry is a hairline or projects to a subpixel shape, so rendering will not
// follow the typical 1/2px outset anti-aliasing that is compatible with clipping.
// In this case, apply the local AA radius to the shape to have a conservative clip
// query while preserving the oriented bounding box.
localOutset += localAAOutset;
}
if (localOutset > 0.f) {
// Propagate style and AA outset into styledShape so clip queries reflect style.
fTransformedShapeBounds.outset(localOutset);
bool inverted = fShape.inverted();
if (fShape.isRRect()) {
// Try to preserve the rounded corners, which can reduce the chance of clipping
// stroked rounded rects that are clipped to a round rect matching their outer edge.
fShape.rrect().outset(localOutset, localOutset, &fShape.rrect());
} else
{
fShape.setRect(fTransformedShapeBounds); // it's still local at this point
}
fShape.setInverted(inverted); // preserve original inversion state
fShapeMatchesGeometry = false;
}
fTransformedShapeBounds = fLocalToDevice->mapRect(fTransformedShapeBounds);
}
fOuterBounds = fTransformedShapeBounds;
fInnerBounds = Rect::InfiniteInverted();
if (this->shapeCanBeModified() && fLocalToDevice->type() <= Transform::Type::kRectStaysRect) {
if (fShape.isRect()) {
fInnerBounds = fOuterBounds;
} else if (fShape.isRRect()) {
SkRect rrectInnerBounds = SkRRectPriv::InnerBounds(fShape.rrect());
if (!rrectInnerBounds.isEmpty()) {
fInnerBounds = fLocalToDevice->mapRect(rrectInnerBounds);
}
}
// Otherwise it's a flood fill, but should have empty bounds anyways
}
// Otherwise we either don't need the inner bounds, or the inner bounds can't be computed
// for a non-axis-aligned transform
return true; // Something can be drawn based on style (might still be clipped out)
}
void ClipStack::DrawShape::applyScissor(const Rect& scissor) {
// Apply the scissor to the outer bounds because it restricts rasterization and will allow
// the SaveRecord::testForDraw() case to detect no clip influence if only the scissor is
// needed.
SkASSERT(scissor == Rect(scissor.asSkIRect())); // `scissor` must be integer valued
fScissor.intersect(scissor); // For first call, fScissor is infinite so this is assignment
fOuterBounds.intersect(scissor);
fInnerBounds.intersect(scissor);
}
Clip ClipStack::DrawShape::toClip(Geometry* geometry,
const NonMSAAClip& analyticClip,
const SkShader* clipShader) {
if (fShapeWasModified) {
// Sync back to the geometry that will be drawn
SkASSERT(this->shapeCanBeModified());
if (geometry->isEdgeAAQuad() && fShape.isRect()) {
// Preserve the EdgeAAQuad geometry type and sync updated edge flags
SkASSERT(geometry->edgeAAQuad().isRect());
geometry->setEdgeAAQuad(EdgeAAQuad(fShape.rect(), fEdgeFlags));
} else {
SkASSERT(fEdgeFlags == EdgeAAQuad::Flags::kAll);
geometry->setShape(fShape);
}
// Reconstruct new transformedShapeBounds and outer bounds
fTransformedShapeBounds = fLocalToDevice->mapRect(fShape.bounds());
fOuterBounds = fTransformedShapeBounds.makeIntersect(fScissor);
}
Rect drawBounds = fShape.inverted() ? fScissor : fOuterBounds;
// If the draw isn't clipped out (empty drawBounds), it should be in the scissor rect
SkASSERT(drawBounds.isEmptyNegativeOrNaN() || fScissor.contains(drawBounds));
// If the scissor is empty, the draw bounds must also be empty
SkASSERT(!fScissor.isEmptyNegativeOrNaN() || drawBounds.isEmptyNegativeOrNaN());
// fScissor.asSkIRect() must be equivalent
SkASSERT(fScissor == Rect(fScissor.asSkIRect()));
return Clip(drawBounds, fTransformedShapeBounds,
fScissor.asSkIRect(), analyticClip, clipShader);
}
void ClipStack::DrawShape::resetToFloodFill() {
if (this->shapeCanBeModified() && !fShape.isFloodFill()) {
fShape.reset();
fShape.setInverted(true);
fEdgeFlags = EdgeAAQuad::Flags::kAll;
fOuterBounds = fInnerBounds = Rect::InfiniteInverted();
fShapeWasModified = true;
}
}
bool ClipStack::DrawShape::intersectClipElement(const RawElement& clip) {
SkASSERT(clip.op() == SkClipOp::kIntersect);
if (this->shapeCanBeModified() &&
intersect_shape(clip.localToDevice(), clip.shape(),
*fLocalToDevice, &fShape, &fEdgeFlags)) {
SkASSERT(!fShape.inverted());
if (fOuterBounds.isEmptyNegativeOrNaN()) {
// Changing from a flood fill to the clip's shape
fOuterBounds = clip.outerBounds();
fInnerBounds = clip.innerBounds();
} else {
// Restricting the shape's geometry by the clip
fOuterBounds.intersect(clip.outerBounds());
fInnerBounds.intersect(clip.innerBounds());
SkASSERT(!fOuterBounds.isEmptyNegativeOrNaN()); // Should have been caught earlier
}
fShapeWasModified = true;
return true;
}
return false;
}
///////////////////////////////////////////////////////////////////////////////
// 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(b/238763003): Integrating clipShader into graphite needs more thought, particularly 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,
PixelSnapping snapping) {
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, snapping};
// 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();
}
}
}
// Decide whether we can use this shape to do analytic clipping. Only rects and certain
// rrects are supported. We assume these have been pre-transformed by the RawElement
// constructor, so only identity transforms are allowed.
namespace {
AnalyticClip can_apply_analytic_clip(const Shape& shape,
const Transform& localToDevice) {
if (localToDevice.type() != Transform::Type::kIdentity) {
return {};
}
// The circular rrect clip only handles rrect radii >= kRadiusMin.
static constexpr SkScalar kRadiusMin = SK_ScalarHalf;
// Can handle Rect directly.
if (shape.isRect()) {
return {shape.rect(), kRadiusMin, AnalyticClip::kNone_EdgeFlag, shape.inverted()};
}
// Otherwise we only handle certain kinds of RRects.
if (!shape.isRRect()) {
return {};
}
const SkRRect& rrect = shape.rrect();
if (rrect.isOval() || rrect.isSimple()) {
SkVector radii = SkRRectPriv::GetSimpleRadii(rrect);
if (radii.fX < kRadiusMin || radii.fY < kRadiusMin) {
// In this case the corners are extremely close to rectangular and we collapse the
// clip to a rectangular clip.
return {rrect.rect(), kRadiusMin, AnalyticClip::kNone_EdgeFlag, shape.inverted()};
}
if (SkScalarNearlyEqual(radii.fX, radii.fY)) {
return {rrect.rect(), radii.fX, AnalyticClip::kAll_EdgeFlag, shape.inverted()};
} else {
return {};
}
}
if (rrect.isComplex() || rrect.isNinePatch()) {
// Check for the "tab" cases - two adjacent circular corners and two square corners.
constexpr uint32_t kCornerFlags[4] = {
AnalyticClip::kTop_EdgeFlag | AnalyticClip::kLeft_EdgeFlag,
AnalyticClip::kTop_EdgeFlag | AnalyticClip::kRight_EdgeFlag,
AnalyticClip::kBottom_EdgeFlag | AnalyticClip::kRight_EdgeFlag,
AnalyticClip::kBottom_EdgeFlag | AnalyticClip::kLeft_EdgeFlag,
};
SkScalar circularRadius = 0;
uint32_t edgeFlags = 0;
for (int corner = 0; corner < 4; ++corner) {
SkVector radii = rrect.radii((SkRRect::Corner)corner);
// Can only handle circular radii.
// Also applies to corners with both zero and non-zero radii.
if (!SkScalarNearlyEqual(radii.fX, radii.fY)) {
return {};
}
if (radii.fX < kRadiusMin || radii.fY < kRadiusMin) {
// The corner is square, so no need to flag as circular.
continue;
}
// First circular corner seen
if (!edgeFlags) {
circularRadius = radii.fX;
} else if (!SkScalarNearlyEqual(radii.fX, circularRadius)) {
// Radius doesn't match previously seen circular radius
return {};
}
edgeFlags |= kCornerFlags[corner];
}
if (edgeFlags == AnalyticClip::kNone_EdgeFlag) {
// It's a rect
return {rrect.rect(), kRadiusMin, edgeFlags, shape.inverted()};
} else {
// If any rounded corner pairs are non-adjacent or if there are three rounded
// corners all edge flags will be set, which is not valid.
if (edgeFlags == AnalyticClip::kAll_EdgeFlag) {
return {};
// At least one corner is rounded, or two adjacent corners are rounded.
} else {
return {rrect.rect(), circularRadius, edgeFlags, shape.inverted()};
}
}
}
return {};
}
} // anonymous namespace
Clip ClipStack::visitClipStackForDraw(const Transform& localToDevice,
Geometry* geometry,
const SkStrokeRec& style,
bool msaaSupported,
ClipStack::ElementList* outEffectiveElements) const {
static const Clip kClippedOut = {
Rect::InfiniteInverted(), Rect::InfiniteInverted(), SkIRect::MakeEmpty(),
/* nonMSAAClip= */ {}, /* shader= */ nullptr};
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 kClippedOut;
}
// 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();
DrawShape draw{localToDevice, *geometry};
if (!draw.applyStyle(style, deviceBounds)) {
return kClippedOut;
}
// For intersect clips, the scissor rectangle is snapped outer bounds (to loosely restrict
// rasterization if absolutely necessary). Cases where the draw is fully inside the scissor are
// automatically handled during GPU command generation.
//
// For difference clips, a tight scissor could be `subtract(drawBounds, cs.innerBounds())`
// but this is only useful when the clip spans across an axis of the draw and can otherwise
// lead to scissor state thrashing since it's connected to the draw's bounds as well. So just
// use the device bounds for simplicity.
draw.applyScissor(cs.op() == SkClipOp::kIntersect ? snap_scissor(cs.outerBounds(), deviceBounds)
: deviceBounds);
switch (cs.testForDraw(draw)) {
case DrawInfluence::kClipsOutDraw:
// The draw is offscreen or clipped out, so there is no need to visit the clip elements.
return kClippedOut;
case DrawInfluence::kNone:
// The draw is unaffected by the clip stack (except possibly `scissor`), and there's no
// need to visit each clip element.
return draw.toClip(geometry, {}, cs.shader());
case DrawInfluence::kReplacesDraw:
// The draw covers the clip entirely. Replace the shape with a flood fill, which can
// intersect with shapes efficiently.
draw.resetToFloodFill();
[[fallthrough]];
case DrawInfluence::kComplexInteraction:
// Check each element's influence on the draw below
break;
}
SkASSERT(outEffectiveElements);
SkASSERT(outEffectiveElements->empty());
int i = fElements.count();
NonMSAAClip nonMSAAClip;
for (const 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;
}
switch (e.testForDraw(draw)) {
case DrawInfluence::kClipsOutDraw:
// Per-element check was able to completely reject the draw.
outEffectiveElements->clear();
return kClippedOut;
case DrawInfluence::kNone:
// This element does not interact, so continue to the next
continue;
case DrawInfluence::kReplacesDraw:
// This element is covered entirely by the draw, so the draw's geometry can be
// replaced assuming the coordinate spaces are compatible. To facilitate this, we
// switch the drawn geometry to a flood fill and then fall through to intersection.
// Even if the coordinate spaces aren't in alignment, this eliminates the draw's
// source of analytic coverage.
draw.resetToFloodFill();
[[fallthrough]];
case DrawInfluence::kComplexInteraction:
// First try to handle the clip geometrically
if (e.op() == SkClipOp::kIntersect && draw.intersectClipElement(e)) {
continue;
}
// Second try to tighten the scissor, which is lighter weight than adding an
// analytic clip pipeline variation or triggering MSAA.
if (e.clipType() == ClipState::kDeviceRect) {
Rect scissor = e.shape().rect().makeRound();
if (e.shape().rect().nearlyEquals(scissor)) {
// Pass in `scissor` since these need to be integral values while
// nearlyEquals allows the original rect coordinates to be slightly
// different (causing problems later with asSkIRect()).
draw.applyScissor(scissor);
continue;
}
}
// // Third try to handle the clip analytically in the shader
if (nonMSAAClip.fAnalyticClip.isEmpty()) {
nonMSAAClip.fAnalyticClip = can_apply_analytic_clip(e.shape(),
e.localToDevice());
if (!nonMSAAClip.fAnalyticClip.isEmpty()) {
continue;
}
}
// Fourth, remember the element for later, either to be a depth-only draw or to be
// flattened into a clip mask.
// Otherwise, accumulate it for later. Depending on how many elements are collected
// we may use the scissor, analytic clip, or MSAA/atlas.
outEffectiveElements->push_back(&e);
break;
}
}
#if !defined(SK_DISABLE_GRAPHITE_CLIP_ATLAS)
// If there is no MSAA supported, rasterize any remaining elements by flattening them
// into a single mask and storing in an atlas. Otherwise these will be handled by
// Device::drawClip().
AtlasProvider* atlasProvider = fDevice->recorder()->priv().atlasProvider();
if (!msaaSupported && !outEffectiveElements->empty()) {
ClipAtlasManager* clipAtlas = atlasProvider->getClipAtlasManager();
SkASSERT(clipAtlas);
AtlasClip* atlasClip = &nonMSAAClip.fAtlasClip;
SkIRect iMaskBounds = cs.outerBounds().makeRoundOut().asSkIRect();
sk_sp<TextureProxy> proxy = clipAtlas->findOrCreateEntry(cs.genID(),
outEffectiveElements,
iMaskBounds,
&atlasClip->fOutPos);
if (proxy) {
// Add to Clip
atlasClip->fMaskBounds = iMaskBounds;
atlasClip->fAtlasTexture = std::move(proxy);
// Elements are represented in the clip atlas, discard.
outEffectiveElements->clear();
}
}
#endif
return draw.toClip(geometry, nonMSAAClip, cs.shader());
}
CompressedPaintersOrder ClipStack::updateClipStateForDraw(const Clip& clip,
const ElementList& effectiveElements,
const BoundsManager* boundsManager,
PaintersDepth z) {
if (clip.isClippedOut()) {
return DrawOrder::kNoIntersection;
}
SkDEBUGCODE(const SaveRecord& cs = this->currentSaveRecord();)
SkASSERT(cs.state() != ClipState::kEmpty);
Rect deviceBounds = this->deviceBounds();
CompressedPaintersOrder maxClipOrder = DrawOrder::kNoIntersection;
for (int i = 0; i < effectiveElements.size(); ++i) {
// ClipStack owns the elements in the `clipState` so it's OK to downcast and cast away
// const.
// TODO: Enforce the ownership? In debug builds we could invalidate a `ClipStateForDraw` if
// its element pointers become dangling and assert validity here.
const RawElement* e = static_cast<const RawElement*>(effectiveElements[i]);
CompressedPaintersOrder order = const_cast<RawElement*>(e)->updateForDraw(
boundsManager, deviceBounds, clip.drawBounds(), z);
maxClipOrder = std::max(order, maxClipOrder);
}
return 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::graphite