skia2/tests/GrClipStackTest.cpp
Brian Osman 3695bdb587 Refactor SkMatrixProvider slightly
There was only one virtual method, so switch that to a bool stored in
the base class. The derived types exist as hints for the reader, and an
easy way to adjust how the new localToDevice is constructed.

With this change, we don't need SkSimpleMatrixProvider. SkMatrixProvider
is concrete, so we can use it directly. SkOverrideDeviceMatrixProvider
no longer needs the original provider for anything, so remove that
parameter. It now exists solely to inhibit the hitsPixelCenters flag.

Fix a few spots (SkParticleBinding, some sites in SkRuntimeEffect) where
we used SkSimpleMatrixProvider, even though the local coordinates being
passed did not obey the hits-pixel-centers constraints.

Most importantly, document how localToDeviceHitsPixelCenters works.

Change-Id: Ibe9060bac0822d0edf52a507d390bd198d8e6dbd
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/482176
Reviewed-by: John Stiles <johnstiles@google.com>
Commit-Queue: Brian Osman <brianosman@google.com>
2021-12-09 20:10:58 +00:00

2158 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/v1/ClipStack.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/GrDirectContextPriv.h"
#include "src/gpu/GrProxyProvider.h"
#include "src/gpu/ops/GrDrawOp.h"
#include "src/gpu/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 %d but matched only %d",
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", (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",
(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) {
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});
}