aboutsummaryrefslogtreecommitdiffhomepage
path: root/src
diff options
context:
space:
mode:
authorGravatar Brian Osman <brianosman@google.com>2017-06-06 16:46:38 -0400
committerGravatar Skia Commit-Bot <skia-commit-bot@chromium.org>2017-06-07 14:20:39 +0000
commit6383b298489504d7f8f822d7da575b04b14a9737 (patch)
treea7ca649cb21391d5f53075ab957545788f3c14dd /src
parentb52fa02227d8e7413d174bc5b3808f795d0215f0 (diff)
Handle too many (or too large) paths in GrDefaultPathRenderer
PathGeoBuilder constructs the geometry with the same basic technique as before, but allows interrupting the process to emit multiple draws. Original test case was 2000 non-AA stroked circles, which created ~66000 vertices. That now renders, as do various tests with a single large path (as well as filled paths). TODO: I think that this could be extracted and re-used for MSAA path renderer without too much work? I need to read that code more carefully to make sure it lines up. Bug: skia:6695 Change-Id: I18983ba3d4f475ae0651946958b4911008aa623f Reviewed-on: https://skia-review.googlesource.com/18360 Reviewed-by: Brian Salomon <bsalomon@google.com> Commit-Queue: Brian Osman <brianosman@google.com>
Diffstat (limited to 'src')
-rw-r--r--src/gpu/GrPathUtils.cpp13
-rw-r--r--src/gpu/GrPathUtils.h3
-rw-r--r--src/gpu/ops/GrDefaultPathRenderer.cpp475
3 files changed, 278 insertions, 213 deletions
diff --git a/src/gpu/GrPathUtils.cpp b/src/gpu/GrPathUtils.cpp
index 91a48f7b6b..918493982a 100644
--- a/src/gpu/GrPathUtils.cpp
+++ b/src/gpu/GrPathUtils.cpp
@@ -11,7 +11,6 @@
#include "SkGeometry.h"
#include "SkMathPriv.h"
-static const int MAX_POINTS_PER_CURVE = 1 << 10;
static const SkScalar gMinCurveTol = 0.0001f;
SkScalar GrPathUtils::scaleToleranceToSrc(SkScalar devTol,
@@ -45,7 +44,7 @@ uint32_t GrPathUtils::quadraticPointCount(const SkPoint points[], SkScalar tol)
SkScalar d = points[1].distanceToLineSegmentBetween(points[0], points[2]);
if (!SkScalarIsFinite(d)) {
- return MAX_POINTS_PER_CURVE;
+ return kMaxPointsPerCurve;
} else if (d <= tol) {
return 1;
} else {
@@ -55,7 +54,7 @@ uint32_t GrPathUtils::quadraticPointCount(const SkPoint points[], SkScalar tol)
// 2^(log4(x)) = sqrt(x);
SkScalar divSqrt = SkScalarSqrt(d / tol);
if (((SkScalar)SK_MaxS32) <= divSqrt) {
- return MAX_POINTS_PER_CURVE;
+ return kMaxPointsPerCurve;
} else {
int temp = SkScalarCeilToInt(divSqrt);
int pow2 = GrNextPow2(temp);
@@ -65,7 +64,7 @@ uint32_t GrPathUtils::quadraticPointCount(const SkPoint points[], SkScalar tol)
if (pow2 < 1) {
pow2 = 1;
}
- return SkTMin(pow2, MAX_POINTS_PER_CURVE);
+ return SkTMin(pow2, kMaxPointsPerCurve);
}
}
}
@@ -105,13 +104,13 @@ uint32_t GrPathUtils::cubicPointCount(const SkPoint points[],
points[2].distanceToLineSegmentBetweenSqd(points[0], points[3]));
d = SkScalarSqrt(d);
if (!SkScalarIsFinite(d)) {
- return MAX_POINTS_PER_CURVE;
+ return kMaxPointsPerCurve;
} else if (d <= tol) {
return 1;
} else {
SkScalar divSqrt = SkScalarSqrt(d / tol);
if (((SkScalar)SK_MaxS32) <= divSqrt) {
- return MAX_POINTS_PER_CURVE;
+ return kMaxPointsPerCurve;
} else {
int temp = SkScalarCeilToInt(SkScalarSqrt(d / tol));
int pow2 = GrNextPow2(temp);
@@ -121,7 +120,7 @@ uint32_t GrPathUtils::cubicPointCount(const SkPoint points[],
if (pow2 < 1) {
pow2 = 1;
}
- return SkTMin(pow2, MAX_POINTS_PER_CURVE);
+ return SkTMin(pow2, kMaxPointsPerCurve);
}
}
}
diff --git a/src/gpu/GrPathUtils.h b/src/gpu/GrPathUtils.h
index fdfd375427..421fca129b 100644
--- a/src/gpu/GrPathUtils.h
+++ b/src/gpu/GrPathUtils.h
@@ -165,5 +165,8 @@ namespace GrPathUtils {
// This value was chosen to approximate the supersampling accuracy of the raster path (16
// samples, or one quarter pixel).
static const SkScalar kDefaultTolerance = SkDoubleToScalar(0.25);
+
+ // We guarantee that no quad or cubic will ever produce more than this many points
+ static const int kMaxPointsPerCurve = 1 << 10;
};
#endif
diff --git a/src/gpu/ops/GrDefaultPathRenderer.cpp b/src/gpu/ops/GrDefaultPathRenderer.cpp
index c282036971..22e24e6544 100644
--- a/src/gpu/ops/GrDefaultPathRenderer.cpp
+++ b/src/gpu/ops/GrDefaultPathRenderer.cpp
@@ -59,37 +59,269 @@ GrDefaultPathRenderer::onGetStencilSupport(const GrShape& shape) const {
}
}
-static inline void append_countour_edge_indices(bool hairLine,
- uint16_t fanCenterIdx,
- uint16_t edgeV0Idx,
- uint16_t** indices) {
- // when drawing lines we're appending line segments along
- // the contour. When applying the other fill rules we're
- // drawing triangle fans around fanCenterIdx.
- if (!hairLine) {
- *((*indices)++) = fanCenterIdx;
- }
- *((*indices)++) = edgeV0Idx;
- *((*indices)++) = edgeV0Idx + 1;
-}
+// Needs to be large enough to handle quads/cubics, which have a worst-case of 1k points
+static const int kVerticesPerChunk = 16384;
+
+class PathGeoBuilder {
+public:
+ PathGeoBuilder(GrPrimitiveType primitiveType, GrMeshDrawOp::Target* target,
+ GrGeometryProcessor* geometryProcessor, const GrPipeline* pipeline)
+ : fMesh(primitiveType)
+ , fTarget(target)
+ , fVertexStride(sizeof(SkPoint))
+ , fGeometryProcessor(geometryProcessor)
+ , fPipeline(pipeline)
+ , fIndexBuffer(nullptr)
+ , fFirstIndex(0)
+ , fIndices(nullptr) {
+ this->allocNewBuffers();
+ }
+
+ ~PathGeoBuilder() {
+ this->emitMesh();
+ this->putBackReserve();
+ }
+
+ // Called before we start each path
+ void beginInstance() {
+ fSubpathIndexStart = fVertexOffset;
+ fCurIdx = fIndices + fIndexOffset;
+ fCurVert = fVertices + fVertexOffset;
+ }
-static inline void add_quad(SkPoint** vert, const SkPoint* base, const SkPoint pts[],
- SkScalar srcSpaceTolSqd, SkScalar srcSpaceTol, bool indexed,
- bool isHairline, uint16_t subpathIdxStart, int offset, uint16_t** idx) {
- // first pt of quad is the pt we ended on in previous step
- uint16_t firstQPtIdx = (uint16_t)(*vert - base) - 1 + offset;
- uint16_t numPts = (uint16_t)
- GrPathUtils::generateQuadraticPoints(
- pts[0], pts[1], pts[2],
- srcSpaceTolSqd, vert,
- GrPathUtils::quadraticPointCount(pts, srcSpaceTol));
- if (indexed) {
- for (uint16_t i = 0; i < numPts; ++i) {
- append_countour_edge_indices(isHairline, subpathIdxStart,
- firstQPtIdx + i, idx);
+ // Called after we end each path
+ void endInstance() {
+ fVertexOffset = fCurVert - fVertices;
+ fIndexOffset = fCurIdx - fIndices;
+ SkASSERT(fVertexOffset <= kVerticesPerChunk);
+ SkASSERT(fIndexOffset <= this->maxIndices());
+ }
+
+ /**
+ * Path verbs
+ */
+ void moveTo(const SkPoint& p) {
+ needSpace(1);
+
+ fSubpathIndexStart = this->currentIndex();
+ *(fCurVert++) = p;
+ }
+
+ void addLine(const SkPoint& p) {
+ needSpace(1, this->indexScale());
+
+ if (this->isIndexed()) {
+ uint16_t prevIdx = this->currentIndex() - 1;
+ appendCountourEdgeIndices(prevIdx);
}
+ *(fCurVert++) = p;
+ }
+
+ void addQuad(const SkPoint pts[], SkScalar srcSpaceTolSqd, SkScalar srcSpaceTol) {
+ this->needSpace(GrPathUtils::kMaxPointsPerCurve,
+ GrPathUtils::kMaxPointsPerCurve * this->indexScale());
+
+ // First pt of quad is the pt we ended on in previous step
+ uint16_t firstQPtIdx = this->currentIndex() - 1;
+ uint16_t numPts = (uint16_t)GrPathUtils::generateQuadraticPoints(
+ pts[0], pts[1], pts[2], srcSpaceTolSqd, &fCurVert,
+ GrPathUtils::quadraticPointCount(pts, srcSpaceTol));
+ if (this->isIndexed()) {
+ for (uint16_t i = 0; i < numPts; ++i) {
+ appendCountourEdgeIndices(firstQPtIdx + i);
+ }
+ }
+ }
+
+ void addConic(SkScalar weight, const SkPoint pts[], SkScalar srcSpaceTolSqd,
+ SkScalar srcSpaceTol) {
+ SkAutoConicToQuads converter;
+ const SkPoint* quadPts = converter.computeQuads(pts, weight, srcSpaceTol);
+ for (int i = 0; i < converter.countQuads(); ++i) {
+ this->addQuad(quadPts + i * 2, srcSpaceTolSqd, srcSpaceTol);
+ }
+ }
+
+ void addCubic(const SkPoint pts[], SkScalar srcSpaceTolSqd, SkScalar srcSpaceTol) {
+ this->needSpace(GrPathUtils::kMaxPointsPerCurve,
+ GrPathUtils::kMaxPointsPerCurve * this->indexScale());
+
+ // First pt of cubic is the pt we ended on in previous step
+ uint16_t firstCPtIdx = this->currentIndex() - 1;
+ uint16_t numPts = (uint16_t) GrPathUtils::generateCubicPoints(
+ pts[0], pts[1], pts[2], pts[3], srcSpaceTolSqd, &fCurVert,
+ GrPathUtils::cubicPointCount(pts, srcSpaceTol));
+ if (this->isIndexed()) {
+ for (uint16_t i = 0; i < numPts; ++i) {
+ appendCountourEdgeIndices(firstCPtIdx + i);
+ }
+ }
+ }
+
+ void addPath(const SkPath& path, SkScalar srcSpaceTol) {
+ SkScalar srcSpaceTolSqd = srcSpaceTol * srcSpaceTol;
+
+ SkPath::Iter iter(path, false);
+ SkPoint pts[4];
+
+ bool done = false;
+ while (!done) {
+ SkPath::Verb verb = iter.next(pts);
+ switch (verb) {
+ case SkPath::kMove_Verb:
+ this->moveTo(pts[0]);
+ break;
+ case SkPath::kLine_Verb:
+ this->addLine(pts[1]);
+ break;
+ case SkPath::kConic_Verb:
+ this->addConic(iter.conicWeight(), pts, srcSpaceTolSqd, srcSpaceTol);
+ break;
+ case SkPath::kQuad_Verb:
+ this->addQuad(pts, srcSpaceTolSqd, srcSpaceTol);
+ break;
+ case SkPath::kCubic_Verb:
+ this->addCubic(pts, srcSpaceTolSqd, srcSpaceTol);
+ break;
+ case SkPath::kClose_Verb:
+ break;
+ case SkPath::kDone_Verb:
+ done = true;
+ }
+ }
+ }
+
+ static bool PathHasMultipleSubpaths(const SkPath& path) {
+ bool first = true;
+
+ SkPath::Iter iter(path, false);
+ SkPath::Verb verb;
+
+ SkPoint pts[4];
+ while ((verb = iter.next(pts)) != SkPath::kDone_Verb) {
+ if (SkPath::kMove_Verb == verb && !first) {
+ return true;
+ }
+ first = false;
+ }
+ return false;
+ }
+
+private:
+ /**
+ * Derived properties
+ * TODO: Cache some of these for better performance, rather than re-computing?
+ */
+ bool isIndexed() const {
+ return kLines_GrPrimitiveType == fMesh.primitiveType() ||
+ kTriangles_GrPrimitiveType == fMesh.primitiveType();
+ }
+ bool isHairline() const {
+ return kLines_GrPrimitiveType == fMesh.primitiveType() ||
+ kLineStrip_GrPrimitiveType == fMesh.primitiveType();
+ }
+ int indexScale() const {
+ switch (fMesh.primitiveType()) {
+ case kLines_GrPrimitiveType:
+ return 2;
+ case kTriangles_GrPrimitiveType:
+ return 3;
+ default:
+ return 0;
+ }
+ }
+ int maxIndices() const { return kVerticesPerChunk * this->indexScale(); }
+
+ uint16_t currentIndex() const { return fCurVert - fVertices; }
+
+ void putBackReserve() {
+ fTarget->putBackIndices((size_t)(this->maxIndices() - fIndexOffset));
+ fTarget->putBackVertices((size_t)(kVerticesPerChunk - fVertexOffset), fVertexStride);
+ }
+
+ // Allocate vertex and (possibly) index buffers
+ void allocNewBuffers() {
+ fVertices = static_cast<SkPoint*>(fTarget->makeVertexSpace(fVertexStride, kVerticesPerChunk,
+ &fVertexBuffer, &fFirstVertex));
+ if (this->isIndexed()) {
+ fIndices = fTarget->makeIndexSpace(this->maxIndices(), &fIndexBuffer, &fFirstIndex);
+ }
+ fVertexOffset = 0;
+ fIndexOffset = 0;
+ }
+
+ void appendCountourEdgeIndices(uint16_t edgeV0Idx) {
+ // When drawing lines we're appending line segments along the countour. When applying the
+ // other fill rules we're drawing triangle fans around the start of the current (sub)path.
+ if (!this->isHairline()) {
+ *(fCurIdx++) = fSubpathIndexStart;
+ }
+ *(fCurIdx++) = edgeV0Idx;
+ *(fCurIdx++) = edgeV0Idx + 1;
}
-}
+
+ // Emits a single draw with all accumulated vertex/index data
+ void emitMesh() {
+ if (!this->isIndexed()) {
+ fMesh.setNonIndexedNonInstanced(fVertexOffset);
+ } else {
+ fMesh.setIndexed(fIndexBuffer, fIndexOffset, fFirstIndex, 0, fVertexOffset - 1);
+ }
+ fMesh.setVertexData(fVertexBuffer, fFirstVertex);
+ fTarget->draw(fGeometryProcessor, fPipeline, fMesh);
+ }
+
+ void needSpace(int vertsNeeded, int indicesNeeded = 0) {
+ if (fCurVert + vertsNeeded > fVertices + kVerticesPerChunk ||
+ fCurIdx + indicesNeeded > fIndices + this->maxIndices()) {
+ // We are about to run out of space (possibly)
+
+ // To maintain continuity, we need to remember one or two points from the current mesh.
+ // Lines only need the last point, fills need the first point from the current contour.
+ // We always grab both here, and append the ones we need at the end of this process.
+ SkPoint lastPt = *(fCurVert - 1);
+ SkPoint subpathStartPt = fVertices[fSubpathIndexStart];
+
+ // Pretend that we've reached the end of an entire path, so our offsets are correct
+ this->endInstance();
+
+ // Draw the mesh we've accumulated
+ this->emitMesh();
+
+ // Put back any unused space, get new buffers
+ this->putBackReserve();
+ this->allocNewBuffers();
+
+ // Start a "new" path, which is really just a continuation of the in-progress one
+ this->beginInstance();
+
+ // Append copies of the points we saved so the two meshes will weld properly
+ if (!this->isHairline()) {
+ *(fCurVert++) = subpathStartPt;
+ }
+ *(fCurVert++) = lastPt;
+ }
+ }
+
+ GrMesh fMesh;
+ GrMeshDrawOp::Target* fTarget;
+ size_t fVertexStride;
+ GrGeometryProcessor* fGeometryProcessor;
+ const GrPipeline* fPipeline;
+
+ const GrBuffer* fVertexBuffer;
+ int fFirstVertex;
+ SkPoint* fVertices;
+ SkPoint* fCurVert;
+ int fVertexOffset;
+
+ const GrBuffer* fIndexBuffer;
+ int fFirstIndex;
+ uint16_t* fIndices;
+ uint16_t* fCurIdx;
+ int fIndexOffset;
+ uint16_t fSubpathIndexStart;
+};
class DefaultPathOp final : public GrLegacyMeshDrawOp {
public:
@@ -153,112 +385,35 @@ private:
gp = GrDefaultGeoProcFactory::Make(color, coverage, localCoords, this->viewMatrix());
}
- size_t vertexStride = gp->getVertexStride();
- SkASSERT(vertexStride == sizeof(SkPoint));
+ SkASSERT(gp->getVertexStride() == sizeof(SkPoint));
int instanceCount = fPaths.count();
- // compute number of vertices
- int maxVertices = 0;
-
// We will use index buffers if we have multiple paths or one path with multiple contours
bool isIndexed = instanceCount > 1;
- for (int i = 0; i < instanceCount; i++) {
+ for (int i = 0; !isIndexed && i < instanceCount; i++) {
const PathData& args = fPaths[i];
-
- int contourCount;
- maxVertices += GrPathUtils::worstCasePointCount(args.fPath, &contourCount,
- args.fTolerance);
-
- isIndexed = isIndexed || contourCount > 1;
- }
-
- if (maxVertices == 0 || maxVertices > ((int)SK_MaxU16 + 1)) {
- //SkDebugf("Cannot render path (%d)\n", maxVertices);
- return;
+ isIndexed = isIndexed || PathGeoBuilder::PathHasMultipleSubpaths(args.fPath);
}
// determine primitiveType
- int maxIndices = 0;
GrPrimitiveType primitiveType;
if (this->isHairline()) {
- if (isIndexed) {
- maxIndices = 2 * maxVertices;
- primitiveType = kLines_GrPrimitiveType;
- } else {
- primitiveType = kLineStrip_GrPrimitiveType;
- }
+ primitiveType = isIndexed ? kLines_GrPrimitiveType : kLineStrip_GrPrimitiveType;
} else {
- if (isIndexed) {
- maxIndices = 3 * maxVertices;
- primitiveType = kTriangles_GrPrimitiveType;
- } else {
- primitiveType = kTriangleFan_GrPrimitiveType;
- }
- }
-
- // allocate vertex / index buffers
- const GrBuffer* vertexBuffer;
- int firstVertex;
-
- void* verts = target->makeVertexSpace(vertexStride, maxVertices,
- &vertexBuffer, &firstVertex);
-
- if (!verts) {
- SkDebugf("Could not allocate vertices\n");
- return;
+ primitiveType = isIndexed ? kTriangles_GrPrimitiveType : kTriangleFan_GrPrimitiveType;
}
- const GrBuffer* indexBuffer = nullptr;
- int firstIndex = 0;
-
- void* indices = nullptr;
- if (isIndexed) {
- indices = target->makeIndexSpace(maxIndices, &indexBuffer, &firstIndex);
-
- if (!indices) {
- SkDebugf("Could not allocate indices\n");
- return;
- }
- }
+ PathGeoBuilder pathGeoBuilder(primitiveType, target, gp.get(), this->pipeline());
// fill buffers
- int vertexOffset = 0;
- int indexOffset = 0;
for (int i = 0; i < instanceCount; i++) {
const PathData& args = fPaths[i];
- int vertexCnt = 0;
- int indexCnt = 0;
- if (!this->createGeom(verts,
- vertexOffset,
- indices,
- indexOffset,
- &vertexCnt,
- &indexCnt,
- args.fPath,
- args.fTolerance,
- isIndexed)) {
- return;
- }
-
- vertexOffset += vertexCnt;
- indexOffset += indexCnt;
- SkASSERT(vertexOffset <= maxVertices && indexOffset <= maxIndices);
+ pathGeoBuilder.beginInstance();
+ pathGeoBuilder.addPath(args.fPath, args.fTolerance);
+ pathGeoBuilder.endInstance();
}
-
- GrMesh mesh(primitiveType);
- if (!isIndexed) {
- mesh.setNonIndexedNonInstanced(vertexOffset);
- } else {
- mesh.setIndexed(indexBuffer, indexOffset, firstIndex, 0, vertexOffset - 1);
- }
- mesh.setVertexData(vertexBuffer, firstVertex);
- target->draw(gp.get(), this->pipeline(), mesh);
-
- // put back reserves
- target->putBackIndices((size_t)(maxIndices - indexOffset));
- target->putBackVertices((size_t)(maxVertices - vertexOffset), (size_t)vertexStride);
}
bool onCombineIfPossible(GrOp* t, const GrCaps& caps) override {
@@ -289,98 +444,6 @@ private:
return true;
}
- bool createGeom(void* vertices,
- size_t vertexOffset,
- void* indices,
- size_t indexOffset,
- int* vertexCnt,
- int* indexCnt,
- const SkPath& path,
- SkScalar srcSpaceTol,
- bool isIndexed) const {
- SkScalar srcSpaceTolSqd = srcSpaceTol * srcSpaceTol;
-
- uint16_t indexOffsetU16 = (uint16_t)indexOffset;
- uint16_t vertexOffsetU16 = (uint16_t)vertexOffset;
-
- uint16_t* idxBase = reinterpret_cast<uint16_t*>(indices) + indexOffsetU16;
- uint16_t* idx = idxBase;
- uint16_t subpathIdxStart = vertexOffsetU16;
-
- SkPoint* base = reinterpret_cast<SkPoint*>(vertices) + vertexOffset;
- SkPoint* vert = base;
-
- SkPoint pts[4];
-
- bool first = true;
- int subpath = 0;
-
- SkPath::Iter iter(path, false);
-
- bool done = false;
- while (!done) {
- SkPath::Verb verb = iter.next(pts);
- switch (verb) {
- case SkPath::kMove_Verb:
- if (!first) {
- uint16_t currIdx = (uint16_t) (vert - base) + vertexOffsetU16;
- subpathIdxStart = currIdx;
- ++subpath;
- }
- *vert = pts[0];
- vert++;
- break;
- case SkPath::kLine_Verb:
- if (isIndexed) {
- uint16_t prevIdx = (uint16_t)(vert - base) - 1 + vertexOffsetU16;
- append_countour_edge_indices(this->isHairline(), subpathIdxStart,
- prevIdx, &idx);
- }
- *(vert++) = pts[1];
- break;
- case SkPath::kConic_Verb: {
- SkScalar weight = iter.conicWeight();
- SkAutoConicToQuads converter;
- const SkPoint* quadPts = converter.computeQuads(pts, weight, srcSpaceTol);
- for (int i = 0; i < converter.countQuads(); ++i) {
- add_quad(&vert, base, quadPts + i*2, srcSpaceTolSqd, srcSpaceTol,
- isIndexed, this->isHairline(), subpathIdxStart,
- (int)vertexOffset, &idx);
- }
- break;
- }
- case SkPath::kQuad_Verb:
- add_quad(&vert, base, pts, srcSpaceTolSqd, srcSpaceTol, isIndexed,
- this->isHairline(), subpathIdxStart, (int)vertexOffset, &idx);
- break;
- case SkPath::kCubic_Verb: {
- // first pt of cubic is the pt we ended on in previous step
- uint16_t firstCPtIdx = (uint16_t)(vert - base) - 1 + vertexOffsetU16;
- uint16_t numPts = (uint16_t) GrPathUtils::generateCubicPoints(
- pts[0], pts[1], pts[2], pts[3],
- srcSpaceTolSqd, &vert,
- GrPathUtils::cubicPointCount(pts, srcSpaceTol));
- if (isIndexed) {
- for (uint16_t i = 0; i < numPts; ++i) {
- append_countour_edge_indices(this->isHairline(), subpathIdxStart,
- firstCPtIdx + i, &idx);
- }
- }
- break;
- }
- case SkPath::kClose_Verb:
- break;
- case SkPath::kDone_Verb:
- done = true;
- }
- first = false;
- }
-
- *vertexCnt = static_cast<int>(vert - base);
- *indexCnt = static_cast<int>(idx - idxBase);
- return true;
- }
-
GrColor color() const { return fColor; }
uint8_t coverage() const { return fCoverage; }
bool usesLocalCoords() const { return fUsesLocalCoords; }