Implement stroking with GPU tessellation
Bootstraps tessellated stroking using GrStrokeGeometry mostly as
written. Strokes get decomposed into tessellation patches that
represent either a "cubic" (single stroked bezier curve with butt
caps) or a "join". The patches get drawn directly to the canvas
without any intermediate stencil steps. For the first revision, only
opaque, constant-color strokes are supported.
Bug: skia:10419
Change-Id: I601289189b93ebdf2f1efecd08628a6e0d9acb01
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/299142
Reviewed-by: Michael Ludwig <michaelludwig@google.com>
Commit-Queue: Chris Dalton <csmartdalton@google.com>
diff --git a/src/gpu/GrProcessor.h b/src/gpu/GrProcessor.h
index 80ec7e3..ebddb0c 100644
--- a/src/gpu/GrProcessor.h
+++ b/src/gpu/GrProcessor.h
@@ -164,6 +164,7 @@
kTessellate_GrMiddleOutCubicShader_ClassID,
kTessellate_GrStencilTriangleShader_ClassID,
kTessellate_GrTessellateCubicShader_ClassID,
+ kTessellate_GrTessellateStrokeShader_ClassID,
kTessellate_GrTessellateWedgeShader_ClassID,
kTestFP_ClassID,
kTestRectOp_ClassID,
diff --git a/src/gpu/tessellate/GrStrokeGeometry.cpp b/src/gpu/tessellate/GrStrokeGeometry.cpp
index b097c73..9ff4c5c 100644
--- a/src/gpu/tessellate/GrStrokeGeometry.cpp
+++ b/src/gpu/tessellate/GrStrokeGeometry.cpp
@@ -155,13 +155,7 @@
// Decide how many flat line segments to chop the curve into.
int numSegments = wangs_formula_quadratic(p0, p1, p2);
- numSegments = std::min(numSegments, 1 << kMaxNumLinearSegmentsLog2);
- if (numSegments <= 1) {
- this->rotateTo(leftJoinVerb, normals[0]);
- this->lineTo(Verb::kInternalRoundJoin, P[2]);
- this->rotateTo(Verb::kInternalRoundJoin, normals[1]);
- return;
- }
+ numSegments = std::clamp(numSegments, 1, 1 << kMaxNumLinearSegmentsLog2);
// At + B gives a vector tangent to the quadratic.
Sk2f A = p0 - p1*2 + p2;
@@ -193,8 +187,7 @@
// FIXME: This is quite costly and the vast majority of curves only have moderate curvature. We
// would benefit significantly from a quick reject that detects curves that don't need special
// treatment for strong curvature.
- bool isCurvatureTooStrong = calc_curvature_costheta(leftTan, rightTan) < fMaxCurvatureCosTheta;
- if (isCurvatureTooStrong) {
+ if (numSegments > 1 && calc_curvature_costheta(leftTan, rightTan) < fMaxCurvatureCosTheta) {
SkPoint ptsBuffer[5];
const SkPoint* currQuadratic = P;
@@ -206,7 +199,7 @@
}
currQuadratic = ptsBuffer + 2;
} else {
- this->rotateTo(leftJoinVerb, normals[0]);
+ this->rotateTo(leftJoinVerb, normals[0], currQuadratic[1]);
}
if (rightT < 1) {
@@ -215,10 +208,18 @@
this->quadraticTo(Verb::kInternalRoundJoin, ptsBuffer + 2, /*maxCurvatureT=*/0);
} else {
this->lineTo(Verb::kInternalRoundJoin, currQuadratic[2]);
- this->rotateTo(Verb::kInternalRoundJoin, normals[1]);
+ this->rotateTo(Verb::kInternalRoundJoin, normals[1],
+ currQuadratic[2]*2 - currQuadratic[1]);
}
return;
}
+ if (numSegments > fMaxTessellationSegments) {
+ SkPoint ptsBuffer[5];
+ SkChopQuadAt(P, ptsBuffer, 0.5f);
+ this->quadraticTo(leftJoinVerb, ptsBuffer, 0);
+ this->quadraticTo(Verb::kInternalRoundJoin, ptsBuffer + 3, 0);
+ return;
+ }
this->recordLeftJoinIfNotEmpty(leftJoinVerb, normals[0]);
fNormals.push_back_n(2, normals);
@@ -283,13 +284,7 @@
// Decide how many flat line segments to chop the curve into.
int numSegments = wangs_formula_cubic(p0, p1, p2, p3);
- numSegments = std::min(numSegments, 1 << kMaxNumLinearSegmentsLog2);
- if (numSegments <= 1) {
- this->rotateTo(leftJoinVerb, normals[0]);
- this->lineTo(leftJoinVerb, P[3]);
- this->rotateTo(Verb::kInternalRoundJoin, normals[1]);
- return;
- }
+ numSegments = std::clamp(numSegments, 1, 1 << kMaxNumLinearSegmentsLog2);
// At^2 + Bt + C gives a vector tangent to the cubic. (More specifically, it's the derivative
// minus an irrelevant scale by 3, since all we care about is the direction.)
@@ -323,8 +318,7 @@
// FIXME: This is quite costly and the vast majority of curves only have moderate curvature. We
// would benefit significantly from a quick reject that detects curves that don't need special
// treatment for strong curvature.
- bool isCurvatureTooStrong = calc_curvature_costheta(leftTan, rightTan) < fMaxCurvatureCosTheta;
- if (isCurvatureTooStrong) {
+ if (numSegments > 1 && calc_curvature_costheta(leftTan, rightTan) < fMaxCurvatureCosTheta) {
SkPoint ptsBuffer[7];
p0.store(ptsBuffer);
p1.store(ptsBuffer + 1);
@@ -346,7 +340,8 @@
}
currCubic = ptsBuffer + 3;
} else {
- this->rotateTo(leftJoinVerb, normals[0]);
+ SkPoint c1 = (ptsBuffer[1] == ptsBuffer[0]) ? ptsBuffer[2] : ptsBuffer[1];
+ this->rotateTo(leftJoinVerb, normals[0], c1);
}
if (rightT < 1) {
@@ -357,7 +352,8 @@
kLeftMaxCurvatureNone, kRightMaxCurvatureNone);
} else {
this->lineTo(Verb::kInternalRoundJoin, currCubic[3]);
- this->rotateTo(Verb::kInternalRoundJoin, normals[1]);
+ SkPoint c2 = (currCubic[2] == currCubic[3]) ? currCubic[1] : currCubic[2];
+ this->rotateTo(Verb::kInternalRoundJoin, normals[1], currCubic[3]*2 - c2);
}
return;
}
@@ -374,6 +370,14 @@
kRightMaxCurvatureNone);
return;
}
+ if (numSegments > fMaxTessellationSegments) {
+ SkPoint ptsBuffer[7];
+ SkChopCubicAt(P, ptsBuffer, 0.5f);
+ this->cubicTo(leftJoinVerb, ptsBuffer, 0, kLeftMaxCurvatureNone, kRightMaxCurvatureNone);
+ this->cubicTo(Verb::kInternalRoundJoin, ptsBuffer + 3, 0, kLeftMaxCurvatureNone,
+ kRightMaxCurvatureNone);
+ return;
+ }
this->recordLeftJoinIfNotEmpty(leftJoinVerb, normals[0]);
fNormals.push_back_n(2, normals);
@@ -386,17 +390,19 @@
void GrStrokeGeometry::recordStroke(Verb verb, int numSegmentsLog2) {
SkASSERT(Verb::kLinearStroke != verb || 0 == numSegmentsLog2);
- SkASSERT(numSegmentsLog2 <= kMaxNumLinearSegmentsLog2);
fVerbs.push_back(verb);
if (Verb::kLinearStroke != verb) {
- SkASSERT(numSegmentsLog2 > 0);
fParams.push_back().fNumLinearSegmentsLog2 = numSegmentsLog2;
}
++fCurrStrokeTallies->fStrokes[numSegmentsLog2];
}
-void GrStrokeGeometry::rotateTo(Verb leftJoinVerb, SkVector normal) {
+void GrStrokeGeometry::rotateTo(Verb leftJoinVerb, SkVector normal, SkPoint controlPoint) {
+ SkASSERT(fPoints.count() > fCurrContourFirstPtIdx);
this->recordLeftJoinIfNotEmpty(leftJoinVerb, normal);
+ fVerbs.push_back(Verb::kRotate);
+ fPoints.push_back(controlPoint);
+ fPoints.push_back(fPoints[fPoints.count() - 2]);
fNormals.push_back(normal);
}
@@ -406,105 +412,7 @@
SkASSERT(fNormals.count() == fCurrContourFirstNormalIdx);
return;
}
-
- if (Verb::kBevelJoin == joinVerb) {
- this->recordBevelJoin(Verb::kBevelJoin);
- return;
- }
-
- Sk2f n0 = Sk2f::Load(&fNormals.back());
- Sk2f n1 = Sk2f::Load(&nextNormal);
- Sk2f base = n1 - n0;
- if ((base.abs() * fCurrStrokeRadius < kMaxErrorFromLinearization).allTrue()) {
- // Treat any join as a bevel when the outside corners of the two adjoining strokes are
- // close enough to each other. This is important because "miterCapHeightOverWidth" becomes
- // unstable when n0 and n1 are nearly equal.
- this->recordBevelJoin(joinVerb);
- return;
- }
-
- // We implement miters and round joins by placing a triangle-shaped cap on top of a bevel join.
- // (For round joins this triangle cap comprises the conic control points.) Find how tall to make
- // this triangle cap, relative to its width.
- //
- // NOTE: This value would be infinite at 180 degrees, but we clamp miterCapHeightOverWidth at
- // near-infinity. 180-degree round joins still look perfectly acceptable like this (though
- // technically not pure arcs).
- Sk2f cross = base * SkNx_shuffle<1,0>(n0);
- Sk2f dot = base * n0;
- float miterCapHeight = SkScalarAbs(dot[0] + dot[1]);
- float miterCapWidth = SkScalarAbs(cross[0] - cross[1]) * 2;
-
- if (Verb::kMiterJoin == joinVerb) {
- if (miterCapHeight > fMiterMaxCapHeightOverWidth * miterCapWidth) {
- // This join is tighter than the miter limit. Treat it as a bevel.
- this->recordBevelJoin(Verb::kMiterJoin);
- return;
- }
- this->recordMiterJoin(miterCapHeight / miterCapWidth);
- return;
- }
-
- SkASSERT(Verb::kRoundJoin == joinVerb || Verb::kInternalRoundJoin == joinVerb);
-
- // Conic arcs become unstable when they approach 180 degrees. When the conic control point
- // begins shooting off to infinity (i.e., height/width > 32), split the conic into two.
- static constexpr float kAlmost180Degrees = 32;
- if (miterCapHeight > kAlmost180Degrees * miterCapWidth) {
- Sk2f bisect = normalize(n0 - n1);
- this->rotateTo(joinVerb, SkVector::Make(-bisect[1], bisect[0]));
- this->recordLeftJoinIfNotEmpty(joinVerb, nextNormal);
- return;
- }
-
- float miterCapHeightOverWidth = miterCapHeight / miterCapWidth;
-
- // Find the heights of this round join's conic control point as well as the arc itself.
- Sk2f X, Y;
- transpose(base * base, n0 * n1, &X, &Y);
- Sk2f r = Sk2f::Max(X + Y + Sk2f(0, 1), 0.f).sqrt();
- Sk2f heights = SkNx_fma(r, Sk2f(miterCapHeightOverWidth, -SK_ScalarRoot2Over2), Sk2f(0, 1));
- float controlPointHeight = SkScalarAbs(heights[0]);
- float curveHeight = heights[1];
- if (curveHeight * fCurrStrokeRadius < kMaxErrorFromLinearization) {
- // Treat round joins as bevels when their curvature is nearly flat.
- this->recordBevelJoin(joinVerb);
- return;
- }
-
- float w = curveHeight / (controlPointHeight - curveHeight);
- this->recordRoundJoin(joinVerb, miterCapHeightOverWidth, w);
-}
-
-void GrStrokeGeometry::recordBevelJoin(Verb originalJoinVerb) {
- if (!IsInternalJoinVerb(originalJoinVerb)) {
- fVerbs.push_back(Verb::kBevelJoin);
- ++fCurrStrokeTallies->fTriangles;
- } else {
- fVerbs.push_back(Verb::kInternalBevelJoin);
- fCurrStrokeTallies->fTriangles += 2;
- }
-}
-
-void GrStrokeGeometry::recordMiterJoin(float miterCapHeightOverWidth) {
- fVerbs.push_back(Verb::kMiterJoin);
- fParams.push_back().fMiterCapHeightOverWidth = miterCapHeightOverWidth;
- fCurrStrokeTallies->fTriangles += 2;
-}
-
-void GrStrokeGeometry::recordRoundJoin(Verb joinVerb, float miterCapHeightOverWidth,
- float conicWeight) {
fVerbs.push_back(joinVerb);
- fParams.push_back().fConicWeight = conicWeight;
- fParams.push_back().fMiterCapHeightOverWidth = miterCapHeightOverWidth;
- if (Verb::kRoundJoin == joinVerb) {
- ++fCurrStrokeTallies->fTriangles;
- ++fCurrStrokeTallies->fConics;
- } else {
- SkASSERT(Verb::kInternalRoundJoin == joinVerb);
- fCurrStrokeTallies->fTriangles += 2;
- fCurrStrokeTallies->fConics += 2;
- }
}
void GrStrokeGeometry::closeContour() {
@@ -514,14 +422,7 @@
// Draw a line back to the beginning.
this->lineTo(fCurrStrokeJoinVerb, fPoints[fCurrContourFirstPtIdx]);
}
- if (fNormals.count() > fCurrContourFirstNormalIdx) {
- // Join the first and last lines.
- this->rotateTo(fCurrStrokeJoinVerb,fNormals[fCurrContourFirstNormalIdx]);
- } else {
- // This contour is empty. Add a bogus normal since the iterator always expects one.
- SkASSERT(fNormals.count() == fCurrContourFirstNormalIdx);
- fNormals.push_back({0, 0});
- }
+ fVerbs.push_back(fCurrStrokeJoinVerb);
fVerbs.push_back(Verb::kEndContour);
SkDEBUGCODE(fInsideContour = false);
}
diff --git a/src/gpu/tessellate/GrStrokeGeometry.h b/src/gpu/tessellate/GrStrokeGeometry.h
index 98a85ff..2c4a757 100644
--- a/src/gpu/tessellate/GrStrokeGeometry.h
+++ b/src/gpu/tessellate/GrStrokeGeometry.h
@@ -22,8 +22,9 @@
public:
static constexpr int kMaxNumLinearSegmentsLog2 = 15;
- GrStrokeGeometry(int numSkPoints = 0, int numSkVerbs = 0)
- : fVerbs(numSkVerbs * 5/2) // Reserve for a 2.5x expansion in verbs. (Joins get their
+ GrStrokeGeometry(int maxTessellationSegments, int numSkPoints = 0, int numSkVerbs = 0)
+ : fMaxTessellationSegments(maxTessellationSegments)
+ , fVerbs(numSkVerbs * 5/2) // Reserve for a 2.5x expansion in verbs. (Joins get their
// own separate verb in our representation.)
, fParams(numSkVerbs * 3) // Somewhere around 1-2 params per verb.
, fPoints(numSkPoints * 5/4) // Reserve for a 1.25x expansion in points and normals.
@@ -39,6 +40,9 @@
kQuadraticStroke,
kCubicStroke,
+ // Updates the last tangent without moving the current position on the stroke.
+ kRotate,
+
// Joins are a triangles that connect the outer corners of two adjoining strokes. Miters
// have an additional triangle cap on top of the bevel, and round joins have an arc on top.
kBevelJoin,
@@ -102,16 +106,13 @@
float rightMaxCurvatureT);
// Pushes a new normal to fNormals and records a join, without changing the current position.
- void rotateTo(Verb leftJoinVerb, SkVector normal);
+ void rotateTo(Verb leftJoinVerb, SkVector normal, SkPoint controlPoint);
// Records a stroke in fElememts.
void recordStroke(Verb, int numSegmentsLog2);
// Records a join in fElememts with the previous stroke, if the cuurent contour is not empty.
void recordLeftJoinIfNotEmpty(Verb joinType, SkVector nextNormal);
- void recordBevelJoin(Verb originalJoinVerb);
- void recordMiterJoin(float miterCapHeightOverWidth);
- void recordRoundJoin(Verb roundJoinVerb, float miterCapHeightOverWidth, float conicWeight);
void recordCapsIfAny();
@@ -138,6 +139,7 @@
SkDEBUGCODE(bool fInsideContour = false);
+ const int fMaxTessellationSegments;
SkSTArray<128, Verb, true> fVerbs;
SkSTArray<128, Parameter, true> fParams;
SkSTArray<128, SkPoint, true> fPoints;
@@ -164,6 +166,7 @@
case Verb::kLinearStroke:
case Verb::kQuadraticStroke:
case Verb::kCubicStroke:
+ case Verb::kRotate:
case Verb::kBevelJoin:
case Verb::kMiterJoin:
case Verb::kRoundJoin:
diff --git a/src/gpu/tessellate/GrTessellateStrokeOp.cpp b/src/gpu/tessellate/GrTessellateStrokeOp.cpp
new file mode 100644
index 0000000..7c1c29c
--- /dev/null
+++ b/src/gpu/tessellate/GrTessellateStrokeOp.cpp
@@ -0,0 +1,307 @@
+/*
+ * Copyright 2019 Google LLC.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "src/gpu/tessellate/GrTessellateStrokeOp.h"
+
+#include "src/core/SkPathPriv.h"
+#include "src/gpu/tessellate/GrStrokeGeometry.h"
+#include "src/gpu/tessellate/GrTessellateStrokeShader.h"
+
+static SkPath transform_path(const SkMatrix& viewMatrix, const SkPath& path) {
+ SkPath devPath;
+ // The provided matrix must be a similarity matrix for the time being. This is so we can
+ // bootstrap this Op on top of GrStrokeGeometry with minimal modifications.
+ SkASSERT(viewMatrix.isSimilarity());
+ path.transform(viewMatrix, &devPath);
+ return devPath;
+}
+
+static SkStrokeRec transform_stroke(const SkMatrix& viewMatrix, const SkStrokeRec& stroke) {
+ SkStrokeRec devStroke = stroke;
+ // kStrokeAndFill_Style is not yet supported.
+ SkASSERT(stroke.getStyle() == SkStrokeRec::kStroke_Style ||
+ stroke.getStyle() == SkStrokeRec::kHairline_Style);
+ float strokeWidth = (stroke.getStyle() == SkStrokeRec::kHairline_Style) ?
+ 1 : viewMatrix.getMaxScale() * stroke.getWidth();
+ devStroke.setStrokeStyle(strokeWidth, /*strokeAndFill=*/false);
+ return devStroke;
+}
+
+static SkPMColor4f get_paint_constant_blended_color(const GrPaint& paint) {
+ SkPMColor4f constantColor;
+ // Patches can overlap, so until a stencil technique is implemented, the provided paints must be
+ // constant blended colors.
+ SkAssertResult(paint.isConstantBlendedColor(&constantColor));
+ return constantColor;
+}
+
+GrTessellateStrokeOp::GrTessellateStrokeOp(const SkMatrix& viewMatrix, const SkPath& path,
+ const SkStrokeRec& stroke, GrPaint&& paint,
+ GrAAType aaType)
+ : GrDrawOp(ClassID())
+ , fDevPath(transform_path(viewMatrix, path))
+ , fDevStroke(transform_stroke(viewMatrix, stroke))
+ , fAAType(aaType)
+ , fColor(get_paint_constant_blended_color(paint))
+ , fProcessors(std::move(paint)) {
+ SkASSERT(fAAType != GrAAType::kCoverage); // No mixed samples support yet.
+ SkRect devBounds = fDevPath.getBounds();
+ float inflationRadius = fDevStroke.getInflationRadius();
+ devBounds.outset(inflationRadius, inflationRadius);
+ this->setBounds(devBounds, HasAABloat(GrAAType::kCoverage == fAAType), IsHairline::kNo);
+}
+
+GrProcessorSet::Analysis GrTessellateStrokeOp::finalize(const GrCaps& caps,
+ const GrAppliedClip* clip,
+ bool hasMixedSampledCoverage,
+ GrClampType clampType) {
+ return fProcessors.finalize(fColor, GrProcessorAnalysisCoverage::kNone, clip,
+ &GrUserStencilSettings::kUnused, hasMixedSampledCoverage, caps,
+ clampType, &fColor);
+}
+
+GrDrawOp::FixedFunctionFlags GrTessellateStrokeOp::fixedFunctionFlags() const {
+ auto flags = FixedFunctionFlags::kNone;
+ if (GrAAType::kNone != fAAType) {
+ flags |= FixedFunctionFlags::kUsesHWAA;
+ }
+ return flags;
+}
+
+void GrTessellateStrokeOp::onPrePrepare(GrRecordingContext*, const GrSurfaceProxyView* writeView,
+ GrAppliedClip*, const GrXferProcessor::DstProxyView&) {
+}
+
+static SkPoint lerp(const SkPoint& a, const SkPoint& b, float T) {
+ SkASSERT(1 != T); // The below does not guarantee lerp(a, b, 1) === b.
+ return (b - a) * T + a;
+}
+
+static void write_line(SkPoint* patch, const SkPoint& p0, const SkPoint& p1) {
+ patch[0] = p0;
+ patch[1] = lerp(p0, p1, 1/3.f);
+ patch[2] = lerp(p0, p1, 2/3.f);
+ patch[3] = p1;
+}
+
+static void write_quadratic(SkPoint* patch, const SkPoint pts[]) {
+ patch[0] = pts[0];
+ patch[1] = lerp(pts[0], pts[1], 2/3.f);
+ patch[2] = lerp(pts[1], pts[2], 1/3.f);
+ patch[3] = pts[2];
+}
+
+static void write_loop(SkPoint* patch, const SkPoint& intersectionPoint,
+ const SkPoint lastControlPt, const SkPoint& nextControlPt) {
+ patch[0] = intersectionPoint;
+ patch[1] = lastControlPt;
+ patch[2] = nextControlPt;
+ patch[3] = intersectionPoint;
+}
+
+static void write_square_cap(SkPoint* patch, const SkPoint& endPoint,
+ const SkPoint controlPoint, float strokeRadius) {
+ SkVector v = (endPoint - controlPoint);
+ v.normalize();
+ SkPoint capPoint = endPoint + v*strokeRadius;
+ // Construct a line that incorporates controlPoint so we get a water tight edge with the rest of
+ // the stroke. The cubic will technically step outside the cap, but we will force it to only
+ // have one segment, giving edges only at the endpoints.
+ patch[0] = endPoint;
+ patch[1] = controlPoint;
+ // Straddle the midpoint of the cap because the tessellated geometry emits a center point at
+ // T=.5, and we need to ensure that point stays inside the cap.
+ patch[2] = endPoint + capPoint - controlPoint;
+ patch[3] = capPoint;
+}
+
+void GrTessellateStrokeOp::onPrepare(GrOpFlushState* flushState) {
+ float strokeRadius = fDevStroke.getWidth() * .5f;
+
+ // Rebuild the stroke using GrStrokeGeometry.
+ GrStrokeGeometry strokeGeometry(flushState->caps().shaderCaps()->maxTessellationSegments(),
+ fDevPath.countPoints(), fDevPath.countVerbs());
+ GrStrokeGeometry::InstanceTallies tallies = GrStrokeGeometry::InstanceTallies();
+ strokeGeometry.beginPath(fDevStroke, strokeRadius * 2, &tallies);
+ SkPathVerb previousVerb = SkPathVerb::kClose;
+ for (auto [verb, pts, w] : SkPathPriv::Iterate(fDevPath)) {
+ switch (verb) {
+ case SkPathVerb::kMove:
+ if (previousVerb != SkPathVerb::kClose) {
+ strokeGeometry.capContourAndExit();
+ }
+ strokeGeometry.moveTo(pts[0]);
+ break;
+ case SkPathVerb::kClose:
+ strokeGeometry.closeContour();
+ break;
+ case SkPathVerb::kLine:
+ strokeGeometry.lineTo(pts[1]);
+ break;
+ case SkPathVerb::kQuad:
+ strokeGeometry.quadraticTo(pts);
+ break;
+ case SkPathVerb::kCubic:
+ strokeGeometry.cubicTo(pts);
+ break;
+ case SkPathVerb::kConic:
+ SkUNREACHABLE;
+ }
+ previousVerb = verb;
+ }
+ if (previousVerb != SkPathVerb::kClose) {
+ strokeGeometry.capContourAndExit();
+ }
+
+ auto vertexData = static_cast<SkPoint*>(flushState->makeVertexSpace(
+ sizeof(SkPoint), strokeGeometry.verbs().count() * 2 * 5, &fVertexBuffer, &fBaseVertex));
+ if (!vertexData) {
+ return;
+ }
+
+ using Verb = GrStrokeGeometry::Verb;
+
+ // Dump GrStrokeGeometry into tessellation patches.
+ //
+ // This loop is only a temporary adapter for GrStrokeGeometry so we can bootstrap the
+ // tessellation shaders. Once the shaders are landed and tested, we will overhaul
+ // GrStrokeGeometry and remove this loop.
+ int i = 0;
+ const SkTArray<SkPoint, true>& pathPts = strokeGeometry.points();
+ auto pendingJoin = Verb::kEndContour;
+ SkPoint firstJoinControlPoint = {0, 0};
+ SkPoint lastJoinControlPoint = {0, 0};
+ bool hasFirstControlPoint = false;
+ for (auto verb : strokeGeometry.verbs()) {
+ SkPoint patch[4];
+ float overrideNumSegments = 0;
+ switch (verb) {
+ case Verb::kBeginPath:
+ continue;
+ case Verb::kRoundJoin:
+ case Verb::kInternalRoundJoin:
+ case Verb::kMiterJoin:
+ case Verb::kBevelJoin:
+ case Verb::kInternalBevelJoin:
+ pendingJoin = verb;
+ continue;
+ case Verb::kLinearStroke:
+ write_line(patch, pathPts[i], pathPts[i+1]);
+ ++i;
+ break;
+ case Verb::kQuadraticStroke:
+ write_quadratic(patch, &pathPts[i]);
+ i += 2;
+ break;
+ case Verb::kCubicStroke:
+ memcpy(patch, &pathPts[i], sizeof(SkPoint) * 4);
+ i += 3;
+ break;
+ case Verb::kRotate:
+ write_loop(patch, pathPts[i], pathPts[i+1], pathPts[i]*2 - pathPts[i+1]);
+ i += 2;
+ break;
+ case Verb::kSquareCap: {
+ SkASSERT(pendingJoin == Verb::kEndContour);
+ write_square_cap(patch, pathPts[i], lastJoinControlPoint, strokeRadius);
+ // This cubic steps outside the cap, but if we force it to only have one segment, we
+ // will just get the rectangular cap.
+ overrideNumSegments = 1;
+ break;
+ }
+ case Verb::kRoundCap:
+ // A round cap is the same thing as a 180-degree round join.
+ SkASSERT(pendingJoin == Verb::kEndContour);
+ pendingJoin = Verb::kRoundJoin;
+ write_loop(patch, pathPts[i], lastJoinControlPoint, lastJoinControlPoint);
+ break;
+ case Verb::kEndContour:
+ // Final join
+ write_loop(patch, pathPts[i], firstJoinControlPoint, lastJoinControlPoint);
+ ++i;
+ break;
+ }
+
+ SkPoint c1 = (patch[1] == patch[0]) ? patch[2] : patch[1];
+ SkPoint c2 = (patch[2] == patch[3]) ? patch[1] : patch[2];
+
+ if (pendingJoin != Verb::kEndContour) {
+ vertexData[0] = patch[0];
+ vertexData[1] = lastJoinControlPoint;
+ vertexData[2] = c1;
+ vertexData[3] = patch[0];
+ switch (pendingJoin) {
+ case Verb::kBevelJoin:
+ vertexData[4].set(1, strokeRadius);
+ break;
+ case Verb::kMiterJoin:
+ vertexData[4].set(2, strokeRadius);
+ break;
+ case Verb::kRoundJoin:
+ vertexData[4].set(3, strokeRadius);
+ break;
+ case Verb::kInternalRoundJoin:
+ case Verb::kInternalBevelJoin:
+ default:
+ vertexData[4].set(4, strokeRadius);
+ break;
+ }
+ vertexData += 5;
+ fVertexCount += 5;
+ pendingJoin = Verb::kEndContour;
+ }
+
+ if (verb != Verb::kRoundCap) {
+ if (!hasFirstControlPoint) {
+ firstJoinControlPoint = c1;
+ hasFirstControlPoint = true;
+ }
+ lastJoinControlPoint = c2;
+ }
+
+ if (verb == Verb::kEndContour) {
+ // Temporary hack for this adapter in case the next contour is a round cap.
+ lastJoinControlPoint = firstJoinControlPoint;
+ hasFirstControlPoint = false;
+ } else if (verb != Verb::kRotate && verb != Verb::kRoundCap) {
+ memcpy(vertexData, patch, sizeof(SkPoint) * 4);
+ vertexData[4].set(-overrideNumSegments, strokeRadius);
+ vertexData += 5;
+ fVertexCount += 5;
+ }
+ }
+
+ SkASSERT(fVertexCount <= strokeGeometry.verbs().count() * 2 * 5);
+ flushState->putBackVertices(strokeGeometry.verbs().count() * 2 * 5 - fVertexCount,
+ sizeof(SkPoint));
+}
+
+void GrTessellateStrokeOp::onExecute(GrOpFlushState* flushState, const SkRect& chainBounds) {
+ if (!fVertexBuffer) {
+ return;
+ }
+
+ GrPipeline::InitArgs initArgs;
+ if (GrAAType::kNone != fAAType) {
+ initArgs.fInputFlags |= GrPipeline::InputFlags::kHWAntialias;
+ SkASSERT(flushState->proxy()->numSamples() > 1); // No mixed samples yet.
+ SkASSERT(fAAType != GrAAType::kCoverage); // No mixed samples yet.
+ }
+ initArgs.fCaps = &flushState->caps();
+ initArgs.fDstProxyView = flushState->drawOpArgs().dstProxyView();
+ initArgs.fWriteSwizzle = flushState->drawOpArgs().writeSwizzle();
+ GrPipeline pipeline(initArgs, std::move(fProcessors), flushState->detachAppliedClip());
+
+ GrTessellateStrokeShader strokeShader(SkMatrix::I(), fDevStroke.getMiter(), fColor);
+ GrPathShader::ProgramInfo programInfo(flushState->writeView(), &pipeline, &strokeShader);
+
+ flushState->bindPipelineAndScissorClip(programInfo, this->bounds() /*chainBounds??*/);
+ flushState->bindTextures(strokeShader, nullptr, pipeline);
+
+ flushState->bindBuffers(nullptr, nullptr, fVertexBuffer.get());
+ flushState->draw(fVertexCount, fBaseVertex);
+}
diff --git a/src/gpu/tessellate/GrTessellateStrokeOp.h b/src/gpu/tessellate/GrTessellateStrokeOp.h
new file mode 100644
index 0000000..6dadc83
--- /dev/null
+++ b/src/gpu/tessellate/GrTessellateStrokeOp.h
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2020 Google LLC.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#ifndef GrTessellateStrokeOp_DEFINED
+#define GrTessellateStrokeOp_DEFINED
+
+#include "include/core/SkStrokeRec.h"
+#include "src/gpu/ops/GrDrawOp.h"
+
+// Renders opaque, constant-color strokes by decomposing them into standalone tessellation patches.
+// Each patch is either a "cubic" (single stroked bezier curve with butt caps) or a "join". Requires
+// MSAA if antialiasing is desired.
+class GrTessellateStrokeOp : public GrDrawOp {
+ DEFINE_OP_CLASS_ID
+
+ // The provided matrix must be a similarity matrix for the time being. This is so we can
+ // bootstrap this Op on top of GrStrokeGeometry with minimal modifications.
+ //
+ // Patches can overlap, so until a stencil technique is implemented, the provided paint must be
+ // a constant blended color.
+ GrTessellateStrokeOp(const SkMatrix&, const SkPath&, const SkStrokeRec&, GrPaint&&, GrAAType);
+
+ const char* name() const override { return "GrTessellateStrokeOp"; }
+ void visitProxies(const VisitProxyFunc& fn) const override { fProcessors.visitProxies(fn); }
+ GrProcessorSet::Analysis finalize(const GrCaps&, const GrAppliedClip*,
+ bool hasMixedSampledCoverage, GrClampType) override;
+ FixedFunctionFlags fixedFunctionFlags() const override;
+ void onPrePrepare(GrRecordingContext*, const GrSurfaceProxyView*, GrAppliedClip*,
+ const GrXferProcessor::DstProxyView&) override;
+ void onPrepare(GrOpFlushState* state) override;
+ void onExecute(GrOpFlushState*, const SkRect& chainBounds) override;
+
+ const SkPath fDevPath;
+ const SkStrokeRec fDevStroke;
+ const GrAAType fAAType;
+ SkPMColor4f fColor;
+ GrProcessorSet fProcessors;
+
+ sk_sp<const GrBuffer> fVertexBuffer;
+ int fVertexCount = 0;
+ int fBaseVertex;
+
+ friend class GrOpMemoryPool; // For ctor.
+};
+
+#endif
diff --git a/src/gpu/tessellate/GrTessellateStrokeShader.cpp b/src/gpu/tessellate/GrTessellateStrokeShader.cpp
new file mode 100644
index 0000000..d0f4229
--- /dev/null
+++ b/src/gpu/tessellate/GrTessellateStrokeShader.cpp
@@ -0,0 +1,259 @@
+/*
+ * Copyright 2020 Google LLC.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "src/gpu/tessellate/GrTessellateStrokeShader.h"
+
+#include "src/gpu/glsl/GrGLSLFragmentShaderBuilder.h"
+#include "src/gpu/glsl/GrGLSLGeometryProcessor.h"
+#include "src/gpu/glsl/GrGLSLVarying.h"
+#include "src/gpu/glsl/GrGLSLVertexGeoBuilder.h"
+#include "src/gpu/tessellate/GrWangsFormula.h"
+
+class GrTessellateStrokeShader::Impl : public GrGLSLGeometryProcessor {
+public:
+ const char* getMiterLimitUniformName(const GrGLSLUniformHandler& uniformHandler) const {
+ return uniformHandler.getUniformCStr(fMiterLimitUniform);
+ }
+
+private:
+ void onEmitCode(EmitArgs& args, GrGPArgs* gpArgs) override {
+ const auto& shader = args.fGP.cast<GrTessellateStrokeShader>();
+ args.fVaryingHandler->emitAttributes(shader);
+
+ fMiterLimitUniform = args.fUniformHandler->addUniform(
+ nullptr, kTessControl_GrShaderFlag, kFloat_GrSLType, "miterLimit", nullptr);
+
+ const char* colorUniformName;
+ fColorUniform = args.fUniformHandler->addUniform(
+ nullptr, kFragment_GrShaderFlag, kHalf4_GrSLType, "color", &colorUniformName);
+
+ // The vertex shader is pure pass-through. Stroke widths and normals are defined in local
+ // path space, so we don't apply the view matrix until after tessellation.
+ args.fVertBuilder->declareGlobal(GrShaderVar("P", kFloat2_GrSLType,
+ GrShaderVar::TypeModifier::Out));
+ args.fVertBuilder->codeAppendf("P = inputPoint;");
+
+ // The fragment shader just outputs a uniform color.
+ args.fFragBuilder->codeAppendf("%s = %s;", args.fOutputColor, colorUniformName);
+ args.fFragBuilder->codeAppendf("%s = half4(1);", args.fOutputCoverage);
+ }
+
+ void setData(const GrGLSLProgramDataManager& pdman, const GrPrimitiveProcessor& primProc,
+ const CoordTransformRange& transformRange) override {
+ const auto& shader = primProc.cast<GrTessellateStrokeShader>();
+
+ if (fCachedMiterLimitValue != shader.fMiterLimit) {
+ pdman.set1f(fMiterLimitUniform, shader.fMiterLimit);
+ fCachedMiterLimitValue = shader.fMiterLimit;
+ }
+
+ if (fCachedColorValue != shader.fColor) {
+ pdman.set4fv(fColorUniform, 1, shader.fColor.vec());
+ fCachedColorValue = shader.fColor;
+ }
+
+ this->setTransformDataHelper(pdman, transformRange);
+ }
+
+ GrGLSLUniformHandler::UniformHandle fMiterLimitUniform;
+ GrGLSLUniformHandler::UniformHandle fColorUniform;
+
+ float fCachedMiterLimitValue = -1;
+ SkMatrix fCachedViewMatrixValue = SkMatrix::I();
+ SkPMColor4f fCachedColorValue = {-1, -1, -1, -1};
+};
+
+SkString GrTessellateStrokeShader::getTessControlShaderGLSL(
+ const GrGLSLPrimitiveProcessor* glslPrimProc, const char* versionAndExtensionDecls,
+ const GrGLSLUniformHandler& uniformHandler, const GrShaderCaps& shaderCaps) const {
+ auto impl = static_cast<const GrTessellateStrokeShader::Impl*>(glslPrimProc);
+
+ SkString code(versionAndExtensionDecls);
+ code.append("layout(vertices = 1) out;\n");
+
+ // TODO: CCPR stroking was written with a linearization tolerance of 1/8 pixel. Readdress this
+ // ASAP to see if we can use GrTessellationPathRenderer::kLinearizationIntolerance (1/4 pixel)
+ // instead.
+ constexpr static float kIntolerance = 8; // 1/8 pixel.
+ code.appendf("const float kTolerance = %f;\n", 1/kIntolerance);
+ code.appendf("const float kCubicK = %f;\n", GrWangsFormula::cubic_k(kIntolerance));
+
+ const char* miterLimitName = impl->getMiterLimitUniformName(uniformHandler);
+ code.appendf("uniform float %s;\n", miterLimitName);
+ code.appendf("#define uMiterLimit %s\n", miterLimitName);
+
+ code.append(R"(
+ in vec2 P[];
+
+ out vec4 X[];
+ out vec4 Y[];
+ out vec2 fanAngles[];
+ out vec2 strokeRadii[];
+ out vec2 outsetClamp[];
+
+ void main() {
+ // The 5th point contains the patch type and stroke radius.
+ float strokeRadius = P[4].y;
+
+ X[gl_InvocationID /*== 0*/] = vec4(P[0].x, P[1].x, P[2].x, P[3].x);
+ Y[gl_InvocationID /*== 0*/] = vec4(P[0].y, P[1].y, P[2].y, P[3].y);
+ fanAngles[gl_InvocationID /*== 0*/] = vec2(0);
+ strokeRadii[gl_InvocationID /*== 0*/] = vec2(strokeRadius);
+ outsetClamp[gl_InvocationID /*== 0*/] = vec2(-1, 1);
+
+ // Calculate how many linear segments to chop this curve into.
+ // (See GrWangsFormula::cubic().)
+ float numSegments = sqrt(kCubicK * length(max(abs(P[2] - P[1]*2.0 + P[0]),
+ abs(P[3] - P[2]*2.0 + P[1]))));
+
+ // A patch can override the number of segments it gets chopped into by passing a
+ // negative value as P[4].x. (Square caps do this to only draw one segment.)
+ if (P[4].x < 0) {
+ numSegments = -P[4].x;
+ }
+
+ // A positive value in P[4].x means this patch actually represents a join instead
+ // of a stroked cubic. Joins are implemented as radial fans from the junction point.
+ if (P[4].x > 0) {
+ // Start by finding the angle between the tangents coming in and out of the
+ // join.
+ vec2 c0 = P[1] - P[0];
+ vec2 c1 = P[3] - P[2];
+ float theta = atan(determinant(mat2(c0, c1)), dot(c0, c1));
+
+ // Determine the beginning and end angles of our join.
+ fanAngles[gl_InvocationID /*== 0*/] = atan(c0.y, c0.x) + vec2(0, theta);
+
+ float joinType = P[4].x;
+ if (joinType >= 3) {
+ // Round join. Decide how many fan segments we need in order to be smooth.
+ numSegments = abs(theta) / (2 * acos(1 - kTolerance/strokeRadius));
+ } else if (joinType == 2) {
+ // Miter join. Draw a fan with 2 segments and lengthen the interior radius
+ // so it matches the miter point.
+ // (Or draw a 1-segment fan if we exceed the miter limit.)
+ float miterRatio = 1.0 / cos(.5 * theta);
+ strokeRadii[gl_InvocationID /*== 0*/] = strokeRadius * vec2(1, miterRatio);
+ numSegments = (miterRatio <= uMiterLimit) ? 2.0 : 1.0;
+ } else {
+ // Bevel join. Make a fan with only one segment.
+ numSegments = 1;
+ }
+
+ if (strokeRadius * abs(theta) < kTolerance) {
+ // The join angle is too tight to guarantee there won't be gaps on the
+ // inside of the junction. Just in case our join was supposed to only go on
+ // the outside, switch to an internal bevel that ties all 4 incoming
+ // vertices together. The join angle is so tight that bevels, miters, and
+ // rounds will all look the same anyway.
+ numSegments = 1;
+ // Paranoia. The next shader uses "fanAngles.x != fanAngles.y" as the test
+ // to decide whether it is emitting a cubic or a fan. But if theta is close
+ // enough to zero, that might fail. Assign arbitrary, nonequal values. This
+ // is fine because we will only draw one segment with vertices at T=0 and
+ // T=1, and the shader won't use fanAngles on the two outer vertices.
+ fanAngles[gl_InvocationID /*== 0*/] = vec2(1, 0);
+ } else if (joinType != 4) {
+ // This is a standard join. Restrict it to the outside of the junction.
+ outsetClamp[gl_InvocationID /*== 0*/] = mix(
+ vec2(-1, 1), vec2(0), lessThan(vec2(-theta, theta), vec2(0)));
+ }
+ }
+
+ // Tessellate a "strip" of numSegments quads.
+ numSegments = max(1, numSegments);
+ gl_TessLevelInner[0] = numSegments;
+ gl_TessLevelInner[1] = 2.0;
+ gl_TessLevelOuter[0] = 2.0;
+ gl_TessLevelOuter[1] = numSegments;
+ gl_TessLevelOuter[2] = 2.0;
+ gl_TessLevelOuter[3] = numSegments;
+ }
+ )");
+
+ return code;
+}
+
+SkString GrTessellateStrokeShader::getTessEvaluationShaderGLSL(
+ const GrGLSLPrimitiveProcessor*, const char* versionAndExtensionDecls,
+ const GrGLSLUniformHandler&, const GrShaderCaps&) const {
+ SkString code(versionAndExtensionDecls);
+ code.append(R"(
+ layout(quads, equal_spacing, ccw) in;
+
+ in vec4 X[];
+ in vec4 Y[];
+ in vec2 fanAngles[];
+ in vec2 strokeRadii[];
+ in vec2 outsetClamp[];
+
+ uniform vec4 sk_RTAdjust;
+
+ void main() {
+ float strokeRadius = strokeRadii[0].x;
+
+ mat4x2 P = transpose(mat2x4(X[0], Y[0]));
+ float T = gl_TessCoord.x;
+
+ // Evaluate the cubic at T. Use De Casteljau's for its accuracy and stability.
+ vec2 ab = mix(P[0], P[1], T);
+ vec2 bc = mix(P[1], P[2], T);
+ vec2 cd = mix(P[2], P[3], T);
+ vec2 abc = mix(ab, bc, T);
+ vec2 bcd = mix(bc, cd, T);
+ vec2 position = mix(abc, bcd, T);
+
+ // Find the normalized tangent vector at T.
+ vec2 tangent = bcd - abc;
+ if (tangent == vec2(0)) {
+ // We get tangent=0 if (P0 == P1 and T == 0), of if (P2 == P3 and T == 1).
+ tangent = (T == 0) ? P[2] - P[0] : P[3] - P[1];
+ }
+ tangent = normalize(tangent);
+
+ // If the fanAngles are not equal, it means this patch actually represents a join
+ // instead of a stroked cubic. Joins are implemented as radial fans from the
+ // junction point.
+ //
+ // The caller carefully sets up the control points on junctions so the above math
+ // lines up exactly with the incoming stroke vertices at T=0 and T=1, but for
+ // interior T values we fall back on the fan's arc equation instead.
+ if (fanAngles[0].x != fanAngles[0].y && T != 0 && T != 1) {
+ position = P[0];
+ float theta = mix(fanAngles[0].x, fanAngles[0].y, T);
+ tangent = vec2(cos(theta), sin(theta));
+ // Miters use a larger radius for the internal vertex.
+ strokeRadius = strokeRadii[0].y;
+ }
+
+ // Determine how far to outset our vertex orthogonally from the curve.
+ float outset = gl_TessCoord.y * 2 - 1;
+ outset = clamp(outset, outsetClamp[0].x, outsetClamp[0].y);
+ outset *= strokeRadius;
+
+ vec2 vertexpos = position + vec2(-tangent.y, tangent.x) * outset;
+ )");
+
+ // Transform after tessellation. Stroke widths and normals are defined in (pre-transform) local
+ // path space.
+ if (!this->viewMatrix().isIdentity()) {
+ SK_ABORT("Non-identity matrices not supported.");
+ // TODO: implement.
+ }
+
+ code.append(R"(
+ gl_Position = vec4(vertexpos * sk_RTAdjust.xz + sk_RTAdjust.yw, 0.0, 1.0);
+ }
+ )");
+
+ return code;
+}
+
+GrGLSLPrimitiveProcessor* GrTessellateStrokeShader::createGLSLInstance(
+ const GrShaderCaps&) const {
+ return new Impl;
+}
diff --git a/src/gpu/tessellate/GrTessellateStrokeShader.h b/src/gpu/tessellate/GrTessellateStrokeShader.h
new file mode 100644
index 0000000..715e9f2
--- /dev/null
+++ b/src/gpu/tessellate/GrTessellateStrokeShader.h
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2020 Google LLC.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#ifndef GrTessellateStrokeShader_DEFINED
+#define GrTessellateStrokeShader_DEFINED
+
+#include "src/gpu/tessellate/GrPathShader.h"
+
+#include "src/gpu/tessellate/GrTessellationPathRenderer.h"
+
+class GrGLSLUniformHandler;
+
+// Tessellates a batch of stroke patches directly to the canvas. A patch is either a "cubic"
+// (single stroked bezier curve with butt caps) or a "join". A patch is defined by 5 points as
+// follows:
+//
+// P0..P3 : Represent the cubic control points.
+// (P4.x == 0) : The patch is a cubic and the shader decides how many linear segments to produce.
+// (P4.x < 0) : The patch is still a cubic, but will be linearized into exactly |P4.x| segments.
+// (P4.x == 1) : The patch is an outer bevel join.
+// (P4.x == 2) : The patch is an outer miter join.
+// (P4.x == 3) : The patch is an outer round join.
+// (P4.x == 4) : The patch is an inner and outer round join.
+// P4.y : Represents the stroke radius.
+//
+// If a patch is a join, P0 must equal P3, P1 must equal the control point coming into the junction,
+// and P2 must equal the control point going out. It's imperative that a junction's control points
+// match the control points of their neighbor cubics exactly, or the rasterization might not be
+// water tight. (Also note that if P1==P0 or P2==P3, the junction needs to be given its neighbor's
+// opposite cubic control point.)
+//
+// To use this shader, construct a GrProgramInfo with a primitiveType of "kPatches" and a
+// tessellationPatchVertexCount of 5.
+class GrTessellateStrokeShader : public GrPathShader {
+public:
+ GrTessellateStrokeShader(const SkMatrix& viewMatrix, float miterLimit, SkPMColor4f color)
+ : GrPathShader(kTessellate_GrTessellateStrokeShader_ClassID, viewMatrix,
+ GrPrimitiveType::kPatches, 5)
+ , fMiterLimit(miterLimit)
+ , fColor(color) {
+ constexpr static Attribute kInputPointAttrib{"inputPoint", kFloat2_GrVertexAttribType,
+ kFloat2_GrSLType};
+ this->setVertexAttributes(&kInputPointAttrib, 1);
+ }
+
+private:
+ const char* name() const override { return "GrTessellateStrokeShader"; }
+ void getGLSLProcessorKey(const GrShaderCaps&, GrProcessorKeyBuilder* b) const override {
+ b->add32(this->viewMatrix().isIdentity());
+ }
+ GrGLSLPrimitiveProcessor* createGLSLInstance(const GrShaderCaps&) const final;
+
+ SkString getTessControlShaderGLSL(const GrGLSLPrimitiveProcessor*,
+ const char* versionAndExtensionDecls,
+ const GrGLSLUniformHandler&,
+ const GrShaderCaps&) const override;
+ SkString getTessEvaluationShaderGLSL(const GrGLSLPrimitiveProcessor*,
+ const char* versionAndExtensionDecls,
+ const GrGLSLUniformHandler&,
+ const GrShaderCaps&) const override;
+
+ const float fMiterLimit;
+ const SkPMColor4f fColor;
+
+ class Impl;
+};
+
+#endif
diff --git a/src/gpu/tessellate/GrTessellationPathRenderer.cpp b/src/gpu/tessellate/GrTessellationPathRenderer.cpp
index 482a1ef..519a83b 100644
--- a/src/gpu/tessellate/GrTessellationPathRenderer.cpp
+++ b/src/gpu/tessellate/GrTessellationPathRenderer.cpp
@@ -19,6 +19,7 @@
#include "src/gpu/ops/GrFillRectOp.h"
#include "src/gpu/tessellate/GrDrawAtlasPathOp.h"
#include "src/gpu/tessellate/GrTessellatePathOp.h"
+#include "src/gpu/tessellate/GrTessellateStrokeOp.h"
#include "src/gpu/tessellate/GrWangsFormula.h"
constexpr static SkISize kAtlasInitialSize{512, 512};
@@ -100,21 +101,38 @@
GrPathRenderer::CanDrawPath GrTessellationPathRenderer::onCanDrawPath(
const CanDrawPathArgs& args) const {
- if (!args.fShape->style().isSimpleFill() || args.fShape->inverseFilled() ||
+ const GrStyledShape& shape = *args.fShape;
+ if (shape.inverseFilled() || shape.style().hasPathEffect() ||
args.fViewMatrix->hasPerspective()) {
return CanDrawPath::kNo;
}
+
if (GrAAType::kCoverage == args.fAAType) {
SkASSERT(1 == args.fProxy->numSamples());
if (!args.fProxy->canUseMixedSamples(*args.fCaps)) {
return CanDrawPath::kNo;
}
}
+
SkPath path;
- args.fShape->asPath(&path);
+ shape.asPath(&path);
if (SkPathPriv::ConicWeightCnt(path)) {
return CanDrawPath::kNo;
}
+
+ if (!shape.style().isSimpleFill()) {
+ SkPMColor4f constantColor;
+ // These are only temporary restrictions while we bootstrap tessellated stroking. Every one
+ // of them will eventually go away.
+ if (shape.style().strokeRec().getStyle() == SkStrokeRec::kStrokeAndFill_Style ||
+ !args.fCaps->shaderCaps()->tessellationSupport() ||
+ GrAAType::kCoverage == args.fAAType || !args.fViewMatrix->isSimilarity() ||
+ !args.fPaint->isConstantBlendedColor(&constantColor) ||
+ args.fPaint->numCoverageFragmentProcessors()) {
+ return CanDrawPath::kNo;
+ }
+ }
+
return CanDrawPath::kYes;
}
@@ -137,7 +155,8 @@
SkIRect devIBounds;
SkIPoint16 locationInAtlas;
bool transposedInAtlas;
- if (this->tryAddPathToAtlas(*args.fContext->priv().caps(), *args.fViewMatrix, path, devBounds,
+ if (args.fShape->style().isSimpleFill() &&
+ this->tryAddPathToAtlas(*args.fContext->priv().caps(), *args.fViewMatrix, path, devBounds,
args.fAAType, &devIBounds, &locationInAtlas, &transposedInAtlas)) {
#ifdef SK_DEBUG
// If using hardware tessellation in the atlas, make sure the max number of segments is
@@ -158,8 +177,6 @@
return true;
}
- auto drawPathFlags = OpFlags::kNone;
-
// Find the worst-case log2 number of line segments that a curve in this path might need to be
// divided into.
int worstCaseResolveLevel = GrWangsFormula::worst_case_cubic_log2(kLinearizationIntolerance,
@@ -167,16 +184,28 @@
devBounds.height());
if (worstCaseResolveLevel > kMaxResolveLevel) {
// The path is too large for our internal indirect draw shaders. Crop it to the viewport.
- SkPath viewport;
- viewport.addRect(SkRect::MakeIWH(renderTargetContext->width(),
- renderTargetContext->height()).makeOutset(1, 1));
+ auto viewport = SkRect::MakeIWH(renderTargetContext->width(),
+ renderTargetContext->height());
+ float inflationRadius = 1;
+ const SkStrokeRec& stroke = args.fShape->style().strokeRec();
+ if (stroke.getStyle() == SkStrokeRec::kHairline_Style) {
+ inflationRadius += SkStrokeRec::GetInflationRadius(stroke.getJoin(), stroke.getMiter(),
+ stroke.getCap(), 1);
+ } else if (stroke.getStyle() != SkStrokeRec::kFill_Style) {
+ inflationRadius += stroke.getInflationRadius() * args.fViewMatrix->getMaxScale();
+ }
+ viewport.outset(inflationRadius, inflationRadius);
+
+ SkPath viewportPath;
+ viewportPath.addRect(viewport);
// Perform the crop in device space so it's a simple rect-path intersection.
path.transform(*args.fViewMatrix);
- if (!Op(viewport, path, kIntersect_SkPathOp, &path)) {
+ if (!Op(viewportPath, path, kIntersect_SkPathOp, &path)) {
// The crop can fail if the PathOps encounter NaN or infinities. Return true
// because drawing nothing is acceptable behavior for FP overflow.
return true;
}
+
// Transform the path back to its own local space.
SkMatrix inverse;
if (!args.fViewMatrix->invert(&inverse)) {
@@ -193,6 +222,16 @@
SkASSERT(worstCaseResolveLevel <= kMaxResolveLevel);
}
+ if (!args.fShape->style().isSimpleFill()) {
+ const SkStrokeRec& stroke = args.fShape->style().strokeRec();
+ SkASSERT(stroke.getStyle() != SkStrokeRec::kStrokeAndFill_Style);
+ auto op = pool->allocate<GrTessellateStrokeOp>(*args.fViewMatrix, path, stroke,
+ std::move(args.fPaint), args.fAAType);
+ renderTargetContext->addDrawOp(args.fClip, std::move(op));
+ return true;
+ }
+
+ auto drawPathFlags = OpFlags::kNone;
if ((1 << worstCaseResolveLevel) > shaderCaps.maxTessellationSegments()) {
// The path is too large for hardware tessellation; a curve in this bounding box could
// potentially require more segments than are supported by the hardware. Fall back on