skia2/tests/GrClipStackTest.cpp
Michael Ludwig a195d101f9 New GrClipStack supporting only intersect/difference
Overview doc: https://docs.google.com/document/d/1ddIk74A1rL5Kj5kGcnInOYKVAXs3J2IsSgU5BLit0Ng/edit?usp=sharing

This is the new clip stack that will replace GrClipStackClip. The doc
link in the CL description has a much more detailed overview of what the
strategy of the new clip stack is, but at a very high level:

1. Add a temporary #define that lets SkGpuDevice switch between the old
stack and the new stack. For the new GrClipStack, it extends SkBaseDevice
directly and has to implement all of the device clipping virtuals.
   - If you look from patchset 5 and earlier, the define defaults to on
     so I can test it on the bots, etc. but the plan will be for it to
     default to off when this lands so it's only running on unit tests.
     Then in a follow up, I'll turn it on for our bots but keep it off in
     chrome and android. If everything looks good, chrome can then be
     turned on. There is a more extensive migration plan for android
     because of the expanding clip ops, but that is covered at the end of
     the overview doc.

2. GrClipStack manages save/restore logic of the stack and extends GrClip,
so the cpp file also includes code to apply a GrAppliedClip. At the moment
the apply strategy is as close to that in GrReducedClip and
GrClipStackClip as I could make it. Down the road, I think we can explore
other analytic coverage options and a clip atlas that replaces the unified
SW mask.
   - Once GrClipStack is enabled everywhere, it means GrReducedClip and
     GrClipStackClip can be deleted, so I'm not too worried about sharing
     code between the two. A lot is already shared through the use of
     GrSWMaskHelper and GrStencilMaskHelper.
   - SkClipStack and SkClipStackDevice are still used by the PDF and SVG
     backends, so they aren't necessarily deletable.

3. The GrClipStack only handles intersect and difference ops. It
represents all geometric clip operations as an element. The stack itself
is controlled by the "save record", which tracks aggregate bounds, valid
elements, and the non-geometric clip shader.
   - When a new save record is pushed on the stack, older elements are
     inactive. This means they cannot be modified, since they may need to
     be activated again when the current save is popped off the stack.
     However, they can still affect the clip during application.
   - When a new element is pushed on the stack, older elements may be
     invalidated. This means they don't need to be considered any more
     because they are redundant with the new clip shape (e.g. nested round
     rect clips only have to keep the innermost valid).


Bug: skia:10205
Change-Id: I68ccfd414033aa9014b102efaee3ad50a806f793
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/308283
Commit-Queue: Michael Ludwig <michaelludwig@google.com>
Reviewed-by: Robert Phillips <robertphillips@google.com>
Reviewed-by: Brian Salomon <bsalomon@google.com>
2020-09-15 19:27:11 +00:00

1981 lines
94 KiB
C++

/*
* Copyright 2020 Google LLC
*
* Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file.
*/
#include "src/gpu/GrClipStack.h"
#include "tests/Test.h"
#include "include/core/SkPath.h"
#include "include/core/SkRRect.h"
#include "include/core/SkRect.h"
#include "include/core/SkRegion.h"
#include "include/core/SkShader.h"
#include "include/gpu/GrDirectContext.h"
#include "src/core/SkMatrixProvider.h"
#include "src/core/SkRRectPriv.h"
#include "src/core/SkRectPriv.h"
#include "src/gpu/GrContextPriv.h"
#include "src/gpu/GrProxyProvider.h"
#include "src/gpu/GrRenderTargetContext.h"
namespace {
class TestCaseBuilder;
class ElementsBuilder;
enum class SavePolicy {
kNever,
kAtStart,
kAtEnd,
kBetweenEveryOp
};
// TODO: We could add a RestorePolicy enum that tests different places to restore, but that would
// make defining the test expectations and order independence more cumbersome.
class TestCase {
public:
// Provides fluent API to describe actual clip commands and expected clip elements:
// TestCase test = TestCase::Build("example", deviceBounds)
// .actual().rect(r, GrAA::kYes, SkClipOp::kIntersect)
// .localToDevice(matrix)
// .nonAA()
// .difference()
// .path(p1)
// .path(p2)
// .finishElements()
// .expectedState(kDeviceRect)
// .expectedBounds(r.roundOut())
// .expect().rect(r, GrAA::kYes, SkClipOp::kIntersect)
// .finishElements()
// .finishTest();
static TestCaseBuilder Build(const char* name, const SkIRect& deviceBounds);
void run(const std::vector<int>& order, SavePolicy policy, skiatest::Reporter* reporter) const;
const SkIRect& deviceBounds() const { return fDeviceBounds; }
GrClipStack::ClipState expectedState() const { return fExpectedState; }
const std::vector<GrClipStack::Element>& initialElements() const { return fElements; }
const std::vector<GrClipStack::Element>& expectedElements() const { return fExpectedElements; }
private:
friend class TestCaseBuilder;
TestCase(SkString name,
const SkIRect& deviceBounds,
GrClipStack::ClipState expectedState,
std::vector<GrClipStack::Element> actual,
std::vector<GrClipStack::Element> expected)
: fName(name)
, fElements(std::move(actual))
, fDeviceBounds(deviceBounds)
, fExpectedElements(std::move(expected))
, fExpectedState(expectedState) {}
SkString getTestName(const std::vector<int>& order, SavePolicy policy) const;
// This may be tighter than GrClipStack::getConservativeBounds() because this always accounts
// for difference ops, whereas GrClipStack only sometimes can subtract the inner bounds for a
// difference op.
std::pair<SkIRect, bool> getOptimalBounds() const;
SkString fName;
// The input shapes+state to GrClipStack
std::vector<GrClipStack::Element> fElements;
SkIRect fDeviceBounds;
// The expected output of iterating over the GrClipStack after all fElements are added, although
// order is not important
std::vector<GrClipStack::Element> fExpectedElements;
GrClipStack::ClipState fExpectedState;
};
class ElementsBuilder {
public:
// Update the default matrix, aa, and op state for elements that are added.
ElementsBuilder& localToDevice(const SkMatrix& m) { fLocalToDevice = m; return *this; }
ElementsBuilder& aa() { fAA = GrAA::kYes; return *this; }
ElementsBuilder& nonAA() { fAA = GrAA::kNo; return *this; }
ElementsBuilder& intersect() { fOp = SkClipOp::kIntersect; return *this; }
ElementsBuilder& difference() { fOp = SkClipOp::kDifference; return *this; }
// Add rect, rrect, or paths to the list of elements, possibly overriding the last set
// matrix, aa, and op state.
ElementsBuilder& rect(const SkRect& rect) {
return this->rect(rect, fLocalToDevice, fAA, fOp);
}
ElementsBuilder& rect(const SkRect& rect, GrAA aa, SkClipOp op) {
return this->rect(rect, fLocalToDevice, aa, op);
}
ElementsBuilder& rect(const SkRect& rect, const SkMatrix& m, GrAA aa, SkClipOp op) {
fElements->push_back({GrShape(rect), m, op, aa});
return *this;
}
ElementsBuilder& rrect(const SkRRect& rrect) {
return this->rrect(rrect, fLocalToDevice, fAA, fOp);
}
ElementsBuilder& rrect(const SkRRect& rrect, GrAA aa, SkClipOp op) {
return this->rrect(rrect, fLocalToDevice, aa, op);
}
ElementsBuilder& rrect(const SkRRect& rrect, const SkMatrix& m, GrAA aa, SkClipOp op) {
fElements->push_back({GrShape(rrect), m, op, aa});
return *this;
}
ElementsBuilder& path(const SkPath& path) {
return this->path(path, fLocalToDevice, fAA, fOp);
}
ElementsBuilder& path(const SkPath& path, GrAA aa, SkClipOp op) {
return this->path(path, fLocalToDevice, aa, op);
}
ElementsBuilder& path(const SkPath& path, const SkMatrix& m, GrAA aa, SkClipOp op) {
fElements->push_back({GrShape(path), m, op, aa});
return *this;
}
// Finish and return the original test case builder
TestCaseBuilder& finishElements() {
return *fBuilder;
}
private:
friend class TestCaseBuilder;
ElementsBuilder(TestCaseBuilder* builder, std::vector<GrClipStack::Element>* elements)
: fBuilder(builder)
, fElements(elements) {}
SkMatrix fLocalToDevice = SkMatrix::I();
GrAA fAA = GrAA::kNo;
SkClipOp fOp = SkClipOp::kIntersect;
TestCaseBuilder* fBuilder;
std::vector<GrClipStack::Element>* fElements;
};
class TestCaseBuilder {
public:
ElementsBuilder actual() { return ElementsBuilder(this, &fActualElements); }
ElementsBuilder expect() { return ElementsBuilder(this, &fExpectedElements); }
TestCaseBuilder& expectActual() {
fExpectedElements = fActualElements;
return *this;
}
TestCaseBuilder& state(GrClipStack::ClipState state) {
fExpectedState = state;
return *this;
}
TestCase finishTest() {
TestCase test(fName, fDeviceBounds, fExpectedState,
std::move(fActualElements), std::move(fExpectedElements));
fExpectedState = GrClipStack::ClipState::kWideOpen;
return test;
}
private:
friend class TestCase;
explicit TestCaseBuilder(const char* name, const SkIRect& deviceBounds)
: fName(name)
, fDeviceBounds(deviceBounds)
, fExpectedState(GrClipStack::ClipState::kWideOpen) {}
SkString fName;
SkIRect fDeviceBounds;
GrClipStack::ClipState fExpectedState;
std::vector<GrClipStack::Element> fActualElements;
std::vector<GrClipStack::Element> fExpectedElements;
};
TestCaseBuilder TestCase::Build(const char* name, const SkIRect& deviceBounds) {
return TestCaseBuilder(name, deviceBounds);
}
SkString TestCase::getTestName(const std::vector<int>& order, SavePolicy policy) const {
SkString name = fName;
SkString policyName;
switch(policy) {
case SavePolicy::kNever:
policyName = "never";
break;
case SavePolicy::kAtStart:
policyName = "start";
break;
case SavePolicy::kAtEnd:
policyName = "end";
break;
case SavePolicy::kBetweenEveryOp:
policyName = "between";
break;
}
name.appendf("(save %s, order [", policyName.c_str());
for (size_t i = 0; i < order.size(); ++i) {
if (i > 0) {
name.append(",");
}
name.appendf("%d", order[i]);
}
name.append("])");
return name;
}
std::pair<SkIRect, bool> TestCase::getOptimalBounds() const {
if (fExpectedState == GrClipStack::ClipState::kEmpty) {
return {SkIRect::MakeEmpty(), true};
}
bool expectOptimal = true;
SkRegion region(fDeviceBounds);
for (const GrClipStack::Element& e : fExpectedElements) {
bool intersect = (e.fOp == SkClipOp::kIntersect && !e.fShape.inverted()) ||
(e.fOp == SkClipOp::kDifference && e.fShape.inverted());
SkIRect elementBounds;
SkRegion::Op op;
if (intersect) {
op = SkRegion::kIntersect_Op;
expectOptimal &= e.fLocalToDevice.isIdentity();
elementBounds = GrClip::GetPixelIBounds(e.fLocalToDevice.mapRect(e.fShape.bounds()),
e.fAA, GrClip::BoundsType::kExterior);
} else {
op = SkRegion::kDifference_Op;
expectOptimal = false;
if (e.fShape.isRect() && e.fLocalToDevice.isIdentity()) {
elementBounds = GrClip::GetPixelIBounds(e.fShape.rect(), e.fAA,
GrClip::BoundsType::kInterior);
} else if (e.fShape.isRRect() && e.fLocalToDevice.isIdentity()) {
elementBounds = GrClip::GetPixelIBounds(SkRRectPriv::InnerBounds(e.fShape.rrect()),
e.fAA, GrClip::BoundsType::kInterior);
} else {
elementBounds = SkIRect::MakeEmpty();
}
}
region.op(SkRegion(elementBounds), op);
}
return {region.getBounds(), expectOptimal};
}
static bool compare_elements(const GrClipStack::Element& a, const GrClipStack::Element& b) {
if (a.fAA != b.fAA || a.fOp != b.fOp || a.fLocalToDevice != b.fLocalToDevice ||
a.fShape.type() != b.fShape.type()) {
return false;
}
switch(a.fShape.type()) {
case GrShape::Type::kRect:
return a.fShape.rect() == b.fShape.rect();
case GrShape::Type::kRRect:
return a.fShape.rrect() == b.fShape.rrect();
case GrShape::Type::kPath:
// A path's points are never transformed, the only modification is fill type which does
// not change the generation ID. For convex polygons, we check == so that more complex
// test cases can be evaluated.
return a.fShape.path().getGenerationID() == b.fShape.path().getGenerationID() ||
(a.fShape.convex() &&
a.fShape.segmentMask() == SkPathSegmentMask::kLine_SkPathSegmentMask &&
a.fShape.path() == b.fShape.path());
default:
SkDEBUGFAIL("Shape type not handled by test case yet.");
return false;
}
}
void TestCase::run(const std::vector<int>& order, SavePolicy policy,
skiatest::Reporter* reporter) const {
SkASSERT(fElements.size() == order.size());
SkSimpleMatrixProvider matrixProvider(SkMatrix::I());
GrClipStack cs(fDeviceBounds, &matrixProvider, false);
if (policy == SavePolicy::kAtStart) {
cs.save();
}
for (int i : order) {
if (policy == SavePolicy::kBetweenEveryOp) {
cs.save();
}
const GrClipStack::Element& e = fElements[i];
switch(e.fShape.type()) {
case GrShape::Type::kRect:
cs.clipRect(e.fLocalToDevice, e.fShape.rect(), e.fAA, e.fOp);
break;
case GrShape::Type::kRRect:
cs.clipRRect(e.fLocalToDevice, e.fShape.rrect(), e.fAA, e.fOp);
break;
case GrShape::Type::kPath:
cs.clipPath(e.fLocalToDevice, e.fShape.path(), e.fAA, e.fOp);
break;
default:
SkDEBUGFAIL("Shape type not handled by test case yet.");
}
}
if (policy == SavePolicy::kAtEnd) {
cs.save();
}
// Now validate
SkString name = this->getTestName(order, policy);
REPORTER_ASSERT(reporter, cs.clipState() == fExpectedState,
"%s, clip state expected %d, actual %d",
name.c_str(), (int) fExpectedState, (int) cs.clipState());
SkIRect actualBounds = cs.getConservativeBounds();
SkIRect optimalBounds;
bool expectOptimal;
std::tie(optimalBounds, expectOptimal) = this->getOptimalBounds();
if (expectOptimal) {
REPORTER_ASSERT(reporter, actualBounds == optimalBounds,
"%s, bounds expected [%d %d %d %d], actual [%d %d %d %d]",
name.c_str(), optimalBounds.fLeft, optimalBounds.fTop,
optimalBounds.fRight, optimalBounds.fBottom,
actualBounds.fLeft, actualBounds.fTop,
actualBounds.fRight, actualBounds.fBottom);
} else {
REPORTER_ASSERT(reporter, actualBounds.contains(optimalBounds),
"%s, bounds are not conservative, optimal [%d %d %d %d], actual [%d %d %d %d]",
name.c_str(), optimalBounds.fLeft, optimalBounds.fTop,
optimalBounds.fRight, optimalBounds.fBottom,
actualBounds.fLeft, actualBounds.fTop,
actualBounds.fRight, actualBounds.fBottom);
}
size_t matchedElements = 0;
for (const GrClipStack::Element& a : cs) {
bool found = false;
for (const GrClipStack::Element& e : fExpectedElements) {
if (compare_elements(a, e)) {
// shouldn't match multiple expected elements or it's a bad test case
SkASSERT(!found);
found = true;
}
}
REPORTER_ASSERT(reporter, found,
"%s, unexpected clip element in stack: shape %d, aa %d, op %d",
name.c_str(), (int) a.fShape.type(), (int) a.fAA, (int) a.fOp);
matchedElements += found ? 1 : 0;
}
REPORTER_ASSERT(reporter, matchedElements == fExpectedElements.size(),
"%s, did not match all expected elements: expected %d but matched only %d",
name.c_str(), fExpectedElements.size(), matchedElements);
// Validate restoration behavior
if (policy == SavePolicy::kAtEnd) {
GrClipStack::ClipState oldState = cs.clipState();
cs.restore();
REPORTER_ASSERT(reporter, cs.clipState() == oldState,
"%s, restoring an empty save record should not change clip state: "
"expected %d but got %d", (int) oldState, (int) cs.clipState());
} else if (policy != SavePolicy::kNever) {
int restoreCount = policy == SavePolicy::kAtStart ? 1 : (int) order.size();
for (int i = 0; i < restoreCount; ++i) {
cs.restore();
}
// Should be wide open if everything is restored to base state
REPORTER_ASSERT(reporter, cs.clipState() == GrClipStack::ClipState::kWideOpen,
"%s, restore should make stack become wide-open, not %d",
(int) cs.clipState());
}
}
// All clip operations are commutative so applying actual elements in every possible order should
// always produce the same set of expected elements.
static void run_test_case(skiatest::Reporter* r, const TestCase& test) {
int n = (int) test.initialElements().size();
std::vector<int> order(n);
std::vector<int> stack(n);
// Initial order sequence and zeroed stack
for (int i = 0; i < n; ++i) {
order[i] = i;
stack[i] = 0;
}
auto runTest = [&]() {
static const SavePolicy kPolicies[] = { SavePolicy::kNever, SavePolicy::kAtStart,
SavePolicy::kAtEnd, SavePolicy::kBetweenEveryOp };
for (auto policy : kPolicies) {
test.run(order, policy, r);
}
};
// Heap's algorithm (non-recursive) to generate every permutation over the test case's elements
// https://en.wikipedia.org/wiki/Heap%27s_algorithm
runTest();
static constexpr int kMaxRuns = 720; // Don't run more than 6! configurations, even if n > 6
int testRuns = 1;
int i = 0;
while (i < n && testRuns < kMaxRuns) {
if (stack[i] < i) {
using std::swap;
if (i % 2 == 0) {
swap(order[0], order[i]);
} else {
swap(order[stack[i]], order[i]);
}
runTest();
stack[i]++;
i = 0;
testRuns++;
} else {
stack[i] = 0;
++i;
}
}
}
static SkPath make_octagon(const SkRect& r, SkScalar lr, SkScalar tb) {
SkPath p;
p.moveTo(r.fLeft + lr, r.fTop);
p.lineTo(r.fRight - lr, r.fTop);
p.lineTo(r.fRight, r.fTop + tb);
p.lineTo(r.fRight, r.fBottom - tb);
p.lineTo(r.fRight - lr, r.fBottom);
p.lineTo(r.fLeft + lr, r.fBottom);
p.lineTo(r.fLeft, r.fBottom - tb);
p.lineTo(r.fLeft, r.fTop + tb);
p.close();
return p;
}
static SkPath make_octagon(const SkRect& r) {
SkScalar lr = 0.3f * r.width();
SkScalar tb = 0.3f * r.height();
return make_octagon(r, lr, tb);
}
static constexpr SkIRect kDeviceBounds = {0, 0, 100, 100};
} // anonymous namespace
///////////////////////////////////////////////////////////////////////////////
// These tests use the TestCase infrastructure to define clip stacks and
// associated expectations.
// Tests that the initialized state of the clip stack is wide-open
DEF_TEST(GrClipStack_InitialState, r) {
run_test_case(r, TestCase::Build("initial-state", SkIRect::MakeWH(100, 100)).finishTest());
}
// Tests that intersection of rects combine to a single element when they have the same AA type,
// or are pixel-aligned.
DEF_TEST(GrClipStack_RectRectAACombine, r) {
SkRect pixelAligned = {0, 0, 10, 10};
SkRect fracRect1 = pixelAligned.makeOffset(5.3f, 3.7f);
SkRect fracRect2 = {fracRect1.fLeft + 0.75f * fracRect1.width(),
fracRect1.fTop + 0.75f * fracRect1.height(),
fracRect1.fRight, fracRect1.fBottom};
SkRect fracIntersect;
SkAssertResult(fracIntersect.intersect(fracRect1, fracRect2));
SkRect alignedIntersect;
SkAssertResult(alignedIntersect.intersect(pixelAligned, fracRect1));
// Both AA combine to one element
run_test_case(r, TestCase::Build("aa", kDeviceBounds)
.actual().aa().intersect()
.rect(fracRect1).rect(fracRect2)
.finishElements()
.expect().aa().intersect().rect(fracIntersect).finishElements()
.state(GrClipStack::ClipState::kDeviceRect)
.finishTest());
// Both non-AA combine to one element
run_test_case(r, TestCase::Build("nonaa", kDeviceBounds)
.actual().nonAA().intersect()
.rect(fracRect1).rect(fracRect2)
.finishElements()
.expect().nonAA().intersect().rect(fracIntersect).finishElements()
.state(GrClipStack::ClipState::kDeviceRect)
.finishTest());
// Pixel-aligned AA and non-AA combine
run_test_case(r, TestCase::Build("aligned-aa+nonaa", kDeviceBounds)
.actual().intersect()
.aa().rect(pixelAligned).nonAA().rect(fracRect1)
.finishElements()
.expect().nonAA().intersect().rect(alignedIntersect).finishElements()
.state(GrClipStack::ClipState::kDeviceRect)
.finishTest());
// AA and pixel-aligned non-AA combine
run_test_case(r, TestCase::Build("aa+aligned-nonaa", kDeviceBounds)
.actual().intersect()
.aa().rect(fracRect1).nonAA().rect(pixelAligned)
.finishElements()
.expect().aa().intersect().rect(alignedIntersect).finishElements()
.state(GrClipStack::ClipState::kDeviceRect)
.finishTest());
// Other mixed AA modes do not combine
run_test_case(r, TestCase::Build("aa+nonaa", kDeviceBounds)
.actual().intersect()
.aa().rect(fracRect1).nonAA().rect(fracRect2)
.finishElements()
.expectActual()
.state(GrClipStack::ClipState::kComplex)
.finishTest());
}
// Tests that an intersection and a difference op do not combine, even if they would have if both
// were intersection ops.
DEF_TEST(GrClipStack_DifferenceNoCombine, r) {
SkRect r1 = {15.f, 14.f, 23.22f, 58.2f};
SkRect r2 = r1.makeOffset(5.f, 8.f);
SkASSERT(r1.intersects(r2));
run_test_case(r, TestCase::Build("no-combine", kDeviceBounds)
.actual().aa().intersect().rect(r1)
.difference().rect(r2)
.finishElements()
.expectActual()
.state(GrClipStack::ClipState::kComplex)
.finishTest());
}
// Tests that intersection of rects in the same coordinate space can still be combined, but do not
// when the spaces differ.
DEF_TEST(GrClipStack_RectRectNonAxisAligned, r) {
SkRect pixelAligned = {0, 0, 10, 10};
SkRect fracRect1 = pixelAligned.makeOffset(5.3f, 3.7f);
SkRect fracRect2 = {fracRect1.fLeft + 0.75f * fracRect1.width(),
fracRect1.fTop + 0.75f * fracRect1.height(),
fracRect1.fRight, fracRect1.fBottom};
SkRect fracIntersect;
SkAssertResult(fracIntersect.intersect(fracRect1, fracRect2));
SkMatrix lm = SkMatrix::RotateDeg(45.f);
// Both AA combine
run_test_case(r, TestCase::Build("aa", kDeviceBounds)
.actual().aa().intersect().localToDevice(lm)
.rect(fracRect1).rect(fracRect2)
.finishElements()
.expect().aa().intersect().localToDevice(lm)
.rect(fracIntersect).finishElements()
.state(GrClipStack::ClipState::kComplex)
.finishTest());
// Both non-AA combine
run_test_case(r, TestCase::Build("nonaa", kDeviceBounds)
.actual().nonAA().intersect().localToDevice(lm)
.rect(fracRect1).rect(fracRect2)
.finishElements()
.expect().nonAA().intersect().localToDevice(lm)
.rect(fracIntersect).finishElements()
.state(GrClipStack::ClipState::kComplex)
.finishTest());
// Integer-aligned coordinates under a local matrix with mixed AA don't combine, though
run_test_case(r, TestCase::Build("local-aa", kDeviceBounds)
.actual().intersect().localToDevice(lm)
.aa().rect(pixelAligned).nonAA().rect(fracRect1)
.finishElements()
.expectActual()
.state(GrClipStack::ClipState::kComplex)
.finishTest());
}
// Tests that intersection of two round rects can simplify to a single round rect when they have
// the same AA type.
DEF_TEST(GrClipStack_RRectRRectAACombine, r) {
SkRRect r1 = SkRRect::MakeRectXY(SkRect::MakeWH(12, 12), 2.f, 2.f);
SkRRect r2 = r1.makeOffset(6.f, 6.f);
SkRRect intersect = SkRRectPriv::ConservativeIntersect(r1, r2);
SkASSERT(!intersect.isEmpty());
// Both AA combine
run_test_case(r, TestCase::Build("aa", kDeviceBounds)
.actual().aa().intersect()
.rrect(r1).rrect(r2)
.finishElements()
.expect().aa().intersect().rrect(intersect).finishElements()
.state(GrClipStack::ClipState::kDeviceRRect)
.finishTest());
// Both non-AA combine
run_test_case(r, TestCase::Build("nonaa", kDeviceBounds)
.actual().nonAA().intersect()
.rrect(r1).rrect(r2)
.finishElements()
.expect().nonAA().intersect().rrect(intersect).finishElements()
.state(GrClipStack::ClipState::kDeviceRRect)
.finishTest());
// Mixed do not combine
run_test_case(r, TestCase::Build("aa+nonaa", kDeviceBounds)
.actual().intersect()
.aa().rrect(r1).nonAA().rrect(r2)
.finishElements()
.expectActual()
.state(GrClipStack::ClipState::kComplex)
.finishTest());
// Same AA state can combine in the same local coordinate space
SkMatrix lm = SkMatrix::RotateDeg(45.f);
run_test_case(r, TestCase::Build("local-aa", kDeviceBounds)
.actual().aa().intersect().localToDevice(lm)
.rrect(r1).rrect(r2)
.finishElements()
.expect().aa().intersect().localToDevice(lm)
.rrect(intersect).finishElements()
.state(GrClipStack::ClipState::kComplex)
.finishTest());
run_test_case(r, TestCase::Build("local-nonaa", kDeviceBounds)
.actual().nonAA().intersect().localToDevice(lm)
.rrect(r1).rrect(r2)
.finishElements()
.expect().nonAA().intersect().localToDevice(lm)
.rrect(intersect).finishElements()
.state(GrClipStack::ClipState::kComplex)
.finishTest());
}
// Tests that intersection of a round rect and rect can simplify to a new round rect or even a rect.
DEF_TEST(GrClipStack_RectRRectCombine, r) {
SkRRect rrect = SkRRect::MakeRectXY({0, 0, 10, 10}, 2.f, 2.f);
SkRect cutTop = {-10, -10, 10, 4};
SkRect cutMid = {-10, 3, 10, 7};
// Rect + RRect becomes a round rect with some square corners
SkVector cutCorners[4] = {{2.f, 2.f}, {2.f, 2.f}, {0, 0}, {0, 0}};
SkRRect cutRRect;
cutRRect.setRectRadii({0, 0, 10, 4}, cutCorners);
run_test_case(r, TestCase::Build("still-rrect", kDeviceBounds)
.actual().intersect().aa().rrect(rrect).rect(cutTop).finishElements()
.expect().intersect().aa().rrect(cutRRect).finishElements()
.state(GrClipStack::ClipState::kDeviceRRect)
.finishTest());
// Rect + RRect becomes a rect
SkRect cutRect = {0, 3, 10, 7};
run_test_case(r, TestCase::Build("to-rect", kDeviceBounds)
.actual().intersect().aa().rrect(rrect).rect(cutMid).finishElements()
.expect().intersect().aa().rect(cutRect).finishElements()
.state(GrClipStack::ClipState::kDeviceRect)
.finishTest());
// But they can only combine when the intersecting shape is representable as a [r]rect.
cutRect = {0, 0, 1.5f, 5.f};
run_test_case(r, TestCase::Build("no-combine", kDeviceBounds)
.actual().intersect().aa().rrect(rrect).rect(cutRect).finishElements()
.expectActual()
.state(GrClipStack::ClipState::kComplex)
.finishTest());
}
// Tests that a rect shape is actually pre-clipped to the device bounds
DEF_TEST(GrClipStack_RectDeviceClip, r) {
SkRect crossesDeviceEdge = {20.f, kDeviceBounds.fTop - 13.2f,
kDeviceBounds.fRight + 15.5f, 30.f};
SkRect insideDevice = {20.f, kDeviceBounds.fTop, kDeviceBounds.fRight, 30.f};
run_test_case(r, TestCase::Build("device-aa-rect", kDeviceBounds)
.actual().intersect().aa().rect(crossesDeviceEdge).finishElements()
.expect().intersect().aa().rect(insideDevice).finishElements()
.state(GrClipStack::ClipState::kDeviceRect)
.finishTest());
run_test_case(r, TestCase::Build("device-nonaa-rect", kDeviceBounds)
.actual().intersect().nonAA().rect(crossesDeviceEdge).finishElements()
.expect().intersect().nonAA().rect(insideDevice).finishElements()
.state(GrClipStack::ClipState::kDeviceRect)
.finishTest());
}
// Tests that other shapes' bounds are contained by the device bounds, even if their shape is not.
DEF_TEST(GrClipStack_ShapeDeviceBoundsClip, r) {
SkRect crossesDeviceEdge = {20.f, kDeviceBounds.fTop - 13.2f,
kDeviceBounds.fRight + 15.5f, 30.f};
// RRect
run_test_case(r, TestCase::Build("device-rrect", kDeviceBounds)
.actual().intersect().aa()
.rrect(SkRRect::MakeRectXY(crossesDeviceEdge, 4.f, 4.f))
.finishElements()
.expectActual()
.state(GrClipStack::ClipState::kDeviceRRect)
.finishTest());
// Path
run_test_case(r, TestCase::Build("device-path", kDeviceBounds)
.actual().intersect().aa()
.path(make_octagon(crossesDeviceEdge))
.finishElements()
.expectActual()
.state(GrClipStack::ClipState::kComplex)
.finishTest());
}
// Tests that a simplifiable path turns into a simpler element type
DEF_TEST(GrClipStack_PathSimplify, r) {
// Empty, point, and line paths -> empty
SkPath empty;
run_test_case(r, TestCase::Build("empty", kDeviceBounds)
.actual().path(empty).finishElements()
.state(GrClipStack::ClipState::kEmpty)
.finishTest());
SkPath point;
point.moveTo({0.f, 0.f});
run_test_case(r, TestCase::Build("point", kDeviceBounds)
.actual().path(point).finishElements()
.state(GrClipStack::ClipState::kEmpty)
.finishTest());
SkPath line;
line.moveTo({0.f, 0.f});
line.lineTo({10.f, 5.f});
run_test_case(r, TestCase::Build("line", kDeviceBounds)
.actual().path(line).finishElements()
.state(GrClipStack::ClipState::kEmpty)
.finishTest());
// Rect path -> rect element
SkRect rect = {0.f, 2.f, 10.f, 15.4f};
SkPath rectPath;
rectPath.addRect(rect);
run_test_case(r, TestCase::Build("rect", kDeviceBounds)
.actual().path(rectPath).finishElements()
.expect().rect(rect).finishElements()
.state(GrClipStack::ClipState::kDeviceRect)
.finishTest());
// Oval path -> rrect element
SkPath ovalPath;
ovalPath.addOval(rect);
run_test_case(r, TestCase::Build("oval", kDeviceBounds)
.actual().path(ovalPath).finishElements()
.expect().rrect(SkRRect::MakeOval(rect)).finishElements()
.state(GrClipStack::ClipState::kDeviceRRect)
.finishTest());
// RRect path -> rrect element
SkRRect rrect = SkRRect::MakeRectXY(rect, 2.f, 2.f);
SkPath rrectPath;
rrectPath.addRRect(rrect);
run_test_case(r, TestCase::Build("rrect", kDeviceBounds)
.actual().path(rrectPath).finishElements()
.expect().rrect(rrect).finishElements()
.state(GrClipStack::ClipState::kDeviceRRect)
.finishTest());
}
// Tests that repeated identical clip operations are idempotent
DEF_TEST(GrClipStack_RepeatElement, r) {
// Same rect
SkRect rect = {5.3f, 62.f, 20.f, 85.f};
run_test_case(r, TestCase::Build("same-rects", kDeviceBounds)
.actual().rect(rect).rect(rect).rect(rect).finishElements()
.expect().rect(rect).finishElements()
.state(GrClipStack::ClipState::kDeviceRect)
.finishTest());
SkMatrix lm;
lm.setRotate(30.f, rect.centerX(), rect.centerY());
run_test_case(r, TestCase::Build("same-local-rects", kDeviceBounds)
.actual().localToDevice(lm).rect(rect).rect(rect).rect(rect)
.finishElements()
.expect().localToDevice(lm).rect(rect).finishElements()
.state(GrClipStack::ClipState::kComplex)
.finishTest());
// Same rrect
SkRRect rrect = SkRRect::MakeRectXY(rect, 5.f, 2.5f);
run_test_case(r, TestCase::Build("same-rrects", kDeviceBounds)
.actual().rrect(rrect).rrect(rrect).rrect(rrect).finishElements()
.expect().rrect(rrect).finishElements()
.state(GrClipStack::ClipState::kDeviceRRect)
.finishTest());
run_test_case(r, TestCase::Build("same-local-rrects", kDeviceBounds)
.actual().localToDevice(lm).rrect(rrect).rrect(rrect).rrect(rrect)
.finishElements()
.expect().localToDevice(lm).rrect(rrect).finishElements()
.state(GrClipStack::ClipState::kComplex)
.finishTest());
// Same convex path, by ==
run_test_case(r, TestCase::Build("same-convex", kDeviceBounds)
.actual().path(make_octagon(rect)).path(make_octagon(rect))
.finishElements()
.expect().path(make_octagon(rect)).finishElements()
.state(GrClipStack::ClipState::kComplex)
.finishTest());
run_test_case(r, TestCase::Build("same-local-convex", kDeviceBounds)
.actual().localToDevice(lm)
.path(make_octagon(rect)).path(make_octagon(rect))
.finishElements()
.expect().localToDevice(lm).path(make_octagon(rect))
.finishElements()
.state(GrClipStack::ClipState::kComplex)
.finishTest());
// Same complicated path by gen-id but not ==
SkPath path; // an hour glass
path.moveTo({0.f, 0.f});
path.lineTo({20.f, 20.f});
path.lineTo({0.f, 20.f});
path.lineTo({20.f, 0.f});
path.close();
run_test_case(r, TestCase::Build("same-path", kDeviceBounds)
.actual().path(path).path(path).path(path).finishElements()
.expect().path(path).finishElements()
.state(GrClipStack::ClipState::kComplex)
.finishTest());
run_test_case(r, TestCase::Build("same-local-path", kDeviceBounds)
.actual().localToDevice(lm)
.path(path).path(path).path(path).finishElements()
.expect().localToDevice(lm).path(path)
.finishElements()
.state(GrClipStack::ClipState::kComplex)
.finishTest());
}
// Tests that inverse-filled paths are canonicalized to a regular fill and a swapped clip op
DEF_TEST(GrClipStack_InverseFilledPath, r) {
SkRect rect = {0.f, 0.f, 16.f, 17.f};
SkPath rectPath;
rectPath.addRect(rect);
SkPath inverseRectPath = rectPath;
inverseRectPath.toggleInverseFillType();
SkPath complexPath = make_octagon(rect);
SkPath inverseComplexPath = complexPath;
inverseComplexPath.toggleInverseFillType();
// Inverse filled rect + intersect -> diff rect
run_test_case(r, TestCase::Build("inverse-rect-intersect", kDeviceBounds)
.actual().aa().intersect().path(inverseRectPath).finishElements()
.expect().aa().difference().rect(rect).finishElements()
.state(GrClipStack::ClipState::kComplex)
.finishTest());
// Inverse filled rect + difference -> int. rect
run_test_case(r, TestCase::Build("inverse-rect-difference", kDeviceBounds)
.actual().aa().difference().path(inverseRectPath).finishElements()
.expect().aa().intersect().rect(rect).finishElements()
.state(GrClipStack::ClipState::kDeviceRect)
.finishTest());
// Inverse filled path + intersect -> diff path
run_test_case(r, TestCase::Build("inverse-path-intersect", kDeviceBounds)
.actual().aa().intersect().path(inverseComplexPath).finishElements()
.expect().aa().difference().path(complexPath).finishElements()
.state(GrClipStack::ClipState::kComplex)
.finishTest());
// Inverse filled path + difference -> int. path
run_test_case(r, TestCase::Build("inverse-path-difference", kDeviceBounds)
.actual().aa().difference().path(inverseComplexPath).finishElements()
.expect().aa().intersect().path(complexPath).finishElements()
.state(GrClipStack::ClipState::kComplex)
.finishTest());
}
// Tests that clip operations that are offscreen either make the clip empty or stay wide open
DEF_TEST(GrClipStack_Offscreen, r) {
SkRect offscreenRect = {kDeviceBounds.fRight + 10.f, kDeviceBounds.fTop + 20.f,
kDeviceBounds.fRight + 40.f, kDeviceBounds.fTop + 60.f};
SkASSERT(!offscreenRect.intersects(SkRect::Make(kDeviceBounds)));
SkRRect offscreenRRect = SkRRect::MakeRectXY(offscreenRect, 5.f, 5.f);
SkPath offscreenPath = make_octagon(offscreenRect);
// Intersect -> empty
run_test_case(r, TestCase::Build("intersect-combo", kDeviceBounds)
.actual().aa().intersect()
.rect(offscreenRect)
.rrect(offscreenRRect)
.path(offscreenPath)
.finishElements()
.state(GrClipStack::ClipState::kEmpty)
.finishTest());
run_test_case(r, TestCase::Build("intersect-rect", kDeviceBounds)
.actual().aa().intersect()
.rect(offscreenRect)
.finishElements()
.state(GrClipStack::ClipState::kEmpty)
.finishTest());
run_test_case(r, TestCase::Build("intersect-rrect", kDeviceBounds)
.actual().aa().intersect()
.rrect(offscreenRRect)
.finishElements()
.state(GrClipStack::ClipState::kEmpty)
.finishTest());
run_test_case(r, TestCase::Build("intersect-path", kDeviceBounds)
.actual().aa().intersect()
.path(offscreenPath)
.finishElements()
.state(GrClipStack::ClipState::kEmpty)
.finishTest());
// Difference -> wide open
run_test_case(r, TestCase::Build("difference-combo", kDeviceBounds)
.actual().aa().difference()
.rect(offscreenRect)
.rrect(offscreenRRect)
.path(offscreenPath)
.finishElements()
.state(GrClipStack::ClipState::kWideOpen)
.finishTest());
run_test_case(r, TestCase::Build("difference-rect", kDeviceBounds)
.actual().aa().difference()
.rect(offscreenRect)
.finishElements()
.state(GrClipStack::ClipState::kWideOpen)
.finishTest());
run_test_case(r, TestCase::Build("difference-rrect", kDeviceBounds)
.actual().aa().difference()
.rrect(offscreenRRect)
.finishElements()
.state(GrClipStack::ClipState::kWideOpen)
.finishTest());
run_test_case(r, TestCase::Build("difference-path", kDeviceBounds)
.actual().aa().difference()
.path(offscreenPath)
.finishElements()
.state(GrClipStack::ClipState::kWideOpen)
.finishTest());
}
// Tests that an empty shape updates the clip state directly without needing an element
DEF_TEST(GrClipStack_EmptyShape, r) {
// Intersect -> empty
run_test_case(r, TestCase::Build("empty-intersect", kDeviceBounds)
.actual().intersect().rect(SkRect::MakeEmpty()).finishElements()
.state(GrClipStack::ClipState::kEmpty)
.finishTest());
// Difference -> no-op
run_test_case(r, TestCase::Build("empty-difference", kDeviceBounds)
.actual().difference().rect(SkRect::MakeEmpty()).finishElements()
.state(GrClipStack::ClipState::kWideOpen)
.finishTest());
SkRRect rrect = SkRRect::MakeRectXY({4.f, 10.f, 16.f, 32.f}, 2.f, 2.f);
run_test_case(r, TestCase::Build("noop-difference", kDeviceBounds)
.actual().difference().rrect(rrect).rect(SkRect::MakeEmpty())
.finishElements()
.expect().difference().rrect(rrect).finishElements()
.state(GrClipStack::ClipState::kComplex)
.finishTest());
}
// Tests that sufficiently large difference operations can shrink the conservative bounds
DEF_TEST(GrClipStack_DifferenceBounds, r) {
SkRect rightSide = {50.f, -10.f, 2.f * kDeviceBounds.fRight, kDeviceBounds.fBottom + 10.f};
SkRect clipped = rightSide;
SkAssertResult(clipped.intersect(SkRect::Make(kDeviceBounds)));
run_test_case(r, TestCase::Build("difference-cut", kDeviceBounds)
.actual().nonAA().difference().rect(rightSide).finishElements()
.expect().nonAA().difference().rect(clipped).finishElements()
.state(GrClipStack::ClipState::kComplex)
.finishTest());
}
// Tests that intersections can combine even if there's a difference operation in the middle
DEF_TEST(GrClipStack_NoDifferenceInterference, r) {
SkRect intR1 = {0.f, 0.f, 30.f, 30.f};
SkRect intR2 = {15.f, 15.f, 45.f, 45.f};
SkRect intCombo = {15.f, 15.f, 30.f, 30.f};
SkRect diff = {20.f, 6.f, 50.f, 50.f};
run_test_case(r, TestCase::Build("cross-diff-combine", kDeviceBounds)
.actual().rect(intR1, GrAA::kYes, SkClipOp::kIntersect)
.rect(diff, GrAA::kYes, SkClipOp::kDifference)
.rect(intR2, GrAA::kYes, SkClipOp::kIntersect)
.finishElements()
.expect().rect(intCombo, GrAA::kYes, SkClipOp::kIntersect)
.rect(diff, GrAA::kYes, SkClipOp::kDifference)
.finishElements()
.state(GrClipStack::ClipState::kComplex)
.finishTest());
}
// Tests that multiple path operations are all recorded, but not otherwise consolidated
DEF_TEST(GrClipStack_MultiplePaths, r) {
// Chosen to be greater than the number of inline-allocated elements and save records of the
// GrClipStack so that we test heap allocation as well.
static constexpr int kNumOps = 16;
auto b = TestCase::Build("many-paths-difference", kDeviceBounds);
SkRect d = {0.f, 0.f, 12.f, 12.f};
for (int i = 0; i < kNumOps; ++i) {
b.actual().path(make_octagon(d), GrAA::kNo, SkClipOp::kDifference);
d.offset(15.f, 0.f);
if (d.fRight > kDeviceBounds.fRight) {
d.fLeft = 0.f;
d.fRight = 12.f;
d.offset(0.f, 15.f);
}
}
run_test_case(r, b.expectActual()
.state(GrClipStack::ClipState::kComplex)
.finishTest());
b = TestCase::Build("many-paths-intersect", kDeviceBounds);
d = {0.f, 0.f, 12.f, 12.f};
for (int i = 0; i < kNumOps; ++i) {
b.actual().path(make_octagon(d), GrAA::kYes, SkClipOp::kIntersect);
d.offset(0.01f, 0.01f);
}
run_test_case(r, b.expectActual()
.state(GrClipStack::ClipState::kComplex)
.finishTest());
}
// Tests that a single rect is treated as kDeviceRect state when it's axis-aligned and intersect.
DEF_TEST(GrClipStack_DeviceRect, r) {
// Axis-aligned + intersect -> kDeviceRect
SkRect rect = {0, 0, 20, 20};
run_test_case(r, TestCase::Build("device-rect", kDeviceBounds)
.actual().intersect().aa().rect(rect).finishElements()
.expectActual()
.state(GrClipStack::ClipState::kDeviceRect)
.finishTest());
// Not axis-aligned -> kComplex
SkMatrix lm = SkMatrix::RotateDeg(15.f);
run_test_case(r, TestCase::Build("unaligned-rect", kDeviceBounds)
.actual().localToDevice(lm).intersect().aa().rect(rect)
.finishElements()
.expectActual()
.state(GrClipStack::ClipState::kComplex)
.finishTest());
// Not intersect -> kComplex
run_test_case(r, TestCase::Build("diff-rect", kDeviceBounds)
.actual().difference().aa().rect(rect).finishElements()
.expectActual()
.state(GrClipStack::ClipState::kComplex)
.finishTest());
}
// Tests that a single rrect is treated as kDeviceRRect state when it's axis-aligned and intersect.
DEF_TEST(GrClipStack_DeviceRRect, r) {
// Axis-aligned + intersect -> kDeviceRRect
SkRect rect = {0, 0, 20, 20};
SkRRect rrect = SkRRect::MakeRectXY(rect, 5.f, 5.f);
run_test_case(r, TestCase::Build("device-rrect", kDeviceBounds)
.actual().intersect().aa().rrect(rrect).finishElements()
.expectActual()
.state(GrClipStack::ClipState::kDeviceRRect)
.finishTest());
// Not axis-aligned -> kComplex
SkMatrix lm = SkMatrix::RotateDeg(15.f);
run_test_case(r, TestCase::Build("unaligned-rrect", kDeviceBounds)
.actual().localToDevice(lm).intersect().aa().rrect(rrect)
.finishElements()
.expectActual()
.state(GrClipStack::ClipState::kComplex)
.finishTest());
// Not intersect -> kComplex
run_test_case(r, TestCase::Build("diff-rrect", kDeviceBounds)
.actual().difference().aa().rrect(rrect).finishElements()
.expectActual()
.state(GrClipStack::ClipState::kComplex)
.finishTest());
}
// Tests that scale+translate matrices are pre-applied to rects and rrects, which also then allows
// elements with different scale+translate matrices to be consolidated as if they were in the same
// coordinate space.
DEF_TEST(GrClipStack_ScaleTranslate, r) {
SkMatrix lm = SkMatrix::Scale(2.f, 4.f);
lm.postTranslate(15.5f, 14.3f);
// Rect -> matrix is applied up front
SkRect rect = {0.f, 0.f, 10.f, 10.f};
run_test_case(r, TestCase::Build("st+rect", kDeviceBounds)
.actual().rect(rect, lm, GrAA::kYes, SkClipOp::kIntersect)
.finishElements()
.expect().rect(lm.mapRect(rect), GrAA::kYes, SkClipOp::kIntersect)
.finishElements()
.state(GrClipStack::ClipState::kDeviceRect)
.finishTest());
// RRect -> matrix is applied up front
SkRRect localRRect = SkRRect::MakeRectXY(rect, 2.f, 2.f);
SkRRect deviceRRect;
SkAssertResult(localRRect.transform(lm, &deviceRRect));
run_test_case(r, TestCase::Build("st+rrect", kDeviceBounds)
.actual().rrect(localRRect, lm, GrAA::kYes, SkClipOp::kIntersect)
.finishElements()
.expect().rrect(deviceRRect, GrAA::kYes, SkClipOp::kIntersect)
.finishElements()
.state(GrClipStack::ClipState::kDeviceRRect)
.finishTest());
// Path -> matrix is NOT applied
run_test_case(r, TestCase::Build("st+path", kDeviceBounds)
.actual().intersect().localToDevice(lm).path(make_octagon(rect))
.finishElements()
.expectActual()
.state(GrClipStack::ClipState::kComplex)
.finishTest());
}
// Tests that a convex path element can contain a rect or round rect, allowing the stack to be
// simplified
DEF_TEST(GrClipStack_ConvexPathContains, r) {
SkRect rect = {15.f, 15.f, 30.f, 30.f};
SkRRect rrect = SkRRect::MakeRectXY(rect, 5.f, 5.f);
SkPath bigPath = make_octagon(rect.makeOutset(10.f, 10.f), 5.f, 5.f);
// Intersect -> path element isn't kept
run_test_case(r, TestCase::Build("convex+rect-intersect", kDeviceBounds)
.actual().aa().intersect().rect(rect).path(bigPath).finishElements()
.expect().aa().intersect().rect(rect).finishElements()
.state(GrClipStack::ClipState::kDeviceRect)
.finishTest());
run_test_case(r, TestCase::Build("convex+rrect-intersect", kDeviceBounds)
.actual().aa().intersect().rrect(rrect).path(bigPath).finishElements()
.expect().aa().intersect().rrect(rrect).finishElements()
.state(GrClipStack::ClipState::kDeviceRRect)
.finishTest());
// Difference -> path element is the only one left
run_test_case(r, TestCase::Build("convex+rect-difference", kDeviceBounds)
.actual().aa().difference().rect(rect).path(bigPath).finishElements()
.expect().aa().difference().path(bigPath).finishElements()
.state(GrClipStack::ClipState::kComplex)
.finishTest());
run_test_case(r, TestCase::Build("convex+rrect-difference", kDeviceBounds)
.actual().aa().difference().rrect(rrect).path(bigPath)
.finishElements()
.expect().aa().difference().path(bigPath).finishElements()
.state(GrClipStack::ClipState::kComplex)
.finishTest());
// Intersect small shape + difference big path -> empty
run_test_case(r, TestCase::Build("convex-diff+rect-int", kDeviceBounds)
.actual().aa().intersect().rect(rect)
.difference().path(bigPath).finishElements()
.state(GrClipStack::ClipState::kEmpty)
.finishTest());
run_test_case(r, TestCase::Build("convex-diff+rrect-int", kDeviceBounds)
.actual().aa().intersect().rrect(rrect)
.difference().path(bigPath).finishElements()
.state(GrClipStack::ClipState::kEmpty)
.finishTest());
// Diff small shape + intersect big path -> both
run_test_case(r, TestCase::Build("convex-int+rect-diff", kDeviceBounds)
.actual().aa().intersect().path(bigPath).difference().rect(rect)
.finishElements()
.expectActual()
.state(GrClipStack::ClipState::kComplex)
.finishTest());
run_test_case(r, TestCase::Build("convex-int+rrect-diff", kDeviceBounds)
.actual().aa().intersect().path(bigPath).difference().rrect(rrect)
.finishElements()
.expectActual()
.state(GrClipStack::ClipState::kComplex)
.finishTest());
}
// Tests that rects/rrects in different coordinate spaces can be consolidated when one is fully
// contained by the other.
DEF_TEST(GrClipStack_NonAxisAlignedContains, r) {
SkMatrix lm1 = SkMatrix::RotateDeg(45.f);
SkRect bigR = {-20.f, -20.f, 20.f, 20.f};
SkRRect bigRR = SkRRect::MakeRectXY(bigR, 1.f, 1.f);
SkMatrix lm2 = SkMatrix::RotateDeg(-45.f);
SkRect smR = {-10.f, -10.f, 10.f, 10.f};
SkRRect smRR = SkRRect::MakeRectXY(smR, 1.f, 1.f);
// I+I should select the smaller 2nd shape (r2 or rr2)
run_test_case(r, TestCase::Build("rect-rect-ii", kDeviceBounds)
.actual().rect(bigR, lm1, GrAA::kYes, SkClipOp::kIntersect)
.rect(smR, lm2, GrAA::kYes, SkClipOp::kIntersect)
.finishElements()
.expect().rect(smR, lm2, GrAA::kYes, SkClipOp::kIntersect)
.finishElements()
.state(GrClipStack::ClipState::kComplex)
.finishTest());
run_test_case(r, TestCase::Build("rrect-rrect-ii", kDeviceBounds)
.actual().rrect(bigRR, lm1, GrAA::kYes, SkClipOp::kIntersect)
.rrect(smRR, lm2, GrAA::kYes, SkClipOp::kIntersect)
.finishElements()
.expect().rrect(smRR, lm2, GrAA::kYes, SkClipOp::kIntersect)
.finishElements()
.state(GrClipStack::ClipState::kComplex)
.finishTest());
run_test_case(r, TestCase::Build("rect-rrect-ii", kDeviceBounds)
.actual().rect(bigR, lm1, GrAA::kYes, SkClipOp::kIntersect)
.rrect(smRR, lm2, GrAA::kYes, SkClipOp::kIntersect)
.finishElements()
.expect().rrect(smRR, lm2, GrAA::kYes, SkClipOp::kIntersect)
.finishElements()
.state(GrClipStack::ClipState::kComplex)
.finishTest());
run_test_case(r, TestCase::Build("rrect-rect-ii", kDeviceBounds)
.actual().rrect(bigRR, lm1, GrAA::kYes, SkClipOp::kIntersect)
.rect(smR, lm2, GrAA::kYes, SkClipOp::kIntersect)
.finishElements()
.expect().rect(smR, lm2, GrAA::kYes, SkClipOp::kIntersect)
.finishElements()
.state(GrClipStack::ClipState::kComplex)
.finishTest());
// D+D should select the larger shape (r1 or rr1)
run_test_case(r, TestCase::Build("rect-rect-dd", kDeviceBounds)
.actual().rect(bigR, lm1, GrAA::kYes, SkClipOp::kDifference)
.rect(smR, lm2, GrAA::kYes, SkClipOp::kDifference)
.finishElements()
.expect().rect(bigR, lm1, GrAA::kYes, SkClipOp::kDifference)
.finishElements()
.state(GrClipStack::ClipState::kComplex)
.finishTest());
run_test_case(r, TestCase::Build("rrect-rrect-dd", kDeviceBounds)
.actual().rrect(bigRR, lm1, GrAA::kYes, SkClipOp::kDifference)
.rrect(smRR, lm2, GrAA::kYes, SkClipOp::kDifference)
.finishElements()
.expect().rrect(bigRR, lm1, GrAA::kYes, SkClipOp::kDifference)
.finishElements()
.state(GrClipStack::ClipState::kComplex)
.finishTest());
run_test_case(r, TestCase::Build("rect-rrect-dd", kDeviceBounds)
.actual().rect(bigR, lm1, GrAA::kYes, SkClipOp::kDifference)
.rrect(smRR, lm2, GrAA::kYes, SkClipOp::kDifference)
.finishElements()
.expect().rect(bigR, lm1, GrAA::kYes, SkClipOp::kDifference)
.finishElements()
.state(GrClipStack::ClipState::kComplex)
.finishTest());
run_test_case(r, TestCase::Build("rrect-rect-dd", kDeviceBounds)
.actual().rrect(bigRR, lm1, GrAA::kYes, SkClipOp::kDifference)
.rect(smR, lm2, GrAA::kYes, SkClipOp::kDifference)
.finishElements()
.expect().rrect(bigRR, lm1, GrAA::kYes, SkClipOp::kDifference)
.finishElements()
.state(GrClipStack::ClipState::kComplex)
.finishTest());
// D(1)+I(2) should result in empty
run_test_case(r, TestCase::Build("rectD-rectI", kDeviceBounds)
.actual().rect(bigR, lm1, GrAA::kYes, SkClipOp::kDifference)
.rect(smR, lm2, GrAA::kYes, SkClipOp::kIntersect)
.finishElements()
.state(GrClipStack::ClipState::kEmpty)
.finishTest());
run_test_case(r, TestCase::Build("rrectD-rrectI", kDeviceBounds)
.actual().rrect(bigRR, lm1, GrAA::kYes, SkClipOp::kDifference)
.rrect(smRR, lm2, GrAA::kYes, SkClipOp::kIntersect)
.finishElements()
.state(GrClipStack::ClipState::kEmpty)
.finishTest());
run_test_case(r, TestCase::Build("rectD-rrectI", kDeviceBounds)
.actual().rect(bigR, lm1, GrAA::kYes, SkClipOp::kDifference)
.rrect(smRR, lm2, GrAA::kYes, SkClipOp::kIntersect)
.finishElements()
.state(GrClipStack::ClipState::kEmpty)
.finishTest());
run_test_case(r, TestCase::Build("rrectD-rectI", kDeviceBounds)
.actual().rrect(bigRR, lm1, GrAA::kYes, SkClipOp::kDifference)
.rect(smR, lm2, GrAA::kYes, SkClipOp::kIntersect)
.finishElements()
.state(GrClipStack::ClipState::kEmpty)
.finishTest());
// I(1)+D(2) should result in both shapes
run_test_case(r, TestCase::Build("rectI+rectD", kDeviceBounds)
.actual().rect(bigR, lm1, GrAA::kYes, SkClipOp::kIntersect)
.rect(smR, lm2, GrAA::kYes, SkClipOp::kDifference)
.finishElements()
.expectActual()
.state(GrClipStack::ClipState::kComplex)
.finishTest());
run_test_case(r, TestCase::Build("rrectI+rrectD", kDeviceBounds)
.actual().rrect(bigRR, lm1, GrAA::kYes, SkClipOp::kIntersect)
.rrect(smRR, lm2, GrAA::kYes, SkClipOp::kDifference)
.finishElements()
.expectActual()
.state(GrClipStack::ClipState::kComplex)
.finishTest());
run_test_case(r, TestCase::Build("rrectI+rectD", kDeviceBounds)
.actual().rrect(bigRR, lm1, GrAA::kYes, SkClipOp::kIntersect)
.rect(smR, lm2, GrAA::kYes, SkClipOp::kDifference)
.finishElements()
.expectActual()
.state(GrClipStack::ClipState::kComplex)
.finishTest());
run_test_case(r, TestCase::Build("rectI+rrectD", kDeviceBounds)
.actual().rect(bigR, lm1, GrAA::kYes, SkClipOp::kIntersect)
.rrect(smRR, lm2, GrAA::kYes, SkClipOp::kDifference)
.finishElements()
.expectActual()
.state(GrClipStack::ClipState::kComplex)
.finishTest());
}
// Tests that shapes with mixed AA state that contain each other can still be consolidated,
// unless they are too close to the edge and non-AA snapping can't be predicted
DEF_TEST(GrClipStack_MixedAAContains, r) {
SkMatrix lm1 = SkMatrix::RotateDeg(45.f);
SkRect r1 = {-20.f, -20.f, 20.f, 20.f};
SkMatrix lm2 = SkMatrix::RotateDeg(-45.f);
SkRect r2Safe = {-10.f, -10.f, 10.f, 10.f};
SkRect r2Unsafe = {-19.5f, -19.5f, 19.5f, 19.5f};
// Non-AA sufficiently inside AA element can discard the outer AA element
run_test_case(r, TestCase::Build("mixed-outeraa-combine", kDeviceBounds)
.actual().rect(r1, lm1, GrAA::kYes, SkClipOp::kIntersect)
.rect(r2Safe, lm2, GrAA::kNo, SkClipOp::kIntersect)
.finishElements()
.expect().rect(r2Safe, lm2, GrAA::kNo, SkClipOp::kIntersect)
.finishElements()
.state(GrClipStack::ClipState::kComplex)
.finishTest());
// Vice versa
run_test_case(r, TestCase::Build("mixed-inneraa-combine", kDeviceBounds)
.actual().rect(r1, lm1, GrAA::kNo, SkClipOp::kIntersect)
.rect(r2Safe, lm2, GrAA::kYes, SkClipOp::kIntersect)
.finishElements()
.expect().rect(r2Safe, lm2, GrAA::kYes, SkClipOp::kIntersect)
.finishElements()
.state(GrClipStack::ClipState::kComplex)
.finishTest());
// Non-AA too close to AA edges keeps both
run_test_case(r, TestCase::Build("mixed-outeraa-nocombine", kDeviceBounds)
.actual().rect(r1, lm1, GrAA::kYes, SkClipOp::kIntersect)
.rect(r2Unsafe, lm2, GrAA::kNo, SkClipOp::kIntersect)
.finishElements()
.expectActual()
.state(GrClipStack::ClipState::kComplex)
.finishTest());
run_test_case(r, TestCase::Build("mixed-inneraa-nocombine", kDeviceBounds)
.actual().rect(r1, lm1, GrAA::kNo, SkClipOp::kIntersect)
.rect(r2Unsafe, lm2, GrAA::kYes, SkClipOp::kIntersect)
.finishElements()
.expectActual()
.state(GrClipStack::ClipState::kComplex)
.finishTest());
}
// Tests that a shape that contains the device bounds updates the clip state directly
DEF_TEST(GrClipStack_ShapeContainsDevice, r) {
SkRect rect = SkRect::Make(kDeviceBounds).makeOutset(10.f, 10.f);
SkRRect rrect = SkRRect::MakeRectXY(rect, 10.f, 10.f);
SkPath convex = make_octagon(rect, 10.f, 10.f);
// Intersect -> no-op
run_test_case(r, TestCase::Build("rect-intersect", kDeviceBounds)
.actual().intersect().rect(rect).finishElements()
.state(GrClipStack::ClipState::kWideOpen)
.finishTest());
run_test_case(r, TestCase::Build("rrect-intersect", kDeviceBounds)
.actual().intersect().rrect(rrect).finishElements()
.state(GrClipStack::ClipState::kWideOpen)
.finishTest());
run_test_case(r, TestCase::Build("convex-intersect", kDeviceBounds)
.actual().intersect().path(convex).finishElements()
.state(GrClipStack::ClipState::kWideOpen)
.finishTest());
// Difference -> empty
run_test_case(r, TestCase::Build("rect-difference", kDeviceBounds)
.actual().difference().rect(rect).finishElements()
.state(GrClipStack::ClipState::kEmpty)
.finishTest());
run_test_case(r, TestCase::Build("rrect-difference", kDeviceBounds)
.actual().difference().rrect(rrect).finishElements()
.state(GrClipStack::ClipState::kEmpty)
.finishTest());
run_test_case(r, TestCase::Build("convex-difference", kDeviceBounds)
.actual().difference().path(convex).finishElements()
.state(GrClipStack::ClipState::kEmpty)
.finishTest());
}
// Tests that shapes that do not overlap make for an empty clip (when intersecting), pick just the
// intersecting op (when mixed), or are all kept (when diff'ing).
DEF_TEST(GrClipStack_DisjointShapes, r) {
SkRect rt = {10.f, 10.f, 20.f, 20.f};
SkRRect rr = SkRRect::MakeOval(rt.makeOffset({20.f, 0.f}));
SkPath p = make_octagon(rt.makeOffset({0.f, 20.f}));
// I+I
run_test_case(r, TestCase::Build("iii", kDeviceBounds)
.actual().aa().intersect().rect(rt).rrect(rr).path(p).finishElements()
.state(GrClipStack::ClipState::kEmpty)
.finishTest());
// D+D
run_test_case(r, TestCase::Build("ddd", kDeviceBounds)
.actual().nonAA().difference().rect(rt).rrect(rr).path(p)
.finishElements()
.expectActual()
.state(GrClipStack::ClipState::kComplex)
.finishTest());
// I+D from rect
run_test_case(r, TestCase::Build("idd", kDeviceBounds)
.actual().aa().intersect().rect(rt)
.nonAA().difference().rrect(rr).path(p)
.finishElements()
.expect().aa().intersect().rect(rt).finishElements()
.state(GrClipStack::ClipState::kDeviceRect)
.finishTest());
// I+D from rrect
run_test_case(r, TestCase::Build("did", kDeviceBounds)
.actual().aa().intersect().rrect(rr)
.nonAA().difference().rect(rt).path(p)
.finishElements()
.expect().aa().intersect().rrect(rr).finishElements()
.state(GrClipStack::ClipState::kDeviceRRect)
.finishTest());
// I+D from path
run_test_case(r, TestCase::Build("ddi", kDeviceBounds)
.actual().aa().intersect().path(p)
.nonAA().difference().rect(rt).rrect(rr)
.finishElements()
.expect().aa().intersect().path(p).finishElements()
.state(GrClipStack::ClipState::kComplex)
.finishTest());
}
DEF_TEST(GrClipStack_ComplexClip, r) {
static constexpr float kN = 10.f;
static constexpr float kR = kN / 3.f;
// 4 rectangles that overlap by kN x 2kN (horiz), 2kN x kN (vert), or kN x kN (diagonal)
static const SkRect kTL = {0.f, 0.f, 2.f * kN, 2.f * kN};
static const SkRect kTR = {kN, 0.f, 3.f * kN, 2.f * kN};
static const SkRect kBL = {0.f, kN, 2.f * kN, 3.f * kN};
static const SkRect kBR = {kN, kN, 3.f * kN, 3.f * kN};
enum ShapeType { kRect, kRRect, kConvex };
SkRect rects[] = { kTL, kTR, kBL, kBR };
for (ShapeType type : { kRect, kRRect, kConvex }) {
for (int opBits = 6; opBits < 16; ++opBits) {
SkString name;
name.appendf("complex-%d-%d", (int) type, opBits);
SkRect expectedRectIntersection = SkRect::Make(kDeviceBounds);
SkRRect expectedRRectIntersection = SkRRect::MakeRect(expectedRectIntersection);
auto b = TestCase::Build(name.c_str(), kDeviceBounds);
for (int i = 0; i < 4; ++i) {
SkClipOp op = (opBits & (1 << i)) ? SkClipOp::kIntersect : SkClipOp::kDifference;
switch(type) {
case kRect: {
SkRect r = rects[i];
if (op == SkClipOp::kDifference) {
// Shrink the rect for difference ops, otherwise in the rect testcase
// any difference op would remove the intersection of the other ops
// given how the rects are defined, and that's just not interesting.
r.inset(kR, kR);
}
b.actual().rect(r, GrAA::kYes, op);
if (op == SkClipOp::kIntersect) {
SkAssertResult(expectedRectIntersection.intersect(r));
} else {
b.expect().rect(r, GrAA::kYes, SkClipOp::kDifference);
}
break; }
case kRRect: {
SkRRect rrect = SkRRect::MakeRectXY(rects[i], kR, kR);
b.actual().rrect(rrect, GrAA::kYes, op);
if (op == SkClipOp::kIntersect) {
expectedRRectIntersection = SkRRectPriv::ConservativeIntersect(
expectedRRectIntersection, rrect);
SkASSERT(!expectedRRectIntersection.isEmpty());
} else {
b.expect().rrect(rrect, GrAA::kYes, SkClipOp::kDifference);
}
break; }
case kConvex:
b.actual().path(make_octagon(rects[i], kR, kR), GrAA::kYes, op);
// NOTE: We don't set any expectations here, since convex just calls
// expectActual() at the end.
break;
}
}
// The expectations differ depending on the shape type
GrClipStack::ClipState state = GrClipStack::ClipState::kComplex;
if (type == kConvex) {
// The simplest case is when the paths cannot be combined together, so we expect
// the actual elements to be unmodified (both intersect and difference).
b.expectActual();
} else if (opBits) {
// All intersection ops were pre-computed into expectedR[R]ectIntersection
// - difference ops already added in the for loop
if (type == kRect) {
SkASSERT(expectedRectIntersection != SkRect::Make(kDeviceBounds) &&
!expectedRectIntersection.isEmpty());
b.expect().rect(expectedRectIntersection, GrAA::kYes, SkClipOp::kIntersect);
if (opBits == 0xf) {
state = GrClipStack::ClipState::kDeviceRect;
}
} else {
SkASSERT(expectedRRectIntersection !=
SkRRect::MakeRect(SkRect::Make(kDeviceBounds)) &&
!expectedRRectIntersection.isEmpty());
b.expect().rrect(expectedRRectIntersection, GrAA::kYes, SkClipOp::kIntersect);
if (opBits == 0xf) {
state = GrClipStack::ClipState::kDeviceRRect;
}
}
}
run_test_case(r, b.state(state).finishTest());
}
}
}
// ///////////////////////////////////////////////////////////////////////////////
// // These tests do not use the TestCase infrastructure and manipulate a
// // GrClipStack directly.
// Tests that replaceClip() works as expected across save/restores
DEF_TEST(GrClipStack_ReplaceClip, r) {
GrClipStack cs(kDeviceBounds, nullptr, false);
SkRRect rrect = SkRRect::MakeRectXY({15.f, 12.25f, 40.3f, 23.5f}, 4.f, 6.f);
cs.clipRRect(SkMatrix::I(), rrect, GrAA::kYes, SkClipOp::kIntersect);
SkIRect replace = {50, 25, 75, 40}; // Is disjoint from the rrect element
cs.save();
cs.replaceClip(replace);
REPORTER_ASSERT(r, cs.clipState() == GrClipStack::ClipState::kDeviceRect,
"Clip did not become a device rect");
REPORTER_ASSERT(r, cs.getConservativeBounds() == replace, "Unexpected replaced clip bounds");
const GrClipStack::Element& replaceElement = *cs.begin();
REPORTER_ASSERT(r, replaceElement.fShape.rect() == SkRect::Make(replace) &&
replaceElement.fAA == GrAA::kNo &&
replaceElement.fOp == SkClipOp::kIntersect &&
replaceElement.fLocalToDevice == SkMatrix::I(),
"Unexpected replace element state");
// Restore should undo the replaced clip and bring back the rrect
cs.restore();
REPORTER_ASSERT(r, cs.clipState() == GrClipStack::ClipState::kDeviceRRect,
"Unexpected state after restore, not kDeviceRRect");
const GrClipStack::Element& rrectElem = *cs.begin();
REPORTER_ASSERT(r, rrectElem.fShape.rrect() == rrect &&
rrectElem.fAA == GrAA::kYes &&
rrectElem.fOp == SkClipOp::kIntersect &&
rrectElem.fLocalToDevice == SkMatrix::I(),
"RRect element state not restored properly after replace clip undone");
}
// Tests that when a stack is forced to always be AA, non-AA elements become AA
DEF_TEST(GrClipStack_ForceAA, r) {
GrClipStack cs(kDeviceBounds, nullptr, true);
// AA will remain AA
SkRect aaRect = {0.25f, 12.43f, 25.2f, 23.f};
cs.clipRect(SkMatrix::I(), aaRect, GrAA::kYes, SkClipOp::kIntersect);
// Non-AA will become AA
SkPath nonAAPath = make_octagon({2.f, 10.f, 16.f, 20.f});
cs.clipPath(SkMatrix::I(), nonAAPath, GrAA::kNo, SkClipOp::kIntersect);
// Non-AA can combine with AA that wouldn't normally have combined
SkRect nonAARect = {4.5f, 5.f, 17.25f, 18.23f};
cs.clipRect(SkMatrix::I(), nonAARect, GrAA::kNo, SkClipOp::kIntersect);
// The stack reports elements newest first, but the non-AA rect op was combined in place with
// the first aa rect, so we should see nonAAPath as AA, and then the intersection of rects.
SkRect expectedRect = aaRect;
SkAssertResult(expectedRect.intersect(nonAARect));
auto elements = cs.begin();
const GrClipStack::Element& aaPath = *elements;
REPORTER_ASSERT(r, aaPath.fShape.path() == nonAAPath, "Expected path element");
REPORTER_ASSERT(r, aaPath.fAA == GrAA::kYes, "Path element not promoted to AA");
++elements;
const GrClipStack::Element& rect = *elements;
REPORTER_ASSERT(r, rect.fShape.rect() == expectedRect, "Mixed AA rects did not combine");
REPORTER_ASSERT(r, rect.fAA == GrAA::kYes, "Rect elements not promoted to AA");
++elements;
REPORTER_ASSERT(r, !(elements != cs.end()), "Expected only two clip elements");
}
// Tests preApply works as expected for device rects, rrects, and reports clipped-out, etc. as
// expected.
DEF_TEST(GrClipStack_PreApply, r) {
GrClipStack cs(kDeviceBounds, nullptr, false);
// Offscreen is kClippedOut
GrClip::PreClipResult result = cs.preApply({-10.f, -10.f, -1.f, -1.f}, GrAA::kYes);
REPORTER_ASSERT(r, result.fEffect == GrClip::Effect::kClippedOut,
"Offscreen draw is kClippedOut");
// Intersecting screen with wide-open clip is kUnclipped
result = cs.preApply({-10.f, -10.f, 10.f, 10.f}, GrAA::kYes);
REPORTER_ASSERT(r, result.fEffect == GrClip::Effect::kUnclipped,
"Wide open screen intersection is still kUnclipped");
// Empty clip is clipped out
cs.save();
cs.clipRect(SkMatrix::I(), SkRect::MakeEmpty(), GrAA::kNo, SkClipOp::kIntersect);
result = cs.preApply({0.f, 0.f, 20.f, 20.f}, GrAA::kYes);
REPORTER_ASSERT(r, result.fEffect == GrClip::Effect::kClippedOut,
"Empty clip stack preApplies as kClippedOut");
cs.restore();
// Contained inside clip is kUnclipped (using rrect for the outer clip element since paths
// don't support an inner bounds and anything complex is otherwise skipped in preApply).
SkRect rect = {10.f, 10.f, 40.f, 40.f};
SkRRect bigRRect = SkRRect::MakeRectXY(rect.makeOutset(5.f, 5.f), 5.f, 5.f);
cs.save();
cs.clipRRect(SkMatrix::I(), bigRRect, GrAA::kYes, SkClipOp::kIntersect);
result = cs.preApply(rect, GrAA::kYes);
REPORTER_ASSERT(r, result.fEffect == GrClip::Effect::kUnclipped,
"Draw contained within clip is kUnclipped");
// Disjoint from clip (but still on screen) is kClippedOut
result = cs.preApply({50.f, 50.f, 60.f, 60.f}, GrAA::kYes);
REPORTER_ASSERT(r, result.fEffect == GrClip::Effect::kClippedOut,
"Draw not intersecting clip is kClippedOut");
cs.restore();
// Intersecting clip is kClipped for complex shape
cs.save();
SkPath path = make_octagon(rect.makeOutset(5.f, 5.f), 5.f, 5.f);
cs.clipPath(SkMatrix::I(), path, GrAA::kYes, SkClipOp::kIntersect);
result = cs.preApply(path.getBounds(), GrAA::kNo);
REPORTER_ASSERT(r, result.fEffect == GrClip::Effect::kClipped && !result.fIsRRect,
"Draw with complex clip is kClipped, but is not an rrect");
cs.restore();
// Intersecting clip is kDeviceRect for axis-aligned rect clip
cs.save();
cs.clipRect(SkMatrix::I(), rect, GrAA::kYes, SkClipOp::kIntersect);
result = cs.preApply(rect.makeOffset(2.f, 2.f), GrAA::kNo);
REPORTER_ASSERT(r, result.fEffect == GrClip::Effect::kClipped &&
result.fAA == GrAA::kYes &&
result.fIsRRect &&
result.fRRect == SkRRect::MakeRect(rect),
"kDeviceRect clip stack should be reported by preApply");
cs.restore();
// Intersecting clip is kDeviceRRect for axis-aligned rrect clip
cs.save();
SkRRect clipRRect = SkRRect::MakeRectXY(rect, 5.f, 5.f);
cs.clipRRect(SkMatrix::I(), clipRRect, GrAA::kYes, SkClipOp::kIntersect);
result = cs.preApply(rect.makeOffset(2.f, 2.f), GrAA::kNo);
REPORTER_ASSERT(r, result.fEffect == GrClip::Effect::kClipped &&
result.fAA == GrAA::kYes &&
result.fIsRRect &&
result.fRRect == clipRRect,
"kDeviceRRect clip stack should be reported by preApply");
cs.restore();
}
// Tests the clip shader entry point
DEF_TEST(GrClipStack_Shader, r) {
sk_sp<SkShader> shader = SkShaders::Color({0.f, 0.f, 0.f, 0.5f}, nullptr);
SkSimpleMatrixProvider matrixProvider = SkMatrix::I();
sk_sp<GrDirectContext> context = GrDirectContext::MakeMock(nullptr);
std::unique_ptr<GrRenderTargetContext> rtc = GrRenderTargetContext::Make(
context.get(), GrColorType::kRGBA_8888, SkColorSpace::MakeSRGB(),
SkBackingFit::kExact, kDeviceBounds.size());
GrClipStack cs(kDeviceBounds, &matrixProvider, false);
cs.save();
cs.clipShader(shader);
REPORTER_ASSERT(r, cs.clipState() == GrClipStack::ClipState::kComplex,
"A clip shader should be reported as a complex clip");
GrAppliedClip out(kDeviceBounds.size());
SkRect drawBounds = {10.f, 11.f, 16.f, 32.f};
GrClip::Effect effect = cs.apply(context.get(), rtc.get(), GrAAType::kCoverage, false,
&out, &drawBounds);
REPORTER_ASSERT(r, effect == GrClip::Effect::kClipped,
"apply() should return kClipped for a clip shader");
REPORTER_ASSERT(r, out.hasCoverageFragmentProcessor(),
"apply() should have converted clip shader to a coverage FP");
GrAppliedClip out2(kDeviceBounds.size());
drawBounds = {-15.f, -10.f, -1.f, 10.f}; // offscreen
effect = cs.apply(context.get(), rtc.get(), GrAAType::kCoverage, false,
&out2, &drawBounds);
REPORTER_ASSERT(r, effect == GrClip::Effect::kClippedOut,
"apply() should still discard offscreen draws with a clip shader");
cs.restore();
REPORTER_ASSERT(r, cs.clipState() == GrClipStack::ClipState::kWideOpen,
"restore() should get rid of the clip shader");
// Adding a clip shader on top of a device rect clip should prevent preApply from reporting
// it as a device rect
cs.clipRect(SkMatrix::I(), {10, 15, 30, 30}, GrAA::kNo, SkClipOp::kIntersect);
SkASSERT(cs.clipState() == GrClipStack::ClipState::kDeviceRect); // test precondition
cs.clipShader(shader);
GrClip::PreClipResult result = cs.preApply(SkRect::Make(kDeviceBounds), GrAA::kYes);
REPORTER_ASSERT(r, result.fEffect == GrClip::Effect::kClipped && !result.fIsRRect,
"A clip shader should not produce a device rect from preApply");
}
// Tests apply() under simple circumstances, that don't require actual rendering of masks, or
// atlases. This lets us define the test regularly instead of a GPU-only test.
// - This is not exhaustive and is challenging to unit test, so apply() is predominantly tested by
// the GMs instead.
DEF_TEST(GrClipStack_SimpleApply, r) {
SkSimpleMatrixProvider matrixProvider = SkMatrix::I();
sk_sp<GrDirectContext> context = GrDirectContext::MakeMock(nullptr);
std::unique_ptr<GrRenderTargetContext> rtc = GrRenderTargetContext::Make(
context.get(), GrColorType::kRGBA_8888, SkColorSpace::MakeSRGB(),
SkBackingFit::kExact, kDeviceBounds.size());
GrClipStack cs(kDeviceBounds, &matrixProvider, false);
// Offscreen draw is kClippedOut
{
SkRect drawBounds = {-15.f, -15.f, -1.f, -1.f};
GrAppliedClip out(kDeviceBounds.size());
GrClip::Effect effect = cs.apply(context.get(), rtc.get(), GrAAType::kCoverage, false,
&out, &drawBounds);
REPORTER_ASSERT(r, effect == GrClip::Effect::kClippedOut, "Offscreen draw is clipped out");
}
// Draw contained in clip is kUnclipped
{
SkRect drawBounds = {15.4f, 16.3f, 26.f, 32.f};
cs.save();
cs.clipPath(SkMatrix::I(), make_octagon(drawBounds.makeOutset(5.f, 5.f), 5.f, 5.f),
GrAA::kYes, SkClipOp::kIntersect);
GrAppliedClip out(kDeviceBounds.size());
GrClip::Effect effect = cs.apply(context.get(), rtc.get(), GrAAType::kCoverage, false,
&out, &drawBounds);
REPORTER_ASSERT(r, effect == GrClip::Effect::kUnclipped, "Draw inside clip is unclipped");
cs.restore();
}
// Draw bounds are cropped to device space before checking contains
{
SkRect clipRect = {kDeviceBounds.fRight - 20.f, 10.f, kDeviceBounds.fRight, 20.f};
SkRect drawRect = clipRect.makeOffset(10.f, 0.f);
cs.save();
cs.clipRect(SkMatrix::I(), clipRect, GrAA::kNo, SkClipOp::kIntersect);
GrAppliedClip out(kDeviceBounds.size());
GrClip::Effect effect = cs.apply(context.get(), rtc.get(), GrAAType::kCoverage, false,
&out, &drawRect);
REPORTER_ASSERT(r, SkRect::Make(kDeviceBounds).contains(drawRect),
"Draw rect should be clipped to device rect");
REPORTER_ASSERT(r, effect == GrClip::Effect::kUnclipped,
"After device clipping, this should be detected as contained within clip");
cs.restore();
}
// Non-AA device rect intersect is just a scissor
{
SkRect clipRect = {15.3f, 17.23f, 30.2f, 50.8f};
SkRect drawRect = clipRect.makeOutset(10.f, 10.f);
SkIRect expectedScissor = clipRect.round();
cs.save();
cs.clipRect(SkMatrix::I(), clipRect, GrAA::kNo, SkClipOp::kIntersect);
GrAppliedClip out(kDeviceBounds.size());
GrClip::Effect effect = cs.apply(context.get(), rtc.get(), GrAAType::kCoverage, false,
&out, &drawRect);
REPORTER_ASSERT(r, effect == GrClip::Effect::kClipped, "Draw should be clipped by rect");
REPORTER_ASSERT(r, !out.hasCoverageFragmentProcessor(), "Clip should not use coverage FPs");
REPORTER_ASSERT(r, !out.hardClip().hasStencilClip(), "Clip should not need stencil");
REPORTER_ASSERT(r, !out.hardClip().windowRectsState().enabled(),
"Clip should not need window rects");
REPORTER_ASSERT(r, out.scissorState().enabled() &&
out.scissorState().rect() == expectedScissor,
"Clip has unexpected scissor rectangle");
cs.restore();
}
// Analytic coverage FPs
auto testHasCoverageFP = [&](SkRect drawBounds) {
GrAppliedClip out(kDeviceBounds.size());
GrClip::Effect effect = cs.apply(context.get(), rtc.get(), GrAAType::kCoverage, false,
&out, &drawBounds);
REPORTER_ASSERT(r, effect == GrClip::Effect::kClipped, "Draw should be clipped");
REPORTER_ASSERT(r, out.scissorState().enabled(), "Coverage FPs should still set scissor");
REPORTER_ASSERT(r, out.hasCoverageFragmentProcessor(), "Clip should use coverage FP");
};
// Axis-aligned rect can be an analytic FP
{
cs.save();
cs.clipRect(SkMatrix::I(), {10.2f, 8.342f, 63.f, 23.3f}, GrAA::kYes,
SkClipOp::kDifference);
testHasCoverageFP({9.f, 10.f, 30.f, 18.f});
cs.restore();
}
// Axis-aligned round rect can be an analytic FP
{
SkRect rect = {4.f, 8.f, 20.f, 20.f};
cs.save();
cs.clipRRect(SkMatrix::I(), SkRRect::MakeRectXY(rect, 3.f, 3.f), GrAA::kYes,
SkClipOp::kIntersect);
testHasCoverageFP(rect.makeOffset(2.f, 2.f));
cs.restore();
}
// Transformed rect can be an analytic FP
{
SkRect rect = {14.f, 8.f, 30.f, 22.34f};
SkMatrix rot = SkMatrix::RotateDeg(34.f);
cs.save();
cs.clipRect(rot, rect, GrAA::kNo, SkClipOp::kIntersect);
testHasCoverageFP(rot.mapRect(rect));
cs.restore();
}
// Convex polygons can be an analytic FP
{
SkRect rect = {15.f, 15.f, 45.f, 45.f};
cs.save();
cs.clipPath(SkMatrix::I(), make_octagon(rect), GrAA::kYes, SkClipOp::kIntersect);
testHasCoverageFP(rect.makeOutset(2.f, 2.f));
cs.restore();
}
}
// Must disable CCPR in order to trigger SW mask generation when the clip stack is applied.
static void only_allow_default(GrContextOptions* options) {
options->fGpuPathRenderers = GpuPathRenderers::kNone;
}
DEF_GPUTEST_FOR_CONTEXTS(GrClipStack_SWMask,
sk_gpu_test::GrContextFactory::IsRenderingContext,
r, ctxInfo, only_allow_default) {
GrDirectContext* context = ctxInfo.directContext();
std::unique_ptr<GrRenderTargetContext> rtc = GrRenderTargetContext::Make(
context, GrColorType::kRGBA_8888, nullptr, SkBackingFit::kExact, kDeviceBounds.size());
SkSimpleMatrixProvider matrixProvider = SkMatrix::I();
std::unique_ptr<GrClipStack> cs(new GrClipStack(kDeviceBounds, &matrixProvider, false));
auto addMaskRequiringClip = [&](SkScalar x, SkScalar y, SkScalar radius) {
SkPath path;
path.addCircle(x, y, radius);
path.addCircle(x + radius / 2.f, y + radius / 2.f, radius);
path.setFillType(SkPathFillType::kEvenOdd);
// Use AA so that clip application does not route through the stencil buffer
cs->clipPath(SkMatrix::I(), path, GrAA::kYes, SkClipOp::kIntersect);
};
auto drawRect = [&](SkRect drawBounds) {
GrPaint paint;
paint.setColor4f({1.f, 1.f, 1.f, 1.f});
rtc->drawRect(cs.get(), std::move(paint), GrAA::kYes, SkMatrix::I(), drawBounds);
};
auto generateMask = [&](SkRect drawBounds) {
GrUniqueKey priorKey = cs->testingOnly_getLastSWMaskKey();
drawRect(drawBounds);
GrUniqueKey newKey = cs->testingOnly_getLastSWMaskKey();
REPORTER_ASSERT(r, priorKey != newKey, "Did not generate a new SW mask key as expected");
return newKey;
};
auto verifyKeys = [&](const std::vector<GrUniqueKey>& expectedKeys,
const std::vector<GrUniqueKey>& releasedKeys) {
context->flush();
GrProxyProvider* proxyProvider = context->priv().proxyProvider();
#ifdef SK_DEBUG
// The proxy providers key count fluctuates based on proxy lifetime, but we want to
// verify the resource count, and that requires using key tags that are debug-only.
SkASSERT(expectedKeys.size() > 0 || releasedKeys.size() > 0);
const char* tag = expectedKeys.size() > 0 ? expectedKeys[0].tag() : releasedKeys[0].tag();
GrResourceCache* cache = context->priv().getResourceCache();
int numProxies = cache->countUniqueKeysWithTag(tag);
REPORTER_ASSERT(r, (int) expectedKeys.size() == numProxies,
"Unexpected proxy count, got %d, not %d",
numProxies, (int) expectedKeys.size());
#endif
for (const auto& key : expectedKeys) {
auto proxy = proxyProvider->findOrCreateProxyByUniqueKey(key);
REPORTER_ASSERT(r, SkToBool(proxy), "Unable to find resource for expected mask key");
}
for (const auto& key : releasedKeys) {
auto proxy = proxyProvider->findOrCreateProxyByUniqueKey(key);
REPORTER_ASSERT(r, !SkToBool(proxy), "SW mask not released as expected");
}
};
// Creates a mask for a complex clip
cs->save();
addMaskRequiringClip(5.f, 5.f, 20.f);
GrUniqueKey keyADepth1 = generateMask({0.f, 0.f, 20.f, 20.f});
GrUniqueKey keyBDepth1 = generateMask({10.f, 10.f, 30.f, 30.f});
verifyKeys({keyADepth1, keyBDepth1}, {});
// Creates a new mask for a new save record, but doesn't delete the old records
cs->save();
addMaskRequiringClip(6.f, 6.f, 15.f);
GrUniqueKey keyADepth2 = generateMask({0.f, 0.f, 20.f, 20.f});
GrUniqueKey keyBDepth2 = generateMask({10.f, 10.f, 30.f, 30.f});
verifyKeys({keyADepth1, keyBDepth1, keyADepth2, keyBDepth2}, {});
// Release after modifying the current record (even if we don't draw anything)
addMaskRequiringClip(4.f, 4.f, 15.f);
GrUniqueKey keyCDepth2 = generateMask({4.f, 4.f, 16.f, 20.f});
verifyKeys({keyADepth1, keyBDepth1, keyCDepth2}, {keyADepth2, keyBDepth2});
// Release after restoring an older record
cs->restore();
verifyKeys({keyADepth1, keyBDepth1}, {keyCDepth2});
// Drawing finds the old masks at depth 1 still w/o making new ones
drawRect({0.f, 0.f, 20.f, 20.f});
drawRect({10.f, 10.f, 30.f, 30.f});
verifyKeys({keyADepth1, keyBDepth1}, {});
// Drawing something contained within a previous mask also does not make a new one
drawRect({5.f, 5.f, 15.f, 15.f});
verifyKeys({keyADepth1, keyBDepth1}, {});
// Release on destruction
cs = nullptr;
verifyKeys({}, {keyADepth1, keyBDepth1});
}