|  |  | 
|  | /* | 
|  | * 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 "include/core/SkClipOp.h" | 
|  | #include "include/core/SkColorSpace.h" | 
|  | #include "include/core/SkMatrix.h" | 
|  | #include "include/core/SkPath.h" | 
|  | #include "include/core/SkPathTypes.h" | 
|  | #include "include/core/SkPoint.h" | 
|  | #include "include/core/SkRRect.h" | 
|  | #include "include/core/SkRect.h" | 
|  | #include "include/core/SkRefCnt.h" | 
|  | #include "include/core/SkRegion.h" | 
|  | #include "include/core/SkScalar.h" | 
|  | #include "include/core/SkShader.h" | 
|  | #include "include/core/SkString.h" | 
|  | #include "include/core/SkSurfaceProps.h" | 
|  | #include "include/core/SkTypes.h" | 
|  | #include "include/gpu/ganesh/GrContextOptions.h" | 
|  | #include "include/gpu/ganesh/GrDirectContext.h" | 
|  | #include "include/gpu/ganesh/mock/GrMockTypes.h" | 
|  | #include "include/private/base/SkTo.h" | 
|  | #include "include/private/gpu/ganesh/GrTypesPriv.h" | 
|  | #include "src/core/SkRRectPriv.h" | 
|  | #include "src/gpu/ResourceKey.h" | 
|  | #include "src/gpu/SkBackingFit.h" | 
|  | #include "src/gpu/ganesh/ClipStack.h" | 
|  | #include "src/gpu/ganesh/GrAppliedClip.h" | 
|  | #include "src/gpu/ganesh/GrClip.h" | 
|  | #include "src/gpu/ganesh/GrDirectContextPriv.h" | 
|  | #include "src/gpu/ganesh/GrPaint.h" | 
|  | #include "src/gpu/ganesh/GrProcessorSet.h" | 
|  | #include "src/gpu/ganesh/GrProxyProvider.h" | 
|  | #include "src/gpu/ganesh/GrResourceCache.h" | 
|  | #include "src/gpu/ganesh/GrScissorState.h" | 
|  | #include "src/gpu/ganesh/GrWindowRectsState.h" | 
|  | #include "src/gpu/ganesh/SurfaceDrawContext.h" | 
|  | #include "src/gpu/ganesh/geometry/GrShape.h" | 
|  | #include "src/gpu/ganesh/ops/GrDrawOp.h" | 
|  | #include "src/gpu/ganesh/ops/GrOp.h" | 
|  | #include "tests/CtsEnforcement.h" | 
|  | #include "tests/Test.h" | 
|  |  | 
|  | #include <cstddef> | 
|  | #include <initializer_list> | 
|  | #include <memory> | 
|  | #include <tuple> | 
|  | #include <utility> | 
|  | #include <vector> | 
|  |  | 
|  | class GrCaps; | 
|  | class GrDstProxyView; | 
|  | class GrOpFlushState; | 
|  | class GrRecordingContext; | 
|  | class GrSurfaceProxyView; | 
|  | enum class GrXferBarrierFlags; | 
|  |  | 
|  | namespace { | 
|  |  | 
|  | class TestCaseBuilder; | 
|  |  | 
|  | 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::ganesh::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(std::move(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::ganesh::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::ganesh::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::ganesh::ClipStack::Element& a, | 
|  | const skgpu::ganesh::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()); | 
|  |  | 
|  | ClipStack cs(fDeviceBounds, &SkMatrix::I(), false); | 
|  |  | 
|  | if (policy == SavePolicy::kAtStart) { | 
|  | cs.save(); | 
|  | } | 
|  |  | 
|  | for (int i : order) { | 
|  | if (policy == SavePolicy::kBetweenEveryOp) { | 
|  | cs.save(); | 
|  | } | 
|  | const ClipStack::Element& e = fElements[i]; | 
|  | switch(e.fShape.type()) { | 
|  | case GrShape::Type::kRect: | 
|  | cs.clipRect(e.fLocalToDevice, e.fShape.rect(), e.fAA, e.fOp); | 
|  | break; | 
|  | case GrShape::Type::kRRect: | 
|  | cs.clipRRect(e.fLocalToDevice, e.fShape.rrect(), e.fAA, e.fOp); | 
|  | break; | 
|  | case GrShape::Type::kPath: | 
|  | cs.clipPath(e.fLocalToDevice, e.fShape.path(), e.fAA, e.fOp); | 
|  | break; | 
|  | default: | 
|  | SkDEBUGFAIL("Shape type not handled by test case yet."); | 
|  | } | 
|  | } | 
|  |  | 
|  | if (policy == SavePolicy::kAtEnd) { | 
|  | cs.save(); | 
|  | } | 
|  |  | 
|  | // Now validate | 
|  | SkString name = this->getTestName(order, policy); | 
|  | REPORTER_ASSERT(reporter, cs.clipState() == fExpectedState, | 
|  | "%s, clip state expected %d, actual %d", | 
|  | name.c_str(), (int) fExpectedState, (int) cs.clipState()); | 
|  | SkIRect actualBounds = cs.getConservativeBounds(); | 
|  | SkIRect optimalBounds; | 
|  | bool expectOptimal; | 
|  | std::tie(optimalBounds, expectOptimal) = this->getOptimalBounds(); | 
|  |  | 
|  | if (expectOptimal) { | 
|  | REPORTER_ASSERT(reporter, actualBounds == optimalBounds, | 
|  | "%s, bounds expected [%d %d %d %d], actual [%d %d %d %d]", | 
|  | name.c_str(), optimalBounds.fLeft, optimalBounds.fTop, | 
|  | optimalBounds.fRight, optimalBounds.fBottom, | 
|  | actualBounds.fLeft, actualBounds.fTop, | 
|  | actualBounds.fRight, actualBounds.fBottom); | 
|  | } else { | 
|  | REPORTER_ASSERT(reporter, actualBounds.contains(optimalBounds), | 
|  | "%s, bounds are not conservative, optimal [%d %d %d %d], actual [%d %d %d %d]", | 
|  | name.c_str(), optimalBounds.fLeft, optimalBounds.fTop, | 
|  | optimalBounds.fRight, optimalBounds.fBottom, | 
|  | actualBounds.fLeft, actualBounds.fTop, | 
|  | actualBounds.fRight, actualBounds.fBottom); | 
|  | } | 
|  |  | 
|  | size_t matchedElements = 0; | 
|  | for (const ClipStack::Element& a : cs) { | 
|  | bool found = false; | 
|  | for (const ClipStack::Element& e : fExpectedElements) { | 
|  | if (compare_elements(a, e)) { | 
|  | // shouldn't match multiple expected elements or it's a bad test case | 
|  | SkASSERT(!found); | 
|  | found = true; | 
|  | } | 
|  | } | 
|  |  | 
|  | REPORTER_ASSERT(reporter, found, | 
|  | "%s, unexpected clip element in stack: shape %d, aa %d, op %d", | 
|  | name.c_str(), (int) a.fShape.type(), (int) a.fAA, (int) a.fOp); | 
|  | matchedElements += found ? 1 : 0; | 
|  | } | 
|  | REPORTER_ASSERT(reporter, matchedElements == fExpectedElements.size(), | 
|  | "%s, did not match all expected elements: expected %zu but matched only %zu", | 
|  | name.c_str(), fExpectedElements.size(), matchedElements); | 
|  |  | 
|  | // Validate restoration behavior | 
|  | if (policy == SavePolicy::kAtEnd) { | 
|  | ClipStack::ClipState oldState = cs.clipState(); | 
|  | cs.restore(); | 
|  | REPORTER_ASSERT(reporter, cs.clipState() == oldState, | 
|  | "%s, restoring an empty save record should not change clip state: " | 
|  | "expected %d but got %d", | 
|  | name.c_str(), (int) oldState, (int) cs.clipState()); | 
|  | } else if (policy != SavePolicy::kNever) { | 
|  | int restoreCount = policy == SavePolicy::kAtStart ? 1 : (int) order.size(); | 
|  | for (int i = 0; i < restoreCount; ++i) { | 
|  | cs.restore(); | 
|  | } | 
|  | // Should be wide open if everything is restored to base state | 
|  | REPORTER_ASSERT(reporter, cs.clipState() == ClipStack::ClipState::kWideOpen, | 
|  | "%s, restore should make stack become wide-open, not %d", | 
|  | name.c_str(), (int) cs.clipState()); | 
|  | } | 
|  | } | 
|  |  | 
|  | // All clip operations are commutative so applying actual elements in every possible order should | 
|  | // always produce the same set of expected elements. | 
|  | static void run_test_case(skiatest::Reporter* r, const TestCase& test) { | 
|  | int n = (int) test.initialElements().size(); | 
|  | std::vector<int> order(n); | 
|  | std::vector<int> stack(n); | 
|  |  | 
|  | // Initial order sequence and zeroed stack | 
|  | for (int i = 0; i < n; ++i) { | 
|  | order[i] = i; | 
|  | stack[i] = 0; | 
|  | } | 
|  |  | 
|  | auto runTest = [&]() { | 
|  | static const SavePolicy kPolicies[] = { SavePolicy::kNever, SavePolicy::kAtStart, | 
|  | SavePolicy::kAtEnd, SavePolicy::kBetweenEveryOp }; | 
|  | for (auto policy : kPolicies) { | 
|  | test.run(order, policy, r); | 
|  | } | 
|  | }; | 
|  |  | 
|  | // Heap's algorithm (non-recursive) to generate every permutation over the test case's elements | 
|  | // https://en.wikipedia.org/wiki/Heap%27s_algorithm | 
|  | runTest(); | 
|  |  | 
|  | static constexpr int kMaxRuns = 720; // Don't run more than 6! configurations, even if n > 6 | 
|  | int testRuns = 1; | 
|  |  | 
|  | int i = 0; | 
|  | while (i < n && testRuns < kMaxRuns) { | 
|  | if (stack[i] < i) { | 
|  | using std::swap; | 
|  | if (i % 2 == 0) { | 
|  | swap(order[0], order[i]); | 
|  | } else { | 
|  | swap(order[stack[i]], order[i]); | 
|  | } | 
|  |  | 
|  | runTest(); | 
|  | stack[i]++; | 
|  | i = 0; | 
|  | testRuns++; | 
|  | } else { | 
|  | stack[i] = 0; | 
|  | ++i; | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | static SkPath make_octagon(const SkRect& r, SkScalar lr, SkScalar tb) { | 
|  | SkPath p; | 
|  | p.moveTo(r.fLeft + lr, r.fTop); | 
|  | p.lineTo(r.fRight - lr, r.fTop); | 
|  | p.lineTo(r.fRight, r.fTop + tb); | 
|  | p.lineTo(r.fRight, r.fBottom - tb); | 
|  | p.lineTo(r.fRight - lr, r.fBottom); | 
|  | p.lineTo(r.fLeft + lr, r.fBottom); | 
|  | p.lineTo(r.fLeft, r.fBottom - tb); | 
|  | p.lineTo(r.fLeft, r.fTop + tb); | 
|  | p.close(); | 
|  | return p; | 
|  | } | 
|  |  | 
|  | static SkPath make_octagon(const SkRect& r) { | 
|  | SkScalar lr = 0.3f * r.width(); | 
|  | SkScalar tb = 0.3f * r.height(); | 
|  | return make_octagon(r, lr, tb); | 
|  | } | 
|  |  | 
|  | static constexpr SkIRect kDeviceBounds = {0, 0, 100, 100}; | 
|  |  | 
|  | class NoOp : public GrDrawOp { | 
|  | public: | 
|  | static NoOp* Get() { | 
|  | static NoOp gNoOp; | 
|  | return &gNoOp; | 
|  | } | 
|  | private: | 
|  | DEFINE_OP_CLASS_ID | 
|  | NoOp() : GrDrawOp(ClassID()) {} | 
|  | const char* name() const override { return "NoOp"; } | 
|  | GrProcessorSet::Analysis finalize(const GrCaps&, const GrAppliedClip*, GrClampType) override { | 
|  | return GrProcessorSet::EmptySetAnalysis(); | 
|  | } | 
|  | void onPrePrepare(GrRecordingContext*, const GrSurfaceProxyView&, GrAppliedClip*, const | 
|  | GrDstProxyView&, GrXferBarrierFlags, GrLoadOp) override {} | 
|  | void onPrepare(GrOpFlushState*) override {} | 
|  | void onExecute(GrOpFlushState*, const SkRect&) override {} | 
|  | }; | 
|  |  | 
|  | } // anonymous namespace | 
|  |  | 
|  | /////////////////////////////////////////////////////////////////////////////// | 
|  | // These tests use the TestCase infrastructure to define clip stacks and | 
|  | // associated expectations. | 
|  |  | 
|  | // Tests that the initialized state of the clip stack is wide-open | 
|  | DEF_TEST(ClipStack_InitialState, r) { | 
|  | run_test_case(r, TestCase::Build("initial-state", SkIRect::MakeWH(100, 100)).finishTest()); | 
|  | } | 
|  |  | 
|  | // Tests that intersection of rects combine to a single element when they have the same AA type, | 
|  | // or are pixel-aligned. | 
|  | DEF_TEST(ClipStack_RectRectAACombine, r) { | 
|  | using ClipState = skgpu::ganesh::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::ganesh::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::ganesh::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::ganesh::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::ganesh::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::ganesh::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::ganesh::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::ganesh::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::ganesh::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::ganesh::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::ganesh::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::ganesh::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::ganesh::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::ganesh::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::ganesh::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::ganesh::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::ganesh::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::ganesh::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::ganesh::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::ganesh::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::ganesh::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::ganesh::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::ganesh::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::ganesh::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::ganesh::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::ganesh::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::ganesh::ClipStack; | 
|  | using SurfaceDrawContext = skgpu::ganesh::SurfaceDrawContext; | 
|  |  | 
|  | GrMockOptions options; | 
|  | options.fMaxWindowRectangles = 8; | 
|  |  | 
|  | 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(), | 
|  | /*label=*/{}); | 
|  |  | 
|  | ClipStack cs(kDeviceBounds, &SkMatrix::I(), 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::ganesh::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::ganesh::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::ganesh::ClipStack; | 
|  | using SurfaceDrawContext = skgpu::ganesh::SurfaceDrawContext; | 
|  |  | 
|  | sk_sp<SkShader> shader = SkShaders::Color({0.f, 0.f, 0.f, 0.5f}, nullptr); | 
|  |  | 
|  | 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(), | 
|  | /*label=*/{}); | 
|  |  | 
|  | ClipStack cs(kDeviceBounds, &SkMatrix::I(), 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::ganesh::ClipStack; | 
|  | using SurfaceDrawContext = skgpu::ganesh::SurfaceDrawContext; | 
|  |  | 
|  | 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(), | 
|  | /*label=*/{}); | 
|  |  | 
|  | ClipStack cs(kDeviceBounds, &SkMatrix::I(), 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_GANESH_TEST_FOR_CONTEXTS(ClipStack_SWMask, | 
|  | skgpu::IsRenderingContext, | 
|  | r, | 
|  | ctxInfo, | 
|  | disable_tessellation_atlas, | 
|  | CtsEnforcement::kNever) { | 
|  | using ClipStack = skgpu::ganesh::ClipStack; | 
|  | using SurfaceDrawContext = skgpu::ganesh::SurfaceDrawContext; | 
|  |  | 
|  | GrDirectContext* context = ctxInfo.directContext(); | 
|  | std::unique_ptr<SurfaceDrawContext> sdc = SurfaceDrawContext::Make( | 
|  | context, GrColorType::kRGBA_8888, nullptr, SkBackingFit::kExact, kDeviceBounds.size(), | 
|  | SkSurfaceProps(), /*label=*/{}); | 
|  |  | 
|  | std::unique_ptr<ClipStack> cs(new ClipStack(kDeviceBounds, &SkMatrix::I(), false)); | 
|  |  | 
|  | auto addMaskRequiringClip = [&](SkScalar x, SkScalar y, SkScalar radius) { | 
|  | SkPath path; | 
|  | path.addCircle(x, y, radius); | 
|  | path.addCircle(x + radius / 2.f, y + radius / 2.f, radius); | 
|  | path.setFillType(SkPathFillType::kEvenOdd); | 
|  |  | 
|  | // Use AA so that clip application does not route through the stencil buffer | 
|  | cs->clipPath(SkMatrix::I(), path, GrAA::kYes, SkClipOp::kIntersect); | 
|  | }; | 
|  |  | 
|  | auto drawRect = [&](SkRect drawBounds) { | 
|  | GrPaint paint; | 
|  | paint.setColor4f({1.f, 1.f, 1.f, 1.f}); | 
|  | sdc->drawRect(cs.get(), std::move(paint), GrAA::kYes, SkMatrix::I(), drawBounds); | 
|  | }; | 
|  |  | 
|  | auto generateMask = [&](SkRect drawBounds) { | 
|  | skgpu::UniqueKey priorKey = cs->testingOnly_getLastSWMaskKey(); | 
|  | drawRect(drawBounds); | 
|  | skgpu::UniqueKey newKey = cs->testingOnly_getLastSWMaskKey(); | 
|  | REPORTER_ASSERT(r, priorKey != newKey, "Did not generate a new SW mask key as expected"); | 
|  | return newKey; | 
|  | }; | 
|  |  | 
|  | auto verifyKeys = [&](const std::vector<skgpu::UniqueKey>& expectedKeys, | 
|  | const std::vector<skgpu::UniqueKey>& releasedKeys) { | 
|  | context->flush(); | 
|  | GrProxyProvider* proxyProvider = context->priv().proxyProvider(); | 
|  |  | 
|  | #ifdef SK_DEBUG | 
|  | // The proxy providers key count fluctuates based on proxy lifetime, but we want to | 
|  | // verify the resource count, and that requires using key tags that are debug-only. | 
|  | SkASSERT(expectedKeys.size() > 0 || releasedKeys.size() > 0); | 
|  | const char* tag = expectedKeys.size() > 0 ? expectedKeys[0].tag() : releasedKeys[0].tag(); | 
|  | GrResourceCache* cache = context->priv().getResourceCache(); | 
|  | int numProxies = cache->countUniqueKeysWithTag(tag); | 
|  | REPORTER_ASSERT(r, (int) expectedKeys.size() == numProxies, | 
|  | "Unexpected proxy count, got %d, not %d", | 
|  | numProxies, (int) expectedKeys.size()); | 
|  | #endif | 
|  |  | 
|  | for (const auto& key : expectedKeys) { | 
|  | auto proxy = proxyProvider->findOrCreateProxyByUniqueKey(key); | 
|  | REPORTER_ASSERT(r, SkToBool(proxy), "Unable to find resource for expected mask key"); | 
|  | } | 
|  | for (const auto& key : releasedKeys) { | 
|  | auto proxy = proxyProvider->findOrCreateProxyByUniqueKey(key); | 
|  | REPORTER_ASSERT(r, !SkToBool(proxy), "SW mask not released as expected"); | 
|  | } | 
|  | }; | 
|  |  | 
|  | // Creates a mask for a complex clip | 
|  | cs->save(); | 
|  | addMaskRequiringClip(5.f, 5.f, 20.f); | 
|  | skgpu::UniqueKey keyADepth1 = generateMask({0.f, 0.f, 20.f, 20.f}); | 
|  | skgpu::UniqueKey keyBDepth1 = generateMask({10.f, 10.f, 30.f, 30.f}); | 
|  | verifyKeys({keyADepth1, keyBDepth1}, {}); | 
|  |  | 
|  | // Creates a new mask for a new save record, but doesn't delete the old records | 
|  | cs->save(); | 
|  | addMaskRequiringClip(6.f, 6.f, 15.f); | 
|  | skgpu::UniqueKey keyADepth2 = generateMask({0.f, 0.f, 20.f, 20.f}); | 
|  | skgpu::UniqueKey keyBDepth2 = generateMask({10.f, 10.f, 30.f, 30.f}); | 
|  | verifyKeys({keyADepth1, keyBDepth1, keyADepth2, keyBDepth2}, {}); | 
|  |  | 
|  | // Release after modifying the current record (even if we don't draw anything) | 
|  | addMaskRequiringClip(4.f, 4.f, 15.f); | 
|  | skgpu::UniqueKey keyCDepth2 = generateMask({4.f, 4.f, 16.f, 20.f}); | 
|  | verifyKeys({keyADepth1, keyBDepth1, keyCDepth2}, {keyADepth2, keyBDepth2}); | 
|  |  | 
|  | // Release after restoring an older record | 
|  | cs->restore(); | 
|  | verifyKeys({keyADepth1, keyBDepth1}, {keyCDepth2}); | 
|  |  | 
|  | // Drawing finds the old masks at depth 1 still w/o making new ones | 
|  | drawRect({0.f, 0.f, 20.f, 20.f}); | 
|  | drawRect({10.f, 10.f, 30.f, 30.f}); | 
|  | verifyKeys({keyADepth1, keyBDepth1}, {}); | 
|  |  | 
|  | // Drawing something contained within a previous mask also does not make a new one | 
|  | drawRect({5.f, 5.f, 15.f, 15.f}); | 
|  | verifyKeys({keyADepth1, keyBDepth1}, {}); | 
|  |  | 
|  | // Release on destruction | 
|  | cs = nullptr; | 
|  | verifyKeys({}, {keyADepth1, keyBDepth1}); | 
|  | } |