[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:
parent
7216659f71
commit
b1be53d4b3
@ -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",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -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
|
||||||
|
@ -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);
|
||||||
|
Loading…
Reference in New Issue
Block a user