719239cd69
Change-Id: I238d29ba0250224fa593845ae65192653f58faff Reviewed-on: https://skia-review.googlesource.com/c/skia/+/528156 Reviewed-by: Kevin Lubick <kjlubick@google.com> Reviewed-by: Jim Van Verth <jvanverth@google.com> Commit-Queue: Greg Daniel <egdaniel@google.com>
2160 lines
99 KiB
C++
2160 lines
99 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/ganesh/v1/ClipStack.h"
|
|
#include "tests/Test.h"
|
|
|
|
#include "include/core/SkColorSpace.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/ganesh/GrDirectContextPriv.h"
|
|
#include "src/gpu/ganesh/GrProxyProvider.h"
|
|
#include "src/gpu/ganesh/ops/GrDrawOp.h"
|
|
#include "src/gpu/ganesh/v1/SurfaceDrawContext_v1.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:
|
|
using ClipStack = skgpu::v1::ClipStack;
|
|
|
|
// 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; }
|
|
ClipStack::ClipState expectedState() const { return fExpectedState; }
|
|
const std::vector<ClipStack::Element>& initialElements() const { return fElements; }
|
|
const std::vector<ClipStack::Element>& expectedElements() const { return fExpectedElements; }
|
|
|
|
private:
|
|
friend class TestCaseBuilder;
|
|
|
|
TestCase(SkString name,
|
|
const SkIRect& deviceBounds,
|
|
ClipStack::ClipState expectedState,
|
|
std::vector<ClipStack::Element> actual,
|
|
std::vector<ClipStack::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 ClipStack::getConservativeBounds() because this always accounts
|
|
// for difference ops, whereas ClipStack only sometimes can subtract the inner bounds for a
|
|
// difference op.
|
|
std::pair<SkIRect, bool> getOptimalBounds() const;
|
|
|
|
SkString fName;
|
|
|
|
// The input shapes+state to ClipStack
|
|
std::vector<ClipStack::Element> fElements;
|
|
SkIRect fDeviceBounds;
|
|
|
|
// The expected output of iterating over the ClipStack after all fElements are added, although
|
|
// order is not important
|
|
std::vector<ClipStack::Element> fExpectedElements;
|
|
ClipStack::ClipState fExpectedState;
|
|
};
|
|
|
|
class ElementsBuilder {
|
|
public:
|
|
using ClipStack = skgpu::v1::ClipStack;
|
|
|
|
// 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<ClipStack::Element>* elements)
|
|
: fBuilder(builder)
|
|
, fElements(elements) {}
|
|
|
|
SkMatrix fLocalToDevice = SkMatrix::I();
|
|
GrAA fAA = GrAA::kNo;
|
|
SkClipOp fOp = SkClipOp::kIntersect;
|
|
|
|
TestCaseBuilder* fBuilder;
|
|
std::vector<ClipStack::Element>* fElements;
|
|
};
|
|
|
|
class TestCaseBuilder {
|
|
public:
|
|
using ClipStack = skgpu::v1::ClipStack;
|
|
|
|
ElementsBuilder actual() { return ElementsBuilder(this, &fActualElements); }
|
|
ElementsBuilder expect() { return ElementsBuilder(this, &fExpectedElements); }
|
|
|
|
TestCaseBuilder& expectActual() {
|
|
fExpectedElements = fActualElements;
|
|
return *this;
|
|
}
|
|
|
|
TestCaseBuilder& state(ClipStack::ClipState state) {
|
|
fExpectedState = state;
|
|
return *this;
|
|
}
|
|
|
|
TestCase finishTest() {
|
|
TestCase test(fName, fDeviceBounds, fExpectedState,
|
|
std::move(fActualElements), std::move(fExpectedElements));
|
|
|
|
fExpectedState = ClipStack::ClipState::kWideOpen;
|
|
return test;
|
|
}
|
|
|
|
private:
|
|
friend class TestCase;
|
|
|
|
explicit TestCaseBuilder(const char* name, const SkIRect& deviceBounds)
|
|
: fName(name)
|
|
, fDeviceBounds(deviceBounds)
|
|
, fExpectedState(ClipStack::ClipState::kWideOpen) {}
|
|
|
|
SkString fName;
|
|
SkIRect fDeviceBounds;
|
|
ClipStack::ClipState fExpectedState;
|
|
|
|
std::vector<ClipStack::Element> fActualElements;
|
|
std::vector<ClipStack::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 == ClipStack::ClipState::kEmpty) {
|
|
return {SkIRect::MakeEmpty(), true};
|
|
}
|
|
|
|
bool expectOptimal = true;
|
|
SkRegion region(fDeviceBounds);
|
|
for (const ClipStack::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 skgpu::v1::ClipStack::Element& a,
|
|
const skgpu::v1::ClipStack::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());
|
|
|
|
SkMatrixProvider matrixProvider(SkMatrix::I());
|
|
ClipStack cs(fDeviceBounds, &matrixProvider, false);
|
|
|
|
if (policy == SavePolicy::kAtStart) {
|
|
cs.save();
|
|
}
|
|
|
|
for (int i : order) {
|
|
if (policy == SavePolicy::kBetweenEveryOp) {
|
|
cs.save();
|
|
}
|
|
const ClipStack::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 ClipStack::Element& a : cs) {
|
|
bool found = false;
|
|
for (const ClipStack::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 %zu but matched only %zu",
|
|
name.c_str(), fExpectedElements.size(), matchedElements);
|
|
|
|
// Validate restoration behavior
|
|
if (policy == SavePolicy::kAtEnd) {
|
|
ClipStack::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",
|
|
name.c_str(), (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() == ClipStack::ClipState::kWideOpen,
|
|
"%s, restore should make stack become wide-open, not %d",
|
|
name.c_str(), (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};
|
|
|
|
class NoOp : public GrDrawOp {
|
|
public:
|
|
static NoOp* Get() {
|
|
static NoOp gNoOp;
|
|
return &gNoOp;
|
|
}
|
|
private:
|
|
DEFINE_OP_CLASS_ID
|
|
NoOp() : GrDrawOp(ClassID()) {}
|
|
const char* name() const override { return "NoOp"; }
|
|
GrProcessorSet::Analysis finalize(const GrCaps&, const GrAppliedClip*, GrClampType) override {
|
|
return GrProcessorSet::EmptySetAnalysis();
|
|
}
|
|
void onPrePrepare(GrRecordingContext*, const GrSurfaceProxyView&, GrAppliedClip*, const
|
|
GrDstProxyView&, GrXferBarrierFlags, GrLoadOp) override {}
|
|
void onPrepare(GrOpFlushState*) override {}
|
|
void onExecute(GrOpFlushState*, const SkRect&) override {}
|
|
};
|
|
|
|
} // 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(ClipStack_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(ClipStack_RectRectAACombine, r) {
|
|
using ClipState = skgpu::v1::ClipStack::ClipState;
|
|
|
|
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(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(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(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(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(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(ClipStack_DifferenceNoCombine, r) {
|
|
using ClipState = skgpu::v1::ClipStack::ClipState;
|
|
|
|
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(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(ClipStack_RectRectNonAxisAligned, r) {
|
|
using ClipState = skgpu::v1::ClipStack::ClipState;
|
|
|
|
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(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(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(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(ClipStack_RRectRRectAACombine, r) {
|
|
using ClipState = skgpu::v1::ClipStack::ClipState;
|
|
|
|
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(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(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(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(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(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(ClipStack_RectRRectCombine, r) {
|
|
using ClipState = skgpu::v1::ClipStack::ClipState;
|
|
|
|
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(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(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(ClipState::kComplex)
|
|
.finishTest());
|
|
}
|
|
|
|
// Tests that a rect shape is actually pre-clipped to the device bounds
|
|
DEF_TEST(ClipStack_RectDeviceClip, r) {
|
|
using ClipState = skgpu::v1::ClipStack::ClipState;
|
|
|
|
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(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(ClipState::kDeviceRect)
|
|
.finishTest());
|
|
}
|
|
|
|
// Tests that other shapes' bounds are contained by the device bounds, even if their shape is not.
|
|
DEF_TEST(ClipStack_ShapeDeviceBoundsClip, r) {
|
|
using ClipState = skgpu::v1::ClipStack::ClipState;
|
|
|
|
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(ClipState::kDeviceRRect)
|
|
.finishTest());
|
|
|
|
// Path
|
|
run_test_case(r, TestCase::Build("device-path", kDeviceBounds)
|
|
.actual().intersect().aa()
|
|
.path(make_octagon(crossesDeviceEdge))
|
|
.finishElements()
|
|
.expectActual()
|
|
.state(ClipState::kComplex)
|
|
.finishTest());
|
|
}
|
|
|
|
// Tests that a simplifiable path turns into a simpler element type
|
|
DEF_TEST(ClipStack_PathSimplify, r) {
|
|
using ClipState = skgpu::v1::ClipStack::ClipState;
|
|
|
|
// Empty, point, and line paths -> empty
|
|
SkPath empty;
|
|
run_test_case(r, TestCase::Build("empty", kDeviceBounds)
|
|
.actual().path(empty).finishElements()
|
|
.state(ClipState::kEmpty)
|
|
.finishTest());
|
|
SkPath point;
|
|
point.moveTo({0.f, 0.f});
|
|
run_test_case(r, TestCase::Build("point", kDeviceBounds)
|
|
.actual().path(point).finishElements()
|
|
.state(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(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(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(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(ClipState::kDeviceRRect)
|
|
.finishTest());
|
|
}
|
|
|
|
// Tests that repeated identical clip operations are idempotent
|
|
DEF_TEST(ClipStack_RepeatElement, r) {
|
|
using ClipState = skgpu::v1::ClipStack::ClipState;
|
|
|
|
// 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(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(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(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(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(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(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(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(ClipState::kComplex)
|
|
.finishTest());
|
|
}
|
|
|
|
// Tests that inverse-filled paths are canonicalized to a regular fill and a swapped clip op
|
|
DEF_TEST(ClipStack_InverseFilledPath, r) {
|
|
using ClipState = skgpu::v1::ClipStack::ClipState;
|
|
|
|
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(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(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(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(ClipState::kComplex)
|
|
.finishTest());
|
|
}
|
|
|
|
// Tests that clip operations that are offscreen either make the clip empty or stay wide open
|
|
DEF_TEST(ClipStack_Offscreen, r) {
|
|
using ClipState = skgpu::v1::ClipStack::ClipState;
|
|
|
|
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(ClipState::kEmpty)
|
|
.finishTest());
|
|
run_test_case(r, TestCase::Build("intersect-rect", kDeviceBounds)
|
|
.actual().aa().intersect()
|
|
.rect(offscreenRect)
|
|
.finishElements()
|
|
.state(ClipState::kEmpty)
|
|
.finishTest());
|
|
run_test_case(r, TestCase::Build("intersect-rrect", kDeviceBounds)
|
|
.actual().aa().intersect()
|
|
.rrect(offscreenRRect)
|
|
.finishElements()
|
|
.state(ClipState::kEmpty)
|
|
.finishTest());
|
|
run_test_case(r, TestCase::Build("intersect-path", kDeviceBounds)
|
|
.actual().aa().intersect()
|
|
.path(offscreenPath)
|
|
.finishElements()
|
|
.state(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(ClipState::kWideOpen)
|
|
.finishTest());
|
|
run_test_case(r, TestCase::Build("difference-rect", kDeviceBounds)
|
|
.actual().aa().difference()
|
|
.rect(offscreenRect)
|
|
.finishElements()
|
|
.state(ClipState::kWideOpen)
|
|
.finishTest());
|
|
run_test_case(r, TestCase::Build("difference-rrect", kDeviceBounds)
|
|
.actual().aa().difference()
|
|
.rrect(offscreenRRect)
|
|
.finishElements()
|
|
.state(ClipState::kWideOpen)
|
|
.finishTest());
|
|
run_test_case(r, TestCase::Build("difference-path", kDeviceBounds)
|
|
.actual().aa().difference()
|
|
.path(offscreenPath)
|
|
.finishElements()
|
|
.state(ClipState::kWideOpen)
|
|
.finishTest());
|
|
}
|
|
|
|
// Tests that an empty shape updates the clip state directly without needing an element
|
|
DEF_TEST(ClipStack_EmptyShape, r) {
|
|
using ClipState = skgpu::v1::ClipStack::ClipState;
|
|
|
|
// Intersect -> empty
|
|
run_test_case(r, TestCase::Build("empty-intersect", kDeviceBounds)
|
|
.actual().intersect().rect(SkRect::MakeEmpty()).finishElements()
|
|
.state(ClipState::kEmpty)
|
|
.finishTest());
|
|
|
|
// Difference -> no-op
|
|
run_test_case(r, TestCase::Build("empty-difference", kDeviceBounds)
|
|
.actual().difference().rect(SkRect::MakeEmpty()).finishElements()
|
|
.state(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(ClipState::kComplex)
|
|
.finishTest());
|
|
}
|
|
|
|
// Tests that sufficiently large difference operations can shrink the conservative bounds
|
|
DEF_TEST(ClipStack_DifferenceBounds, r) {
|
|
using ClipState = skgpu::v1::ClipStack::ClipState;
|
|
|
|
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(ClipState::kComplex)
|
|
.finishTest());
|
|
}
|
|
|
|
// Tests that intersections can combine even if there's a difference operation in the middle
|
|
DEF_TEST(ClipStack_NoDifferenceInterference, r) {
|
|
using ClipState = skgpu::v1::ClipStack::ClipState;
|
|
|
|
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(ClipState::kComplex)
|
|
.finishTest());
|
|
}
|
|
|
|
// Tests that multiple path operations are all recorded, but not otherwise consolidated
|
|
DEF_TEST(ClipStack_MultiplePaths, r) {
|
|
using ClipState = skgpu::v1::ClipStack::ClipState;
|
|
|
|
// Chosen to be greater than the number of inline-allocated elements and save records of the
|
|
// ClipStack 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(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(ClipState::kComplex)
|
|
.finishTest());
|
|
}
|
|
|
|
// Tests that a single rect is treated as kDeviceRect state when it's axis-aligned and intersect.
|
|
DEF_TEST(ClipStack_DeviceRect, r) {
|
|
using ClipState = skgpu::v1::ClipStack::ClipState;
|
|
|
|
// 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(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(ClipState::kComplex)
|
|
.finishTest());
|
|
|
|
// Not intersect -> kComplex
|
|
run_test_case(r, TestCase::Build("diff-rect", kDeviceBounds)
|
|
.actual().difference().aa().rect(rect).finishElements()
|
|
.expectActual()
|
|
.state(ClipState::kComplex)
|
|
.finishTest());
|
|
}
|
|
|
|
// Tests that a single rrect is treated as kDeviceRRect state when it's axis-aligned and intersect.
|
|
DEF_TEST(ClipStack_DeviceRRect, r) {
|
|
using ClipState = skgpu::v1::ClipStack::ClipState;
|
|
|
|
// 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(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(ClipState::kComplex)
|
|
.finishTest());
|
|
|
|
// Not intersect -> kComplex
|
|
run_test_case(r, TestCase::Build("diff-rrect", kDeviceBounds)
|
|
.actual().difference().aa().rrect(rrect).finishElements()
|
|
.expectActual()
|
|
.state(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(ClipStack_ScaleTranslate, r) {
|
|
using ClipState = skgpu::v1::ClipStack::ClipState;
|
|
|
|
SkMatrix lm = SkMatrix::Scale(2.f, 4.f);
|
|
lm.postTranslate(15.5f, 14.3f);
|
|
SkASSERT(lm.preservesAxisAlignment() && lm.isScaleTranslate());
|
|
|
|
// 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(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(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(ClipState::kComplex)
|
|
.finishTest());
|
|
}
|
|
|
|
// Tests that rect-stays-rect matrices that are not scale+translate matrices are pre-applied.
|
|
DEF_TEST(ClipStack_PreserveAxisAlignment, r) {
|
|
using ClipState = skgpu::v1::ClipStack::ClipState;
|
|
|
|
SkMatrix lm = SkMatrix::RotateDeg(90.f);
|
|
lm.postTranslate(15.5f, 14.3f);
|
|
SkASSERT(lm.preservesAxisAlignment() && !lm.isScaleTranslate());
|
|
|
|
// Rect -> matrix is applied up front
|
|
SkRect rect = {0.f, 0.f, 10.f, 10.f};
|
|
run_test_case(r, TestCase::Build("r90+rect", kDeviceBounds)
|
|
.actual().rect(rect, lm, GrAA::kYes, SkClipOp::kIntersect)
|
|
.finishElements()
|
|
.expect().rect(lm.mapRect(rect), GrAA::kYes, SkClipOp::kIntersect)
|
|
.finishElements()
|
|
.state(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("r90+rrect", kDeviceBounds)
|
|
.actual().rrect(localRRect, lm, GrAA::kYes, SkClipOp::kIntersect)
|
|
.finishElements()
|
|
.expect().rrect(deviceRRect, GrAA::kYes, SkClipOp::kIntersect)
|
|
.finishElements()
|
|
.state(ClipState::kDeviceRRect)
|
|
.finishTest());
|
|
|
|
// Path -> matrix is NOT applied
|
|
run_test_case(r, TestCase::Build("r90+path", kDeviceBounds)
|
|
.actual().intersect().localToDevice(lm).path(make_octagon(rect))
|
|
.finishElements()
|
|
.expectActual()
|
|
.state(ClipState::kComplex)
|
|
.finishTest());
|
|
}
|
|
|
|
// Tests that a convex path element can contain a rect or round rect, allowing the stack to be
|
|
// simplified
|
|
DEF_TEST(ClipStack_ConvexPathContains, r) {
|
|
using ClipState = skgpu::v1::ClipStack::ClipState;
|
|
|
|
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(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(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(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(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(ClipState::kEmpty)
|
|
.finishTest());
|
|
run_test_case(r, TestCase::Build("convex-diff+rrect-int", kDeviceBounds)
|
|
.actual().aa().intersect().rrect(rrect)
|
|
.difference().path(bigPath).finishElements()
|
|
.state(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(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(ClipState::kComplex)
|
|
.finishTest());
|
|
}
|
|
|
|
// Tests that rects/rrects in different coordinate spaces can be consolidated when one is fully
|
|
// contained by the other.
|
|
DEF_TEST(ClipStack_NonAxisAlignedContains, r) {
|
|
using ClipState = skgpu::v1::ClipStack::ClipState;
|
|
|
|
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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(ClipStack_MixedAAContains, r) {
|
|
using ClipState = skgpu::v1::ClipStack::ClipState;
|
|
|
|
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(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(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(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(ClipState::kComplex)
|
|
.finishTest());
|
|
}
|
|
|
|
// Tests that a shape that contains the device bounds updates the clip state directly
|
|
DEF_TEST(ClipStack_ShapeContainsDevice, r) {
|
|
using ClipState = skgpu::v1::ClipStack::ClipState;
|
|
|
|
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(ClipState::kWideOpen)
|
|
.finishTest());
|
|
run_test_case(r, TestCase::Build("rrect-intersect", kDeviceBounds)
|
|
.actual().intersect().rrect(rrect).finishElements()
|
|
.state(ClipState::kWideOpen)
|
|
.finishTest());
|
|
run_test_case(r, TestCase::Build("convex-intersect", kDeviceBounds)
|
|
.actual().intersect().path(convex).finishElements()
|
|
.state(ClipState::kWideOpen)
|
|
.finishTest());
|
|
|
|
// Difference -> empty
|
|
run_test_case(r, TestCase::Build("rect-difference", kDeviceBounds)
|
|
.actual().difference().rect(rect).finishElements()
|
|
.state(ClipState::kEmpty)
|
|
.finishTest());
|
|
run_test_case(r, TestCase::Build("rrect-difference", kDeviceBounds)
|
|
.actual().difference().rrect(rrect).finishElements()
|
|
.state(ClipState::kEmpty)
|
|
.finishTest());
|
|
run_test_case(r, TestCase::Build("convex-difference", kDeviceBounds)
|
|
.actual().difference().path(convex).finishElements()
|
|
.state(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(ClipStack_DisjointShapes, r) {
|
|
using ClipState = skgpu::v1::ClipStack::ClipState;
|
|
|
|
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(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(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(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(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(ClipState::kComplex)
|
|
.finishTest());
|
|
}
|
|
|
|
DEF_TEST(ClipStack_ComplexClip, reporter) {
|
|
using ClipStack = skgpu::v1::ClipStack;
|
|
|
|
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
|
|
ClipStack::ClipState state = ClipStack::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 = ClipStack::ClipState::kDeviceRect;
|
|
}
|
|
} else {
|
|
SkASSERT(expectedRRectIntersection !=
|
|
SkRRect::MakeRect(SkRect::Make(kDeviceBounds)) &&
|
|
!expectedRRectIntersection.isEmpty());
|
|
b.expect().rrect(expectedRRectIntersection, GrAA::kYes, SkClipOp::kIntersect);
|
|
if (opBits == 0xf) {
|
|
state = ClipStack::ClipState::kDeviceRRect;
|
|
}
|
|
}
|
|
}
|
|
|
|
run_test_case(reporter, b.state(state).finishTest());
|
|
}
|
|
}
|
|
}
|
|
|
|
// ///////////////////////////////////////////////////////////////////////////////
|
|
// // These tests do not use the TestCase infrastructure and manipulate a
|
|
// // ClipStack directly.
|
|
|
|
// Tests that replaceClip() works as expected across save/restores
|
|
DEF_TEST(ClipStack_ReplaceClip, r) {
|
|
using ClipStack = skgpu::v1::ClipStack;
|
|
|
|
ClipStack 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() == ClipStack::ClipState::kDeviceRect,
|
|
"Clip did not become a device rect");
|
|
REPORTER_ASSERT(r, cs.getConservativeBounds() == replace, "Unexpected replaced clip bounds");
|
|
const ClipStack::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() == ClipStack::ClipState::kDeviceRRect,
|
|
"Unexpected state after restore, not kDeviceRRect");
|
|
const ClipStack::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");
|
|
}
|
|
|
|
// Try to overflow the number of allowed window rects (see skbug.com/10989)
|
|
DEF_TEST(ClipStack_DiffRects, r) {
|
|
using ClipStack = skgpu::v1::ClipStack;
|
|
using SurfaceDrawContext = skgpu::v1::SurfaceDrawContext;
|
|
|
|
GrMockOptions options;
|
|
options.fMaxWindowRectangles = 8;
|
|
|
|
SkMatrixProvider matrixProvider = SkMatrix::I();
|
|
sk_sp<GrDirectContext> context = GrDirectContext::MakeMock(&options);
|
|
std::unique_ptr<SurfaceDrawContext> sdc = SurfaceDrawContext::Make(
|
|
context.get(), GrColorType::kRGBA_8888, SkColorSpace::MakeSRGB(),
|
|
SkBackingFit::kExact, kDeviceBounds.size(), SkSurfaceProps());
|
|
|
|
ClipStack cs(kDeviceBounds, &matrixProvider, false);
|
|
|
|
cs.save();
|
|
for (int y = 0; y < 10; ++y) {
|
|
for (int x = 0; x < 10; ++x) {
|
|
cs.clipRect(SkMatrix::I(), SkRect::MakeXYWH(10*x+1, 10*y+1, 8, 8),
|
|
GrAA::kNo, SkClipOp::kDifference);
|
|
}
|
|
}
|
|
|
|
GrAppliedClip out(kDeviceBounds.size());
|
|
SkRect drawBounds = SkRect::Make(kDeviceBounds);
|
|
GrClip::Effect effect = cs.apply(context.get(), sdc.get(), NoOp::Get(), GrAAType::kCoverage,
|
|
&out, &drawBounds);
|
|
|
|
REPORTER_ASSERT(r, effect == GrClip::Effect::kClipped);
|
|
REPORTER_ASSERT(r, out.windowRectsState().numWindows() == 8);
|
|
|
|
cs.restore();
|
|
}
|
|
|
|
// Tests that when a stack is forced to always be AA, non-AA elements become AA
|
|
DEF_TEST(ClipStack_ForceAA, r) {
|
|
using ClipStack = skgpu::v1::ClipStack;
|
|
|
|
ClipStack 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 rects remain non-AA so they can be applied as a scissor
|
|
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.
|
|
auto elements = cs.begin();
|
|
|
|
const ClipStack::Element& nonAARectElement = *elements;
|
|
REPORTER_ASSERT(r, nonAARectElement.fShape.isRect(), "Expected rect element");
|
|
REPORTER_ASSERT(r, nonAARectElement.fAA == GrAA::kNo,
|
|
"Axis-aligned non-AA rect ignores forceAA");
|
|
REPORTER_ASSERT(r, nonAARectElement.fShape.rect() == nonAARect,
|
|
"Mixed AA rects should not combine");
|
|
|
|
++elements;
|
|
const ClipStack::Element& aaPathElement = *elements;
|
|
REPORTER_ASSERT(r, aaPathElement.fShape.isPath(), "Expected path element");
|
|
REPORTER_ASSERT(r, aaPathElement.fShape.path() == nonAAPath, "Wrong path element");
|
|
REPORTER_ASSERT(r, aaPathElement.fAA == GrAA::kYes, "Path element not promoted to AA");
|
|
|
|
++elements;
|
|
const ClipStack::Element& aaRectElement = *elements;
|
|
REPORTER_ASSERT(r, aaRectElement.fShape.isRect(), "Expected rect element");
|
|
REPORTER_ASSERT(r, aaRectElement.fShape.rect() == aaRect,
|
|
"Mixed AA rects should not combine");
|
|
REPORTER_ASSERT(r, aaRectElement.fAA == GrAA::kYes, "Rect element stays AA");
|
|
|
|
++elements;
|
|
REPORTER_ASSERT(r, !(elements != cs.end()), "Expected only three clip elements");
|
|
}
|
|
|
|
// Tests preApply works as expected for device rects, rrects, and reports clipped-out, etc. as
|
|
// expected.
|
|
DEF_TEST(ClipStack_PreApply, r) {
|
|
using ClipStack = skgpu::v1::ClipStack;
|
|
|
|
ClipStack 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(ClipStack_Shader, r) {
|
|
using ClipStack = skgpu::v1::ClipStack;
|
|
using SurfaceDrawContext = skgpu::v1::SurfaceDrawContext;
|
|
|
|
sk_sp<SkShader> shader = SkShaders::Color({0.f, 0.f, 0.f, 0.5f}, nullptr);
|
|
|
|
SkMatrixProvider matrixProvider = SkMatrix::I();
|
|
sk_sp<GrDirectContext> context = GrDirectContext::MakeMock(nullptr);
|
|
std::unique_ptr<SurfaceDrawContext> sdc = SurfaceDrawContext::Make(
|
|
context.get(), GrColorType::kRGBA_8888, SkColorSpace::MakeSRGB(),
|
|
SkBackingFit::kExact, kDeviceBounds.size(), SkSurfaceProps());
|
|
|
|
ClipStack cs(kDeviceBounds, &matrixProvider, false);
|
|
cs.save();
|
|
cs.clipShader(shader);
|
|
|
|
REPORTER_ASSERT(r, cs.clipState() == ClipStack::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(), sdc.get(), NoOp::Get(), GrAAType::kCoverage,
|
|
&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(), sdc.get(), NoOp::Get(), GrAAType::kCoverage, &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() == ClipStack::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() == ClipStack::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(ClipStack_SimpleApply, r) {
|
|
using ClipStack = skgpu::v1::ClipStack;
|
|
using SurfaceDrawContext = skgpu::v1::SurfaceDrawContext;
|
|
|
|
SkMatrixProvider matrixProvider = SkMatrix::I();
|
|
sk_sp<GrDirectContext> context = GrDirectContext::MakeMock(nullptr);
|
|
std::unique_ptr<SurfaceDrawContext> sdc = SurfaceDrawContext::Make(
|
|
context.get(), GrColorType::kRGBA_8888, SkColorSpace::MakeSRGB(),
|
|
SkBackingFit::kExact, kDeviceBounds.size(), SkSurfaceProps());
|
|
|
|
ClipStack 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(), sdc.get(), NoOp::Get(), GrAAType::kCoverage,
|
|
&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(), sdc.get(), NoOp::Get(), GrAAType::kCoverage,
|
|
&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(), sdc.get(), NoOp::Get(), GrAAType::kCoverage,
|
|
&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(), sdc.get(), NoOp::Get(), GrAAType::kCoverage,
|
|
&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(), sdc.get(), NoOp::Get(), GrAAType::kCoverage,
|
|
&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 tessellation in order to trigger SW mask generation when the clip stack is applied.
|
|
static void disable_tessellation_atlas(GrContextOptions* options) {
|
|
options->fGpuPathRenderers = GpuPathRenderers::kNone;
|
|
options->fAvoidStencilBuffers = true;
|
|
}
|
|
|
|
DEF_GPUTEST_FOR_CONTEXTS(ClipStack_SWMask,
|
|
sk_gpu_test::GrContextFactory::IsRenderingContext,
|
|
r, ctxInfo, disable_tessellation_atlas) {
|
|
using ClipStack = skgpu::v1::ClipStack;
|
|
using SurfaceDrawContext = skgpu::v1::SurfaceDrawContext;
|
|
|
|
GrDirectContext* context = ctxInfo.directContext();
|
|
std::unique_ptr<SurfaceDrawContext> sdc = SurfaceDrawContext::Make(
|
|
context, GrColorType::kRGBA_8888, nullptr, SkBackingFit::kExact, kDeviceBounds.size(),
|
|
SkSurfaceProps());
|
|
|
|
SkMatrixProvider matrixProvider = SkMatrix::I();
|
|
std::unique_ptr<ClipStack> cs(new ClipStack(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});
|
|
sdc->drawRect(cs.get(), std::move(paint), GrAA::kYes, SkMatrix::I(), drawBounds);
|
|
};
|
|
|
|
auto generateMask = [&](SkRect drawBounds) {
|
|
skgpu::UniqueKey priorKey = cs->testingOnly_getLastSWMaskKey();
|
|
drawRect(drawBounds);
|
|
skgpu::UniqueKey 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<skgpu::UniqueKey>& expectedKeys,
|
|
const std::vector<skgpu::UniqueKey>& 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);
|
|
skgpu::UniqueKey keyADepth1 = generateMask({0.f, 0.f, 20.f, 20.f});
|
|
skgpu::UniqueKey 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);
|
|
skgpu::UniqueKey keyADepth2 = generateMask({0.f, 0.f, 20.f, 20.f});
|
|
skgpu::UniqueKey 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);
|
|
skgpu::UniqueKey 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});
|
|
}
|