diff options
author | bsalomon <bsalomon@google.com> | 2016-04-25 13:37:22 -0700 |
---|---|---|
committer | Commit bot <commit-bot@chromium.org> | 2016-04-25 13:37:22 -0700 |
commit | c885dacfe4625af8b0e2e5c6e8a8ae8dc2d620a8 (patch) | |
tree | 8fc3e643c7e8695c02735e5e0e58e4f28d5bffe8 | |
parent | 21736bd8b51cb78dba14a5bedf6690619fdb9ed3 (diff) |
Add initial implementation of GrShape and GrStyle classes and tests
The initial intent is to use GrShape to simplify the mask blur code paths. However, I also want to use this to explore a more unified drawing code flow for different geometry types. The goal is to have a single representation for geometries+styling that attempts to always keep the geometry in the simplest form (e.g. preferring rrects to paths). It also allows for converting styling information into modified geometry and for computing consistent keys.
BUG=skia:
GOLD_TRYBOT_URL= https://gold.skia.org/search2?unt=true&query=source_type%3Dgm&master=false&issue=1822723003
Review URL: https://codereview.chromium.org/1822723003
-rw-r--r-- | gyp/gpu.gypi | 4 | ||||
-rw-r--r-- | src/gpu/GrShape.cpp | 254 | ||||
-rw-r--r-- | src/gpu/GrShape.h | 160 | ||||
-rw-r--r-- | src/gpu/GrStyle.cpp | 33 | ||||
-rw-r--r-- | src/gpu/GrStyle.h | 92 | ||||
-rw-r--r-- | tests/GrShapeTest.cpp | 315 |
6 files changed, 858 insertions, 0 deletions
diff --git a/gyp/gpu.gypi b/gyp/gpu.gypi index 999b8067d1..d0e78431ec 100644 --- a/gyp/gpu.gypi +++ b/gyp/gpu.gypi @@ -163,12 +163,16 @@ '<(skia_src_path)/gpu/GrResourceCache.h', '<(skia_src_path)/gpu/GrResourceProvider.cpp', '<(skia_src_path)/gpu/GrResourceProvider.h', + '<(skia_src_path)/gpu/GrShape.cpp', + '<(skia_src_path)/gpu/GrShape.h', '<(skia_src_path)/gpu/GrStencil.cpp', '<(skia_src_path)/gpu/GrStencil.h', '<(skia_src_path)/gpu/GrStencilAttachment.cpp', '<(skia_src_path)/gpu/GrStencilAttachment.h', '<(skia_src_path)/gpu/GrStrokeInfo.cpp', '<(skia_src_path)/gpu/GrStrokeInfo.h', + '<(skia_src_path)/gpu/GrStyle.cpp', + '<(skia_src_path)/gpu/GrStyle.h', '<(skia_src_path)/gpu/GrTessellator.cpp', '<(skia_src_path)/gpu/GrTessellator.h', '<(skia_src_path)/gpu/GrTraceMarker.cpp', diff --git a/src/gpu/GrShape.cpp b/src/gpu/GrShape.cpp new file mode 100644 index 0000000000..27450cb695 --- /dev/null +++ b/src/gpu/GrShape.cpp @@ -0,0 +1,254 @@ +/* + * Copyright 2016 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#include "GrShape.h" + +GrShape& GrShape::operator=(const GrShape& that) { + bool wasPath = Type::kPath == fType; + fStyle = that.fStyle; + fType = that.fType; + switch (fType) { + case Type::kEmpty: + if (wasPath) { + fPath.reset(); + } + break; + case Type::kRRect: + if (wasPath) { + fPath.reset(); + } + fRRect = that.fRRect; + break; + case Type::kPath: + if (wasPath) { + *fPath.get() = *that.fPath.get(); + } else { + fPath.set(*that.fPath.get()); + } + break; + } + fInheritedKey.reset(that.fInheritedKey.count()); + memcpy(fInheritedKey.get(), that.fInheritedKey.get(), + sizeof(uint32_t) * fInheritedKey.count()); + return *this; +} + +int GrShape::unstyledKeySize() const { + if (fInheritedKey.count()) { + return fInheritedKey.count(); + } + switch (fType) { + case Type::kEmpty: + return 1; + case Type::kRRect: + SkASSERT(!fInheritedKey.count()); + SkASSERT(0 == SkRRect::kSizeInMemory % sizeof(uint32_t)); + return SkRRect::kSizeInMemory / sizeof(uint32_t); + case Type::kPath: + if (fPath.get()->isVolatile()) { + return -1; + } else { + return 1; + } + } + SkFAIL("Should never get here."); + return 0; +} + +void GrShape::writeUnstyledKey(uint32_t* key) const { + SkASSERT(this->unstyledKeySize()); + SkDEBUGCODE(uint32_t* origKey = key;) + if (fInheritedKey.count()) { + memcpy(key, fInheritedKey.get(), sizeof(uint32_t) * fInheritedKey.count()); + SkDEBUGCODE(key += fInheritedKey.count();) + } else { + switch (fType) { + case Type::kEmpty: + *key++ = 1; + break; + case Type::kRRect: + fRRect.writeToMemory(key); + key += SkRRect::kSizeInMemory / sizeof(uint32_t); + break; + case Type::kPath: + SkASSERT(!fPath.get()->isVolatile()); + *key++ = fPath.get()->getGenerationID(); + break; + } + } + SkASSERT(key - origKey == this->unstyledKeySize()); +} + +int GrShape::StyleKeySize(const GrStyle& style, bool stopAfterPE) { + GR_STATIC_ASSERT(sizeof(uint32_t) == sizeof(SkScalar)); + int size = 0; + if (style.isDashed()) { + // One scalar for dash phase and one for each dash value. + size += 1 + style.dashIntervalCnt(); + } else if (style.pathEffect()) { + // No key for a generic path effect. + return -1; + } + + if (stopAfterPE) { + return size; + } + + if (style.strokeRec().needToApply()) { + // One for style/cap/join, 2 for miter and width. + size += 3; + } + return size; +} + +void GrShape::StyleKey(uint32_t* key, const GrStyle& style, bool stopAfterPE) { + SkASSERT(key); + SkASSERT(StyleKeySize(style, stopAfterPE) >= 0); + GR_STATIC_ASSERT(sizeof(uint32_t) == sizeof(SkScalar)); + + int i = 0; + if (style.isDashed()) { + GR_STATIC_ASSERT(sizeof(style.dashPhase()) == sizeof(uint32_t)); + SkScalar phase = style.dashPhase(); + memcpy(&key[i++], &phase, sizeof(SkScalar)); + + int32_t count = style.dashIntervalCnt(); + // Dash count should always be even. + SkASSERT(0 == (count & 0x1)); + const SkScalar* intervals = style.dashIntervals(); + int intervalByteCnt = count * sizeof(SkScalar); + memcpy(&key[i], intervals, intervalByteCnt); + SkDEBUGCODE(i += count); + } else { + SkASSERT(!style.pathEffect()); + } + + if (!stopAfterPE && style.strokeRec().needToApply()) { + enum { + kStyleBits = 2, + kJoinBits = 2, + kCapBits = 32 - kStyleBits - kJoinBits, + + kJoinShift = kStyleBits, + kCapShift = kJoinShift + kJoinBits, + }; + GR_STATIC_ASSERT(SkStrokeRec::kStyleCount <= (1 << kStyleBits)); + GR_STATIC_ASSERT(SkPaint::kJoinCount <= (1 << kJoinBits)); + GR_STATIC_ASSERT(SkPaint::kCapCount <= (1 << kCapBits)); + key[i++] = style.strokeRec().getStyle() | + style.strokeRec().getJoin() << kJoinShift | + style.strokeRec().getCap() << kCapShift; + + SkScalar scalar; + // Miter limit only affects miter joins + scalar = SkPaint::kMiter_Join == style.strokeRec().getJoin() + ? style.strokeRec().getMiter() + : -1.f; + memcpy(&key[i++], &scalar, sizeof(scalar)); + + scalar = style.strokeRec().getWidth(); + memcpy(&key[i++], &scalar, sizeof(scalar)); + } + SkASSERT(StyleKeySize(style, stopAfterPE) == i); +} + +void GrShape::setInheritedKey(const GrShape &parent, bool stopAfterPE){ + SkASSERT(!fInheritedKey.count()); + // If the output shape turns out to be simple, then we will just use its geometric key + if (Type::kPath == fType) { + // We want ApplyFullStyle(ApplyPathEffect(shape)) to have the same key as + // ApplyFullStyle(shape). + // The full key is structured as (geo,path_effect,stroke). + // If we do ApplyPathEffect we get get,path_effect as the inherited key. If we then + // do ApplyFullStyle we'll memcpy geo,path_effect into the new inherited key + // and then append the style key (which should now be stroke only) at the end. + int parentCnt = parent.fInheritedKey.count(); + bool useParentGeoKey = !parentCnt; + if (useParentGeoKey) { + parentCnt = parent.unstyledKeySize(); + } + int styleCnt = StyleKeySize(parent.fStyle, stopAfterPE); + if (styleCnt < 0) { + // The style doesn't allow a key, set the path to volatile so that we fail when + // we try to get a key for the shape. + fPath.get()->setIsVolatile(true); + } else { + fInheritedKey.reset(parentCnt + styleCnt); + if (useParentGeoKey) { + // This will be the geo key. + parent.writeUnstyledKey(fInheritedKey.get()); + } else { + // This should be geo,path_effect + memcpy(fInheritedKey.get(), parent.fInheritedKey.get(), + parentCnt * sizeof(uint32_t)); + } + // Now turn (geo,path_effect) or (geo) into (geo,path_effect,stroke) + StyleKey(fInheritedKey.get() + parentCnt, parent.fStyle, stopAfterPE); + } + } +} + +GrShape::GrShape(const GrShape& that) : fType(that.fType), fStyle(that.fStyle) { + switch (fType) { + case Type::kEmpty: + return; + case Type::kRRect: + fRRect = that.fRRect; + return; + case Type::kPath: + fPath.set(*that.fPath.get()); + return; + } + fInheritedKey.reset(that.fInheritedKey.count()); + memcpy(fInheritedKey.get(), that.fInheritedKey.get(), + sizeof(uint32_t) * fInheritedKey.count()); +} + +GrShape::GrShape(const GrShape& parent, bool stopAfterPE) { + fType = Type::kEmpty; + SkPathEffect* pe = parent.fStyle.pathEffect(); + const SkPath* inPath; + SkStrokeRec strokeRec = parent.fStyle.strokeRec(); + if (pe) { + fType = Type::kPath; + fPath.init(); + if (parent.fType == Type::kPath) { + inPath = parent.fPath.get(); + } else { + inPath = fPath.get(); + parent.asPath(fPath.get()); + } + // Should we consider bounds? Would have to include in key, but it'd be nice to know + // if the bounds actually modified anything before including in key. + if (!pe->filterPath(fPath.get(), *inPath, &strokeRec, nullptr)) { + // Make an empty unstyled shape if filtering fails. + fType = Type::kEmpty; + fStyle = GrStyle(); + fPath.reset(); + return; + } + inPath = fPath.get(); + } else if (stopAfterPE || !strokeRec.needToApply()) { + *this = parent; + return; + } else { + fType = Type::kPath; + fPath.init(); + if (parent.fType == Type::kPath) { + inPath = parent.fPath.get(); + } else { + inPath = fPath.get(); + parent.asPath(fPath.get()); + } + } + if (!stopAfterPE) { + strokeRec.applyToPath(fPath.get(), *inPath); + } else { + fStyle = GrStyle(strokeRec, nullptr); + } + this->setInheritedKey(parent, stopAfterPE); +} diff --git a/src/gpu/GrShape.h b/src/gpu/GrShape.h new file mode 100644 index 0000000000..3c3f9ecec7 --- /dev/null +++ b/src/gpu/GrShape.h @@ -0,0 +1,160 @@ +/* + * Copyright 2016 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef GrShape_DEFINED +#define GrShape_DEFINED + +#include "GrStyle.h" +#include "SkPath.h" +#include "SkRRect.h" +#include "SkTemplates.h" +#include "SkTLazy.h" + +/** + * Represents a geometric shape (rrect or path) and the GrStyle that it should be rendered with. + * It is possible to apply the style to the GrShape to produce a new GrShape where the geometry + * reflects the styling information (e.g. is stroked). It is also possible to apply just the + * path effect from the style. In this case the resulting shape will include any remaining + * stroking information that is to be applied after the path effect. + * + * Shapes can produce keys that represent only the geometry information, not the style. Note that + * when styling information is applied to produce a new shape then the style has been converted + * to geometric information and is included in the new shape's key. When the same style is applied + * to two shapes that reflect the same underlying geometry the computed keys of the stylized shapes + * will be the same. + * + * Currently this can only be constructed from a rrect, though it can become a path by applying + * style to the geometry. The idea is to expand this to cover most or all of the geometries that + * have SkCanvas::draw APIs. + */ +class GrShape { +public: + GrShape() : fType(Type::kEmpty) {} + + explicit GrShape(const SkRRect& rrect) : fType(Type::kRRect), fRRect(rrect) {} + explicit GrShape(const SkRect& rect) : fType(Type::kRRect), fRRect(SkRRect::MakeRect(rect)) {} + + GrShape(const SkRRect& rrect, const GrStyle& style) + : fType(Type::kRRect) + , fRRect(rrect) + , fStyle(style) {} + + GrShape(const SkRect& rect, const GrStyle& style) + : fType(Type::kRRect) + , fRRect(SkRRect::MakeRect(rect)) + , fStyle(style) {} + + GrShape(const SkRRect& rrect, const SkPaint& paint) + : fType(Type::kRRect) + , fRRect(rrect) + , fStyle(paint) {} + + GrShape(const SkRect& rect, const SkPaint& paint) + : fType(Type::kRRect) + , fRRect(SkRRect::MakeRect(rect)) + , fStyle(paint) {} + + GrShape(const GrShape&); + GrShape& operator=(const GrShape& that); + + ~GrShape() { + if (Type::kPath == fType) { + fPath.reset(); + } + } + + const GrStyle& style() const { return fStyle; } + + /** + * Returns a GrShape where the shape's geometry fully reflects the original shape's GrStyle. + * The GrStyle of the returned shape will either be fill or hairline. + */ + GrShape applyFullStyle() { return GrShape(*this, false); } + + /** + * Similar to above but applies only the path effect. Path effects take the original geometry + * and fill/stroking information and compute a new geometry and residual fill/stroking + * information to be applied. The path effect's output geometry and stroking will be captured + * in the returned GrShape. + */ + GrShape applyPathEffect() { return GrShape(*this, true); } + + bool asRRect(SkRRect* rrect) const { + if (Type::kRRect != fType) { + return false; + } + if (rrect) { + *rrect = fRRect; + } + return true; + } + + void asPath(SkPath* out) const { + switch (fType) { + case Type::kRRect: + out->reset(); + out->addRRect(fRRect); + break; + case Type::kPath: + *out = *fPath.get(); + break; + case Type::kEmpty: + out->reset(); + break; + } + } + + /** + * Gets the size of the key for the shape represented by this GrShape (ignoring its styling). + * A negative value is returned if the shape has no key (shouldn't be cached). + */ + int unstyledKeySize() const; + + /** + * Writes unstyledKeySize() bytes into the provided pointer. Assumes that there is enough + * space allocated for the key and that unstyledKeySize() does not return a negative value + * for this shape. + */ + void writeUnstyledKey(uint32_t* key) const; + +private: + /** + * Computes the key length for a GrStyle. The return will be negative if it cannot be turned + * into a key. + */ + static int StyleKeySize(const GrStyle& , bool stopAfterPE); + + /** + * Writes a unique key for the style into the provided buffer. This function assumes the buffer + * has room for at least StyleKeySize() values. It assumes that StyleKeySize() returns a + * positive value for the style and stopAfterPE param. This is written so that the key for just + * dash application followed by the key for the remaining SkStrokeRec is the same as the + * key for applying dashing and SkStrokeRec all at once. + */ + static void StyleKey(uint32_t*, const GrStyle&, bool stopAfterPE); + + /** Constructor used by Apply* functions */ + GrShape(const GrShape& parentShape, bool stopAfterPE); + + /** + * Determines the key we should inherit from the input shape's geometry and style when + * we are applying the style to create a new shape. + */ + void setInheritedKey(const GrShape& parentShape, bool stopAfterPE); + + enum class Type { + kEmpty, + kRRect, + kPath, + } fType; + + SkRRect fRRect; + SkTLazy<SkPath> fPath; + GrStyle fStyle; + SkAutoSTArray<8, uint32_t> fInheritedKey; +}; +#endif diff --git a/src/gpu/GrStyle.cpp b/src/gpu/GrStyle.cpp new file mode 100644 index 0000000000..40a148bb4a --- /dev/null +++ b/src/gpu/GrStyle.cpp @@ -0,0 +1,33 @@ +/* + * Copyright 2016 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#include "GrStyle.h" + +void GrStyle::initPathEffect(SkPathEffect* pe) { + if (!pe) { + fDashInfo.fType = SkPathEffect::kNone_DashType; + return; + } + SkPathEffect::DashInfo info; + if (SkPathEffect::kDash_DashType == pe->asADash(&info)) { + if (fStrokeRec.getStyle() == SkStrokeRec::kFill_Style) { + fPathEffect.reset(nullptr); + } else { + fPathEffect.reset(SkSafeRef(pe)); + fDashInfo.fType = SkPathEffect::kDash_DashType; + fDashInfo.fIntervals.reset(info.fCount); + fDashInfo.fPhase = info.fPhase; + info.fIntervals = fDashInfo.fIntervals.get(); + pe->asADash(&info); + return; + } + } else { + fPathEffect.reset(SkSafeRef(pe)); + } + fDashInfo.fType = SkPathEffect::kNone_DashType; + fDashInfo.fIntervals.reset(0); +} diff --git a/src/gpu/GrStyle.h b/src/gpu/GrStyle.h new file mode 100644 index 0000000000..4eef252de4 --- /dev/null +++ b/src/gpu/GrStyle.h @@ -0,0 +1,92 @@ +/* + * Copyright 2016 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#ifndef GrStyle_DEFINED +#define GrStyle_DEFINED + +#include "GrTypes.h" +#include "SkPathEffect.h" +#include "SkStrokeRec.h" +#include "SkTemplates.h" + +/** + * Represents the various ways that a GrShape can be styled. It has fill/stroking information + * as well as an optional path effect. If the path effect represents dashing, the dashing + * information is extracted from the path effect and stored explicitly. + * + * This object does not support stroke-and-fill styling. It is expected that stroking and filling + * is handled by drawing a stroke and a fill separately. + * + * This will replace GrStrokeInfo as GrShape is deployed. + */ +class GrStyle { +public: + GrStyle() : fStrokeRec(SkStrokeRec::kFill_InitStyle) { + fDashInfo.fType = SkPathEffect::kNone_DashType; + } + + GrStyle(const SkStrokeRec& strokeRec, SkPathEffect* pe) : fStrokeRec(strokeRec) { + SkASSERT(SkStrokeRec::kStrokeAndFill_Style != strokeRec.getStyle()); + this->initPathEffect(pe); + } + + GrStyle(const GrStyle& that) : fStrokeRec(SkStrokeRec::kFill_InitStyle) { + *this = that; + } + + explicit GrStyle(const SkPaint& paint) : fStrokeRec(paint) { + SkASSERT(SkStrokeRec::kStrokeAndFill_Style != fStrokeRec.getStyle()); + this->initPathEffect(paint.getPathEffect()); + } + + GrStyle& operator=(const GrStyle& that) { + fPathEffect = that.fPathEffect; + fDashInfo = that.fDashInfo; + fStrokeRec = that.fStrokeRec; + return *this; + } + SkPathEffect* pathEffect() const { return fPathEffect.get(); } + + bool isDashed() const { return SkPathEffect::kDash_DashType == fDashInfo.fType; } + SkScalar dashPhase() const { + SkASSERT(this->isDashed()); + return fDashInfo.fPhase; + } + int dashIntervalCnt() const { + SkASSERT(this->isDashed()); + return fDashInfo.fIntervals.count(); + } + const SkScalar* dashIntervals() const { + SkASSERT(this->isDashed()); + return fDashInfo.fIntervals.get(); + } + + const SkStrokeRec& strokeRec() const { return fStrokeRec; } + +private: + void initPathEffect(SkPathEffect* pe); + + struct DashInfo { + DashInfo& operator=(const DashInfo& that) { + fType = that.fType; + fPhase = that.fPhase; + fIntervals.reset(that.fIntervals.count()); + memcpy(fIntervals.get(), that.fIntervals.get(), + sizeof(SkScalar) * that.fIntervals.count()); + return *this; + } + SkPathEffect::DashType fType; + SkScalar fPhase; + SkAutoSTArray<4, SkScalar> fIntervals; + }; + + SkStrokeRec fStrokeRec; + sk_sp<SkPathEffect> fPathEffect; + DashInfo fDashInfo; +}; + +#endif diff --git a/tests/GrShapeTest.cpp b/tests/GrShapeTest.cpp new file mode 100644 index 0000000000..8c81c51ed3 --- /dev/null +++ b/tests/GrShapeTest.cpp @@ -0,0 +1,315 @@ +/* + * Copyright 2016 Google Inc. + * + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +#include <initializer_list> +#include <functional> +#include "Test.h" +#if SK_SUPPORT_GPU +#include "GrShape.h" +#include "SkPath.h" +#include "SkDashPathEffect.h" + +namespace { +class TestCase { +public: + TestCase(const SkRRect& rrect, const SkPaint& paint) : fBase(rrect, paint) { + this->init(); + } + + struct SelfExpectations { + bool fPEHasEffect; + bool fPEHasValidKey; + bool fStrokeApplies; + }; + + void testExpectations(skiatest::Reporter* reporter, SelfExpectations expectations) const; + + enum ComparisonExpecation { + kAllDifferent_ComparisonExpecation, + kSameUpToPE_ComparisonExpecation, + kSameUpToStroke_ComparisonExpecation, + kAllSame_ComparisonExpecation, + }; + + void compare(skiatest::Reporter*, const TestCase& that, ComparisonExpecation) const; + +private: + void init() { + fAppliedPE = fBase.applyPathEffect(); + fAppliedPEThenStroke = fAppliedPE.applyFullStyle(); + fAppliedFull = fBase.applyFullStyle(); + + fBaseKeyIsValid = MakeKey(&fBaseKey, fBase); + fAppliedPEKeyIsValid = MakeKey(&fAppliedPEKey, fAppliedPE); + fAppliedPEThenStrokeKeyIsValid = MakeKey(&fAppliedPEThenStrokeKey, fAppliedPEThenStroke); + fAppliedFullKeyIsValid = MakeKey(&fAppliedFullKey, fAppliedFull); + } + + using Key = SkTArray<uint32_t>; + + static bool MakeKey(Key* key, const GrShape& shape) { + int size = shape.unstyledKeySize(); + if (size <= 0) { + return false; + } + key->reset(size); + shape.writeUnstyledKey(key->begin()); + return true; + } + + GrShape fBase; + GrShape fAppliedPE; + GrShape fAppliedPEThenStroke; + GrShape fAppliedFull; + + Key fBaseKey; + Key fAppliedPEKey; + Key fAppliedPEThenStrokeKey; + Key fAppliedFullKey; + + bool fBaseKeyIsValid; + bool fAppliedPEKeyIsValid; + bool fAppliedPEThenStrokeKeyIsValid; + bool fAppliedFullKeyIsValid; +}; + +void TestCase::testExpectations(skiatest::Reporter* reporter, SelfExpectations expectations) const { + // Applying the path effect and then the stroke should always be the same as applying + // both in one go. + REPORTER_ASSERT(reporter, fAppliedPEThenStrokeKey == fAppliedFullKey); + // The base's key should always be valid (unless the path is volatile) + REPORTER_ASSERT(reporter, fBaseKeyIsValid); + if (expectations.fPEHasEffect) { + REPORTER_ASSERT(reporter, fBaseKey != fAppliedPEKey); + REPORTER_ASSERT(reporter, expectations.fPEHasEffect == fAppliedPEKeyIsValid); + REPORTER_ASSERT(reporter, fBaseKey != fAppliedFullKey); + REPORTER_ASSERT(reporter, expectations.fPEHasEffect == fAppliedFullKeyIsValid); + if (expectations.fStrokeApplies && expectations.fPEHasValidKey) { + REPORTER_ASSERT(reporter, fAppliedPEKey != fAppliedFullKey); + REPORTER_ASSERT(reporter, expectations.fPEHasEffect == fAppliedFullKeyIsValid); + } + } else { + REPORTER_ASSERT(reporter, fBaseKey == fAppliedPEKey); + if (expectations.fStrokeApplies) { + REPORTER_ASSERT(reporter, fBaseKey != fAppliedFullKey); + } else { + REPORTER_ASSERT(reporter, fBaseKey == fAppliedFullKey); + } + } +} + +void TestCase::compare(skiatest::Reporter* reporter, const TestCase& that, + ComparisonExpecation expectation) const { + switch (expectation) { + case kAllDifferent_ComparisonExpecation: + REPORTER_ASSERT(reporter, fBaseKey != that.fBaseKey); + REPORTER_ASSERT(reporter, fAppliedPEKey != that.fAppliedPEKey); + REPORTER_ASSERT(reporter, fAppliedFullKey != that.fAppliedFullKey); + break; + case kSameUpToPE_ComparisonExpecation: + REPORTER_ASSERT(reporter, fBaseKey == that.fBaseKey); + REPORTER_ASSERT(reporter, fAppliedPEKey != that.fAppliedPEKey); + REPORTER_ASSERT(reporter, fAppliedFullKey != that.fAppliedFullKey); + break; + case kSameUpToStroke_ComparisonExpecation: + REPORTER_ASSERT(reporter, fBaseKey == that.fBaseKey); + REPORTER_ASSERT(reporter, fAppliedPEKey == that.fAppliedPEKey); + REPORTER_ASSERT(reporter, fAppliedFullKey != that.fAppliedFullKey); + break; + case kAllSame_ComparisonExpecation: + REPORTER_ASSERT(reporter, fBaseKey == that.fBaseKey); + REPORTER_ASSERT(reporter, fAppliedPEKey == that.fAppliedPEKey); + REPORTER_ASSERT(reporter, fAppliedFullKey == that.fAppliedFullKey); + break; + } +} +} // namespace + +static sk_sp<SkPathEffect> make_dash() { + static const SkScalar kIntervals[] = { 0.25, 3.f, 0.5, 2.f }; + static const SkScalar kPhase = 0.75; + return SkDashPathEffect::Make(kIntervals, SK_ARRAY_COUNT(kIntervals), kPhase); +} + +static sk_sp<SkPathEffect> make_null_dash() { + static const SkScalar kNullIntervals[] = {0, 0, 0, 0, 0, 0}; + return SkDashPathEffect::Make(kNullIntervals, SK_ARRAY_COUNT(kNullIntervals), 0.f); +} + +static void test_basic(skiatest::Reporter* reporter, const SkRRect& rrect) { + sk_sp<SkPathEffect> dashPE = make_dash(); + + TestCase::SelfExpectations expectations; + SkPaint fill; + + TestCase fillCase(rrect, fill); + expectations.fPEHasEffect = false; + expectations.fPEHasValidKey = false; + expectations.fStrokeApplies = false; + fillCase.testExpectations(reporter, expectations); + // Test that another GrShape instance built from the same primitive is the same. + TestCase(rrect, fill).compare(reporter, fillCase, TestCase::kAllSame_ComparisonExpecation); + + SkPaint stroke2RoundBevel; + stroke2RoundBevel.setStyle(SkPaint::kStroke_Style); + stroke2RoundBevel.setStrokeCap(SkPaint::kRound_Cap); + stroke2RoundBevel.setStrokeJoin(SkPaint::kBevel_Join); + stroke2RoundBevel.setStrokeWidth(2.f); + TestCase stroke2RoundBevelCase(rrect, stroke2RoundBevel); + expectations.fPEHasValidKey = true; + expectations.fPEHasEffect = false; + expectations.fStrokeApplies = true; + stroke2RoundBevelCase.testExpectations(reporter, expectations); + TestCase(rrect, stroke2RoundBevel).compare(reporter, stroke2RoundBevelCase, + TestCase::kAllSame_ComparisonExpecation); + + SkPaint stroke2RoundBevelDash = stroke2RoundBevel; + stroke2RoundBevelDash.setPathEffect(make_dash()); + TestCase stroke2RoundBevelDashCase(rrect, stroke2RoundBevelDash); + expectations.fPEHasValidKey = true; + expectations.fPEHasEffect = true; + expectations.fStrokeApplies = true; + stroke2RoundBevelDashCase.testExpectations(reporter, expectations); + TestCase(rrect, stroke2RoundBevelDash).compare(reporter, stroke2RoundBevelDashCase, + TestCase::kAllSame_ComparisonExpecation); + + fillCase.compare(reporter, stroke2RoundBevelCase, + TestCase::kSameUpToStroke_ComparisonExpecation); + fillCase.compare(reporter, stroke2RoundBevelDashCase, + TestCase::kSameUpToPE_ComparisonExpecation); + stroke2RoundBevelCase.compare(reporter, stroke2RoundBevelDashCase, + TestCase::kSameUpToPE_ComparisonExpecation); +} + +template <typename T> +static void test_stroke_param(skiatest::Reporter* reporter, const SkRRect& rrect, + std::function<void(SkPaint*, T)> setter, T a, T b) { + // Set the stroke width so that we don't get hairline. However, call the function second so that + // it can override. + SkPaint strokeA; + strokeA.setStyle(SkPaint::kStroke_Style); + strokeA.setStrokeWidth(2.f); + setter(&strokeA, a); + SkPaint strokeB; + strokeB.setStyle(SkPaint::kStroke_Style); + strokeB.setStrokeWidth(2.f); + setter(&strokeB, b); + + TestCase strokeACase(rrect, strokeA); + TestCase strokeBCase(rrect, strokeB); + strokeACase.compare(reporter, strokeBCase, TestCase::kSameUpToStroke_ComparisonExpecation); + + // Make sure stroking params don't affect fill style. + SkPaint fillA = strokeA, fillB = strokeB; + fillA.setStyle(SkPaint::kFill_Style); + fillB.setStyle(SkPaint::kFill_Style); + TestCase fillACase(rrect, fillA); + TestCase fillBCase(rrect, fillB); + fillACase.compare(reporter, fillBCase, TestCase::kAllSame_ComparisonExpecation); + + // Make sure just applying the dash but not stroke gives the same key for both stroking + // variations. + SkPaint dashA = strokeA, dashB = strokeB; + dashA.setPathEffect(make_dash()); + dashB.setPathEffect(make_dash()); + TestCase dashACase(rrect, dashA); + TestCase dashBCase(rrect, dashB); + dashACase.compare(reporter, dashBCase, TestCase::kSameUpToStroke_ComparisonExpecation); +} + +static void test_miter_limit(skiatest::Reporter* reporter, const SkRRect& rrect) { + // Miter limit should only matter when stroking with miter joins. It shouldn't affect other + // joins or fills. + SkPaint miterA; + miterA.setStyle(SkPaint::kStroke_Style); + miterA.setStrokeWidth(2.f); + miterA.setStrokeJoin(SkPaint::kMiter_Join); + miterA.setStrokeMiter(0.5f); + SkPaint miterB = miterA; + miterA.setStrokeMiter(0.6f); + + TestCase miterACase(rrect, miterA); + TestCase miterBCase(rrect, miterB); + miterACase.compare(reporter, miterBCase, TestCase::kSameUpToStroke_ComparisonExpecation); + + SkPaint noMiterA = miterA, noMiterB = miterB; + noMiterA.setStrokeJoin(SkPaint::kRound_Join); + noMiterB.setStrokeJoin(SkPaint::kRound_Join); + TestCase noMiterACase(rrect, noMiterA); + TestCase noMiterBCase(rrect, noMiterB); + noMiterACase.compare(reporter, noMiterBCase, TestCase::kAllSame_ComparisonExpecation); + + SkPaint fillA = miterA, fillB = miterB; + fillA.setStyle(SkPaint::kFill_Style); + fillB.setStyle(SkPaint::kFill_Style); + TestCase fillACase(rrect, fillA); + TestCase fillBCase(rrect, fillB); + fillACase.compare(reporter, fillBCase, TestCase::kAllSame_ComparisonExpecation); +} + +static void test_dash_fill(skiatest::Reporter* reporter, const SkRRect& rrect) { + // A dash with no stroke should have no effect + for (auto md : {make_dash, make_null_dash}) { + SkPaint dashFill; + dashFill.setPathEffect(md()); + TestCase dashFillCase(rrect, dashFill); + + TestCase fillCase(rrect, SkPaint()); + dashFillCase.compare(reporter, fillCase, TestCase::kAllSame_ComparisonExpecation); + } +} + +void test_null_dash(skiatest::Reporter* reporter, const SkRRect& rrect) { + SkPaint fill; + SkPaint stroke; + stroke.setStyle(SkPaint::kStroke_Style); + stroke.setStrokeWidth(1.f); + SkPaint dash; + dash.setStyle(SkPaint::kStroke_Style); + dash.setStrokeWidth(1.f); + dash.setPathEffect(make_dash()); + SkPaint nullDash; + nullDash.setStyle(SkPaint::kStroke_Style); + nullDash.setStrokeWidth(1.f); + nullDash.setPathEffect(make_null_dash()); + + TestCase fillCase(rrect, fill); + TestCase strokeCase(rrect, stroke); + TestCase dashCase(rrect, dash); + TestCase nullDashCase(rrect, nullDash); + + nullDashCase.compare(reporter, fillCase, TestCase::kSameUpToStroke_ComparisonExpecation); + nullDashCase.compare(reporter, strokeCase, TestCase::kAllSame_ComparisonExpecation); + nullDashCase.compare(reporter, dashCase, TestCase::kSameUpToPE_ComparisonExpecation); +} + +DEF_TEST(GrShape, reporter) { + sk_sp<SkPathEffect> dashPE = make_dash(); + + for (auto rr : { SkRRect::MakeRect(SkRect::MakeWH(10, 10)), + SkRRect::MakeRectXY(SkRect::MakeWH(10, 10), 3, 4)}) { + test_basic(reporter, rr); + test_dash_fill(reporter, rr); + test_null_dash(reporter, rr); + // Test modifying various stroke params. + test_stroke_param<SkScalar>( + reporter, rr, + [](SkPaint* p, SkScalar w) { p->setStrokeWidth(w);}, + SkIntToScalar(2), SkIntToScalar(4)); + test_stroke_param<SkPaint::Cap>( + reporter, rr, + [](SkPaint* p, SkPaint::Cap c) { p->setStrokeCap(c);}, + SkPaint::kButt_Cap, SkPaint::kRound_Cap); + test_stroke_param<SkPaint::Join>( + reporter, rr, + [](SkPaint* p, SkPaint::Join j) { p->setStrokeJoin(j);}, + SkPaint::kMiter_Join, SkPaint::kRound_Join); + test_miter_limit(reporter, rr); + } +} + +#endif |