a195d101f9
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>
1981 lines
94 KiB
C++
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});
|
|
}
|