3695bdb587
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>
2158 lines
99 KiB
C++
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});
|
|
}
|