[graphite] Track draw usage per clip element

Bug: skia:12698
Change-Id: I326a4bc34fde675e9fc7ad7835558ce6f307d268
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/527842
Reviewed-by: Greg Daniel <egdaniel@google.com>
Reviewed-by: Jim Van Verth <jvanverth@google.com>
Commit-Queue: Michael Ludwig <michaelludwig@google.com>
This commit is contained in:
Michael Ludwig 2022-04-11 10:25:11 -04:00 committed by SkCQ
parent 7216659f71
commit b1be53d4b3
3 changed files with 195 additions and 14 deletions

View File

@ -1067,5 +1067,6 @@ generated_cc_atom(
"//src/core:SkPathPriv_hdr", "//src/core:SkPathPriv_hdr",
"//src/core:SkRRectPriv_hdr", "//src/core:SkRRectPriv_hdr",
"//src/core:SkRectPriv_hdr", "//src/core:SkRectPriv_hdr",
"//src/core:SkTLazy_hdr",
], ],
) )

View File

@ -16,6 +16,7 @@
#include "src/core/SkPathPriv.h" #include "src/core/SkPathPriv.h"
#include "src/core/SkRRectPriv.h" #include "src/core/SkRRectPriv.h"
#include "src/core/SkRectPriv.h" #include "src/core/SkRectPriv.h"
#include "src/core/SkTLazy.h"
namespace skgpu::graphite { namespace skgpu::graphite {
@ -410,6 +411,76 @@ void ClipStack::RawElement::updateForElement(RawElement* added, const SaveRecord
} }
} }
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 (fOrder == DrawOrder::kNoIntersection) {
// 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 { ClipStack::ClipState ClipStack::RawElement::clipType() const {
// Map from the internal shape kind to the clip state enum // Map from the internal shape kind to the clip state enum
switch (fShape.type()) { switch (fShape.type()) {
@ -473,6 +544,37 @@ ClipStack::ClipState ClipStack::SaveRecord::state() const {
} }
} }
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) { void ClipStack::SaveRecord::removeElements(RawElement::Stack* elements) {
while (elements->count() > fStartingElementIndex) { while (elements->count() > fStartingElementIndex) {
elements->pop_back(); elements->pop_back();
@ -530,7 +632,7 @@ bool ClipStack::SaveRecord::addElement(RawElement&& toAdd, RawElement::Stack* el
// shape is potentially very different from its aggregate outer bounds. // shape is potentially very different from its aggregate outer bounds.
Shape outerSaveBounds{fOuterBounds}; Shape outerSaveBounds{fOuterBounds};
TransformedShape save{kIdentity, outerSaveBounds, fOuterBounds, fInnerBounds, fStackOp, TransformedShape save{kIdentity, outerSaveBounds, fOuterBounds, fInnerBounds, fStackOp,
/*containsChecksBoundsOnly=*/true}; /*containsChecksOnlyBounds=*/true};
// In this invocation, 'A' refers to the existing stack's bounds and 'B' refers to the new // In this invocation, 'A' refers to the existing stack's bounds and 'B' refers to the new
// element. // element.
@ -858,32 +960,95 @@ std::pair<Clip, CompressedPaintersOrder> ClipStack::applyClipToDraw(
const Shape& shape, const Shape& shape,
const SkStrokeRec& style, const SkStrokeRec& style,
PaintersDepth z) { PaintersDepth z) {
// TODO: The Clip's scissor is defined in terms of integer pixel coords, but if we move to const SaveRecord& cs = this->currentSaveRecord();
// clip plane distances in the vertex shader, it can be defined in terms of the original float if (cs.state() == ClipState::kEmpty) {
// coordinates. // We know the draw is clipped out so don't bother computing the base draw bounds.
Rect scissor = this->conservativeBounds().makeRoundOut(); 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{fDeviceBounds};
// 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{shape};
Rect drawBounds = shape.bounds(); Rect drawBounds = shape.bounds();
if (!style.isHairlineStyle()) { if (!style.isHairlineStyle()) {
float localStyleOutset = style.getInflationRadius(); float localStyleOutset = style.getInflationRadius();
drawBounds.outset(localStyleOutset); drawBounds.outset(localStyleOutset);
if (!style.isFillStyle()) {
// While this loses any shape type, the bounds remain local so can be fairly accurate.
styledShape.writable()->setRect(drawBounds);
}
} }
drawBounds = localToDevice.mapRect(drawBounds); drawBounds = localToDevice.mapRect(drawBounds);
// Hairlines get an extra pixel *after* transforming to device space // Hairlines get an extra pixel *after* transforming to device space
if (style.isHairlineStyle()) { if (style.isHairlineStyle()) {
drawBounds.outset(0.5f); drawBounds.outset(0.5f);
// and the associated transform must be kIdentity since drawBounds has been mapped by
// localToDevice already.
styledShape.writable()->setRect(drawBounds);
} }
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); drawBounds.intersect(scissor);
if (drawBounds.isEmptyNegativeOrNaN()) { if (drawBounds.isEmptyNegativeOrNaN() || cs.innerBounds().contains(drawBounds)) {
// Trivially clipped out, so return now // Like above, in both cases drawBounds holds the right value and can return kNoIntersection
return {{drawBounds, scissor.asSkIRect()}, DrawOrder::kNoIntersection}; return {Clip{drawBounds, scissor.asSkIRect()}, DrawOrder::kNoIntersection};
} }
// TODO: iterate the clip stack and accumulate draw bounds into clip usage // If we made it here, the clip stack affects the draw in a complex way so iterate each element.
return {{drawBounds, scissor.asSkIRect()}, DrawOrder::kNoIntersection}; // 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};
} }
} // namespace skgpu::graphite } // namespace skgpu::graphite

View File

@ -160,6 +160,16 @@ private:
// is handled by modifying 'added'. // is handled by modifying 'added'.
void updateForElement(RawElement* added, const SaveRecord& current); void updateForElement(RawElement* added, const SaveRecord& current);
// Updates usage tracking to incorporate the bounds and Z value for the new draw call.
// If this element hasn't affected any prior draws, it will use the bounds manager to
// assign itself a compressed painters order for later rendering.
//
// Returns whether or not this element clips out the draw with more detailed analysis, and
// if not, returns the painters order the draw must sort after.
std::pair<bool, CompressedPaintersOrder> updateForDraw(const BoundsManager* boundsManager,
const TransformedShape& draw,
PaintersDepth drawZ);
void validate() const; void validate() const;
private: private:
@ -179,15 +189,18 @@ private:
// Would need to store both original and complement, since the intersection test is // Would need to store both original and complement, since the intersection test is
// Rect + ComplementRect and Element/SaveRecord could be on either side of operation. // Rect + ComplementRect and Element/SaveRecord could be on either side of operation.
// State tracking how this clip element needs to be recorded into the draw context. As the
// clip stack is applied to additional draws, the clip's Z and usage bounds grow to account
// for it; its compressed painter's order is selected the first time a draw is affected.
Rect fUsageBounds;
CompressedPaintersOrder fOrder;
PaintersDepth fMaxZ;
// Elements are invalidated by SaveRecords as the record is updated with new elements that // Elements are invalidated by SaveRecords as the record is updated with new elements that
// override old geometry. An invalidated element stores the index of the first element of // override old geometry. An invalidated element stores the index of the first element of
// the save record that invalidated it. This makes it easy to undo when the save record is // the save record that invalidated it. This makes it easy to undo when the save record is
// popped from the stack, and is stable as the current save record is modified. // popped from the stack, and is stable as the current save record is modified.
int fInvalidatedByIndex; int fInvalidatedByIndex;
// TODO: Need to store the CompressedPaintersOrder the clip needs to be drawn at, the
// union of the draw bounds it affects to act as its own scissor, and the highest paint Z
// it affects.
}; };
// Represents a saved point in the clip stack, and manages the life time of elements added to // Represents a saved point in the clip stack, and manages the life time of elements added to
@ -211,6 +224,8 @@ private:
int oldestElementIndex() const { return fOldestValidIndex; } int oldestElementIndex() const { return fOldestValidIndex; }
bool canBeUpdated() const { return (fDeferredSaveCount == 0); } bool canBeUpdated() const { return (fDeferredSaveCount == 0); }
Rect scissor(const Rect& deviceBounds, const Rect& drawBounds) const;
// Deferred save manipulation // Deferred save manipulation
void pushSave() { void pushSave() {
SkASSERT(fDeferredSaveCount >= 0); SkASSERT(fDeferredSaveCount >= 0);