/*
* Copyright 2012 Google Inc.
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "GrReducedClip.h"
typedef SkClipStack::Element Element;
static void reduced_stack_walker(const SkClipStack& stack,
const SkRect& queryBounds,
GrReducedClip::ElementList* result,
int32_t* resultGenID,
GrReducedClip::InitialState* initialState,
bool* requiresAA) {
// walk backwards until we get to:
// a) the beginning
// b) an operation that is known to make the bounds all inside/outside
// c) a replace operation
static const GrReducedClip::InitialState kUnknown_InitialState =
static_cast<GrReducedClip::InitialState>(-1);
*initialState = kUnknown_InitialState;
// During our backwards walk, track whether we've seen ops that either grow or shrink the clip.
// TODO: track these per saved clip so that we can consider them on the forward pass.
bool embiggens = false;
bool emsmallens = false;
SkClipStack::Iter iter(stack, SkClipStack::Iter::kTop_IterStart);
int numAAElements = 0;
while ((kUnknown_InitialState == *initialState)) {
const Element* element = iter.prev();
if (nullptr == element) {
*initialState = GrReducedClip::kAllIn_InitialState;
break;
}
if (SkClipStack::kEmptyGenID == element->getGenID()) {
*initialState = GrReducedClip::kAllOut_InitialState;
break;
}
if (SkClipStack::kWideOpenGenID == element->getGenID()) {
*initialState = GrReducedClip::kAllIn_InitialState;
break;
}
bool skippable = false;
bool isFlip = false; // does this op just flip the in/out state of every point in the bounds
switch (element->getOp()) {
case SkRegion::kDifference_Op:
// check if the shape subtracted either contains the entire bounds (and makes
// the clip empty) or is outside the bounds and therefore can be skipped.
if (element->isInverseFilled()) {
if (element->contains(queryBounds)) {
skippable = true;
} else if (!SkRect::Intersects(element->getBounds(), queryBounds)) {
*initialState = GrReducedClip::kAllOut_InitialState;
skippable = true;
}
} else {
if (element->contains(queryBounds)) {
*initialState = GrReducedClip::kAllOut_InitialState;
skippable = true;
} else if (!SkRect::Intersects(element->getBounds(), queryBounds)) {
skippable = true;
}
}
if (!skippable) {
emsmallens = true;
}
break;
case SkRegion::kIntersect_Op:
// check if the shape intersected contains the entire bounds and therefore can
// be skipped or it is outside the entire bounds and therefore makes the clip
// empty.
if (element->isInverseFilled()) {
if (element->contains(queryBounds)) {
*initialState = GrReducedClip::kAllOut_InitialState;
skippable = true;
} else if (!SkRect::Intersects(element->getBounds(), queryBounds)) {
skippable = true;
}
} else {
if (element->contains(queryBounds)) {
skippable = true;
} else if (!SkRect::Intersects(element->getBounds(), queryBounds)) {
*initialState = GrReducedClip::kAllOut_InitialState;
skippable = true;
}
}
if (!skippable) {
emsmallens = true;
}
break;
case SkRegion::kUnion_Op:
// If the union-ed shape contains the entire bounds then after this element
// the bounds is entirely inside the clip. If the union-ed shape is outside the
// bounds then this op can be skipped.
if (element->isInverseFilled()) {
if (element->contains(queryBounds)) {
skippable = true;
} else if (!SkRect::Intersects(element->getBounds(), queryBounds)) {
*initialState = GrReducedClip::kAllIn_InitialState;
skippable = true;
}
} else {
if (element->contains(queryBounds)) {
*initialState = GrReducedClip::kAllIn_InitialState;
skippable = true;
} else if (!SkRect::Intersects(element->getBounds(), queryBounds)) {
skippable = true;
}
}
if (!skippable) {
embiggens = true;
}
break;
case SkRegion::kXOR_Op:
// If the bounds is entirely inside the shape being xor-ed then the effect is
// to flip the inside/outside state of every point in the bounds. We may be
// able to take advantage of this in the forward pass. If the xor-ed shape
// doesn't intersect the bounds then it can be skipped.
if (element->isInverseFilled()) {
if (element->contains(queryBounds)) {
skippable = true;
} else if (!SkRect::Intersects(element->getBounds(), queryBounds)) {
isFlip = true;
}
} else {
if (element->contains(queryBounds)) {
isFlip = true;
} else if (!SkRect::Intersects(element->getBounds(), queryBounds)) {
skippable = true;
}
}
if (!skippable) {
emsmallens = embiggens = true;
}
break;
case SkRegion::kReverseDifference_Op:
// When the bounds is entirely within the rev-diff shape then this behaves like xor
// and reverses every point inside the bounds. If the shape is completely outside
// the bounds then we know after this element is applied that the bounds will be
// all outside the current clip.B
if (element->isInverseFilled()) {
if (element->contains(queryBounds)) {
*initialState = GrReducedClip::kAllOut_InitialState;
skippable = true;
} else if (!SkRect::Intersects(element->getBounds(), queryBounds)) {
isFlip = true;
}
} else {
if (element->contains(queryBounds)) {
isFlip = true;
} else if (!SkRect::Intersects(element->getBounds(), queryBounds)) {
*initialState = GrReducedClip::kAllOut_InitialState;
skippable = true;
}
}
if (!skippable) {
emsmallens = embiggens = true;
}
break;
case SkRegion::kReplace_Op:
// Replace will always terminate our walk. We will either begin the forward walk
// at the replace op or detect here than the shape is either completely inside
// or completely outside the bounds. In this latter case it can be skipped by
// setting the correct value for initialState.
if (element->isInverseFilled()) {
if (element->contains(queryBounds)) {
*initialState = GrReducedClip::kAllOut_InitialState;
skippable = true;
} else if (!SkRect::Intersects(element->getBounds(), queryBounds)) {
*initialState = GrReducedClip::kAllIn_InitialState;
skippable = true;
}
} else {
if (element->contains(queryBounds)) {
*initialState = GrReducedClip::kAllIn_InitialState;
skippable = true;
} else if (!SkRect::Intersects(element->getBounds(), queryBounds)) {
*initialState = GrReducedClip::kAllOut_InitialState;
skippable = true;
}
}
if (!skippable) {
*initialState = GrReducedClip::kAllOut_InitialState;
embiggens = emsmallens = true;
}
break;
default:
SkDEBUGFAIL("Unexpected op.");
break;
}
if (!skippable) {
if (0 == result->count()) {
// This will be the last element. Record the stricter genID.
*resultGenID = element->getGenID();
}
// if it is a flip, change it to a bounds-filling rect
if (isFlip) {
SkASSERT(SkRegion::kXOR_Op == element->getOp() ||
SkRegion::kReverseDifference_Op == element->getOp());
result->addToHead(queryBounds, SkRegion::kReverseDifference_Op, false);
} else {
Element* newElement = result->addToHead(*element);
if (newElement->isAA()) {
++numAAElements;
}
// Intersecting an inverse shape is the same as differencing the non-inverse shape.
// Replacing with an inverse shape is the same as setting initialState=kAllIn and
// differencing the non-inverse shape.
bool isReplace = SkRegion::kReplace_Op == newElement->getOp();
if (newElement->isInverseFilled() &&
(SkRegion::kIntersect_Op == newElement->getOp() || isReplace)) {
newElement->invertShapeFillType();
newElement->setOp(SkRegion::kDifference_Op);
if (isReplace) {
SkASSERT(GrReducedClip::kAllOut_InitialState == *initialState);
*initialState = GrReducedClip::kAllIn_InitialState;
}
}
}
}
}
if ((GrReducedClip::kAllOut_InitialState == *initialState && !embiggens) ||
(GrReducedClip::kAllIn_InitialState == *initialState && !emsmallens)) {
result->reset();
} else {
Element* element = result->headIter().get();
while (element) {
bool skippable = false;
switch (element->getOp()) {
case SkRegion::kDifference_Op:
// subtracting from the empty set yields the empty set.
skippable = GrReducedClip::kAllOut_InitialState == *initialState;
break;
case SkRegion::kIntersect_Op:
// intersecting with the empty set yields the empty set
if (GrReducedClip::kAllOut_InitialState == *initialState) {
skippable = true;
} else {
// We can clear to zero and then simply draw the clip element.
*initialState = GrReducedClip::kAllOut_InitialState;
element->setOp(SkRegion::kReplace_Op);
}
break;
case SkRegion::kUnion_Op:
if (GrReducedClip::kAllIn_InitialState == *initialState) {
// unioning the infinite plane with anything is a no-op.
skippable = true;
} else {
// unioning the empty set with a shape is the shape.
element->setOp(SkRegion::kReplace_Op);
}
break;
case SkRegion::kXOR_Op:
if (GrReducedClip::kAllOut_InitialState == *initialState) {
// xor could be changed to diff in the kAllIn case, not sure it's a win.
element->setOp(SkRegion::kReplace_Op);
}
break;
case SkRegion::kReverseDifference_Op:
if (GrReducedClip::kAllIn_InitialState == *initialState) {
// subtracting the whole plane will yield the empty set.
skippable = true;
*initialState = GrReducedClip::kAllOut_InitialState;
} else {
// this picks up flips inserted in the backwards pass.
skippable = element->isInverseFilled() ?
!SkRect::Intersects(element->getBounds(), queryBounds) :
element->contains(queryBounds);
if (skippable) {
*initialState = GrReducedClip::kAllIn_InitialState;
} else {
element->setOp(SkRegion::kReplace_Op);
}
}
break;
case SkRegion::kReplace_Op:
skippable = false; // we would have skipped it in the backwards walk if we
// could've.
break;
default:
SkDEBUGFAIL("Unexpected op.");
break;
}
if (!skippable) {
break;
} else {
if (element->isAA()) {
--numAAElements;
}
result->popHead();
element = result->headIter().get();
}
}
}
if (requiresAA) {
*requiresAA = numAAElements > 0;
}
if (0 == result->count()) {
if (*initialState == GrReducedClip::kAllIn_InitialState) {
*resultGenID = SkClipStack::kWideOpenGenID;
} else {
*resultGenID = SkClipStack::kEmptyGenID;
}
}
}
/*
There are plenty of optimizations that could be added here. Maybe flips could be folded into
earlier operations. Or would inserting flips and reversing earlier ops ever be a win? Perhaps
for the case where the bounds are kInsideOut_BoundsType. We could restrict earlier operations
based on later intersect operations, and perhaps remove intersect-rects. We could optionally
take a rect in case the caller knows a bound on what is to be drawn through this clip.
*/
void GrReducedClip::ReduceClipStack(const SkClipStack& stack,
const SkIRect& queryBounds,
ElementList* result,
int32_t* resultGenID,
InitialState* initialState,
SkIRect* tighterBounds,
bool* requiresAA) {
result->reset();
// The clip established by the element list might be cached based on the last
// generation id. When we make early returns, we do not know what was the generation
// id that lead to the state. Make a conservative guess.
*resultGenID = stack.getTopmostGenID();
if (stack.isWideOpen()) {
*initialState = kAllIn_InitialState;
return;
}
// We initially look at whether the bounds alone is sufficient. We also use the stack bounds to
// attempt to compute the tighterBounds.
SkClipStack::BoundsType stackBoundsType;
SkRect stackBounds;
bool iior;
stack.getBounds(&stackBounds, &stackBoundsType, &iior);
const SkIRect* bounds = &queryBounds;
SkRect scalarQueryBounds = SkRect::Make(queryBounds);
if (iior) {
SkASSERT(SkClipStack::kNormal_BoundsType == stackBoundsType);
SkRect isectRect;
if (stackBounds.contains(scalarQueryBounds)) {
*initialState = GrReducedClip::kAllIn_InitialState;
if (tighterBounds) {
*tighterBounds = queryBounds;
}
if (requiresAA) {
*requiresAA = false;
}
} else if (isectRect.intersect(stackBounds, scalarQueryBounds)) {
// If the caller asked for tighter integer bounds we may be able to
// return kAllIn and give the bounds with no elements
if (tighterBounds) {
isectRect.roundOut(tighterBounds);
SkRect scalarTighterBounds = SkRect::Make(*tighterBounds);
if (scalarTighterBounds == isectRect) {
// the round-out didn't add any area outside the clip rect.
if (requiresAA) {
*requiresAA = false;
}
*initialState = GrReducedClip::kAllIn_InitialState;
return;
}
}
*initialState = kAllOut_InitialState;
// iior should only be true if aa/non-aa status matches among all elements.
SkClipStack::Iter iter(stack, SkClipStack::Iter::kTop_IterStart);
bool doAA = iter.prev()->isAA();
result->addToHead(isectRect, SkRegion::kReplace_Op, doAA);
if (requiresAA) {
*requiresAA = doAA;
}
} else {
*initialState = kAllOut_InitialState;
if (requiresAA) {
*requiresAA = false;
}
}
return;
} else {
if (SkClipStack::kNormal_BoundsType == stackBoundsType) {
if (!SkRect::Intersects(stackBounds, scalarQueryBounds)) {
*initialState = kAllOut_InitialState;
if (requiresAA) {
*requiresAA = false;
}
return;
}
if (tighterBounds) {
SkIRect stackIBounds;
stackBounds.roundOut(&stackIBounds);
if (!tighterBounds->intersect(queryBounds, stackIBounds)) {
SkASSERT(0);
tighterBounds->setEmpty();
}
bounds = tighterBounds;
}
} else {
if (stackBounds.contains(scalarQueryBounds)) {
*initialState = kAllOut_InitialState;
// We know that the bounding box contains all the pixels that are outside the clip,
// but we don't know that *all* the pixels in the box are outside the clip. So
// proceed to walking the stack.
}
if (tighterBounds) {
*tighterBounds = queryBounds;
}
}
}
SkRect scalarBounds = SkRect::Make(*bounds);
// Now that we have determined the bounds to use and filtered out the trivial cases, call the
// helper that actually walks the stack.
reduced_stack_walker(stack, scalarBounds, result, resultGenID, initialState, requiresAA);
// The list that was computed in this function may be cached based on the gen id of the last
// element.
SkASSERT(SkClipStack::kInvalidGenID != *resultGenID);
}