diff --git a/include/gpu/GrClip.h b/include/gpu/GrClip.h index ab83441f41..1cb1a2bcf6 100644 --- a/include/gpu/GrClip.h +++ b/include/gpu/GrClip.h @@ -113,25 +113,76 @@ public: virtual ~GrClip() {} -protected: /** - * Returns true if a clip can safely disable its scissor test for a particular draw. + * This is the maximum distance that a draw may extend beyond a clip's boundary and still count + * count as "on the other side". We leave some slack because floating point rounding error is + * likely to blame. The rationale for 1e-3 is that in the coverage case (and barring unexpected + * rounding), as long as coverage stays within 0.5 * 1/256 of its intended value it shouldn't + * have any effect on the final pixel values. */ - static bool CanIgnoreScissor(const SkIRect& scissorRect, const SkRect& drawBounds) { - // This is the maximum distance that a draw may extend beyond a clip's scissor and still - // count as inside. We use a sloppy compare because the draw may have chosen its bounds in a - // different coord system. The rationale for 1e-3 is that in the coverage case (and barring - // unexpected rounding), as long as coverage stays below 0.5 * 1/256 we ought to be OK. - constexpr SkScalar fuzz = 1e-3f; - SkASSERT(!scissorRect.isEmpty()); - SkASSERT(!drawBounds.isEmpty()); - return scissorRect.fLeft <= drawBounds.fLeft + fuzz && - scissorRect.fTop <= drawBounds.fTop + fuzz && - scissorRect.fRight >= drawBounds.fRight - fuzz && - scissorRect.fBottom >= drawBounds.fBottom - fuzz; + constexpr static SkScalar kBoundsTolerance = 1e-3f; + + /** + * Returns true if the given query bounds count as entirely inside the clip. + * + * @param innerClipBounds device-space rect contained by the clip (SkRect or SkIRect). + * @param queryBounds device-space bounds of the query region. + */ + template constexpr static bool IsInsideClip(const TRect& innerClipBounds, + const SkRect& queryBounds) { + return innerClipBounds.fRight - innerClipBounds.fLeft >= kBoundsTolerance && + innerClipBounds.fBottom - innerClipBounds.fTop >= kBoundsTolerance && + innerClipBounds.fLeft <= queryBounds.fLeft + kBoundsTolerance && + innerClipBounds.fTop <= queryBounds.fTop + kBoundsTolerance && + innerClipBounds.fRight >= queryBounds.fRight - kBoundsTolerance && + innerClipBounds.fBottom >= queryBounds.fBottom - kBoundsTolerance; } - friend class GrClipMaskManager; + /** + * Returns true if the given query bounds count as entirely outside the clip. + * + * @param outerClipBounds device-space rect that contains the clip (SkRect or SkIRect). + * @param queryBounds device-space bounds of the query region. + */ + template constexpr static bool IsOutsideClip(const TRect& outerClipBounds, + const SkRect& queryBounds) { + return outerClipBounds.fRight - outerClipBounds.fLeft < kBoundsTolerance || + outerClipBounds.fBottom - outerClipBounds.fTop < kBoundsTolerance || + outerClipBounds.fLeft > queryBounds.fRight - kBoundsTolerance || + outerClipBounds.fTop > queryBounds.fBottom - kBoundsTolerance || + outerClipBounds.fRight < queryBounds.fLeft + kBoundsTolerance || + outerClipBounds.fBottom < queryBounds.fTop + kBoundsTolerance; + } + + /** + * Returns the minimal integer rect that counts as containing a given set of bounds. + */ + static SkIRect GetPixelIBounds(const SkRect& bounds) { + return SkIRect::MakeLTRB(SkScalarFloorToInt(bounds.fLeft + kBoundsTolerance), + SkScalarFloorToInt(bounds.fTop + kBoundsTolerance), + SkScalarCeilToInt(bounds.fRight - kBoundsTolerance), + SkScalarCeilToInt(bounds.fBottom - kBoundsTolerance)); + } + + /** + * Returns the minimal pixel-aligned rect that counts as containing a given set of bounds. + */ + static SkRect GetPixelBounds(const SkRect& bounds) { + return SkRect::MakeLTRB(SkScalarFloorToScalar(bounds.fLeft + kBoundsTolerance), + SkScalarFloorToScalar(bounds.fTop + kBoundsTolerance), + SkScalarCeilToScalar(bounds.fRight - kBoundsTolerance), + SkScalarCeilToScalar(bounds.fBottom - kBoundsTolerance)); + } + + /** + * Returns true if the given rect counts as aligned with pixel boundaries. + */ + static bool IsPixelAligned(const SkRect& rect) { + return SkScalarAbs(SkScalarRoundToScalar(rect.fLeft) - rect.fLeft) <= kBoundsTolerance && + SkScalarAbs(SkScalarRoundToScalar(rect.fTop) - rect.fTop) <= kBoundsTolerance && + SkScalarAbs(SkScalarRoundToScalar(rect.fRight) - rect.fRight) <= kBoundsTolerance && + SkScalarAbs(SkScalarRoundToScalar(rect.fBottom) - rect.fBottom) <= kBoundsTolerance; + } }; /** diff --git a/src/gpu/GrClip.cpp b/src/gpu/GrClip.cpp index dc2208ad43..b0c8db3744 100644 --- a/src/gpu/GrClip.cpp +++ b/src/gpu/GrClip.cpp @@ -54,10 +54,10 @@ bool GrFixedClip::apply(GrContext*, SkIRect::MakeWH(drawContext->width(), drawContext->height()))) { return false; } - if (devBounds && !devBounds->intersects(SkRect::Make(tightScissor))) { + if (devBounds && IsOutsideClip(tightScissor, *devBounds)) { return false; } - if (!devBounds || !CanIgnoreScissor(fScissorState.rect(), *devBounds)) { + if (!devBounds || !IsInsideClip(fScissorState.rect(), *devBounds)) { if (fHasStencilClip) { out->makeScissoredStencil(tightScissor, &fDeviceBounds); } else { diff --git a/src/gpu/GrClipMaskManager.cpp b/src/gpu/GrClipMaskManager.cpp index 6e2a303844..67c6f6eac3 100644 --- a/src/gpu/GrClipMaskManager.cpp +++ b/src/gpu/GrClipMaskManager.cpp @@ -26,6 +26,7 @@ #include "effects/GrTextureDomain.h" typedef SkClipStack::Element Element; +typedef GrReducedClip::InitialState InitialState; static const int kMaxAnalyticElements = 4; @@ -146,7 +147,7 @@ bool GrClipMaskManager::UseSWOnlyPath(GrContext* context, static bool get_analytic_clip_processor(const GrReducedClip::ElementList& elements, bool abortIfAA, - SkVector& clipToRTOffset, + const SkVector& clipToRTOffset, const SkRect& drawBounds, sk_sp* resultFP) { SkRect boundsInClipSpace; @@ -237,40 +238,32 @@ bool GrClipMaskManager::SetupClipping(GrContext* context, return true; } + SkRect devBounds = SkRect::MakeIWH(drawContext->width(), drawContext->height()); + if (origDevBounds && !devBounds.intersect(*origDevBounds)) { + return false; + } + + const SkScalar clipX = SkIntToScalar(clip.origin().x()), + clipY = SkIntToScalar(clip.origin().y()); + GrReducedClip::ElementList elements; int32_t genID = 0; - GrReducedClip::InitialState initialState = GrReducedClip::kAllIn_InitialState; SkIRect clipSpaceIBounds; bool requiresAA = false; - SkIRect clipSpaceReduceQueryBounds; - SkRect devBounds; - if (origDevBounds) { - if (!devBounds.intersect(SkRect::MakeIWH(drawContext->width(), drawContext->height()), - *origDevBounds)) { - return false; - } - devBounds.roundOut(&clipSpaceReduceQueryBounds); - clipSpaceReduceQueryBounds.offset(clip.origin()); - } else { - devBounds = SkRect::MakeIWH(drawContext->width(), drawContext->height()); - clipSpaceReduceQueryBounds.setXYWH(0, 0, drawContext->width(), drawContext->height()); - clipSpaceReduceQueryBounds.offset(clip.origin()); - } - GrReducedClip::ReduceClipStack(*clip.clipStack(), - clipSpaceReduceQueryBounds, - &elements, - &genID, - &initialState, - &clipSpaceIBounds, - &requiresAA); + InitialState initialState = GrReducedClip::ReduceClipStack(*clip.clipStack(), + devBounds.makeOffset(clipX, clipY), + &elements, + &genID, + &clipSpaceIBounds, + &requiresAA); if (elements.isEmpty()) { if (GrReducedClip::kAllOut_InitialState == initialState) { return false; } else { SkIRect scissorSpaceIBounds(clipSpaceIBounds); scissorSpaceIBounds.offset(-clip.origin()); - if (!GrClip::CanIgnoreScissor(scissorSpaceIBounds, devBounds)) { + if (!GrClip::IsInsideClip(scissorSpaceIBounds, devBounds)) { out->makeScissored(scissorSpaceIBounds); } return true; @@ -286,8 +279,6 @@ bool GrClipMaskManager::SetupClipping(GrContext* context, // configuration's relative costs of switching RTs to generate a mask vs // longer shaders. if (elements.count() <= kMaxAnalyticElements) { - SkVector clipToRTOffset = { SkIntToScalar(-clip.origin().fX), - SkIntToScalar(-clip.origin().fY) }; // When there are multiple samples we want to do per-sample clipping, not compute a // fractional pixel coverage. bool disallowAnalyticAA = drawContext->isStencilBufferMultisampled(); @@ -299,11 +290,11 @@ bool GrClipMaskManager::SetupClipping(GrContext* context, } sk_sp clipFP; if (requiresAA && - get_analytic_clip_processor(elements, disallowAnalyticAA, clipToRTOffset, devBounds, + get_analytic_clip_processor(elements, disallowAnalyticAA, {-clipX, -clipY}, devBounds, &clipFP)) { SkIRect scissorSpaceIBounds(clipSpaceIBounds); scissorSpaceIBounds.offset(-clip.origin()); - if (GrClip::CanIgnoreScissor(scissorSpaceIBounds, devBounds)) { + if (GrClip::IsInsideClip(scissorSpaceIBounds, devBounds)) { out->makeFPBased(std::move(clipFP), SkRect::Make(scissorSpaceIBounds)); } else { out->makeScissoredFPBased(std::move(clipFP), scissorSpaceIBounds); @@ -370,7 +361,7 @@ bool GrClipMaskManager::SetupClipping(GrContext* context, // use both stencil and scissor test to the bounds for the final draw. SkIRect scissorSpaceIBounds(clipSpaceIBounds); scissorSpaceIBounds.offset(clipSpaceToStencilSpaceOffset); - if (GrClip::CanIgnoreScissor(scissorSpaceIBounds, devBounds)) { + if (GrClip::IsInsideClip(scissorSpaceIBounds, devBounds)) { out->makeStencil(true, devBounds); } else { out->makeScissoredStencil(scissorSpaceIBounds); diff --git a/src/gpu/GrReducedClip.cpp b/src/gpu/GrReducedClip.cpp index 26b8936952..2940f6a682 100644 --- a/src/gpu/GrReducedClip.cpp +++ b/src/gpu/GrReducedClip.cpp @@ -7,14 +7,16 @@ #include "GrReducedClip.h" +#include "GrClip.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) { +static GrReducedClip::InitialState reduced_stack_walker(const SkClipStack& stack, + const SkRect& queryBounds, + const SkIRect& clipIBounds, + GrReducedClip::ElementList* result, + int32_t* resultGenID, + bool* requiresAA) { // walk backwards until we get to: // a) the beginning @@ -23,27 +25,32 @@ static void reduced_stack_walker(const SkClipStack& stack, static const GrReducedClip::InitialState kUnknown_InitialState = static_cast(-1); - *initialState = kUnknown_InitialState; + GrReducedClip::InitialState 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; + // We use a slightly relaxed set of query bounds for element containment tests. This is to + // account for floating point rounding error that may have occurred during coord transforms. + SkRect relaxedQueryBounds = queryBounds.makeInset(GrClip::kBoundsTolerance, + GrClip::kBoundsTolerance); + SkClipStack::Iter iter(stack, SkClipStack::Iter::kTop_IterStart); int numAAElements = 0; - while ((kUnknown_InitialState == *initialState)) { + while (kUnknown_InitialState == initialState) { const Element* element = iter.prev(); if (nullptr == element) { - *initialState = GrReducedClip::kAllIn_InitialState; + initialState = GrReducedClip::kAllIn_InitialState; break; } if (SkClipStack::kEmptyGenID == element->getGenID()) { - *initialState = GrReducedClip::kAllOut_InitialState; + initialState = GrReducedClip::kAllOut_InitialState; break; } if (SkClipStack::kWideOpenGenID == element->getGenID()) { - *initialState = GrReducedClip::kAllIn_InitialState; + initialState = GrReducedClip::kAllIn_InitialState; break; } @@ -55,17 +62,17 @@ static void reduced_stack_walker(const SkClipStack& stack, // 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)) { + if (element->contains(relaxedQueryBounds)) { skippable = true; - } else if (!SkRect::Intersects(element->getBounds(), queryBounds)) { - *initialState = GrReducedClip::kAllOut_InitialState; + } else if (GrClip::IsOutsideClip(element->getBounds(), queryBounds)) { + initialState = GrReducedClip::kAllOut_InitialState; skippable = true; } } else { - if (element->contains(queryBounds)) { - *initialState = GrReducedClip::kAllOut_InitialState; + if (element->contains(relaxedQueryBounds)) { + initialState = GrReducedClip::kAllOut_InitialState; skippable = true; - } else if (!SkRect::Intersects(element->getBounds(), queryBounds)) { + } else if (GrClip::IsOutsideClip(element->getBounds(), queryBounds)) { skippable = true; } } @@ -78,17 +85,17 @@ static void reduced_stack_walker(const SkClipStack& stack, // 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; + if (element->contains(relaxedQueryBounds)) { + initialState = GrReducedClip::kAllOut_InitialState; skippable = true; - } else if (!SkRect::Intersects(element->getBounds(), queryBounds)) { + } else if (GrClip::IsOutsideClip(element->getBounds(), queryBounds)) { skippable = true; } } else { - if (element->contains(queryBounds)) { + if (element->contains(relaxedQueryBounds)) { skippable = true; - } else if (!SkRect::Intersects(element->getBounds(), queryBounds)) { - *initialState = GrReducedClip::kAllOut_InitialState; + } else if (GrClip::IsOutsideClip(element->getBounds(), queryBounds)) { + initialState = GrReducedClip::kAllOut_InitialState; skippable = true; } } @@ -101,17 +108,17 @@ static void reduced_stack_walker(const SkClipStack& stack, // 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)) { + if (element->contains(relaxedQueryBounds)) { skippable = true; - } else if (!SkRect::Intersects(element->getBounds(), queryBounds)) { - *initialState = GrReducedClip::kAllIn_InitialState; + } else if (GrClip::IsOutsideClip(element->getBounds(), queryBounds)) { + initialState = GrReducedClip::kAllIn_InitialState; skippable = true; } } else { - if (element->contains(queryBounds)) { - *initialState = GrReducedClip::kAllIn_InitialState; + if (element->contains(relaxedQueryBounds)) { + initialState = GrReducedClip::kAllIn_InitialState; skippable = true; - } else if (!SkRect::Intersects(element->getBounds(), queryBounds)) { + } else if (GrClip::IsOutsideClip(element->getBounds(), queryBounds)) { skippable = true; } } @@ -125,15 +132,15 @@ static void reduced_stack_walker(const SkClipStack& stack, // 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)) { + if (element->contains(relaxedQueryBounds)) { skippable = true; - } else if (!SkRect::Intersects(element->getBounds(), queryBounds)) { + } else if (GrClip::IsOutsideClip(element->getBounds(), queryBounds)) { isFlip = true; } } else { - if (element->contains(queryBounds)) { + if (element->contains(relaxedQueryBounds)) { isFlip = true; - } else if (!SkRect::Intersects(element->getBounds(), queryBounds)) { + } else if (GrClip::IsOutsideClip(element->getBounds(), queryBounds)) { skippable = true; } } @@ -147,17 +154,17 @@ static void reduced_stack_walker(const SkClipStack& stack, // 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; + if (element->contains(relaxedQueryBounds)) { + initialState = GrReducedClip::kAllOut_InitialState; skippable = true; - } else if (!SkRect::Intersects(element->getBounds(), queryBounds)) { + } else if (GrClip::IsOutsideClip(element->getBounds(), queryBounds)) { isFlip = true; } } else { - if (element->contains(queryBounds)) { + if (element->contains(relaxedQueryBounds)) { isFlip = true; - } else if (!SkRect::Intersects(element->getBounds(), queryBounds)) { - *initialState = GrReducedClip::kAllOut_InitialState; + } else if (GrClip::IsOutsideClip(element->getBounds(), queryBounds)) { + initialState = GrReducedClip::kAllOut_InitialState; skippable = true; } } @@ -165,30 +172,31 @@ static void reduced_stack_walker(const SkClipStack& stack, 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; + if (element->contains(relaxedQueryBounds)) { + initialState = GrReducedClip::kAllOut_InitialState; skippable = true; - } else if (!SkRect::Intersects(element->getBounds(), queryBounds)) { - *initialState = GrReducedClip::kAllIn_InitialState; + } else if (GrClip::IsOutsideClip(element->getBounds(), queryBounds)) { + initialState = GrReducedClip::kAllIn_InitialState; skippable = true; } } else { - if (element->contains(queryBounds)) { - *initialState = GrReducedClip::kAllIn_InitialState; + if (element->contains(relaxedQueryBounds)) { + initialState = GrReducedClip::kAllIn_InitialState; skippable = true; - } else if (!SkRect::Intersects(element->getBounds(), queryBounds)) { - *initialState = GrReducedClip::kAllOut_InitialState; + } else if (GrClip::IsOutsideClip(element->getBounds(), queryBounds)) { + initialState = GrReducedClip::kAllOut_InitialState; skippable = true; } } if (!skippable) { - *initialState = GrReducedClip::kAllOut_InitialState; + initialState = GrReducedClip::kAllOut_InitialState; embiggens = emsmallens = true; } break; @@ -206,7 +214,8 @@ static void reduced_stack_walker(const SkClipStack& stack, if (isFlip) { SkASSERT(SkRegion::kXOR_Op == element->getOp() || SkRegion::kReverseDifference_Op == element->getOp()); - result->addToHead(queryBounds, SkRegion::kReverseDifference_Op, false); + result->addToHead(SkRect::Make(clipIBounds), SkRegion::kReverseDifference_Op, + false); } else { Element* newElement = result->addToHead(*element); if (newElement->isAA()) { @@ -221,17 +230,18 @@ static void reduced_stack_walker(const SkClipStack& stack, newElement->invertShapeFillType(); newElement->setOp(SkRegion::kDifference_Op); if (isReplace) { - SkASSERT(GrReducedClip::kAllOut_InitialState == *initialState); - *initialState = GrReducedClip::kAllIn_InitialState; + SkASSERT(GrReducedClip::kAllOut_InitialState == initialState); + initialState = GrReducedClip::kAllIn_InitialState; } } } } } - if ((GrReducedClip::kAllOut_InitialState == *initialState && !embiggens) || - (GrReducedClip::kAllIn_InitialState == *initialState && !emsmallens)) { + if ((GrReducedClip::kAllOut_InitialState == initialState && !embiggens) || + (GrReducedClip::kAllIn_InitialState == initialState && !emsmallens)) { result->reset(); + numAAElements = 0; } else { Element* element = result->headIter().get(); while (element) { @@ -239,20 +249,20 @@ static void reduced_stack_walker(const SkClipStack& stack, switch (element->getOp()) { case SkRegion::kDifference_Op: // subtracting from the empty set yields the empty set. - skippable = GrReducedClip::kAllOut_InitialState == *initialState; + skippable = GrReducedClip::kAllOut_InitialState == initialState; break; case SkRegion::kIntersect_Op: // intersecting with the empty set yields the empty set - if (GrReducedClip::kAllOut_InitialState == *initialState) { + if (GrReducedClip::kAllOut_InitialState == initialState) { skippable = true; } else { // We can clear to zero and then simply draw the clip element. - *initialState = GrReducedClip::kAllOut_InitialState; + initialState = GrReducedClip::kAllOut_InitialState; element->setOp(SkRegion::kReplace_Op); } break; case SkRegion::kUnion_Op: - if (GrReducedClip::kAllIn_InitialState == *initialState) { + if (GrReducedClip::kAllIn_InitialState == initialState) { // unioning the infinite plane with anything is a no-op. skippable = true; } else { @@ -261,23 +271,23 @@ static void reduced_stack_walker(const SkClipStack& stack, } break; case SkRegion::kXOR_Op: - if (GrReducedClip::kAllOut_InitialState == *initialState) { + 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) { + if (GrReducedClip::kAllIn_InitialState == initialState) { // subtracting the whole plane will yield the empty set. skippable = true; - *initialState = GrReducedClip::kAllOut_InitialState; + 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); + GrClip::IsOutsideClip(element->getBounds(), queryBounds) : + element->contains(relaxedQueryBounds); if (skippable) { - *initialState = GrReducedClip::kAllIn_InitialState; + initialState = GrReducedClip::kAllIn_InitialState; } else { element->setOp(SkRegion::kReplace_Op); } @@ -305,12 +315,15 @@ static void reduced_stack_walker(const SkClipStack& stack, *requiresAA = numAAElements > 0; if (0 == result->count()) { - if (*initialState == GrReducedClip::kAllIn_InitialState) { + if (initialState == GrReducedClip::kAllIn_InitialState) { *resultGenID = SkClipStack::kWideOpenGenID; } else { *resultGenID = SkClipStack::kEmptyGenID; } } + + SkASSERT(SkClipStack::kInvalidGenID != *resultGenID); + return initialState; } /* @@ -320,15 +333,13 @@ for the case where the bounds are kInsideOut_BoundsType. We could restrict earli 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) { - SkASSERT(tighterBounds); - SkASSERT(requiresAA); +GrReducedClip::InitialState GrReducedClip::ReduceClipStack(const SkClipStack& stack, + const SkRect& queryBounds, + ElementList* result, + int32_t* resultGenID, + SkIRect* clipIBounds, + bool* requiresAA) { + SkASSERT(!queryBounds.isEmpty()); result->reset(); // The clip established by the element list might be cached based on the last @@ -336,85 +347,55 @@ void GrReducedClip::ReduceClipStack(const SkClipStack& stack, // id that lead to the state. Make a conservative guess. *resultGenID = stack.getTopmostGenID(); + // TODO: instead devise a way of telling the caller to disregard some or all of the clip bounds. + *clipIBounds = GrClip::GetPixelIBounds(queryBounds); + if (stack.isWideOpen()) { - *initialState = kAllIn_InitialState; - return; + return kAllIn_InitialState; } - - // 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; - *tighterBounds = queryBounds; - *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 - isectRect.roundOut(tighterBounds); - SkRect scalarTighterBounds = SkRect::Make(*tighterBounds); - if (scalarTighterBounds == isectRect) { - // the round-out didn't add any area outside the clip rect. - *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); - *requiresAA = doAA; - } else { - *initialState = kAllOut_InitialState; - *requiresAA = false; - } - return; - } else { - if (SkClipStack::kNormal_BoundsType == stackBoundsType) { - if (!SkRect::Intersects(stackBounds, scalarQueryBounds)) { - *initialState = kAllOut_InitialState; - *requiresAA = false; - return; - } - 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. - } - *tighterBounds = queryBounds; - } + if (stackBounds.isEmpty() || GrClip::IsOutsideClip(stackBounds, queryBounds)) { + bool insideOut = SkClipStack::kInsideOut_BoundsType == stackBoundsType; + return insideOut ? kAllIn_InitialState : kAllOut_InitialState; } - SkRect scalarBounds = SkRect::Make(*bounds); + if (iior) { + // "Is intersection of rects" means the clip is a single rect indicated by the stack bounds. + // This should only be true if aa/non-aa status matches among all elements. + SkASSERT(SkClipStack::kNormal_BoundsType == stackBoundsType); + SkClipStack::Iter iter(stack, SkClipStack::Iter::kTop_IterStart); + if (!iter.prev()->isAA() || GrClip::IsPixelAligned(stackBounds)) { + // The clip is a non-aa rect. This is the one spot where we can actually implement the + // clip (using clipIBounds) rather than just telling the caller what it should be. + stackBounds.round(clipIBounds); + return kAllIn_InitialState; + } + if (GrClip::IsInsideClip(stackBounds, queryBounds)) { + return kAllIn_InitialState; + } + + // Implement the clip with an AA rect element. + result->addToHead(stackBounds, SkRegion::kReplace_Op, true/*doAA*/); + *requiresAA = true; + + SkAssertResult(clipIBounds->intersect(GrClip::GetPixelIBounds(stackBounds))); + return kAllOut_InitialState; + } + + SkRect tighterQuery = queryBounds; + if (SkClipStack::kNormal_BoundsType == stackBoundsType) { + // Tighten the query by introducing a new clip at the stack's pixel boundaries. (This new + // clip will be enforced by the scissor through clipIBounds.) + SkAssertResult(tighterQuery.intersect(GrClip::GetPixelBounds(stackBounds))); + *clipIBounds = GrClip::GetPixelIBounds(tighterQuery); + } // 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); + return reduced_stack_walker(stack, tighterQuery, *clipIBounds, result, resultGenID, requiresAA); } diff --git a/src/gpu/GrReducedClip.h b/src/gpu/GrReducedClip.h index da0bae6bfc..780f6f1ff5 100644 --- a/src/gpu/GrReducedClip.h +++ b/src/gpu/GrReducedClip.h @@ -21,24 +21,27 @@ public: }; /** - * This function takes a clip stack and a query rectangle and it produces a - * reduced set of SkClipStack::Elements that are equivalent to applying the - * full stack to the rectangle. The clip stack generation id that represents - * the list of elements is returned in resultGenID. The initial state of the - * query rectangle before the first clip element is applied is returned via - * initialState. The reducer output tighterBounds is a tighter bounds on the - * clip. tighterBounds will always be contained by queryBounds after return. - * It is assumed that the caller will not draw outside of tighterBounds. - * The requiresAA output will indicate whether anti-aliasing is required to - * process any of the elements in the element list result. + * This function produces a reduced set of SkClipStack::Elements that are equivalent to applying + * a full clip stack within a specified query rectangle. + * + * @param stack the clip stack to reduce. + * @param queryBounds bounding box of geometry the stack will clip. + * @param result populated with a minimal list of elements that implement the clip + * within the provided query bounds. + * @param resultGenID uniquely identifies the resulting reduced clip. + * @param clipIBounds bounding box within which the reduced clip is valid. The caller must + * not draw any pixels outside this box. NOTE: this box may be undefined + * if no pixels are valid (e.g. empty result, "all out" initial state.) + * @param requiresAA indicates whether anti-aliasing is required to process any of the + * elements in the element list result. Undefined if the result is empty. + * @return the initial clip state within clipIBounds ("all in" or "all out"). */ - static void ReduceClipStack(const SkClipStack& stack, - const SkIRect& queryBounds, - ElementList* result, - int32_t* resultGenID, - InitialState* initialState, - SkIRect* tighterBounds, - bool* requiresAA); + static InitialState ReduceClipStack(const SkClipStack& stack, + const SkRect& queryBounds, + ElementList* result, + int32_t* resultGenID, + SkIRect* clipIBounds, + bool* requiresAA); }; #endif diff --git a/src/gpu/gl/GrGLIRect.h b/src/gpu/gl/GrGLIRect.h index 44f5280fd6..41ac13b753 100644 --- a/src/gpu/gl/GrGLIRect.h +++ b/src/gpu/gl/GrGLIRect.h @@ -54,9 +54,7 @@ struct GrGLIRect { } fHeight = height; - SkASSERT(fLeft >= 0); SkASSERT(fWidth >= 0); - SkASSERT(fBottom >= 0); SkASSERT(fHeight >= 0); } diff --git a/src/utils/SkLua.cpp b/src/utils/SkLua.cpp index d2ff5551af..a3a1685eae 100644 --- a/src/utils/SkLua.cpp +++ b/src/utils/SkLua.cpp @@ -638,12 +638,12 @@ static int lcanvas_getClipStack(lua_State* L) { int SkLua::lcanvas_getReducedClipStack(lua_State* L) { #if SK_SUPPORT_GPU const SkCanvas* canvas = get_ref(L, 1); - SkIRect queryBounds = canvas->getTopLayerBounds(); + SkRect queryBounds = SkRect::Make(canvas->getTopLayerBounds()); GrReducedClip::ElementList elements; - GrReducedClip::InitialState initialState; int32_t genID; SkIRect resultBounds; + bool requiresAA; const SkClipStack& stack = *canvas->getClipStack(); @@ -651,9 +651,8 @@ int SkLua::lcanvas_getReducedClipStack(lua_State* L) { queryBounds, &elements, &genID, - &initialState, &resultBounds, - nullptr); + &requiresAA); GrReducedClip::ElementList::Iter iter(elements); int i = 0; diff --git a/tests/ClipStackTest.cpp b/tests/ClipStackTest.cpp index 4d375ee8bc..f6edb8ce5b 100644 --- a/tests/ClipStackTest.cpp +++ b/tests/ClipStackTest.cpp @@ -6,15 +6,18 @@ */ #include "Test.h" -#if SK_SUPPORT_GPU - #include "GrReducedClip.h" -#endif #include "SkClipStack.h" #include "SkPath.h" #include "SkRandom.h" #include "SkRect.h" #include "SkRegion.h" +#if SK_SUPPORT_GPU +#include "GrReducedClip.h" +typedef GrReducedClip::ElementList ElementList; +typedef GrReducedClip::InitialState InitialState; +#endif + static void test_assign_and_comparison(skiatest::Reporter* reporter) { SkClipStack s; bool doAA = false; @@ -849,41 +852,45 @@ static void test_invfill_diff_bug(skiatest::Reporter* reporter) { typedef void (*AddElementFunc) (const SkRect& rect, bool invert, SkRegion::Op op, - SkClipStack* stack); + SkClipStack* stack, + bool doAA); -static void add_round_rect(const SkRect& rect, bool invert, SkRegion::Op op, SkClipStack* stack) { +static void add_round_rect(const SkRect& rect, bool invert, SkRegion::Op op, SkClipStack* stack, + bool doAA) { SkScalar rx = rect.width() / 10; SkScalar ry = rect.height() / 20; if (invert) { SkPath path; path.addRoundRect(rect, rx, ry); path.setFillType(SkPath::kInverseWinding_FillType); - stack->clipDevPath(path, op, false); + stack->clipDevPath(path, op, doAA); } else { SkRRect rrect; rrect.setRectXY(rect, rx, ry); - stack->clipDevRRect(rrect, op, false); + stack->clipDevRRect(rrect, op, doAA); } }; -static void add_rect(const SkRect& rect, bool invert, SkRegion::Op op, SkClipStack* stack) { +static void add_rect(const SkRect& rect, bool invert, SkRegion::Op op, SkClipStack* stack, + bool doAA) { if (invert) { SkPath path; path.addRect(rect); path.setFillType(SkPath::kInverseWinding_FillType); - stack->clipDevPath(path, op, false); + stack->clipDevPath(path, op, doAA); } else { - stack->clipDevRect(rect, op, false); + stack->clipDevRect(rect, op, doAA); } }; -static void add_oval(const SkRect& rect, bool invert, SkRegion::Op op, SkClipStack* stack) { +static void add_oval(const SkRect& rect, bool invert, SkRegion::Op op, SkClipStack* stack, + bool doAA) { SkPath path; path.addOval(rect); if (invert) { path.setFillType(SkPath::kInverseWinding_FillType); } - stack->clipDevPath(path, op, false); + stack->clipDevPath(path, op, doAA); }; static void add_elem_to_stack(const SkClipStack::Element& element, SkClipStack* stack) { @@ -912,7 +919,7 @@ static void test_reduced_clip_stack(skiatest::Reporter* reporter) { static const SkRect kBounds = SkRect::MakeWH(100, 100); enum { - kNumTests = 200, + kNumTests = 250, kMinElemsPerTest = 1, kMaxElemsPerTest = 50, }; @@ -938,6 +945,8 @@ static void test_reduced_clip_stack(skiatest::Reporter* reporter) { // We want to test inverse fills. However, they are quite rare in practice so don't over do it. static const SkScalar kFractionInverted = SK_Scalar1 / kMaxElemsPerTest; + static const SkScalar kFractionAntialiased = 0.25; + static const AddElementFunc kElementFuncs[] = { add_rect, add_round_rect, @@ -947,9 +956,13 @@ static void test_reduced_clip_stack(skiatest::Reporter* reporter) { SkRandom r; for (int i = 0; i < kNumTests; ++i) { + SkString testCase; + testCase.printf("Iteration %d", i); + // Randomly generate a clip stack. SkClipStack stack; int numElems = r.nextRangeU(kMinElemsPerTest, kMaxElemsPerTest); + bool doAA = r.nextBiasedBool(kFractionAntialiased); for (int e = 0; e < numElems; ++e) { SkRegion::Op op = kOps[r.nextULessThan(SK_ARRAY_COUNT(kOps))]; if (op == SkRegion::kReplace_Op) { @@ -963,43 +976,66 @@ static void test_reduced_clip_stack(skiatest::Reporter* reporter) { bool doSave = r.nextBool(); SkSize size = SkSize::Make( - SkScalarFloorToScalar(SkScalarMul(kBounds.width(), r.nextRangeScalar(kMinElemSizeFrac, kMaxElemSizeFrac))), - SkScalarFloorToScalar(SkScalarMul(kBounds.height(), r.nextRangeScalar(kMinElemSizeFrac, kMaxElemSizeFrac)))); + SkScalarMul(kBounds.width(), r.nextRangeScalar(kMinElemSizeFrac, kMaxElemSizeFrac)), + SkScalarMul(kBounds.height(), r.nextRangeScalar(kMinElemSizeFrac, kMaxElemSizeFrac))); - SkPoint xy = {SkScalarFloorToScalar(r.nextRangeScalar(kBounds.fLeft, kBounds.fRight - size.fWidth)), - SkScalarFloorToScalar(r.nextRangeScalar(kBounds.fTop, kBounds.fBottom - size.fHeight))}; + SkPoint xy = {r.nextRangeScalar(kBounds.fLeft, kBounds.fRight - size.fWidth), + r.nextRangeScalar(kBounds.fTop, kBounds.fBottom - size.fHeight)}; - SkRect rect = SkRect::MakeXYWH(xy.fX, xy.fY, size.fWidth, size.fHeight); + SkRect rect; + if (doAA) { + rect.setXYWH(xy.fX, xy.fY, size.fWidth, size.fHeight); + if (GrClip::IsPixelAligned(rect)) { + // Don't create an element that may accidentally become not antialiased. + rect.outset(0.5f, 0.5f); + } + SkASSERT(!GrClip::IsPixelAligned(rect)); + } else { + rect.setXYWH(SkScalarFloorToScalar(xy.fX), + SkScalarFloorToScalar(xy.fY), + SkScalarCeilToScalar(size.fWidth), + SkScalarCeilToScalar(size.fHeight)); + } bool invert = r.nextBiasedBool(kFractionInverted); - kElementFuncs[r.nextULessThan(SK_ARRAY_COUNT(kElementFuncs))](rect, invert, op, &stack); + kElementFuncs[r.nextULessThan(SK_ARRAY_COUNT(kElementFuncs))](rect, invert, op, &stack, + doAA); if (doSave) { stack.save(); } } - SkRect inflatedBounds = kBounds; - inflatedBounds.outset(kBounds.width() / 2, kBounds.height() / 2); - SkIRect inflatedIBounds; - inflatedBounds.roundOut(&inflatedIBounds); + SkRect queryBounds = kBounds; + queryBounds.outset(kBounds.width() / 2, kBounds.height() / 2); - typedef GrReducedClip::ElementList ElementList; // Get the reduced version of the stack. ElementList reducedClips; int32_t reducedGenID; - GrReducedClip::InitialState initial; - SkIRect tighterBounds; + SkIRect clipIBounds; bool requiresAA; - GrReducedClip::ReduceClipStack(stack, - inflatedIBounds, - &reducedClips, - &reducedGenID, - &initial, - &tighterBounds, - &requiresAA); + InitialState initial = GrReducedClip::ReduceClipStack(stack, + queryBounds, + &reducedClips, + &reducedGenID, + &clipIBounds, + &requiresAA); - REPORTER_ASSERT(reporter, SkClipStack::kInvalidGenID != reducedGenID); + REPORTER_ASSERT_MESSAGE(reporter, SkClipStack::kInvalidGenID != reducedGenID, + testCase.c_str()); + + if (!reducedClips.isEmpty()) { + SkRect stackBounds; + SkClipStack::BoundsType stackBoundsType; + stack.getBounds(&stackBounds, &stackBoundsType); + if (SkClipStack::kNormal_BoundsType == stackBoundsType) { + // Unless GrReducedClip starts doing some heroic tightening of the clip bounds, this + // will be true since the stack bounds are completely contained inside the query. + REPORTER_ASSERT_MESSAGE(reporter, GrClip::IsInsideClip(clipIBounds, stackBounds), + testCase.c_str()); + } + REPORTER_ASSERT_MESSAGE(reporter, requiresAA == doAA, testCase.c_str()); + } // Build a new clip stack based on the reduced clip elements SkClipStack reducedStack; @@ -1012,18 +1048,16 @@ static void test_reduced_clip_stack(skiatest::Reporter* reporter) { } // GrReducedClipStack assumes that the final result is clipped to the returned bounds - reducedStack.clipDevRect(tighterBounds, SkRegion::kIntersect_Op); - stack.clipDevRect(tighterBounds, SkRegion::kIntersect_Op); + reducedStack.clipDevRect(clipIBounds, SkRegion::kIntersect_Op); + stack.clipDevRect(clipIBounds, SkRegion::kIntersect_Op); // convert both the original stack and reduced stack to SkRegions and see if they're equal SkRegion region; - set_region_to_stack(stack, inflatedIBounds, ®ion); + set_region_to_stack(stack, clipIBounds, ®ion); SkRegion reducedRegion; - set_region_to_stack(reducedStack, inflatedIBounds, &reducedRegion); + set_region_to_stack(reducedStack, clipIBounds, &reducedRegion); - SkString testCase; - testCase.printf("Iteration %d", i); REPORTER_ASSERT_MESSAGE(reporter, region == reducedRegion, testCase.c_str()); } } @@ -1039,19 +1073,17 @@ static void test_reduced_clip_stack_genid(skiatest::Reporter* reporter) { SkClipStack stack; stack.clipDevRect(SkRect::MakeXYWH(0, 0, 100, 100), SkRegion::kReplace_Op, true); stack.clipDevRect(SkRect::MakeXYWH(0, 0, SkScalar(50.3), SkScalar(50.3)), SkRegion::kReplace_Op, true); - SkIRect inflatedIBounds = SkIRect::MakeXYWH(0, 0, 100, 100); + SkRect bounds = SkRect::MakeXYWH(0, 0, 100, 100); - GrReducedClip::ElementList reducedClips; + ElementList reducedClips; int32_t reducedGenID; - GrReducedClip::InitialState initial; SkIRect tightBounds; bool requiresAA; GrReducedClip::ReduceClipStack(stack, - inflatedIBounds, + bounds, &reducedClips, &reducedGenID, - &initial, &tightBounds, &requiresAA); @@ -1076,9 +1108,10 @@ static void test_reduced_clip_stack_genid(skiatest::Reporter* reporter) { int32_t genIDD = stack.getTopmostGenID(); -#define XYWH SkIRect::MakeXYWH +#define IXYWH SkIRect::MakeXYWH +#define XYWH SkRect::MakeXYWH - SkIRect stackBounds = XYWH(0, 0, 76, 76); + SkIRect stackBounds = IXYWH(0, 0, 76, 76); // The base test is to test each rect in two ways: // 1) The box dimensions. (Should reduce to "all in", no elements). @@ -1089,55 +1122,58 @@ static void test_reduced_clip_stack_genid(skiatest::Reporter* reporter) { // Not passing in tighter bounds is tested for consistency. static const struct SUPPRESS_VISIBILITY_WARNING { - SkIRect testBounds; + SkRect testBounds; int reducedClipCount; int32_t reducedGenID; - GrReducedClip::InitialState initialState; - SkIRect tighterBounds; // If this is empty, the query will not pass tighter bounds + InitialState initialState; + SkIRect clipIRect; // parameter. } testCases[] = { // Rect A. - { XYWH(0, 0, 25, 25), 0, SkClipStack::kWideOpenGenID, GrReducedClip::kAllIn_InitialState, XYWH(0, 0, 25, 25) }, - { XYWH(0, 0, 27, 27), 1, genIDA, GrReducedClip::kAllOut_InitialState, XYWH(0, 0, 27, 27)}, + { XYWH(0, 0, 25, 25), 0, SkClipStack::kWideOpenGenID, GrReducedClip::kAllIn_InitialState, IXYWH(0, 0, 25, 25) }, + { XYWH(0.1f, 0.1f, 25.1f, 25.1f), 0, SkClipStack::kWideOpenGenID, GrReducedClip::kAllIn_InitialState, IXYWH(0, 0, 26, 26) }, + { XYWH(0, 0, 27, 27), 1, genIDA, GrReducedClip::kAllOut_InitialState, IXYWH(0, 0, 27, 27)}, // Rect B. - { XYWH(50, 0, 25, 25), 0, SkClipStack::kWideOpenGenID, GrReducedClip::kAllIn_InitialState, XYWH(50, 0, 25, 25) }, - { XYWH(50, 0, 27, 27), 1, genIDB, GrReducedClip::kAllOut_InitialState, XYWH(50, 0, 26, 27) }, + { XYWH(50, 0, 25, 25), 0, SkClipStack::kWideOpenGenID, GrReducedClip::kAllIn_InitialState, IXYWH(50, 0, 25, 25) }, + { XYWH(50, 0, 25.3f, 25.3f), 0, SkClipStack::kWideOpenGenID, GrReducedClip::kAllIn_InitialState, IXYWH(50, 0, 26, 26) }, + { XYWH(50, 0, 27, 27), 1, genIDB, GrReducedClip::kAllOut_InitialState, IXYWH(50, 0, 26, 27) }, // Rect C. - { XYWH(0, 50, 25, 25), 0, SkClipStack::kWideOpenGenID, GrReducedClip::kAllIn_InitialState, XYWH(0, 50, 25, 25) }, - { XYWH(0, 50, 27, 27), 1, genIDC, GrReducedClip::kAllOut_InitialState, XYWH(0, 50, 27, 26) }, + { XYWH(0, 50, 25, 25), 0, SkClipStack::kWideOpenGenID, GrReducedClip::kAllIn_InitialState, IXYWH(0, 50, 25, 25) }, + { XYWH(0.2f, 50.1f, 25.1f, 25.2f), 0, SkClipStack::kWideOpenGenID, GrReducedClip::kAllIn_InitialState, IXYWH(0, 50, 26, 26) }, + { XYWH(0, 50, 27, 27), 1, genIDC, GrReducedClip::kAllOut_InitialState, IXYWH(0, 50, 27, 26) }, // Rect D. - { XYWH(50, 50, 25, 25), 0, SkClipStack::kWideOpenGenID, GrReducedClip::kAllIn_InitialState, XYWH(50, 50, 25, 25)}, - { XYWH(50, 50, 27, 27), 1, genIDD, GrReducedClip::kAllOut_InitialState, XYWH(50, 50, 26, 26)}, + { XYWH(50, 50, 25, 25), 0, SkClipStack::kWideOpenGenID, GrReducedClip::kAllIn_InitialState, IXYWH(50, 50, 25, 25)}, + { XYWH(50.3f, 50.3f, 25, 25), 0, SkClipStack::kWideOpenGenID, GrReducedClip::kAllIn_InitialState, IXYWH(50, 50, 26, 26)}, + { XYWH(50, 50, 27, 27), 1, genIDD, GrReducedClip::kAllOut_InitialState, IXYWH(50, 50, 26, 26)}, // Other tests: { XYWH(0, 0, 100, 100), 4, genIDD, GrReducedClip::kAllOut_InitialState, stackBounds }, // Rect in the middle, touches none. - { XYWH(26, 26, 24, 24), 0, SkClipStack::kEmptyGenID, GrReducedClip::kAllOut_InitialState, XYWH(26, 26, 24, 24) }, + { XYWH(26, 26, 24, 24), 0, SkClipStack::kEmptyGenID, GrReducedClip::kAllOut_InitialState, IXYWH(26, 26, 24, 24) }, // Rect in the middle, touches all the rects. GenID is the last rect. - { XYWH(24, 24, 27, 27), 4, genIDD, GrReducedClip::kAllOut_InitialState, XYWH(24, 24, 27, 27) }, + { XYWH(24, 24, 27, 27), 4, genIDD, GrReducedClip::kAllOut_InitialState, IXYWH(24, 24, 27, 27) }, }; #undef XYWH +#undef IXYWH for (size_t i = 0; i < SK_ARRAY_COUNT(testCases); ++i) { - GrReducedClip::ElementList reducedClips; + ElementList reducedClips; int32_t reducedGenID; - GrReducedClip::InitialState initial; - SkIRect tightBounds; + SkIRect clipIRect; bool requiresAA; - GrReducedClip::ReduceClipStack(stack, - testCases[i].testBounds, - &reducedClips, - &reducedGenID, - &initial, - &tightBounds, - &requiresAA); + InitialState initial = GrReducedClip::ReduceClipStack(stack, + testCases[i].testBounds, + &reducedClips, + &reducedGenID, + &clipIRect, + &requiresAA); REPORTER_ASSERT(reporter, reducedClips.count() == testCases[i].reducedClipCount); SkASSERT(reducedClips.count() == testCases[i].reducedClipCount); @@ -1145,8 +1181,8 @@ static void test_reduced_clip_stack_genid(skiatest::Reporter* reporter) { SkASSERT(reducedGenID == testCases[i].reducedGenID); REPORTER_ASSERT(reporter, initial == testCases[i].initialState); SkASSERT(initial == testCases[i].initialState); - REPORTER_ASSERT(reporter, tightBounds == testCases[i].tighterBounds); - SkASSERT(tightBounds == testCases[i].tighterBounds); + REPORTER_ASSERT(reporter, clipIRect == testCases[i].clipIRect); + SkASSERT(clipIRect == testCases[i].clipIRect); } } } @@ -1155,26 +1191,182 @@ static void test_reduced_clip_stack_no_aa_crash(skiatest::Reporter* reporter) { SkClipStack stack; stack.clipDevRect(SkIRect::MakeXYWH(0, 0, 100, 100), SkRegion::kReplace_Op); stack.clipDevRect(SkIRect::MakeXYWH(0, 0, 50, 50), SkRegion::kReplace_Op); - SkIRect inflatedIBounds = SkIRect::MakeXYWH(0, 0, 100, 100); + SkRect bounds = SkRect::MakeXYWH(0, 0, 100, 100); - GrReducedClip::ElementList reducedClips; + ElementList reducedClips; int32_t reducedGenID; - GrReducedClip::InitialState initial; SkIRect tightBounds; bool requiresAA; // At the time, this would crash. GrReducedClip::ReduceClipStack(stack, - inflatedIBounds, + bounds, &reducedClips, &reducedGenID, - &initial, &tightBounds, &requiresAA); REPORTER_ASSERT(reporter, 0 == reducedClips.count()); } +enum class ClipMethod { + kSkipDraw, + kIgnoreClip, + kScissor, + kAAElements +}; + +static void test_aa_query(skiatest::Reporter* reporter, const SkString& testName, + const SkClipStack& stack, const SkMatrix& queryXform, + const SkRect& preXformQuery, ClipMethod expectedMethod, + int numExpectedElems = 0) { + ElementList reducedElems; + int32_t reducedGenID; + SkIRect clipIBounds; + bool requiresAA; + + SkRect queryBounds; + queryXform.mapRect(&queryBounds, preXformQuery); + + InitialState initialState = GrReducedClip::ReduceClipStack(stack, + queryBounds, + &reducedElems, + &reducedGenID, + &clipIBounds, + &requiresAA); + + SkClipStack::BoundsType stackBoundsType; + SkRect stackBounds; + stack.getBounds(&stackBounds, &stackBoundsType); + + switch (expectedMethod) { + case ClipMethod::kSkipDraw: + SkASSERT(0 == numExpectedElems); + REPORTER_ASSERT_MESSAGE(reporter, reducedElems.isEmpty(), testName.c_str()); + REPORTER_ASSERT_MESSAGE(reporter, GrReducedClip::kAllOut_InitialState == initialState, + testName.c_str()); + return; + case ClipMethod::kIgnoreClip: + SkASSERT(0 == numExpectedElems); + REPORTER_ASSERT_MESSAGE(reporter, reducedElems.isEmpty(), testName.c_str()); + REPORTER_ASSERT_MESSAGE(reporter, GrClip::IsInsideClip(clipIBounds, queryBounds), + testName.c_str()); + REPORTER_ASSERT_MESSAGE(reporter, GrReducedClip::kAllIn_InitialState == initialState, + testName.c_str()); + return; + case ClipMethod::kScissor: { + SkASSERT(SkClipStack::kNormal_BoundsType == stackBoundsType); + SkASSERT(0 == numExpectedElems); + SkIRect expectedScissor; + stackBounds.round(&expectedScissor); + REPORTER_ASSERT_MESSAGE(reporter, reducedElems.isEmpty(), testName.c_str()); + REPORTER_ASSERT_MESSAGE(reporter, expectedScissor == clipIBounds, testName.c_str()); + REPORTER_ASSERT_MESSAGE(reporter, GrReducedClip::kAllIn_InitialState == initialState, + testName.c_str()); + return; + } + case ClipMethod::kAAElements: { + SkIRect expectedClipIBounds = GrClip::GetPixelIBounds(queryBounds); + if (SkClipStack::kNormal_BoundsType == stackBoundsType) { + SkAssertResult(expectedClipIBounds.intersect(GrClip::GetPixelIBounds(stackBounds))); + } + REPORTER_ASSERT_MESSAGE(reporter, numExpectedElems == reducedElems.count(), + testName.c_str()); + REPORTER_ASSERT_MESSAGE(reporter, expectedClipIBounds == clipIBounds, testName.c_str()); + REPORTER_ASSERT_MESSAGE(reporter, requiresAA == !reducedElems.isEmpty(), + testName.c_str()); + break; + } + } +} + +static void test_reduced_clip_stack_aa(skiatest::Reporter* reporter) { + constexpr SkScalar IL = 2, IT = 1, IR = 6, IB = 7; // Pixel aligned rect. + constexpr SkScalar L = 2.2f, T = 1.7f, R = 5.8f, B = 7.3f; // Generic rect. + constexpr SkScalar l = 3.3f, t = 2.8f, r = 4.7f, b = 6.2f; // Small rect contained in R. + + SkRect alignedRect = {IL, IT, IR, IB}; + SkRect rect = {L, T, R, B}; + SkRect innerRect = {l, t, r, b}; + + SkMatrix m; + m.setIdentity(); + + constexpr SkScalar kMinScale = 2.0001f; + constexpr SkScalar kMaxScale = 3; + constexpr int kNumIters = 8; + + SkString name; + SkRandom rand; + + for (int i = 0; i < kNumIters; ++i) { + // Pixel-aligned rect (iior=true). + name.printf("Pixel-aligned rect test, iter %i", i); + SkClipStack stack; + stack.clipDevRect(alignedRect, SkRegion::kIntersect_Op, true); + test_aa_query(reporter, name, stack, m, {IL, IT, IR, IB}, ClipMethod::kIgnoreClip); + test_aa_query(reporter, name, stack, m, {IL, IT-1, IR, IT}, ClipMethod::kSkipDraw); + test_aa_query(reporter, name, stack, m, {IL, IT, IR, IB}, ClipMethod::kScissor); + test_aa_query(reporter, name, stack, m, {IL, IT+2, IR, IB-3}, ClipMethod::kScissor); + + // Rect (iior=true). + name.printf("Rect test, iter %i", i); + stack.reset(); + stack.clipDevRect(rect, SkRegion::kIntersect_Op, true); + test_aa_query(reporter, name, stack, m, {L, T, R, B}, ClipMethod::kIgnoreClip); + test_aa_query(reporter, name, stack, m, {L-.1f, T, L, B}, ClipMethod::kSkipDraw); + test_aa_query(reporter, name, stack, m, {L-.1f, T, L+.1f, B}, ClipMethod::kAAElements, 1); + + // Difference rect (iior=false, inside-out bounds). + name.printf("Difference rect test, iter %i", i); + stack.reset(); + stack.clipDevRect(rect, SkRegion::kDifference_Op, true); + test_aa_query(reporter, name, stack, m, {L, T, R, B}, ClipMethod::kSkipDraw); + test_aa_query(reporter, name, stack, m, {L, T-.1f, R, T}, ClipMethod::kIgnoreClip); + test_aa_query(reporter, name, stack, m, {L, T-.1f, R, T+.1f}, ClipMethod::kAAElements, 1); + + // Complex clip (iior=false, normal bounds). + name.printf("Complex clip test, iter %i", i); + stack.reset(); + stack.clipDevRect(rect, SkRegion::kIntersect_Op, true); + stack.clipDevRect(innerRect, SkRegion::kXOR_Op, true); + test_aa_query(reporter, name, stack, m, {l, t, r, b}, ClipMethod::kSkipDraw); + test_aa_query(reporter, name, stack, m, {r-.1f, t, R, b}, ClipMethod::kAAElements, 1); + test_aa_query(reporter, name, stack, m, {r-.1f, t, R+.1f, b}, ClipMethod::kAAElements, 2); + test_aa_query(reporter, name, stack, m, {r, t, R+.1f, b}, ClipMethod::kAAElements, 1); + test_aa_query(reporter, name, stack, m, {r, t, R, b}, ClipMethod::kIgnoreClip); + test_aa_query(reporter, name, stack, m, {R, T, R+.1f, B}, ClipMethod::kSkipDraw); + + // Complex clip where outer rect is pixel aligned (iior=false, normal bounds). + name.printf("Aligned Complex clip test, iter %i", i); + stack.reset(); + stack.clipDevRect(alignedRect, SkRegion::kIntersect_Op, true); + stack.clipDevRect(innerRect, SkRegion::kXOR_Op, true); + test_aa_query(reporter, name, stack, m, {l, t, r, b}, ClipMethod::kSkipDraw); + test_aa_query(reporter, name, stack, m, {l, b-.1f, r, IB}, ClipMethod::kAAElements, 1); + test_aa_query(reporter, name, stack, m, {l, b-.1f, r, IB+.1f}, ClipMethod::kAAElements, 1); + test_aa_query(reporter, name, stack, m, {l, b, r, IB+.1f}, ClipMethod::kAAElements, 0); + test_aa_query(reporter, name, stack, m, {l, b, r, IB}, ClipMethod::kIgnoreClip); + test_aa_query(reporter, name, stack, m, {IL, IB, IR, IB+.1f}, ClipMethod::kSkipDraw); + + // Apply random transforms and try again. This ensures the clip stack reduction is hardened + // against FP rounding error. + SkScalar sx = rand.nextRangeScalar(kMinScale, kMaxScale); + sx = SkScalarFloorToScalar(sx * alignedRect.width()) / alignedRect.width(); + SkScalar sy = rand.nextRangeScalar(kMinScale, kMaxScale); + sy = SkScalarFloorToScalar(sy * alignedRect.height()) / alignedRect.height(); + SkScalar tx = SkScalarRoundToScalar(sx * alignedRect.x()) - sx * alignedRect.x(); + SkScalar ty = SkScalarRoundToScalar(sy * alignedRect.y()) - sy * alignedRect.y(); + + SkMatrix xform = SkMatrix::MakeScale(sx, sy); + xform.postTranslate(tx, ty); + xform.mapRect(&alignedRect); + xform.mapRect(&rect); + xform.mapRect(&innerRect); + m.postConcat(xform); + } +} + #endif DEF_TEST(ClipStack, reporter) { @@ -1226,5 +1418,6 @@ DEF_TEST(ClipStack, reporter) { test_reduced_clip_stack(reporter); test_reduced_clip_stack_genid(reporter); test_reduced_clip_stack_no_aa_crash(reporter); + test_reduced_clip_stack_aa(reporter); #endif }