Write GPU buffers directly from GrStrokeGeometry

Removes the intermediate stroke representation that GrStrokeGeometry
used to generate. Uses GrOpFlushState::makeVertexStateAtLeast instead
and writes patches directly to a vertex buffer as we iterate the path.
If the vertex buffer runs out of room we simply allocate a new one and
draw the stroke in chunks.

Bug: skia:10419
Bug: skia:10460
Change-Id: Ic743158366e43d4d3f5a4ff97b039d48c9c9c65b
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/305380
Commit-Queue: Chris Dalton <csmartdalton@google.com>
Reviewed-by: Michael Ludwig <michaelludwig@google.com>
This commit is contained in:
Chris Dalton 2020-07-23 15:36:25 -06:00
parent d140e767b8
commit 9eea916c05
6 changed files with 394 additions and 625 deletions

View File

@ -26,7 +26,6 @@
#include "src/gpu/GrResourceProvider.h" #include "src/gpu/GrResourceProvider.h"
#include "src/gpu/ccpr/GrCCCoverageProcessor.h" #include "src/gpu/ccpr/GrCCCoverageProcessor.h"
#include "src/gpu/ccpr/GrCCFillGeometry.h" #include "src/gpu/ccpr/GrCCFillGeometry.h"
#include "src/gpu/ccpr/GrCCStroker.h"
#include "src/gpu/ccpr/GrGSCoverageProcessor.h" #include "src/gpu/ccpr/GrGSCoverageProcessor.h"
#include "src/gpu/ccpr/GrVSCoverageProcessor.h" #include "src/gpu/ccpr/GrVSCoverageProcessor.h"
#include "src/gpu/geometry/GrPathUtils.h" #include "src/gpu/geometry/GrPathUtils.h"
@ -66,7 +65,7 @@ class CCPRGeometryView : public Sample {
void updateGpuData(); void updateGpuData();
PrimitiveType fPrimitiveType = PrimitiveType::kTriangles; PrimitiveType fPrimitiveType = PrimitiveType::kCubics;
SkCubicType fCubicType; SkCubicType fCubicType;
SkMatrix fCubicKLM; SkMatrix fCubicKLM;
@ -75,7 +74,9 @@ class CCPRGeometryView : public Sample {
float fConicWeight = .5; float fConicWeight = .5;
float fStrokeWidth = 40; float fStrokeWidth = 40;
bool fDoStroke = false; SkPaint::Join fStrokeJoin = SkPaint::kMiter_Join;
SkPaint::Cap fStrokeCap = SkPaint::kButt_Cap;
bool fDoStroke = true;
SkTArray<TriPointInstance> fTriPointInstances; SkTArray<TriPointInstance> fTriPointInstances;
SkTArray<QuadPointInstance> fQuadPointInstances; SkTArray<QuadPointInstance> fQuadPointInstances;
@ -176,14 +177,18 @@ static void draw_klm_line(int w, int h, SkCanvas* canvas, const SkScalar line[3]
void CCPRGeometryView::onDrawContent(SkCanvas* canvas) { void CCPRGeometryView::onDrawContent(SkCanvas* canvas) {
canvas->clear(SK_ColorBLACK); canvas->clear(SK_ColorBLACK);
if (!fDoStroke) {
SkPaint outlinePaint; SkPaint outlinePaint;
outlinePaint.setColor(0x80ffffff); outlinePaint.setColor(0xff808080);
outlinePaint.setStyle(SkPaint::kStroke_Style); outlinePaint.setStyle(SkPaint::kStroke_Style);
if (fDoStroke) {
outlinePaint.setStrokeWidth(fStrokeWidth);
} else {
outlinePaint.setStrokeWidth(0); outlinePaint.setStrokeWidth(0);
}
outlinePaint.setStrokeJoin(fStrokeJoin);
outlinePaint.setStrokeCap(fStrokeCap);
outlinePaint.setAntiAlias(true); outlinePaint.setAntiAlias(true);
canvas->drawPath(fPath, outlinePaint); canvas->drawPath(fPath, outlinePaint);
}
#if 0 #if 0
SkPaint gridPaint; SkPaint gridPaint;
@ -200,7 +205,18 @@ void CCPRGeometryView::onDrawContent(SkCanvas* canvas) {
#endif #endif
SkString caption; SkString caption;
if (GrRenderTargetContext* rtc = canvas->internal_private_accessTopLayerRenderTargetContext()) { caption.appendf("PrimitiveType_%s",
GrCCCoverageProcessor::PrimitiveTypeName(fPrimitiveType));
if (PrimitiveType::kCubics == fPrimitiveType) {
caption.appendf(" (%s)", SkCubicTypeName(fCubicType));
} else if (PrimitiveType::kConics == fPrimitiveType) {
caption.appendf(" (w=%f)", fConicWeight);
}
if (fDoStroke) {
caption.appendf(" (stroke_width=%f)", fStrokeWidth);
} else if (GrRenderTargetContext* rtc =
canvas->internal_private_accessTopLayerRenderTargetContext()) {
// Render coverage count. // Render coverage count.
auto ctx = canvas->recordingContext(); auto ctx = canvas->recordingContext();
SkASSERT(ctx); SkASSERT(ctx);
@ -222,18 +238,6 @@ void CCPRGeometryView::onDrawContent(SkCanvas* canvas) {
paint.setPorterDuffXPFactory(SkBlendMode::kSrcOver); paint.setPorterDuffXPFactory(SkBlendMode::kSrcOver);
rtc->drawRect(nullptr, std::move(paint), GrAA::kNo, SkMatrix::I(), rtc->drawRect(nullptr, std::move(paint), GrAA::kNo, SkMatrix::I(),
SkRect::MakeIWH(this->width(), this->height())); SkRect::MakeIWH(this->width(), this->height()));
// Add label.
caption.appendf("PrimitiveType_%s",
GrCCCoverageProcessor::PrimitiveTypeName(fPrimitiveType));
if (PrimitiveType::kCubics == fPrimitiveType) {
caption.appendf(" (%s)", SkCubicTypeName(fCubicType));
} else if (PrimitiveType::kConics == fPrimitiveType) {
caption.appendf(" (w=%f)", fConicWeight);
}
if (fDoStroke) {
caption.appendf(" (stroke_width=%f)", fStrokeWidth);
}
} else { } else {
caption = "Use GPU backend to visualize geometry."; caption = "Use GPU backend to visualize geometry.";
} }
@ -345,8 +349,8 @@ void CCPRGeometryView::updateGpuData() {
void CCPRGeometryView::DrawCoverageCountOp::onExecute(GrOpFlushState* state, void CCPRGeometryView::DrawCoverageCountOp::onExecute(GrOpFlushState* state,
const SkRect& chainBounds) { const SkRect& chainBounds) {
GrResourceProvider* rp = state->resourceProvider(); GrResourceProvider* rp = state->resourceProvider();
auto direct = state->gpu()->getContext();
#ifdef SK_GL #ifdef SK_GL
auto direct = state->gpu()->getContext();
GrGLGpu* glGpu = GrBackendApi::kOpenGL == direct->backend() GrGLGpu* glGpu = GrBackendApi::kOpenGL == direct->backend()
? static_cast<GrGLGpu*>(state->gpu()) ? static_cast<GrGLGpu*>(state->gpu())
: nullptr; : nullptr;
@ -370,7 +374,6 @@ void CCPRGeometryView::DrawCoverageCountOp::onExecute(GrOpFlushState* state,
GrOpsRenderPass* renderPass = state->opsRenderPass(); GrOpsRenderPass* renderPass = state->opsRenderPass();
if (!fView->fDoStroke) {
for (int i = 0; i < proc->numSubpasses(); ++i) { for (int i = 0; i < proc->numSubpasses(); ++i) {
proc->reset(fView->fPrimitiveType, i, rp); proc->reset(fView->fPrimitiveType, i, rp);
proc->bindPipeline(state, pipeline, this->bounds()); proc->bindPipeline(state, pipeline, this->bounds());
@ -396,27 +399,6 @@ void CCPRGeometryView::DrawCoverageCountOp::onExecute(GrOpFlushState* state,
} }
} }
} }
} else if (PrimitiveType::kConics != fView->fPrimitiveType) { // No conic stroke support yet.
GrCCStroker stroker(0,0,0);
SkPaint p;
p.setStyle(SkPaint::kStroke_Style);
p.setStrokeWidth(fView->fStrokeWidth);
p.setStrokeJoin(SkPaint::kMiter_Join);
p.setStrokeMiter(4);
// p.setStrokeCap(SkPaint::kRound_Cap);
stroker.parseDeviceSpaceStroke(fView->fPath, SkPathPriv::PointData(fView->fPath),
SkStrokeRec(p), p.getStrokeWidth(), GrScissorTest::kDisabled,
SkIRect::MakeWH(fView->width(), fView->height()), {0, 0});
GrCCStroker::BatchID batchID = stroker.closeCurrentBatch();
GrOnFlushResourceProvider onFlushRP(direct->priv().drawingManager());
stroker.prepareToDraw(&onFlushRP);
SkIRect ibounds;
this->bounds().roundOut(&ibounds);
stroker.drawStrokes(state, proc.get(), batchID, ibounds);
}
#ifdef SK_GL #ifdef SK_GL
if (glGpu) { if (glGpu) {
@ -511,6 +493,17 @@ bool CCPRGeometryView::onChar(SkUnichar unichar) {
if (unichar == 'S') { if (unichar == 'S') {
fDoStroke = !fDoStroke; fDoStroke = !fDoStroke;
this->updateAndInval(); this->updateAndInval();
return true;
}
if (unichar == 'J') {
fStrokeJoin = (SkPaint::Join)((fStrokeJoin + 1) % 3);
this->updateAndInval();
return true;
}
if (unichar == 'C') {
fStrokeCap = (SkPaint::Cap)((fStrokeCap + 1) % 3);
this->updateAndInval();
return true;
} }
return false; return false;
} }

View File

@ -11,22 +11,24 @@
#include "include/private/SkNx.h" #include "include/private/SkNx.h"
#include "src/core/SkGeometry.h" #include "src/core/SkGeometry.h"
#include "src/core/SkMathPriv.h" #include "src/core/SkMathPriv.h"
#include "src/core/SkPathPriv.h"
// This is the maximum distance in pixels that we can stray from the edge of a stroke when // This is the maximum distance in pixels that we can stray from the edge of a stroke when
// converting it to flat line segments. // converting it to flat line segments.
static constexpr float kMaxErrorFromLinearization = 1/8.f; static constexpr float kMaxErrorFromLinearization = 1/8.f;
constexpr static float kInternalRoundJoinType = GrTessellateStrokeShader::kInternalRoundJoinType;
static Sk2f lerp(const Sk2f& a, const Sk2f& b, float T) {
SkASSERT(1 != T); // The below does not guarantee lerp(a, b, 1) === b.
return (b - a) * T + a;
}
static inline float length(const Sk2f& n) { static inline float length(const Sk2f& n) {
Sk2f nn = n*n; Sk2f nn = n*n;
return SkScalarSqrt(nn[0] + nn[1]); return SkScalarSqrt(nn[0] + nn[1]);
} }
static inline Sk2f normalize(const Sk2f& v) {
Sk2f vv = v*v;
vv += SkNx_shuffle<1,0>(vv);
return v * vv.rsqrt();
}
static inline void transpose(const Sk2f& a, const Sk2f& b, Sk2f* X, Sk2f* Y) { static inline void transpose(const Sk2f& a, const Sk2f& b, Sk2f* X, Sk2f* Y) {
float transpose[4]; float transpose[4];
a.store(transpose); a.store(transpose);
@ -34,13 +36,6 @@ static inline void transpose(const Sk2f& a, const Sk2f& b, Sk2f* X, Sk2f* Y) {
Sk2f::Load2(transpose, X, Y); Sk2f::Load2(transpose, X, Y);
} }
static inline void normalize2(const Sk2f& v0, const Sk2f& v1, SkPoint out[2]) {
Sk2f X, Y;
transpose(v0, v1, &X, &Y);
Sk2f invlength = (X*X + Y*Y).rsqrt();
Sk2f::Store2(out, Y * invlength, -X * invlength);
}
static inline float calc_curvature_costheta(const Sk2f& leftTan, const Sk2f& rightTan) { static inline float calc_curvature_costheta(const Sk2f& leftTan, const Sk2f& rightTan) {
Sk2f X, Y; Sk2f X, Y;
transpose(leftTan, rightTan, &X, &Y); transpose(leftTan, rightTan, &X, &Y);
@ -49,80 +44,194 @@ static inline float calc_curvature_costheta(const Sk2f& leftTan, const Sk2f& rig
return (dotprod[0] + dotprod[1]) * invlength[0] * invlength[1]; return (dotprod[0] + dotprod[1]) * invlength[0] * invlength[1];
} }
static GrStrokeGeometry::Verb join_verb_from_join(SkPaint::Join join) { void GrStrokeGeometry::allocVertexChunk(int minVertexAllocCount) {
using Verb = GrStrokeGeometry::Verb; VertexChunk* chunk = &fVertexChunkArray->push_back();
switch (join) { fCurrChunkVertexData = (SkPoint*)fTarget->makeVertexSpaceAtLeast(
case SkPaint::kBevel_Join: sizeof(SkPoint), minVertexAllocCount, minVertexAllocCount, &chunk->fVertexBuffer,
return Verb::kBevelJoin; &chunk->fBaseVertex, &fCurrChunkVertexCapacity);
case SkPaint::kMiter_Join: fCurrChunkMinVertexAllocCount = minVertexAllocCount;
return Verb::kMiterJoin;
case SkPaint::kRound_Join:
return Verb::kRoundJoin;
}
SK_ABORT("Invalid SkPaint::Join.");
} }
void GrStrokeGeometry::beginPath(const SkStrokeRec& stroke, float strokeDevWidth, SkPoint* GrStrokeGeometry::reservePatch() {
InstanceTallies* tallies) { constexpr static int kNumVerticesPerPatch = GrTessellateStrokeShader::kNumVerticesPerPatch;
SkASSERT(!fInsideContour); if (fVertexChunkArray->back().fVertexCount + kNumVerticesPerPatch > fCurrChunkVertexCapacity) {
// No need to put back vertices; the buffer is full.
this->allocVertexChunk(fCurrChunkMinVertexAllocCount * 2);
}
if (!fCurrChunkVertexData) {
SkDebugf("WARNING: Failed to allocate vertex buffer for tessellated stroke.");
return nullptr;
}
SkASSERT(fVertexChunkArray->back().fVertexCount + kNumVerticesPerPatch <=
fCurrChunkVertexCapacity);
SkPoint* patch = fCurrChunkVertexData + fVertexChunkArray->back().fVertexCount;
fVertexChunkArray->back().fVertexCount += kNumVerticesPerPatch;
return patch;
}
void GrStrokeGeometry::writeCubicSegment(float leftJoinType, const SkPoint pts[4],
float overrideNumSegments) {
SkPoint c1 = (pts[1] == pts[0]) ? pts[2] : pts[1];
SkPoint c2 = (pts[2] == pts[3]) ? pts[1] : pts[2];
if (fHasPreviousSegment) {
this->writeJoin(leftJoinType, pts[0], fLastControlPoint, c1);
} else {
fCurrContourFirstControlPoint = c1;
fHasPreviousSegment = true;
}
if (SkPoint* patch = this->reservePatch()) {
memcpy(patch, pts, sizeof(SkPoint) * 4);
patch[4].set(-overrideNumSegments, fCurrStrokeRadius);
}
fLastControlPoint = c2;
fCurrentPoint = pts[3];
}
void GrStrokeGeometry::writeJoin(float joinType, const SkPoint& anchorPoint,
const SkPoint& prevControlPoint, const SkPoint& nextControlPoint) {
if (SkPoint* joinPatch = this->reservePatch()) {
joinPatch[0] = anchorPoint;
joinPatch[1] = prevControlPoint;
joinPatch[2] = nextControlPoint;
joinPatch[3] = anchorPoint;
joinPatch[4].set(joinType, fCurrStrokeRadius);
}
}
void GrStrokeGeometry::writeSquareCap(const SkPoint& endPoint, const SkPoint& controlPoint) {
SkVector v = (endPoint - controlPoint);
v.normalize();
SkPoint capPoint = endPoint + v*fCurrStrokeRadius;
// 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.
if (SkPoint* capPatch = this->reservePatch()) {
capPatch[0] = endPoint;
capPatch[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.
capPatch[2] = endPoint + capPoint - controlPoint;
capPatch[3] = capPoint;
capPatch[4].set(-1, fCurrStrokeRadius);
}
}
void GrStrokeGeometry::writeCaps() {
if (!fHasPreviousSegment) {
// We don't have any control points to orient the caps. In this case, square and round caps
// are specified to be drawn as an axis-aligned square or circle respectively. Assign
// default control points that achieve this.
fCurrContourFirstControlPoint = fCurrContourStartPoint - SkPoint{1,0};
fLastControlPoint = fCurrContourStartPoint + SkPoint{1,0};
fCurrentPoint = fCurrContourStartPoint;
}
switch (fCurrStrokeCapType) {
case SkPaint::kButt_Cap:
break;
case SkPaint::kRound_Cap:
// A round cap is the same thing as a 180-degree round join.
this->writeJoin(3, fCurrContourStartPoint, fCurrContourFirstControlPoint,
fCurrContourFirstControlPoint);
this->writeJoin(3, fCurrentPoint, fLastControlPoint, fLastControlPoint);
break;
case SkPaint::kSquare_Cap:
this->writeSquareCap(fCurrContourStartPoint, fCurrContourFirstControlPoint);
this->writeSquareCap(fCurrentPoint, fLastControlPoint);
break;
}
}
void GrStrokeGeometry::addPath(const SkPath& path, const SkStrokeRec& stroke) {
this->beginPath(stroke, stroke.getWidth());
SkPathVerb previousVerb = SkPathVerb::kClose;
for (auto [verb, pts, w] : SkPathPriv::Iterate(path)) {
switch (verb) {
case SkPathVerb::kMove:
// "A subpath ... consisting of a single moveto shall not be stroked."
// https://www.w3.org/TR/SVG11/painting.html#StrokeProperties
if (previousVerb != SkPathVerb::kMove && previousVerb != SkPathVerb::kClose) {
this->writeCaps();
}
this->moveTo(pts[0]);
break;
case SkPathVerb::kClose:
this->close();
break;
case SkPathVerb::kLine:
SkASSERT(previousVerb != SkPathVerb::kClose);
this->lineTo(pts[0], pts[1]);
break;
case SkPathVerb::kQuad:
SkASSERT(previousVerb != SkPathVerb::kClose);
this->quadraticTo(pts);
break;
case SkPathVerb::kCubic:
SkASSERT(previousVerb != SkPathVerb::kClose);
this->cubicTo(pts);
break;
case SkPathVerb::kConic:
SkASSERT(previousVerb != SkPathVerb::kClose);
SkUNREACHABLE;
}
previousVerb = verb;
}
if (previousVerb != SkPathVerb::kMove && previousVerb != SkPathVerb::kClose) {
this->writeCaps();
}
}
static float join_type_from_join(SkPaint::Join join) {
switch (join) {
case SkPaint::kBevel_Join:
return GrTessellateStrokeShader::kBevelJoinType;
case SkPaint::kMiter_Join:
return GrTessellateStrokeShader::kMiterJoinType;
case SkPaint::kRound_Join:
return GrTessellateStrokeShader::kRoundJoinType;
}
SkUNREACHABLE;
}
void GrStrokeGeometry::beginPath(const SkStrokeRec& stroke, float strokeDevWidth) {
// Client should have already converted the stroke to device space (i.e. width=1 for hairline). // Client should have already converted the stroke to device space (i.e. width=1 for hairline).
SkASSERT(strokeDevWidth > 0); SkASSERT(strokeDevWidth > 0);
fCurrStrokeRadius = strokeDevWidth/2; fCurrStrokeRadius = strokeDevWidth/2;
fCurrStrokeJoinVerb = join_verb_from_join(stroke.getJoin()); fCurrStrokeJoinType = join_type_from_join(stroke.getJoin());
fCurrStrokeCapType = stroke.getCap(); fCurrStrokeCapType = stroke.getCap();
fCurrStrokeTallies = tallies;
if (Verb::kMiterJoin == fCurrStrokeJoinVerb) {
// We implement miters by placing a triangle-shaped cap on top of a bevel join. Convert the
// "miter limit" to how tall that triangle cap can be.
float m = stroke.getMiter();
fMiterMaxCapHeightOverWidth = .5f * SkScalarSqrt(m*m - 1);
}
// Find the angle of curvature where the arc height above a simple line from point A to point B // Find the angle of curvature where the arc height above a simple line from point A to point B
// is equal to kMaxErrorFromLinearization. // is equal to kMaxErrorFromLinearization.
float r = std::max(1 - kMaxErrorFromLinearization / fCurrStrokeRadius, 0.f); float r = std::max(1 - kMaxErrorFromLinearization / fCurrStrokeRadius, 0.f);
fMaxCurvatureCosTheta = 2*r*r - 1; fMaxCurvatureCosTheta = 2*r*r - 1;
fCurrContourFirstPtIdx = -1; fHasPreviousSegment = false;
fCurrContourFirstNormalIdx = -1;
fVerbs.push_back(Verb::kBeginPath);
} }
void GrStrokeGeometry::moveTo(SkPoint pt) { void GrStrokeGeometry::moveTo(const SkPoint& pt) {
SkASSERT(!fInsideContour); fHasPreviousSegment = false;
fCurrContourFirstPtIdx = fPoints.count(); fCurrContourStartPoint = pt;
fCurrContourFirstNormalIdx = fNormals.count();
fPoints.push_back(pt);
SkDEBUGCODE(fInsideContour = true);
} }
void GrStrokeGeometry::lineTo(SkPoint pt) { void GrStrokeGeometry::lineTo(const SkPoint& p0, const SkPoint& p1) {
SkASSERT(fInsideContour); this->lineTo(fCurrStrokeJoinType, p0, p1);
this->lineTo(fCurrStrokeJoinVerb, pt);
} }
void GrStrokeGeometry::lineTo(Verb leftJoinVerb, SkPoint pt) { void GrStrokeGeometry::lineTo(float leftJoinType, const SkPoint& pt0, const SkPoint& pt1) {
Sk2f tan = Sk2f::Load(&pt) - Sk2f::Load(&fPoints.back()); Sk2f p0 = Sk2f::Load(&pt0);
if ((tan == 0).allTrue()) { Sk2f p1 = Sk2f::Load(&pt1);
if ((p0 == p1).allTrue()) {
return; return;
} }
this->writeCubicSegment(leftJoinType, p0, lerp(p0, p1, 1/3.f), lerp(p0, p1, 2/3.f), p1, 1);
tan = normalize(tan);
SkVector n = SkVector::Make(tan[1], -tan[0]);
this->recordLeftJoinIfNotEmpty(leftJoinVerb, n);
fNormals.push_back(n);
this->recordStroke(Verb::kLinearStroke, 0);
fPoints.push_back(pt);
} }
void GrStrokeGeometry::quadraticTo(const SkPoint P[3]) { void GrStrokeGeometry::quadraticTo(const SkPoint P[3]) {
SkASSERT(fInsideContour); this->quadraticTo(fCurrStrokeJoinType, P, SkFindQuadMaxCurvature(P));
this->quadraticTo(fCurrStrokeJoinVerb, P, SkFindQuadMaxCurvature(P));
} }
// Wang's formula for quadratics (1985) gives us the number of evenly spaced (in the parametric // Wang's formula for quadratics (1985) gives us the number of evenly spaced (in the parametric
@ -134,7 +243,7 @@ static inline float wangs_formula_quadratic(const Sk2f& p0, const Sk2f& p1, cons
return SkScalarCeilToInt(f); return SkScalarCeilToInt(f);
} }
void GrStrokeGeometry::quadraticTo(Verb leftJoinVerb, const SkPoint P[3], float maxCurvatureT) { void GrStrokeGeometry::quadraticTo(float leftJoinType, const SkPoint P[3], float maxCurvatureT) {
Sk2f p0 = Sk2f::Load(P); Sk2f p0 = Sk2f::Load(P);
Sk2f p1 = Sk2f::Load(P+1); Sk2f p1 = Sk2f::Load(P+1);
Sk2f p2 = Sk2f::Load(P+2); Sk2f p2 = Sk2f::Load(P+2);
@ -146,16 +255,13 @@ void GrStrokeGeometry::quadraticTo(Verb leftJoinVerb, const SkPoint P[3], float
// an issue. // an issue.
if ((tan0.abs() < SK_ScalarNearlyZero).allTrue() || // p0 ~= p1 if ((tan0.abs() < SK_ScalarNearlyZero).allTrue() || // p0 ~= p1
(tan1.abs() < SK_ScalarNearlyZero).allTrue()) { // p1 ~= p2 (tan1.abs() < SK_ScalarNearlyZero).allTrue()) { // p1 ~= p2
this->lineTo(leftJoinVerb, P[2]); this->lineTo(leftJoinType, P[0], P[2]);
return; return;
} }
SkPoint normals[2];
normalize2(tan0, tan1, normals);
// Decide how many flat line segments to chop the curve into. // Decide how many flat line segments to chop the curve into.
int numSegments = wangs_formula_quadratic(p0, p1, p2); int numSegments = wangs_formula_quadratic(p0, p1, p2);
numSegments = SkTPin(numSegments, 1, 1 << kMaxNumLinearSegmentsLog2); numSegments = std::max(numSegments, 1);
// At + B gives a vector tangent to the quadratic. // At + B gives a vector tangent to the quadratic.
Sk2f A = p0 - p1*2 + p2; Sk2f A = p0 - p1*2 + p2;
@ -193,22 +299,22 @@ void GrStrokeGeometry::quadraticTo(Verb leftJoinVerb, const SkPoint P[3], float
if (leftT > 0) { if (leftT > 0) {
SkChopQuadAt(currQuadratic, ptsBuffer, leftT); SkChopQuadAt(currQuadratic, ptsBuffer, leftT);
this->quadraticTo(leftJoinVerb, ptsBuffer, /*maxCurvatureT=*/1); this->quadraticTo(leftJoinType, ptsBuffer, /*maxCurvatureT=*/1);
if (rightT < 1) { if (rightT < 1) {
rightT = (rightT - leftT) / (1 - leftT); rightT = (rightT - leftT) / (1 - leftT);
} }
currQuadratic = ptsBuffer + 2; currQuadratic = ptsBuffer + 2;
} else { } else {
this->rotateTo(leftJoinVerb, normals[0], currQuadratic[1]); this->rotateTo(leftJoinType, currQuadratic[0], currQuadratic[1]);
} }
if (rightT < 1) { if (rightT < 1) {
SkChopQuadAt(currQuadratic, ptsBuffer, rightT); SkChopQuadAt(currQuadratic, ptsBuffer, rightT);
this->lineTo(Verb::kInternalRoundJoin, ptsBuffer[2]); this->lineTo(kInternalRoundJoinType, ptsBuffer[0], ptsBuffer[2]);
this->quadraticTo(Verb::kInternalRoundJoin, ptsBuffer + 2, /*maxCurvatureT=*/0); this->quadraticTo(kInternalRoundJoinType, ptsBuffer + 2, /*maxCurvatureT=*/0);
} else { } else {
this->lineTo(Verb::kInternalRoundJoin, currQuadratic[2]); this->lineTo(kInternalRoundJoinType, currQuadratic[0], currQuadratic[2]);
this->rotateTo(Verb::kInternalRoundJoin, normals[1], this->rotateTo(kInternalRoundJoinType, currQuadratic[2],
currQuadratic[2]*2 - currQuadratic[1]); currQuadratic[2]*2 - currQuadratic[1]);
} }
return; return;
@ -216,24 +322,18 @@ void GrStrokeGeometry::quadraticTo(Verb leftJoinVerb, const SkPoint P[3], float
if (numSegments > fMaxTessellationSegments) { if (numSegments > fMaxTessellationSegments) {
SkPoint ptsBuffer[5]; SkPoint ptsBuffer[5];
SkChopQuadAt(P, ptsBuffer, 0.5f); SkChopQuadAt(P, ptsBuffer, 0.5f);
this->quadraticTo(leftJoinVerb, ptsBuffer, 0); this->quadraticTo(leftJoinType, ptsBuffer, 0);
this->quadraticTo(Verb::kInternalRoundJoin, ptsBuffer + 3, 0); this->quadraticTo(kInternalRoundJoinType, ptsBuffer + 3, 0);
return; return;
} }
this->recordLeftJoinIfNotEmpty(leftJoinVerb, normals[0]); this->writeCubicSegment(leftJoinType, p0, lerp(p0, p1, 2/3.f), lerp(p1, p2, 1/3.f), p2);
fNormals.push_back_n(2, normals);
this->recordStroke(Verb::kQuadraticStroke, SkNextLog2(numSegments));
p1.store(&fPoints.push_back());
p2.store(&fPoints.push_back());
} }
void GrStrokeGeometry::cubicTo(const SkPoint P[4]) { void GrStrokeGeometry::cubicTo(const SkPoint P[4]) {
SkASSERT(fInsideContour);
float roots[3]; float roots[3];
int numRoots = SkFindCubicMaxCurvature(P, roots); int numRoots = SkFindCubicMaxCurvature(P, roots);
this->cubicTo(fCurrStrokeJoinVerb, P, this->cubicTo(fCurrStrokeJoinType, P,
numRoots > 0 ? roots[numRoots/2] : 0, numRoots > 0 ? roots[numRoots/2] : 0,
numRoots > 1 ? roots[0] : kLeftMaxCurvatureNone, numRoots > 1 ? roots[0] : kLeftMaxCurvatureNone,
numRoots > 2 ? roots[2] : kRightMaxCurvatureNone); numRoots > 2 ? roots[2] : kRightMaxCurvatureNone);
@ -250,7 +350,7 @@ static inline float wangs_formula_cubic(const Sk2f& p0, const Sk2f& p1, const Sk
return SkScalarCeilToInt(f); return SkScalarCeilToInt(f);
} }
void GrStrokeGeometry::cubicTo(Verb leftJoinVerb, const SkPoint P[4], float maxCurvatureT, void GrStrokeGeometry::cubicTo(float leftJoinType, const SkPoint P[4], float maxCurvatureT,
float leftMaxCurvatureT, float rightMaxCurvatureT) { float leftMaxCurvatureT, float rightMaxCurvatureT) {
Sk2f p0 = Sk2f::Load(P); Sk2f p0 = Sk2f::Load(P);
Sk2f p1 = Sk2f::Load(P+1); Sk2f p1 = Sk2f::Load(P+1);
@ -265,7 +365,7 @@ void GrStrokeGeometry::cubicTo(Verb leftJoinVerb, const SkPoint P[4], float maxC
p1 = p0; p1 = p0;
tan0 = p2 - p0; tan0 = p2 - p0;
if ((tan0.abs() < SK_ScalarNearlyZero).allTrue()) { // p0 ~= p1 ~= p2 if ((tan0.abs() < SK_ScalarNearlyZero).allTrue()) { // p0 ~= p1 ~= p2
this->lineTo(leftJoinVerb, P[3]); this->lineTo(leftJoinType, P[0], P[3]);
return; return;
} }
} }
@ -274,17 +374,14 @@ void GrStrokeGeometry::cubicTo(Verb leftJoinVerb, const SkPoint P[4], float maxC
tan1 = p3 - p1; tan1 = p3 - p1;
if ((tan1.abs() < SK_ScalarNearlyZero).allTrue() || // p1 ~= p2 ~= p3 if ((tan1.abs() < SK_ScalarNearlyZero).allTrue() || // p1 ~= p2 ~= p3
(p0 == p1).allTrue()) { // p0 ~= p1 AND p2 ~= p3 (p0 == p1).allTrue()) { // p0 ~= p1 AND p2 ~= p3
this->lineTo(leftJoinVerb, P[3]); this->lineTo(leftJoinType, P[0], P[3]);
return; return;
} }
} }
SkPoint normals[2];
normalize2(tan0, tan1, normals);
// Decide how many flat line segments to chop the curve into. // Decide how many flat line segments to chop the curve into.
int numSegments = wangs_formula_cubic(p0, p1, p2, p3); int numSegments = wangs_formula_cubic(p0, p1, p2, p3);
numSegments = SkTPin(numSegments, 1, 1 << kMaxNumLinearSegmentsLog2); numSegments = std::max(numSegments, 1);
// At^2 + Bt + C gives a vector tangent to the cubic. (More specifically, it's the derivative // 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.) // minus an irrelevant scale by 3, since all we care about is the direction.)
@ -328,7 +425,7 @@ void GrStrokeGeometry::cubicTo(Verb leftJoinVerb, const SkPoint P[4], float maxC
if (leftT > 0) { if (leftT > 0) {
SkChopCubicAt(currCubic, ptsBuffer, leftT); SkChopCubicAt(currCubic, ptsBuffer, leftT);
this->cubicTo(leftJoinVerb, ptsBuffer, /*maxCurvatureT=*/1, this->cubicTo(leftJoinType, ptsBuffer, /*maxCurvatureT=*/1,
(kLeftMaxCurvatureNone != leftMaxCurvatureT) (kLeftMaxCurvatureNone != leftMaxCurvatureT)
? leftMaxCurvatureT/leftT : kLeftMaxCurvatureNone, ? leftMaxCurvatureT/leftT : kLeftMaxCurvatureNone,
kRightMaxCurvatureNone); kRightMaxCurvatureNone);
@ -341,144 +438,73 @@ void GrStrokeGeometry::cubicTo(Verb leftJoinVerb, const SkPoint P[4], float maxC
currCubic = ptsBuffer + 3; currCubic = ptsBuffer + 3;
} else { } else {
SkPoint c1 = (ptsBuffer[1] == ptsBuffer[0]) ? ptsBuffer[2] : ptsBuffer[1]; SkPoint c1 = (ptsBuffer[1] == ptsBuffer[0]) ? ptsBuffer[2] : ptsBuffer[1];
this->rotateTo(leftJoinVerb, normals[0], c1); this->rotateTo(leftJoinType, ptsBuffer[0], c1);
} }
if (rightT < 1) { if (rightT < 1) {
SkChopCubicAt(currCubic, ptsBuffer, rightT); SkChopCubicAt(currCubic, ptsBuffer, rightT);
this->lineTo(Verb::kInternalRoundJoin, ptsBuffer[3]); this->lineTo(kInternalRoundJoinType, ptsBuffer[0], ptsBuffer[3]);
currCubic = ptsBuffer + 3; currCubic = ptsBuffer + 3;
this->cubicTo(Verb::kInternalRoundJoin, currCubic, /*maxCurvatureT=*/0, this->cubicTo(kInternalRoundJoinType, currCubic, /*maxCurvatureT=*/0,
kLeftMaxCurvatureNone, kRightMaxCurvatureNone); kLeftMaxCurvatureNone, kRightMaxCurvatureNone);
} else { } else {
this->lineTo(Verb::kInternalRoundJoin, currCubic[3]); this->lineTo(kInternalRoundJoinType, currCubic[0], currCubic[3]);
SkPoint c2 = (currCubic[2] == currCubic[3]) ? currCubic[1] : currCubic[2]; SkPoint c2 = (currCubic[2] == currCubic[3]) ? currCubic[1] : currCubic[2];
this->rotateTo(Verb::kInternalRoundJoin, normals[1], currCubic[3]*2 - c2); this->rotateTo(kInternalRoundJoinType, currCubic[3], currCubic[3]*2 - c2);
} }
return; return;
} }
// Recurse and check the other two points of max curvature, if any. // Recurse and check the other two points of max curvature, if any.
if (kRightMaxCurvatureNone != rightMaxCurvatureT) { if (kRightMaxCurvatureNone != rightMaxCurvatureT) {
this->cubicTo(leftJoinVerb, P, rightMaxCurvatureT, leftMaxCurvatureT, this->cubicTo(leftJoinType, P, rightMaxCurvatureT, leftMaxCurvatureT,
kRightMaxCurvatureNone); kRightMaxCurvatureNone);
return; return;
} }
if (kLeftMaxCurvatureNone != leftMaxCurvatureT) { if (kLeftMaxCurvatureNone != leftMaxCurvatureT) {
SkASSERT(kRightMaxCurvatureNone == rightMaxCurvatureT); SkASSERT(kRightMaxCurvatureNone == rightMaxCurvatureT);
this->cubicTo(leftJoinVerb, P, leftMaxCurvatureT, kLeftMaxCurvatureNone, this->cubicTo(leftJoinType, P, leftMaxCurvatureT, kLeftMaxCurvatureNone,
kRightMaxCurvatureNone); kRightMaxCurvatureNone);
return; return;
} }
if (numSegments > fMaxTessellationSegments) { if (numSegments > fMaxTessellationSegments) {
SkPoint ptsBuffer[7]; SkPoint ptsBuffer[7];
SkChopCubicAt(P, ptsBuffer, 0.5f); SkChopCubicAt(P, ptsBuffer, 0.5f);
this->cubicTo(leftJoinVerb, ptsBuffer, 0, kLeftMaxCurvatureNone, kRightMaxCurvatureNone); this->cubicTo(leftJoinType, ptsBuffer, 0, kLeftMaxCurvatureNone, kRightMaxCurvatureNone);
this->cubicTo(Verb::kInternalRoundJoin, ptsBuffer + 3, 0, kLeftMaxCurvatureNone, this->cubicTo(kInternalRoundJoinType, ptsBuffer + 3, 0, kLeftMaxCurvatureNone,
kRightMaxCurvatureNone); kRightMaxCurvatureNone);
return; return;
} }
this->recordLeftJoinIfNotEmpty(leftJoinVerb, normals[0]); this->writeCubicSegment(leftJoinType, p0, p1, p2, p3);
fNormals.push_back_n(2, normals);
this->recordStroke(Verb::kCubicStroke, SkNextLog2(numSegments));
p1.store(&fPoints.push_back());
p2.store(&fPoints.push_back());
p3.store(&fPoints.push_back());
} }
void GrStrokeGeometry::recordStroke(Verb verb, int numSegmentsLog2) { void GrStrokeGeometry::rotateTo(float leftJoinType, const SkPoint& anchorPoint,
SkASSERT(Verb::kLinearStroke != verb || 0 == numSegmentsLog2); const SkPoint& controlPoint) {
fVerbs.push_back(verb); // Effectively rotate the current normal by drawing a zero length, 1-segment cubic.
if (Verb::kLinearStroke != verb) { // writeCubicSegment automatically adds the necessary join and the zero length cubic serves as
fParams.push_back().fNumLinearSegmentsLog2 = numSegmentsLog2; // a glue that guarantees a water tight rasterized edge between the new join and the segment
} // that comes after the rotate.
++fCurrStrokeTallies->fStrokes[numSegmentsLog2]; SkPoint pts[4] = {anchorPoint, controlPoint, anchorPoint*2 - controlPoint, anchorPoint};
this->writeCubicSegment(leftJoinType, pts, 1);
} }
void GrStrokeGeometry::rotateTo(Verb leftJoinVerb, SkVector normal, SkPoint controlPoint) { void GrStrokeGeometry::close() {
SkASSERT(fPoints.count() > fCurrContourFirstPtIdx); if (!fHasPreviousSegment) {
this->recordLeftJoinIfNotEmpty(leftJoinVerb, normal); // Draw caps instead of closing if the subpath is zero length:
fVerbs.push_back(Verb::kRotate); //
fPoints.push_back(controlPoint); // "Any zero length subpath ... shall be stroked if the 'stroke-linecap' property has a
fPoints.push_back(fPoints[fPoints.count() - 2]); // value of round or square producing respectively a circle or a square."
fNormals.push_back(normal); //
} // (https://www.w3.org/TR/SVG11/painting.html#StrokeProperties)
//
void GrStrokeGeometry::recordLeftJoinIfNotEmpty(Verb joinVerb, SkVector nextNormal) { this->writeCaps();
if (fNormals.count() <= fCurrContourFirstNormalIdx) {
// The contour is empty. Nothing to join with.
SkASSERT(fNormals.count() == fCurrContourFirstNormalIdx);
return;
}
fVerbs.push_back(joinVerb);
}
void GrStrokeGeometry::closeContour() {
SkASSERT(fInsideContour);
SkASSERT(fPoints.count() > fCurrContourFirstPtIdx);
if (fPoints.back() != fPoints[fCurrContourFirstPtIdx]) {
// Draw a line back to the beginning.
this->lineTo(fCurrStrokeJoinVerb, fPoints[fCurrContourFirstPtIdx]);
}
fVerbs.push_back(fCurrStrokeJoinVerb);
fVerbs.push_back(Verb::kEndContour);
SkDEBUGCODE(fInsideContour = false);
}
void GrStrokeGeometry::capContourAndExit() {
SkASSERT(fInsideContour);
if (fCurrContourFirstNormalIdx >= fNormals.count()) {
// This contour is empty. Add a normal in the direction that caps orient on empty geometry.
SkASSERT(fNormals.count() == fCurrContourFirstNormalIdx);
fNormals.push_back({1, 0});
}
this->recordCapsIfAny();
fVerbs.push_back(Verb::kEndContour);
SkDEBUGCODE(fInsideContour = false);
}
void GrStrokeGeometry::recordCapsIfAny() {
SkASSERT(fInsideContour);
SkASSERT(fCurrContourFirstNormalIdx < fNormals.count());
if (SkPaint::kButt_Cap == fCurrStrokeCapType) {
return; return;
} }
Verb capVerb; // Draw a line back to the beginning. (This will be discarded if
if (SkPaint::kSquare_Cap == fCurrStrokeCapType) { // fCurrentPoint == fCurrContourStartPoint.)
if (fCurrStrokeRadius * SK_ScalarRoot2Over2 < kMaxErrorFromLinearization) { this->lineTo(fCurrStrokeJoinType, fCurrentPoint, fCurrContourStartPoint);
return; this->writeJoin(fCurrStrokeJoinType, fCurrContourStartPoint, fLastControlPoint,
} fCurrContourFirstControlPoint);
capVerb = Verb::kSquareCap;
fCurrStrokeTallies->fStrokes[0] += 2;
} else {
SkASSERT(SkPaint::kRound_Cap == fCurrStrokeCapType);
if (fCurrStrokeRadius < kMaxErrorFromLinearization) {
return;
}
capVerb = Verb::kRoundCap;
fCurrStrokeTallies->fTriangles += 2;
fCurrStrokeTallies->fConics += 4;
}
fVerbs.push_back(capVerb);
fVerbs.push_back(Verb::kEndContour);
fVerbs.push_back(capVerb);
// Reserve the space first, since push_back() takes the point by reference and might
// invalidate the reference if the array grows.
fPoints.reserve(fPoints.count() + 1);
fPoints.push_back(fPoints[fCurrContourFirstPtIdx]);
// Reserve the space first, since push_back() takes the normal by reference and might
// invalidate the reference if the array grows. (Although in this case we should be fine
// since there is a negate operator.)
fNormals.reserve(fNormals.count() + 1);
fNormals.push_back(-fNormals[fCurrContourFirstNormalIdx]);
} }

View File

@ -11,120 +11,108 @@
#include "include/core/SkPaint.h" #include "include/core/SkPaint.h"
#include "include/core/SkPoint.h" #include "include/core/SkPoint.h"
#include "include/private/SkTArray.h" #include "include/private/SkTArray.h"
#include "src/gpu/ops/GrMeshDrawOp.h"
#include "src/gpu/tessellate/GrTessellateStrokeShader.h"
class SkStrokeRec; class SkStrokeRec;
/** // This is an RAII class that expands strokes into tessellation patches for consumption by
* This class converts post-transform stroked paths into a set of independent strokes, joins, and // GrTessellateStrokeShader. The provided GrMeshDrawOp::Target must not be used externally for the
* caps that map directly to GPU instances. // entire lifetime of this class. e.g.:
*/ //
// void onPrepare(GrOpFlushState* target) {
// GrStrokeGeometry g(target, &fMyVertexChunks, count); // Locks target.
// for (...) {
// g.addPath(path, stroke);
// }
// }
// ... target can now be used normally again.
// ... fMyVertexChunks now contains chunks that can be drawn during onExecute.
class GrStrokeGeometry { class GrStrokeGeometry {
public: public:
static constexpr int kMaxNumLinearSegmentsLog2 = 15; // We generate vertex buffers in chunks. Normally there will only be one chunk, but in rare
// cases the first can run out of space if too many cubics needed to be subdivided.
GrStrokeGeometry(int maxTessellationSegments, int numSkPoints = 0, int numSkVerbs = 0) struct VertexChunk {
: fMaxTessellationSegments(maxTessellationSegments) sk_sp<const GrBuffer> fVertexBuffer;
, fVerbs(numSkVerbs * 5/2) // Reserve for a 2.5x expansion in verbs. (Joins get their int fVertexCount = 0;
// own separate verb in our representation.) int fBaseVertex;
, fParams(numSkVerbs * 3) // Somewhere around 1-2 params per verb.
, fPoints(numSkPoints * 5/4) // Reserve for a 1.25x expansion in points and normals.
, fNormals(numSkPoints * 5/4) {}
// A string of verbs and their corresponding, params, points, and normals are a compact
// representation of what will eventually be independent instances in GPU buffers.
enum class Verb : uint8_t {
kBeginPath, // Instructs the iterator to advance its stroke width, atlas offset, etc.
// Independent strokes of a single line or curve, with (antialiased) butt caps on the ends.
kLinearStroke,
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,
kMiterJoin,
kRoundJoin,
// We use internal joins when we have to internally break up a stroke because its curvature
// is too strong for a triangle strip. They are coverage-counted, self-intersecting
// quadrilaterals that tie the four corners of two adjoining strokes together a like a
// shoelace. (Coverage is negative on the inside half.) We place an arc on both ends of an
// internal round join.
kInternalBevelJoin,
kInternalRoundJoin,
kSquareCap,
kRoundCap,
kEndContour // Instructs the iterator to advance its internal point and normal ptrs.
};
static bool IsInternalJoinVerb(Verb verb);
// Some verbs require additional parameters(s).
union Parameter {
// For cubic and quadratic strokes: How many flat line segments to chop the curve into?
int fNumLinearSegmentsLog2;
// For miter and round joins: How tall should the triangle cap be on top of the join?
// (This triangle is the conic control points for a round join.)
float fMiterCapHeightOverWidth;
float fConicWeight; // Round joins only.
}; };
const SkTArray<Verb, true>& verbs() const { SkASSERT(!fInsideContour); return fVerbs; } // Stores raw pointers to the provided target and vertexChunkArray, which this class will use
const SkTArray<Parameter, true>& params() const { SkASSERT(!fInsideContour); return fParams; } // and push to as addPath is called. The caller is responsible to bind and draw each chunk that
const SkTArray<SkPoint, true>& points() const { SkASSERT(!fInsideContour); return fPoints; } // gets pushed to the array. (See GrTessellateStrokeShader.)
const SkTArray<SkVector, true>& normals() const { SkASSERT(!fInsideContour); return fNormals; } GrStrokeGeometry(GrMeshDrawOp::Target* target, SkTArray<VertexChunk>* vertexChunkArray,
int totalCombinedVerbCnt)
: fMaxTessellationSegments(target->caps().shaderCaps()->maxTessellationSegments())
, fTarget(target)
, fVertexChunkArray(vertexChunkArray) {
this->allocVertexChunk(
(totalCombinedVerbCnt * 3) * GrTessellateStrokeShader::kNumVerticesPerPatch);
}
// These track the numbers of instances required to draw all the recorded strokes. // "Releases" the target to be used externally again by putting back any unused pre-allocated
struct InstanceTallies { // vertices.
int fStrokes[kMaxNumLinearSegmentsLog2 + 1]; ~GrStrokeGeometry() {
int fTriangles; fTarget->putBackVertices(fCurrChunkVertexCapacity - fVertexChunkArray->back().fVertexCount,
int fConics; sizeof(SkPoint));
}
InstanceTallies operator+(const InstanceTallies&) const; void addPath(const SkPath&, const SkStrokeRec&);
};
void beginPath(const SkStrokeRec&, float strokeDevWidth, InstanceTallies*);
void moveTo(SkPoint);
void lineTo(SkPoint);
void quadraticTo(const SkPoint[3]);
void cubicTo(const SkPoint[4]);
void closeContour(); // Connect back to the first point in the contour and exit.
void capContourAndExit(); // Add endcaps (if any) and exit the contour.
private: private:
void lineTo(Verb leftJoinVerb, SkPoint); void allocVertexChunk(int minVertexAllocCount);
void quadraticTo(Verb leftJoinVerb, const SkPoint[3], float maxCurvatureT); SkPoint* reservePatch();
// Join types are written as floats in P4.x. See GrTessellateStrokeShader for definitions.
void writeCubicSegment(float leftJoinType, const SkPoint pts[4], float overrideNumSegments = 0);
void writeCubicSegment(float leftJoinType, const Sk2f& p0, const Sk2f& p1, const Sk2f& p2,
const Sk2f& p3, float overrideNumSegments = 0) {
SkPoint pts[4];
p0.store(&pts[0]);
p1.store(&pts[1]);
p2.store(&pts[2]);
p3.store(&pts[3]);
this->writeCubicSegment(leftJoinType, pts, overrideNumSegments);
}
void writeJoin(float joinType, const SkPoint& anchorPoint, const SkPoint& prevControlPoint,
const SkPoint& nextControlPoint);
void writeSquareCap(const SkPoint& endPoint, const SkPoint& controlPoint);
void writeCaps();
void beginPath(const SkStrokeRec&, float strokeDevWidth);
void moveTo(const SkPoint&);
void lineTo(const SkPoint& p0, const SkPoint& p1);
void quadraticTo(const SkPoint[3]);
void cubicTo(const SkPoint[4]);
void close();
void lineTo(float leftJoinType, const SkPoint& p0, const SkPoint& p1);
void quadraticTo(float leftJoinType, const SkPoint[3], float maxCurvatureT);
static constexpr float kLeftMaxCurvatureNone = 1; static constexpr float kLeftMaxCurvatureNone = 1;
static constexpr float kRightMaxCurvatureNone = 0; static constexpr float kRightMaxCurvatureNone = 0;
void cubicTo(Verb leftJoinVerb, const SkPoint[4], float maxCurvatureT, float leftMaxCurvatureT, void cubicTo(float leftJoinType, const SkPoint[4], float maxCurvatureT, float leftMaxCurvatureT,
float rightMaxCurvatureT); float rightMaxCurvatureT);
// Pushes a new normal to fNormals and records a join, without changing the current position. // TEMPORARY: Rotates the current control point without changing the current position.
void rotateTo(Verb leftJoinVerb, SkVector normal, SkPoint controlPoint); // This is used when we convert a curve to a lineTo, and that behavior will soon go away.
void rotateTo(float leftJoinType, const SkPoint& anchorPoint, const SkPoint& controlPoint);
// Records a stroke in fElememts. const int fMaxTessellationSegments;
void recordStroke(Verb, int numSegmentsLog2);
// Records a join in fElememts with the previous stroke, if the cuurent contour is not empty. // These are raw pointers whose lifetimes are controlled outside this class.
void recordLeftJoinIfNotEmpty(Verb joinType, SkVector nextNormal); GrMeshDrawOp::Target* const fTarget;
SkTArray<VertexChunk>* const fVertexChunkArray;
void recordCapsIfAny(); // Variables related to the vertex chunk that we are currently filling.
int fCurrChunkVertexCapacity;
int fCurrChunkMinVertexAllocCount;
SkPoint* fCurrChunkVertexData;
// Variables related to the path that we are currently iterating.
float fCurrStrokeRadius; float fCurrStrokeRadius;
Verb fCurrStrokeJoinVerb; float fCurrStrokeJoinType; // See GrTessellateStrokeShader for join type definitions .
SkPaint::Cap fCurrStrokeCapType; SkPaint::Cap fCurrStrokeCapType;
InstanceTallies* fCurrStrokeTallies = nullptr;
// We implement miters by placing a triangle-shaped cap on top of a bevel join. This field tells
// us what the miter limit is, restated in terms of how tall that triangle cap can be.
float fMiterMaxCapHeightOverWidth;
// Any curvature on the original curve gets magnified on the outer edge of the stroke, // Any curvature on the original curve gets magnified on the outer edge of the stroke,
// proportional to how thick the stroke radius is. This field tells us the maximum curvature we // proportional to how thick the stroke radius is. This field tells us the maximum curvature we
// can tolerate using the current stroke radius, before linearization artifacts begin to appear // can tolerate using the current stroke radius, before linearization artifacts begin to appear
@ -134,47 +122,12 @@ private:
// section with strong curvature into lineTo's with round joins in between.) // section with strong curvature into lineTo's with round joins in between.)
float fMaxCurvatureCosTheta; float fMaxCurvatureCosTheta;
int fCurrContourFirstPtIdx; // Variables related to the specific contour that we are currently iterating.
int fCurrContourFirstNormalIdx; bool fHasPreviousSegment = false;
SkPoint fCurrContourStartPoint;
SkDEBUGCODE(bool fInsideContour = false); SkPoint fCurrContourFirstControlPoint;
SkPoint fLastControlPoint;
const int fMaxTessellationSegments; SkPoint fCurrentPoint;
SkSTArray<128, Verb, true> fVerbs;
SkSTArray<128, Parameter, true> fParams;
SkSTArray<128, SkPoint, true> fPoints;
SkSTArray<128, SkVector, true> fNormals;
}; };
inline GrStrokeGeometry::InstanceTallies GrStrokeGeometry::InstanceTallies::operator+(
const InstanceTallies& t) const {
InstanceTallies ret;
for (int i = 0; i <= kMaxNumLinearSegmentsLog2; ++i) {
ret.fStrokes[i] = fStrokes[i] + t.fStrokes[i];
}
ret.fTriangles = fTriangles + t.fTriangles;
ret.fConics = fConics + t.fConics;
return ret;
}
inline bool GrStrokeGeometry::IsInternalJoinVerb(Verb verb) {
switch (verb) {
case Verb::kInternalBevelJoin:
case Verb::kInternalRoundJoin:
return true;
case Verb::kBeginPath:
case Verb::kLinearStroke:
case Verb::kQuadraticStroke:
case Verb::kCubicStroke:
case Verb::kRotate:
case Verb::kBevelJoin:
case Verb::kMiterJoin:
case Verb::kRoundJoin:
case Verb::kSquareCap:
case Verb::kRoundCap:
case Verb::kEndContour:
return false;
}
SK_ABORT("Invalid GrStrokeGeometry::Verb.");
}
#endif #endif

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2019 Google LLC. * Copyright 2020 Google LLC.
* *
* Use of this source code is governed by a BSD-style license that can be * Use of this source code is governed by a BSD-style license that can be
* found in the LICENSE file. * found in the LICENSE file.
@ -44,8 +44,7 @@ GrTessellateStrokeOp::GrTessellateStrokeOp(const SkMatrix& viewMatrix, const SkP
GrAAType aaType) GrAAType aaType)
: GrDrawOp(ClassID()) : GrDrawOp(ClassID())
, fPathStrokes(transform_path(viewMatrix, path), transform_stroke(viewMatrix, stroke)) , fPathStrokes(transform_path(viewMatrix, path), transform_stroke(viewMatrix, stroke))
, fNumVerbs(path.countVerbs()) , fTotalCombinedVerbCnt(path.countVerbs())
, fNumPoints(path.countPoints())
, fColor(get_paint_constant_blended_color(paint)) , fColor(get_paint_constant_blended_color(paint))
, fAAType(aaType) , fAAType(aaType)
, fProcessors(std::move(paint)) { , fProcessors(std::move(paint)) {
@ -100,8 +99,7 @@ GrOp::CombineResult GrTessellateStrokeOp::onCombineIfPossible(GrOp* grOp,
SkASSERT(fMiterLimitOrZero == 0 || fMiterLimitOrZero == op->fMiterLimitOrZero); SkASSERT(fMiterLimitOrZero == 0 || fMiterLimitOrZero == op->fMiterLimitOrZero);
fMiterLimitOrZero = op->fMiterLimitOrZero; fMiterLimitOrZero = op->fMiterLimitOrZero;
} }
fNumVerbs += op->fNumVerbs; fTotalCombinedVerbCnt += op->fTotalCombinedVerbCnt;
fNumPoints += op->fNumPoints;
return CombineResult::kMerged; return CombineResult::kMerged;
} }
@ -110,226 +108,14 @@ void GrTessellateStrokeOp::onPrePrepare(GrRecordingContext*, const GrSurfaceProx
GrAppliedClip*, const GrXferProcessor::DstProxyView&) { 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) { void GrTessellateStrokeOp::onPrepare(GrOpFlushState* flushState) {
// Rebuild the stroke using GrStrokeGeometry. GrStrokeGeometry strokeGeometry(flushState, &fVertexChunks, fTotalCombinedVerbCnt);
GrStrokeGeometry strokeGeometry(flushState->caps().shaderCaps()->maxTessellationSegments(),
fNumPoints, fNumVerbs);
for (auto& [path, stroke] : fPathStrokes) { for (auto& [path, stroke] : fPathStrokes) {
float strokeRadius = stroke.getWidth() * .5f; strokeGeometry.addPath(path, stroke);
GrStrokeGeometry::InstanceTallies tallies = GrStrokeGeometry::InstanceTallies();
strokeGeometry.beginPath(stroke, strokeRadius * 2, &tallies);
SkPathVerb previousVerb = SkPathVerb::kClose;
for (auto [verb, pts, w] : SkPathPriv::Iterate(path)) {
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;
float currStrokeRadius = 0;
auto pathStrokesIter = fPathStrokes.begin();
for (auto verb : strokeGeometry.verbs()) {
SkPoint patch[4];
float overrideNumSegments = 0;
switch (verb) {
case Verb::kBeginPath:
SkASSERT(pathStrokesIter != fPathStrokes.end());
pendingJoin = Verb::kEndContour;
firstJoinControlPoint = {0, 0};
lastJoinControlPoint = {0, 0};
hasFirstControlPoint = false;
currStrokeRadius = (*pathStrokesIter).fStroke.getWidth() * .5f;
++pathStrokesIter;
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, currStrokeRadius);
// 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, currStrokeRadius);
break;
case Verb::kMiterJoin:
vertexData[4].set(2, currStrokeRadius);
break;
case Verb::kRoundJoin:
vertexData[4].set(3, currStrokeRadius);
break;
case Verb::kInternalRoundJoin:
case Verb::kInternalBevelJoin:
default:
vertexData[4].set(4, currStrokeRadius);
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, currStrokeRadius);
vertexData += 5;
fVertexCount += 5;
}
}
SkASSERT(pathStrokesIter == fPathStrokes.end());
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) { void GrTessellateStrokeOp::onExecute(GrOpFlushState* flushState, const SkRect& chainBounds) {
if (!fVertexBuffer) {
return;
}
GrPipeline::InitArgs initArgs; GrPipeline::InitArgs initArgs;
if (GrAAType::kNone != fAAType) { if (GrAAType::kNone != fAAType) {
initArgs.fInputFlags |= GrPipeline::InputFlags::kHWAntialias; initArgs.fInputFlags |= GrPipeline::InputFlags::kHWAntialias;
@ -345,9 +131,14 @@ void GrTessellateStrokeOp::onExecute(GrOpFlushState* flushState, const SkRect& c
GrTessellateStrokeShader strokeShader(fViewMatrix, fColor, fMiterLimitOrZero); GrTessellateStrokeShader strokeShader(fViewMatrix, fColor, fMiterLimitOrZero);
GrPathShader::ProgramInfo programInfo(flushState->writeView(), &pipeline, &strokeShader); GrPathShader::ProgramInfo programInfo(flushState->writeView(), &pipeline, &strokeShader);
flushState->bindPipelineAndScissorClip(programInfo, this->bounds() /*chainBounds??*/); SkASSERT(chainBounds == this->bounds());
flushState->bindPipelineAndScissorClip(programInfo, this->bounds());
flushState->bindTextures(strokeShader, nullptr, pipeline); flushState->bindTextures(strokeShader, nullptr, pipeline);
flushState->bindBuffers(nullptr, nullptr, std::move(fVertexBuffer)); for (const auto& chunk : fVertexChunks) {
flushState->draw(fVertexCount, fBaseVertex); if (chunk.fVertexBuffer) {
flushState->bindBuffers(nullptr, nullptr, std::move(chunk.fVertexBuffer));
flushState->draw(chunk.fVertexCount, chunk.fBaseVertex);
}
}
} }

View File

@ -11,6 +11,7 @@
#include "include/core/SkStrokeRec.h" #include "include/core/SkStrokeRec.h"
#include "src/gpu/GrSTArenaList.h" #include "src/gpu/GrSTArenaList.h"
#include "src/gpu/ops/GrDrawOp.h" #include "src/gpu/ops/GrDrawOp.h"
#include "src/gpu/tessellate/GrStrokeGeometry.h"
// Renders opaque, constant-color strokes by decomposing them into standalone tessellation patches. // 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 // Each patch is either a "cubic" (single stroked bezier curve with butt caps) or a "join". Requires
@ -45,8 +46,7 @@ private:
}; };
GrSTArenaList<PathStroke> fPathStrokes; GrSTArenaList<PathStroke> fPathStrokes;
int fNumVerbs; int fTotalCombinedVerbCnt;
int fNumPoints;
SkPMColor4f fColor; SkPMColor4f fColor;
const SkMatrix fViewMatrix = SkMatrix::I(); const SkMatrix fViewMatrix = SkMatrix::I();
@ -54,9 +54,8 @@ private:
float fMiterLimitOrZero = 0; // Zero if there is not a stroke with a miter join type. float fMiterLimitOrZero = 0; // Zero if there is not a stroke with a miter join type.
GrProcessorSet fProcessors; GrProcessorSet fProcessors;
sk_sp<const GrBuffer> fVertexBuffer; // S=1 because we will almost always fit everything into one single chunk.
int fVertexCount = 0; SkSTArray<1, GrStrokeGeometry::VertexChunk> fVertexChunks;
int fBaseVertex;
friend class GrOpMemoryPool; // For ctor. friend class GrOpMemoryPool; // For ctor.
}; };

View File

@ -38,9 +38,16 @@ class GrGLSLUniformHandler;
// tessellationPatchVertexCount of 5. // tessellationPatchVertexCount of 5.
class GrTessellateStrokeShader : public GrPathShader { class GrTessellateStrokeShader : public GrPathShader {
public: public:
constexpr static float kBevelJoinType = 1;
constexpr static float kMiterJoinType = 2;
constexpr static float kRoundJoinType = 3;
constexpr static float kInternalRoundJoinType = 4;
constexpr static int kNumVerticesPerPatch = 5;
GrTessellateStrokeShader(const SkMatrix& viewMatrix, SkPMColor4f color, float miterLimitOrZero) GrTessellateStrokeShader(const SkMatrix& viewMatrix, SkPMColor4f color, float miterLimitOrZero)
: GrPathShader(kTessellate_GrTessellateStrokeShader_ClassID, viewMatrix, : GrPathShader(kTessellate_GrTessellateStrokeShader_ClassID, viewMatrix,
GrPrimitiveType::kPatches, 5) GrPrimitiveType::kPatches, kNumVerticesPerPatch)
, fColor(color) , fColor(color)
, fMiterLimitOrZero(miterLimitOrZero) { , fMiterLimitOrZero(miterLimitOrZero) {
constexpr static Attribute kInputPointAttrib{"inputPoint", kFloat2_GrVertexAttribType, constexpr static Attribute kInputPointAttrib{"inputPoint", kFloat2_GrVertexAttribType,