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/ccpr/GrCCCoverageProcessor.h"
#include "src/gpu/ccpr/GrCCFillGeometry.h"
#include "src/gpu/ccpr/GrCCStroker.h"
#include "src/gpu/ccpr/GrGSCoverageProcessor.h"
#include "src/gpu/ccpr/GrVSCoverageProcessor.h"
#include "src/gpu/geometry/GrPathUtils.h"
@ -66,7 +65,7 @@ class CCPRGeometryView : public Sample {
void updateGpuData();
PrimitiveType fPrimitiveType = PrimitiveType::kTriangles;
PrimitiveType fPrimitiveType = PrimitiveType::kCubics;
SkCubicType fCubicType;
SkMatrix fCubicKLM;
@ -75,7 +74,9 @@ class CCPRGeometryView : public Sample {
float fConicWeight = .5;
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<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) {
canvas->clear(SK_ColorBLACK);
if (!fDoStroke) {
SkPaint outlinePaint;
outlinePaint.setColor(0x80ffffff);
outlinePaint.setStyle(SkPaint::kStroke_Style);
SkPaint outlinePaint;
outlinePaint.setColor(0xff808080);
outlinePaint.setStyle(SkPaint::kStroke_Style);
if (fDoStroke) {
outlinePaint.setStrokeWidth(fStrokeWidth);
} else {
outlinePaint.setStrokeWidth(0);
outlinePaint.setAntiAlias(true);
canvas->drawPath(fPath, outlinePaint);
}
outlinePaint.setStrokeJoin(fStrokeJoin);
outlinePaint.setStrokeCap(fStrokeCap);
outlinePaint.setAntiAlias(true);
canvas->drawPath(fPath, outlinePaint);
#if 0
SkPaint gridPaint;
@ -200,7 +205,18 @@ void CCPRGeometryView::onDrawContent(SkCanvas* canvas) {
#endif
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.
auto ctx = canvas->recordingContext();
SkASSERT(ctx);
@ -222,18 +238,6 @@ void CCPRGeometryView::onDrawContent(SkCanvas* canvas) {
paint.setPorterDuffXPFactory(SkBlendMode::kSrcOver);
rtc->drawRect(nullptr, std::move(paint), GrAA::kNo, SkMatrix::I(),
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 {
caption = "Use GPU backend to visualize geometry.";
}
@ -345,8 +349,8 @@ void CCPRGeometryView::updateGpuData() {
void CCPRGeometryView::DrawCoverageCountOp::onExecute(GrOpFlushState* state,
const SkRect& chainBounds) {
GrResourceProvider* rp = state->resourceProvider();
auto direct = state->gpu()->getContext();
#ifdef SK_GL
auto direct = state->gpu()->getContext();
GrGLGpu* glGpu = GrBackendApi::kOpenGL == direct->backend()
? static_cast<GrGLGpu*>(state->gpu())
: nullptr;
@ -370,52 +374,30 @@ void CCPRGeometryView::DrawCoverageCountOp::onExecute(GrOpFlushState* state,
GrOpsRenderPass* renderPass = state->opsRenderPass();
if (!fView->fDoStroke) {
for (int i = 0; i < proc->numSubpasses(); ++i) {
proc->reset(fView->fPrimitiveType, i, rp);
proc->bindPipeline(state, pipeline, this->bounds());
for (int i = 0; i < proc->numSubpasses(); ++i) {
proc->reset(fView->fPrimitiveType, i, rp);
proc->bindPipeline(state, pipeline, this->bounds());
if (PrimitiveType::kCubics == fView->fPrimitiveType ||
PrimitiveType::kConics == fView->fPrimitiveType) {
sk_sp<GrGpuBuffer> instBuff(rp->createBuffer(
fView->fQuadPointInstances.count() * sizeof(QuadPointInstance),
GrGpuBufferType::kVertex, kDynamic_GrAccessPattern,
fView->fQuadPointInstances.begin()));
if (!fView->fQuadPointInstances.empty() && instBuff) {
proc->bindBuffers(renderPass, std::move(instBuff));
proc->drawInstances(renderPass, fView->fQuadPointInstances.count(), 0);
}
} else {
sk_sp<GrGpuBuffer> instBuff(rp->createBuffer(
fView->fTriPointInstances.count() * sizeof(TriPointInstance),
GrGpuBufferType::kVertex, kDynamic_GrAccessPattern,
fView->fTriPointInstances.begin()));
if (!fView->fTriPointInstances.empty() && instBuff) {
proc->bindBuffers(renderPass, std::move(instBuff));
proc->drawInstances(renderPass, fView->fTriPointInstances.count(), 0);
}
if (PrimitiveType::kCubics == fView->fPrimitiveType ||
PrimitiveType::kConics == fView->fPrimitiveType) {
sk_sp<GrGpuBuffer> instBuff(rp->createBuffer(
fView->fQuadPointInstances.count() * sizeof(QuadPointInstance),
GrGpuBufferType::kVertex, kDynamic_GrAccessPattern,
fView->fQuadPointInstances.begin()));
if (!fView->fQuadPointInstances.empty() && instBuff) {
proc->bindBuffers(renderPass, std::move(instBuff));
proc->drawInstances(renderPass, fView->fQuadPointInstances.count(), 0);
}
} else {
sk_sp<GrGpuBuffer> instBuff(rp->createBuffer(
fView->fTriPointInstances.count() * sizeof(TriPointInstance),
GrGpuBufferType::kVertex, kDynamic_GrAccessPattern,
fView->fTriPointInstances.begin()));
if (!fView->fTriPointInstances.empty() && instBuff) {
proc->bindBuffers(renderPass, std::move(instBuff));
proc->drawInstances(renderPass, fView->fTriPointInstances.count(), 0);
}
}
} 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
@ -511,6 +493,17 @@ bool CCPRGeometryView::onChar(SkUnichar unichar) {
if (unichar == 'S') {
fDoStroke = !fDoStroke;
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;
}

View File

@ -11,22 +11,24 @@
#include "include/private/SkNx.h"
#include "src/core/SkGeometry.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
// converting it to flat line segments.
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) {
Sk2f nn = n*n;
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) {
float transpose[4];
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);
}
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) {
Sk2f 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];
}
static GrStrokeGeometry::Verb join_verb_from_join(SkPaint::Join join) {
using Verb = GrStrokeGeometry::Verb;
switch (join) {
case SkPaint::kBevel_Join:
return Verb::kBevelJoin;
case SkPaint::kMiter_Join:
return Verb::kMiterJoin;
case SkPaint::kRound_Join:
return Verb::kRoundJoin;
}
SK_ABORT("Invalid SkPaint::Join.");
void GrStrokeGeometry::allocVertexChunk(int minVertexAllocCount) {
VertexChunk* chunk = &fVertexChunkArray->push_back();
fCurrChunkVertexData = (SkPoint*)fTarget->makeVertexSpaceAtLeast(
sizeof(SkPoint), minVertexAllocCount, minVertexAllocCount, &chunk->fVertexBuffer,
&chunk->fBaseVertex, &fCurrChunkVertexCapacity);
fCurrChunkMinVertexAllocCount = minVertexAllocCount;
}
void GrStrokeGeometry::beginPath(const SkStrokeRec& stroke, float strokeDevWidth,
InstanceTallies* tallies) {
SkASSERT(!fInsideContour);
SkPoint* GrStrokeGeometry::reservePatch() {
constexpr static int kNumVerticesPerPatch = GrTessellateStrokeShader::kNumVerticesPerPatch;
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).
SkASSERT(strokeDevWidth > 0);
fCurrStrokeRadius = strokeDevWidth/2;
fCurrStrokeJoinVerb = join_verb_from_join(stroke.getJoin());
fCurrStrokeJoinType = join_type_from_join(stroke.getJoin());
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
// is equal to kMaxErrorFromLinearization.
float r = std::max(1 - kMaxErrorFromLinearization / fCurrStrokeRadius, 0.f);
fMaxCurvatureCosTheta = 2*r*r - 1;
fCurrContourFirstPtIdx = -1;
fCurrContourFirstNormalIdx = -1;
fVerbs.push_back(Verb::kBeginPath);
fHasPreviousSegment = false;
}
void GrStrokeGeometry::moveTo(SkPoint pt) {
SkASSERT(!fInsideContour);
fCurrContourFirstPtIdx = fPoints.count();
fCurrContourFirstNormalIdx = fNormals.count();
fPoints.push_back(pt);
SkDEBUGCODE(fInsideContour = true);
void GrStrokeGeometry::moveTo(const SkPoint& pt) {
fHasPreviousSegment = false;
fCurrContourStartPoint = pt;
}
void GrStrokeGeometry::lineTo(SkPoint pt) {
SkASSERT(fInsideContour);
this->lineTo(fCurrStrokeJoinVerb, pt);
void GrStrokeGeometry::lineTo(const SkPoint& p0, const SkPoint& p1) {
this->lineTo(fCurrStrokeJoinType, p0, p1);
}
void GrStrokeGeometry::lineTo(Verb leftJoinVerb, SkPoint pt) {
Sk2f tan = Sk2f::Load(&pt) - Sk2f::Load(&fPoints.back());
if ((tan == 0).allTrue()) {
void GrStrokeGeometry::lineTo(float leftJoinType, const SkPoint& pt0, const SkPoint& pt1) {
Sk2f p0 = Sk2f::Load(&pt0);
Sk2f p1 = Sk2f::Load(&pt1);
if ((p0 == p1).allTrue()) {
return;
}
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);
this->writeCubicSegment(leftJoinType, p0, lerp(p0, p1, 1/3.f), lerp(p0, p1, 2/3.f), p1, 1);
}
void GrStrokeGeometry::quadraticTo(const SkPoint P[3]) {
SkASSERT(fInsideContour);
this->quadraticTo(fCurrStrokeJoinVerb, P, SkFindQuadMaxCurvature(P));
this->quadraticTo(fCurrStrokeJoinType, P, SkFindQuadMaxCurvature(P));
}
// 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);
}
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 p1 = Sk2f::Load(P+1);
Sk2f p2 = Sk2f::Load(P+2);
@ -146,16 +255,13 @@ void GrStrokeGeometry::quadraticTo(Verb leftJoinVerb, const SkPoint P[3], float
// an issue.
if ((tan0.abs() < SK_ScalarNearlyZero).allTrue() || // p0 ~= p1
(tan1.abs() < SK_ScalarNearlyZero).allTrue()) { // p1 ~= p2
this->lineTo(leftJoinVerb, P[2]);
this->lineTo(leftJoinType, P[0], P[2]);
return;
}
SkPoint normals[2];
normalize2(tan0, tan1, normals);
// Decide how many flat line segments to chop the curve into.
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.
Sk2f A = p0 - p1*2 + p2;
@ -193,22 +299,22 @@ void GrStrokeGeometry::quadraticTo(Verb leftJoinVerb, const SkPoint P[3], float
if (leftT > 0) {
SkChopQuadAt(currQuadratic, ptsBuffer, leftT);
this->quadraticTo(leftJoinVerb, ptsBuffer, /*maxCurvatureT=*/1);
this->quadraticTo(leftJoinType, ptsBuffer, /*maxCurvatureT=*/1);
if (rightT < 1) {
rightT = (rightT - leftT) / (1 - leftT);
}
currQuadratic = ptsBuffer + 2;
} else {
this->rotateTo(leftJoinVerb, normals[0], currQuadratic[1]);
this->rotateTo(leftJoinType, currQuadratic[0], currQuadratic[1]);
}
if (rightT < 1) {
SkChopQuadAt(currQuadratic, ptsBuffer, rightT);
this->lineTo(Verb::kInternalRoundJoin, ptsBuffer[2]);
this->quadraticTo(Verb::kInternalRoundJoin, ptsBuffer + 2, /*maxCurvatureT=*/0);
this->lineTo(kInternalRoundJoinType, ptsBuffer[0], ptsBuffer[2]);
this->quadraticTo(kInternalRoundJoinType, ptsBuffer + 2, /*maxCurvatureT=*/0);
} else {
this->lineTo(Verb::kInternalRoundJoin, currQuadratic[2]);
this->rotateTo(Verb::kInternalRoundJoin, normals[1],
this->lineTo(kInternalRoundJoinType, currQuadratic[0], currQuadratic[2]);
this->rotateTo(kInternalRoundJoinType, currQuadratic[2],
currQuadratic[2]*2 - currQuadratic[1]);
}
return;
@ -216,24 +322,18 @@ void GrStrokeGeometry::quadraticTo(Verb leftJoinVerb, const SkPoint P[3], float
if (numSegments > fMaxTessellationSegments) {
SkPoint ptsBuffer[5];
SkChopQuadAt(P, ptsBuffer, 0.5f);
this->quadraticTo(leftJoinVerb, ptsBuffer, 0);
this->quadraticTo(Verb::kInternalRoundJoin, ptsBuffer + 3, 0);
this->quadraticTo(leftJoinType, ptsBuffer, 0);
this->quadraticTo(kInternalRoundJoinType, ptsBuffer + 3, 0);
return;
}
this->recordLeftJoinIfNotEmpty(leftJoinVerb, normals[0]);
fNormals.push_back_n(2, normals);
this->recordStroke(Verb::kQuadraticStroke, SkNextLog2(numSegments));
p1.store(&fPoints.push_back());
p2.store(&fPoints.push_back());
this->writeCubicSegment(leftJoinType, p0, lerp(p0, p1, 2/3.f), lerp(p1, p2, 1/3.f), p2);
}
void GrStrokeGeometry::cubicTo(const SkPoint P[4]) {
SkASSERT(fInsideContour);
float roots[3];
int numRoots = SkFindCubicMaxCurvature(P, roots);
this->cubicTo(fCurrStrokeJoinVerb, P,
this->cubicTo(fCurrStrokeJoinType, P,
numRoots > 0 ? roots[numRoots/2] : 0,
numRoots > 1 ? roots[0] : kLeftMaxCurvatureNone,
numRoots > 2 ? roots[2] : kRightMaxCurvatureNone);
@ -250,8 +350,8 @@ static inline float wangs_formula_cubic(const Sk2f& p0, const Sk2f& p1, const Sk
return SkScalarCeilToInt(f);
}
void GrStrokeGeometry::cubicTo(Verb leftJoinVerb, const SkPoint P[4], float maxCurvatureT,
float leftMaxCurvatureT, float rightMaxCurvatureT) {
void GrStrokeGeometry::cubicTo(float leftJoinType, const SkPoint P[4], float maxCurvatureT,
float leftMaxCurvatureT, float rightMaxCurvatureT) {
Sk2f p0 = Sk2f::Load(P);
Sk2f p1 = Sk2f::Load(P+1);
Sk2f p2 = Sk2f::Load(P+2);
@ -265,7 +365,7 @@ void GrStrokeGeometry::cubicTo(Verb leftJoinVerb, const SkPoint P[4], float maxC
p1 = p0;
tan0 = p2 - p0;
if ((tan0.abs() < SK_ScalarNearlyZero).allTrue()) { // p0 ~= p1 ~= p2
this->lineTo(leftJoinVerb, P[3]);
this->lineTo(leftJoinType, P[0], P[3]);
return;
}
}
@ -274,17 +374,14 @@ void GrStrokeGeometry::cubicTo(Verb leftJoinVerb, const SkPoint P[4], float maxC
tan1 = p3 - p1;
if ((tan1.abs() < SK_ScalarNearlyZero).allTrue() || // p1 ~= p2 ~= p3
(p0 == p1).allTrue()) { // p0 ~= p1 AND p2 ~= p3
this->lineTo(leftJoinVerb, P[3]);
this->lineTo(leftJoinType, P[0], P[3]);
return;
}
}
SkPoint normals[2];
normalize2(tan0, tan1, normals);
// Decide how many flat line segments to chop the curve into.
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
// 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) {
SkChopCubicAt(currCubic, ptsBuffer, leftT);
this->cubicTo(leftJoinVerb, ptsBuffer, /*maxCurvatureT=*/1,
this->cubicTo(leftJoinType, ptsBuffer, /*maxCurvatureT=*/1,
(kLeftMaxCurvatureNone != leftMaxCurvatureT)
? leftMaxCurvatureT/leftT : kLeftMaxCurvatureNone,
kRightMaxCurvatureNone);
@ -341,144 +438,73 @@ void GrStrokeGeometry::cubicTo(Verb leftJoinVerb, const SkPoint P[4], float maxC
currCubic = ptsBuffer + 3;
} else {
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) {
SkChopCubicAt(currCubic, ptsBuffer, rightT);
this->lineTo(Verb::kInternalRoundJoin, ptsBuffer[3]);
this->lineTo(kInternalRoundJoinType, ptsBuffer[0], ptsBuffer[3]);
currCubic = ptsBuffer + 3;
this->cubicTo(Verb::kInternalRoundJoin, currCubic, /*maxCurvatureT=*/0,
this->cubicTo(kInternalRoundJoinType, currCubic, /*maxCurvatureT=*/0,
kLeftMaxCurvatureNone, kRightMaxCurvatureNone);
} else {
this->lineTo(Verb::kInternalRoundJoin, currCubic[3]);
this->lineTo(kInternalRoundJoinType, currCubic[0], currCubic[3]);
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;
}
// Recurse and check the other two points of max curvature, if any.
if (kRightMaxCurvatureNone != rightMaxCurvatureT) {
this->cubicTo(leftJoinVerb, P, rightMaxCurvatureT, leftMaxCurvatureT,
this->cubicTo(leftJoinType, P, rightMaxCurvatureT, leftMaxCurvatureT,
kRightMaxCurvatureNone);
return;
}
if (kLeftMaxCurvatureNone != leftMaxCurvatureT) {
SkASSERT(kRightMaxCurvatureNone == rightMaxCurvatureT);
this->cubicTo(leftJoinVerb, P, leftMaxCurvatureT, kLeftMaxCurvatureNone,
this->cubicTo(leftJoinType, P, leftMaxCurvatureT, kLeftMaxCurvatureNone,
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,
this->cubicTo(leftJoinType, ptsBuffer, 0, kLeftMaxCurvatureNone, kRightMaxCurvatureNone);
this->cubicTo(kInternalRoundJoinType, ptsBuffer + 3, 0, kLeftMaxCurvatureNone,
kRightMaxCurvatureNone);
return;
}
this->recordLeftJoinIfNotEmpty(leftJoinVerb, normals[0]);
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());
this->writeCubicSegment(leftJoinType, p0, p1, p2, p3);
}
void GrStrokeGeometry::recordStroke(Verb verb, int numSegmentsLog2) {
SkASSERT(Verb::kLinearStroke != verb || 0 == numSegmentsLog2);
fVerbs.push_back(verb);
if (Verb::kLinearStroke != verb) {
fParams.push_back().fNumLinearSegmentsLog2 = numSegmentsLog2;
}
++fCurrStrokeTallies->fStrokes[numSegmentsLog2];
void GrStrokeGeometry::rotateTo(float leftJoinType, const SkPoint& anchorPoint,
const SkPoint& controlPoint) {
// Effectively rotate the current normal by drawing a zero length, 1-segment cubic.
// writeCubicSegment automatically adds the necessary join and the zero length cubic serves as
// a glue that guarantees a water tight rasterized edge between the new join and the segment
// that comes after the rotate.
SkPoint pts[4] = {anchorPoint, controlPoint, anchorPoint*2 - controlPoint, anchorPoint};
this->writeCubicSegment(leftJoinType, pts, 1);
}
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);
}
void GrStrokeGeometry::recordLeftJoinIfNotEmpty(Verb joinVerb, SkVector nextNormal) {
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) {
void GrStrokeGeometry::close() {
if (!fHasPreviousSegment) {
// Draw caps instead of closing if the subpath is zero length:
//
// "Any zero length subpath ... shall be stroked if the 'stroke-linecap' property has a
// value of round or square producing respectively a circle or a square."
//
// (https://www.w3.org/TR/SVG11/painting.html#StrokeProperties)
//
this->writeCaps();
return;
}
Verb capVerb;
if (SkPaint::kSquare_Cap == fCurrStrokeCapType) {
if (fCurrStrokeRadius * SK_ScalarRoot2Over2 < kMaxErrorFromLinearization) {
return;
}
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]);
// Draw a line back to the beginning. (This will be discarded if
// fCurrentPoint == fCurrContourStartPoint.)
this->lineTo(fCurrStrokeJoinType, fCurrentPoint, fCurrContourStartPoint);
this->writeJoin(fCurrStrokeJoinType, fCurrContourStartPoint, fLastControlPoint,
fCurrContourFirstControlPoint);
}

View File

@ -11,120 +11,108 @@
#include "include/core/SkPaint.h"
#include "include/core/SkPoint.h"
#include "include/private/SkTArray.h"
#include "src/gpu/ops/GrMeshDrawOp.h"
#include "src/gpu/tessellate/GrTessellateStrokeShader.h"
class SkStrokeRec;
/**
* This class converts post-transform stroked paths into a set of independent strokes, joins, and
* caps that map directly to GPU instances.
*/
// This is an RAII class that expands strokes into tessellation patches for consumption by
// GrTessellateStrokeShader. The provided GrMeshDrawOp::Target must not be used externally for the
// 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 {
public:
static constexpr int kMaxNumLinearSegmentsLog2 = 15;
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.
, 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.
// 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.
struct VertexChunk {
sk_sp<const GrBuffer> fVertexBuffer;
int fVertexCount = 0;
int fBaseVertex;
};
const SkTArray<Verb, true>& verbs() const { SkASSERT(!fInsideContour); return fVerbs; }
const SkTArray<Parameter, true>& params() const { SkASSERT(!fInsideContour); return fParams; }
const SkTArray<SkPoint, true>& points() const { SkASSERT(!fInsideContour); return fPoints; }
const SkTArray<SkVector, true>& normals() const { SkASSERT(!fInsideContour); return fNormals; }
// Stores raw pointers to the provided target and vertexChunkArray, which this class will use
// and push to as addPath is called. The caller is responsible to bind and draw each chunk that
// gets pushed to the array. (See GrTessellateStrokeShader.)
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.
struct InstanceTallies {
int fStrokes[kMaxNumLinearSegmentsLog2 + 1];
int fTriangles;
int fConics;
// "Releases" the target to be used externally again by putting back any unused pre-allocated
// vertices.
~GrStrokeGeometry() {
fTarget->putBackVertices(fCurrChunkVertexCapacity - fVertexChunkArray->back().fVertexCount,
sizeof(SkPoint));
}
InstanceTallies operator+(const InstanceTallies&) const;
};
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.
void addPath(const SkPath&, const SkStrokeRec&);
private:
void lineTo(Verb leftJoinVerb, SkPoint);
void quadraticTo(Verb leftJoinVerb, const SkPoint[3], float maxCurvatureT);
void allocVertexChunk(int minVertexAllocCount);
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 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);
// Pushes a new normal to fNormals and records a join, without changing the current position.
void rotateTo(Verb leftJoinVerb, SkVector normal, SkPoint controlPoint);
// TEMPORARY: Rotates the current control point without changing the current position.
// 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.
void recordStroke(Verb, int numSegmentsLog2);
const int fMaxTessellationSegments;
// Records a join in fElememts with the previous stroke, if the cuurent contour is not empty.
void recordLeftJoinIfNotEmpty(Verb joinType, SkVector nextNormal);
// These are raw pointers whose lifetimes are controlled outside this class.
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;
Verb fCurrStrokeJoinVerb;
float fCurrStrokeJoinType; // See GrTessellateStrokeShader for join type definitions .
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,
// 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
@ -134,47 +122,12 @@ private:
// section with strong curvature into lineTo's with round joins in between.)
float fMaxCurvatureCosTheta;
int fCurrContourFirstPtIdx;
int fCurrContourFirstNormalIdx;
SkDEBUGCODE(bool fInsideContour = false);
const int fMaxTessellationSegments;
SkSTArray<128, Verb, true> fVerbs;
SkSTArray<128, Parameter, true> fParams;
SkSTArray<128, SkPoint, true> fPoints;
SkSTArray<128, SkVector, true> fNormals;
// Variables related to the specific contour that we are currently iterating.
bool fHasPreviousSegment = false;
SkPoint fCurrContourStartPoint;
SkPoint fCurrContourFirstControlPoint;
SkPoint fLastControlPoint;
SkPoint fCurrentPoint;
};
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

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
* found in the LICENSE file.
@ -44,8 +44,7 @@ GrTessellateStrokeOp::GrTessellateStrokeOp(const SkMatrix& viewMatrix, const SkP
GrAAType aaType)
: GrDrawOp(ClassID())
, fPathStrokes(transform_path(viewMatrix, path), transform_stroke(viewMatrix, stroke))
, fNumVerbs(path.countVerbs())
, fNumPoints(path.countPoints())
, fTotalCombinedVerbCnt(path.countVerbs())
, fColor(get_paint_constant_blended_color(paint))
, fAAType(aaType)
, fProcessors(std::move(paint)) {
@ -100,8 +99,7 @@ GrOp::CombineResult GrTessellateStrokeOp::onCombineIfPossible(GrOp* grOp,
SkASSERT(fMiterLimitOrZero == 0 || fMiterLimitOrZero == op->fMiterLimitOrZero);
fMiterLimitOrZero = op->fMiterLimitOrZero;
}
fNumVerbs += op->fNumVerbs;
fNumPoints += op->fNumPoints;
fTotalCombinedVerbCnt += op->fTotalCombinedVerbCnt;
return CombineResult::kMerged;
}
@ -110,226 +108,14 @@ void GrTessellateStrokeOp::onPrePrepare(GrRecordingContext*, const GrSurfaceProx
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) {
// Rebuild the stroke using GrStrokeGeometry.
GrStrokeGeometry strokeGeometry(flushState->caps().shaderCaps()->maxTessellationSegments(),
fNumPoints, fNumVerbs);
GrStrokeGeometry strokeGeometry(flushState, &fVertexChunks, fTotalCombinedVerbCnt);
for (auto& [path, stroke] : fPathStrokes) {
float strokeRadius = stroke.getWidth() * .5f;
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();
}
strokeGeometry.addPath(path, stroke);
}
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) {
if (!fVertexBuffer) {
return;
}
GrPipeline::InitArgs initArgs;
if (GrAAType::kNone != fAAType) {
initArgs.fInputFlags |= GrPipeline::InputFlags::kHWAntialias;
@ -345,9 +131,14 @@ void GrTessellateStrokeOp::onExecute(GrOpFlushState* flushState, const SkRect& c
GrTessellateStrokeShader strokeShader(fViewMatrix, fColor, fMiterLimitOrZero);
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->bindBuffers(nullptr, nullptr, std::move(fVertexBuffer));
flushState->draw(fVertexCount, fBaseVertex);
for (const auto& chunk : fVertexChunks) {
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 "src/gpu/GrSTArenaList.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.
// 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;
int fNumVerbs;
int fNumPoints;
int fTotalCombinedVerbCnt;
SkPMColor4f fColor;
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.
GrProcessorSet fProcessors;
sk_sp<const GrBuffer> fVertexBuffer;
int fVertexCount = 0;
int fBaseVertex;
// S=1 because we will almost always fit everything into one single chunk.
SkSTArray<1, GrStrokeGeometry::VertexChunk> fVertexChunks;
friend class GrOpMemoryPool; // For ctor.
};

View File

@ -38,9 +38,16 @@ class GrGLSLUniformHandler;
// tessellationPatchVertexCount of 5.
class GrTessellateStrokeShader : public GrPathShader {
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)
: GrPathShader(kTessellate_GrTessellateStrokeShader_ClassID, viewMatrix,
GrPrimitiveType::kPatches, 5)
GrPrimitiveType::kPatches, kNumVerticesPerPatch)
, fColor(color)
, fMiterLimitOrZero(miterLimitOrZero) {
constexpr static Attribute kInputPointAttrib{"inputPoint", kFloat2_GrVertexAttribType,