Add conic fill support to the tessellator
This started out as a sandbox to experiment with a Wang's formula
analog for rational quadratics, but it quickly became apparent that
running Wang's formula on the down-projected points was an upper bound
on what the rational version would have been (for both w<1 and w>1).
This CL therefore adds conic support by upgrading the tessellation
shaders to use ratoinal cubics, converting every path verb to a
rational cubic, and then running Wang's formula on the down-projected
points. In the future we can always drop in a better formula if we
work one out.
Bug: skia:10419
Change-Id: I97021ea56afea54fdbe76745bacd3251e350fd97
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/337156
Reviewed-by: Tyler Denniston <tdenniston@google.com>
Reviewed-by: Brian Salomon <bsalomon@google.com>
Commit-Queue: Chris Dalton <csmartdalton@google.com>
diff --git a/samplecode/SampleTessellatedWedge.cpp b/samplecode/SampleTessellatedWedge.cpp
index 929489c..6465d75 100644
--- a/samplecode/SampleTessellatedWedge.cpp
+++ b/samplecode/SampleTessellatedWedge.cpp
@@ -20,6 +20,8 @@
#include "src/gpu/GrRenderTargetContextPriv.h"
#include "src/gpu/tessellate/GrPathTessellateOp.h"
+static float kConicWeight = .5;
+
// This sample enables wireframe and visualizes the triangulation generated by
// GrTessellateWedgeShader.
class TessellatedWedge : public Sample {
@@ -35,9 +37,9 @@
fPath.transform(SkMatrix::Scale(200, 200));
fPath.transform(SkMatrix::Translate(300, 300));
#else
- fPath.moveTo(100, 200);
- fPath.cubicTo(100, 100, 400, 100, 400, 200);
- fPath.lineTo(250, 500);
+ fPath.moveTo(100, 300);
+ fPath.conicTo(300, 100, 500, 300, kConicWeight);
+ fPath.cubicTo(433, 366, 366, 433, 300, 500);
#endif
}
@@ -107,6 +109,14 @@
}
fLastViewMatrix = canvas->getTotalMatrix();
+
+
+ SkString caption;
+ caption.printf("w=%f (=/- and +/_ to change)", kConicWeight);
+ SkFont font(nullptr, 20);
+ SkPaint captionPaint;
+ captionPaint.setColor(SK_ColorWHITE);
+ canvas->drawString(caption, 10, 30, font, captionPaint);
}
class TessellatedWedge::Click : public Sample::Click {
@@ -145,6 +155,32 @@
return true;
}
+static SkPath update_weight(const SkPath& path) {
+ SkPath path_;
+ for (auto [verb, pts, _] : SkPathPriv::Iterate(path)) {
+ switch (verb) {
+ case SkPathVerb::kMove:
+ path_.moveTo(pts[0]);
+ break;
+ case SkPathVerb::kLine:
+ path_.lineTo(pts[1]);
+ break;
+ case SkPathVerb::kQuad:
+ path_.quadTo(pts[1], pts[2]);
+ break;
+ case SkPathVerb::kCubic:
+ path_.cubicTo(pts[1], pts[2], pts[3]);
+ break;
+ case SkPathVerb::kConic:
+ path_.conicTo(pts[1], pts[2], (kConicWeight != 1) ? kConicWeight : .99f);
+ break;
+ default:
+ SkUNREACHABLE;
+ }
+ }
+ return path_;
+}
+
bool TessellatedWedge::onChar(SkUnichar unichar) {
switch (unichar) {
case 'w':
@@ -155,6 +191,22 @@
fPath.dump();
return true;
}
+ case '+':
+ kConicWeight *= 2;
+ fPath = update_weight(fPath);
+ return true;
+ case '=':
+ kConicWeight *= 5/4.f;
+ fPath = update_weight(fPath);
+ return true;
+ case '_':
+ kConicWeight *= .5f;
+ fPath = update_weight(fPath);
+ return true;
+ case '-':
+ kConicWeight *= 4/5.f;
+ fPath = update_weight(fPath);
+ return true;
}
return false;
}
diff --git a/src/gpu/tessellate/GrFillPathShader.cpp b/src/gpu/tessellate/GrFillPathShader.cpp
index 3d6005c..6ae6c98 100644
--- a/src/gpu/tessellate/GrFillPathShader.cpp
+++ b/src/gpu/tessellate/GrFillPathShader.cpp
@@ -63,49 +63,60 @@
void GrFillTriangleShader::emitVertexCode(Impl*, GrGLSLVertexBuilder* v, const char* viewMatrix,
GrGLSLUniformHandler* uniformHandler) const {
v->codeAppendf(R"(
- localcoord = input_point;
- vertexpos = (%s * float3(localcoord, 1)).xy;)", viewMatrix);
+ localcoord = input_point;
+ vertexpos = (%s * float3(localcoord, 1)).xy;)", viewMatrix);
}
void GrFillCubicHullShader::emitVertexCode(Impl*, GrGLSLVertexBuilder* v, const char* viewMatrix,
GrGLSLUniformHandler* uniformHandler) const {
v->codeAppend(R"(
- float4x2 P = float4x2(input_points_0_1, input_points_2_3);
+ float4x2 P = float4x2(input_points_0_1, input_points_2_3);
+ if (isnan(P[3].y)) {
+ // This curve is actually a conic. Convert the control points to a trapeziodal hull
+ // that circumcscribes the conic.
+ float w = P[3].x;
+ float2 p1w = P[1] * w;
+ float T = .51; // Bias outward a bit to ensure we cover the outermost samples.
+ float2 c1 = mix(P[0], p1w, T);
+ float2 c2 = mix(P[2], p1w, T);
+ float iw = 1 / mix(1, w, T);
+ P = float4x2(P[0], c1 * iw, c2 * iw, P[2]);
+ }
- // Translate the points to v0..3 where v0=0.
- float2 v1 = P[1] - P[0], v2 = P[2] - P[0], v3 = P[3] - P[0];
+ // Translate the points to v0..3 where v0=0.
+ float2 v1 = P[1] - P[0], v2 = P[2] - P[0], v3 = P[3] - P[0];
- // Reorder the points so v2 bisects v1 and v3.
- if (sign(determinant(float2x2(v2,v1))) == sign(determinant(float2x2(v2,v3)))) {
- float2 tmp = P[2];
- if (sign(determinant(float2x2(v1,v2))) != sign(determinant(float2x2(v1,v3)))) {
- P[2] = P[1]; // swap(P2, P1)
- P[1] = tmp;
- } else {
- P[2] = P[3]; // swap(P2, P3)
- P[3] = tmp;
- }
- }
+ // Reorder the points so v2 bisects v1 and v3.
+ if (sign(determinant(float2x2(v2,v1))) == sign(determinant(float2x2(v2,v3)))) {
+ float2 tmp = P[2];
+ if (sign(determinant(float2x2(v1,v2))) != sign(determinant(float2x2(v1,v3)))) {
+ P[2] = P[1]; // swap(P2, P1)
+ P[1] = tmp;
+ } else {
+ P[2] = P[3]; // swap(P2, P3)
+ P[3] = tmp;
+ }
+ }
- // Find the "turn direction" of each corner and net turn direction.
- float4 dir;
- float netdir = 0.0;
- for (int i = 0; i < 4; ++i) {
- float2 prev = P[i] - P[(i + 3) & 3], next = P[(i + 1) & 3] - P[i];
- dir[i] = sign(determinant(float2x2(prev, next)));
- netdir += dir[i];
- }
+ // Find the "turn direction" of each corner and net turn direction.
+ float4 dir;
+ float netdir = 0.0;
+ for (int i = 0; i < 4; ++i) {
+ float2 prev = P[i] - P[(i + 3) & 3], next = P[(i + 1) & 3] - P[i];
+ dir[i] = sign(determinant(float2x2(prev, next)));
+ netdir += dir[i];
+ }
- // sk_VertexID comes in fan order. Convert to strip order.
- int vertexidx = sk_VertexID;
- vertexidx ^= vertexidx >> 1;
+ // sk_VertexID comes in fan order. Convert to strip order.
+ int vertexidx = sk_VertexID;
+ vertexidx ^= vertexidx >> 1;
- // Remove the non-convex vertex, if any.
- if (dir[vertexidx] != sign(netdir)) {
- vertexidx = (vertexidx + 1) & 3;
- }
+ // Remove the non-convex vertex, if any.
+ if (dir[vertexidx] != sign(netdir)) {
+ vertexidx = (vertexidx + 1) & 3;
+ }
- localcoord = P[vertexidx];)");
+ localcoord = P[vertexidx];)");
v->codeAppendf("vertexpos = (%s * float3(localcoord, 1)).xy;", viewMatrix);
}
@@ -118,14 +129,14 @@
nullptr, kVertex_GrShaderFlag, kFloat4_GrSLType, "path_bounds", &pathBounds);
v->codeAppendf(R"(
- // Use sk_VertexID and uniforms (instead of vertex data) to find vertex positions.
- float2 T = float2(sk_VertexID & 1, sk_VertexID >> 1);
- localcoord = mix(%s.xy, %s.zw, T);
- vertexpos = (%s * float3(localcoord, 1)).xy;
+ // Use sk_VertexID and uniforms (instead of vertex data) to find vertex positions.
+ float2 T = float2(sk_VertexID & 1, sk_VertexID >> 1);
+ localcoord = mix(%s.xy, %s.zw, T);
+ vertexpos = (%s * float3(localcoord, 1)).xy;
- // Outset to avoid possible T-junctions with extreme edges of the path.
- float2x2 M2 = float2x2(%s);
- float2 devoutset = .25 * sign(M2 * (T - .5));
- localcoord += inverse(M2) * devoutset;
- vertexpos += devoutset;)", pathBounds, pathBounds, viewMatrix, viewMatrix);
+ // Outset to avoid possible T-junctions with extreme edges of the path.
+ float2x2 M2 = float2x2(%s);
+ float2 devoutset = .25 * sign(M2 * (T - .5));
+ localcoord += inverse(M2) * devoutset;
+ vertexpos += devoutset;)", pathBounds, pathBounds, viewMatrix, viewMatrix);
}
diff --git a/src/gpu/tessellate/GrMidpointContourParser.h b/src/gpu/tessellate/GrMidpointContourParser.h
index 1d5d7d9..2b8c87a 100644
--- a/src/gpu/tessellate/GrMidpointContourParser.h
+++ b/src/gpu/tessellate/GrMidpointContourParser.h
@@ -26,7 +26,8 @@
: fPath(path)
, fVerbs(SkPathPriv::VerbData(fPath))
, fNumRemainingVerbs(fPath.countVerbs())
- , fPoints(SkPathPriv::PointData(fPath)) {}
+ , fPoints(SkPathPriv::PointData(fPath))
+ , fWeights(SkPathPriv::ConicWeightData(fPath)) {}
// Advances the internal state to the next contour in the path. Returns false if there are no
// more contours.
bool parseNextContour() {
@@ -47,8 +48,10 @@
case SkPath::kLine_Verb:
++fPtsIdx;
break;
- case SkPath::kQuad_Verb:
case SkPath::kConic_Verb:
+ ++fWtsIdx;
+ [[fallthrough]];
+ case SkPath::kQuad_Verb:
fPtsIdx += 2;
break;
case SkPath::kCubic_Verb:
@@ -64,7 +67,7 @@
// Allows for iterating the current contour using a range-for loop.
SkPathPriv::Iterate currentContour() {
- return SkPathPriv::Iterate(fVerbs, fVerbs + fVerbsIdx, fPoints, nullptr);
+ return SkPathPriv::Iterate(fVerbs, fVerbs + fVerbsIdx, fPoints, fWeights);
}
SkPoint currentMidpoint() { return fMidpoint * (1.f / fMidpointWeight); }
@@ -76,6 +79,8 @@
fVerbsIdx = 0;
fPoints += fPtsIdx;
fPtsIdx = 0;
+ fWeights += fWtsIdx;
+ fWtsIdx = 0;
}
const SkPath& fPath;
@@ -87,6 +92,9 @@
const SkPoint* fPoints;
int fPtsIdx = 0;
+ const float* fWeights;
+ int fWtsIdx = 0;
+
SkPoint fMidpoint;
int fMidpointWeight;
};
diff --git a/src/gpu/tessellate/GrPathShader.h b/src/gpu/tessellate/GrPathShader.h
index c5e1536..c302684 100644
--- a/src/gpu/tessellate/GrPathShader.h
+++ b/src/gpu/tessellate/GrPathShader.h
@@ -14,6 +14,7 @@
#include "src/gpu/GrOpsRenderPass.h"
#include "src/gpu/GrProgramInfo.h"
#include "src/gpu/ops/GrSimpleMeshDrawOpHelper.h"
+#include <limits>
// This is a common base class for shaders in the GPU tessellator.
class GrPathShader : public GrGeometryProcessor {
@@ -66,6 +67,14 @@
renderPassXferBarriers, colorLoadOp);
}
+ // Fills in a 4-point patch in such a way that the shader will recognize it as a conic.
+ static void WriteConicPatch(const SkPoint pts[3], float w, SkPoint patch[4]) {
+ // Write out the 3 conic points to patch[0..2], the weight to patch[3].x, and then set
+ // patch[3].y as NaN to flag this patch as a conic.
+ memcpy(patch, pts, sizeof(SkPoint) * 3);
+ patch[3].set(w, std::numeric_limits<float>::quiet_NaN());
+ }
+
private:
const SkMatrix fViewMatrix;
const GrPrimitiveType fPrimitiveType;
diff --git a/src/gpu/tessellate/GrPathTessellateOp.cpp b/src/gpu/tessellate/GrPathTessellateOp.cpp
index 07dc51a..0e38c17 100644
--- a/src/gpu/tessellate/GrPathTessellateOp.cpp
+++ b/src/gpu/tessellate/GrPathTessellateOp.cpp
@@ -20,6 +20,7 @@
#include "src/gpu/tessellate/GrResolveLevelCounter.h"
#include "src/gpu/tessellate/GrStencilPathShader.h"
#include "src/gpu/tessellate/GrTessellationPathRenderer.h"
+#include "src/gpu/tessellate/GrWangsFormula.h"
constexpr static float kLinearizationIntolerance =
GrTessellationPathRenderer::kLinearizationIntolerance;
@@ -499,11 +500,14 @@
case SkPathVerb::kLine:
middleOut.pushVertex(pts[1]);
break;
+ case SkPathVerb::kConic:
+ // We use the same quadratic formula for conics, ignoring w. This appears to be an
+ // upper bound on what the actual number of subdivisions would have been.
+ [[fallthrough]];
case SkPathVerb::kQuad:
middleOut.pushVertex(pts[2]);
if (resolveLevelCounter) {
- // Quadratics get converted to cubics before rendering.
- resolveLevelCounter->countCubic(GrWangsFormula::quadratic_log2(
+ resolveLevelCounter->countInstance(GrWangsFormula::quadratic_log2(
kLinearizationIntolerance, pts, xform));
break;
}
@@ -512,7 +516,7 @@
case SkPathVerb::kCubic:
middleOut.pushVertex(pts[3]);
if (resolveLevelCounter) {
- resolveLevelCounter->countCubic(GrWangsFormula::cubic_log2(
+ resolveLevelCounter->countInstance(GrWangsFormula::cubic_log2(
kLinearizationIntolerance, pts, xform));
break;
}
@@ -521,8 +525,6 @@
case SkPathVerb::kClose:
middleOut.close();
break;
- case SkPathVerb::kConic:
- SkUNREACHABLE;
}
}
int triangleCount = middleOut.close();
@@ -530,7 +532,7 @@
if (!fStencilTrianglesProgram) {
SkASSERT(resolveLevelCounter);
- int totalInstanceCount = triangleCount + resolveLevelCounter->totalCubicInstanceCount();
+ int totalInstanceCount = triangleCount + resolveLevelCounter->totalInstanceCount();
SkASSERT(vertexAdvancePerTriangle == 4);
target->putBackVertices(maxInnerTriangles + maxCubics - totalInstanceCount,
sizeof(SkPoint) * 4);
@@ -552,15 +554,15 @@
void GrPathTessellateOp::prepareIndirectOuterCubics(
GrMeshDrawOp::Target* target, const GrResolveLevelCounter& resolveLevelCounter) {
- SkASSERT(resolveLevelCounter.totalCubicInstanceCount() >= 0);
- if (resolveLevelCounter.totalCubicInstanceCount() == 0) {
+ SkASSERT(resolveLevelCounter.totalInstanceCount() >= 0);
+ if (resolveLevelCounter.totalInstanceCount() == 0) {
return;
}
// Allocate a buffer to store the cubic data.
SkPoint* cubicData;
int baseInstance;
cubicData = static_cast<SkPoint*>(target->makeVertexSpace(
- sizeof(SkPoint) * 4, resolveLevelCounter.totalCubicInstanceCount(), &fCubicBuffer,
+ sizeof(SkPoint) * 4, resolveLevelCounter.totalInstanceCount(), &fCubicBuffer,
&baseInstance));
if (!cubicData) {
return;
@@ -574,7 +576,7 @@
GrMeshDrawOp::Target* target, const GrResolveLevelCounter& resolveLevelCounter,
SkPoint* cubicData, int numTrianglesAtBeginningOfData) {
SkASSERT(target->caps().drawInstancedSupport());
- SkASSERT(numTrianglesAtBeginningOfData + resolveLevelCounter.totalCubicInstanceCount() > 0);
+ SkASSERT(numTrianglesAtBeginningOfData + resolveLevelCounter.totalInstanceCount() > 0);
SkASSERT(fStencilCubicsProgram);
SkASSERT(cubicData);
SkASSERT(fCubicVertexCount == 0);
@@ -591,7 +593,7 @@
int baseInstance = fBaseCubicVertex >> 2;
// Start preparing the indirect draw buffer.
- fIndirectDrawCount = resolveLevelCounter.totalCubicIndirectDrawCount();
+ fIndirectDrawCount = resolveLevelCounter.totalIndirectDrawCount();
if (numTrianglesAtBeginningOfData) {
++fIndirectDrawCount; // Add an indirect draw for the triangles at the beginning.
}
@@ -633,7 +635,7 @@
#ifdef SK_DEBUG
SkASSERT(indirectIdx == fIndirectDrawCount);
SkASSERT(runningInstanceCount == numTrianglesAtBeginningOfData +
- resolveLevelCounter.totalCubicInstanceCount());
+ resolveLevelCounter.totalInstanceCount());
SkASSERT(fIndirectDrawCount > 0);
SkPoint* endLocations[kMaxResolveLevel + 1];
@@ -647,35 +649,46 @@
lastResolveLevel = resolveLevel;
}
int totalInstanceCount = numTrianglesAtBeginningOfData +
- resolveLevelCounter.totalCubicInstanceCount();
+ resolveLevelCounter.totalInstanceCount();
endLocations[lastResolveLevel] = cubicData + totalInstanceCount * 4;
#endif
fCubicVertexCount = numTrianglesAtBeginningOfData * 4;
- if (resolveLevelCounter.totalCubicInstanceCount()) {
+ if (resolveLevelCounter.totalInstanceCount()) {
GrVectorXform xform(fViewMatrix);
for (auto [verb, pts, w] : SkPathPriv::Iterate(fPath)) {
int level;
switch (verb) {
default:
continue;
+ case SkPathVerb::kConic:
+ // We use the same quadratic formula for conics, ignoring w. This appears to be
+ // an upper bound on what the actual number of subdivisions would have been.
+ [[fallthrough]];
case SkPathVerb::kQuad:
level = GrWangsFormula::quadratic_log2(kLinearizationIntolerance, pts, xform);
- if (level == 0) {
- continue;
- }
- level = std::min(level, kMaxResolveLevel);
- GrPathUtils::convertQuadToCubic(pts, instanceLocations[level]);
break;
case SkPathVerb::kCubic:
level = GrWangsFormula::cubic_log2(kLinearizationIntolerance, pts, xform);
- if (level == 0) {
- continue;
- }
- level = std::min(level, kMaxResolveLevel);
+ break;
+ }
+ if (level == 0) {
+ continue;
+ }
+ level = std::min(level, kMaxResolveLevel);
+ switch (verb) {
+ case SkPathVerb::kQuad:
+ GrPathUtils::convertQuadToCubic(pts, instanceLocations[level]);
+ break;
+ case SkPathVerb::kCubic:
memcpy(instanceLocations[level], pts, sizeof(SkPoint) * 4);
break;
+ case SkPathVerb::kConic:
+ GrPathShader::WriteConicPatch(pts, *w, instanceLocations[level]);
+ break;
+ default:
+ SkUNREACHABLE;
}
instanceLocations[level] += 4;
fCubicVertexCount += 4;
@@ -687,7 +700,7 @@
SkASSERT(instanceLocations[i] == endLocations[i]);
}
SkASSERT(fCubicVertexCount == (numTrianglesAtBeginningOfData +
- resolveLevelCounter.totalCubicInstanceCount()) * 4);
+ resolveLevelCounter.totalInstanceCount()) * 4);
#endif
}
@@ -721,6 +734,10 @@
SkASSERT(fCubicVertexCount < numCountedCurves * 4);
memcpy(vertexData + fCubicVertexCount, pts, sizeof(SkPoint) * 4);
break;
+ case SkPathVerb::kConic:
+ SkASSERT(fCubicVertexCount < numCountedCurves * 4);
+ GrPathShader::WriteConicPatch(pts, *w, vertexData + fCubicVertexCount);
+ break;
}
fCubicVertexCount += 4;
}
@@ -768,7 +785,9 @@
lastPoint = pts[3];
break;
case SkPathVerb::kConic:
- SkUNREACHABLE;
+ GrPathShader::WriteConicPatch(pts, *w, vertexData + fCubicVertexCount);
+ lastPoint = pts[2];
+ break;
}
vertexData[fCubicVertexCount + 4] = midpoint;
fCubicVertexCount += 5;
diff --git a/src/gpu/tessellate/GrResolveLevelCounter.h b/src/gpu/tessellate/GrResolveLevelCounter.h
index d37ee7a..ebce30c 100644
--- a/src/gpu/tessellate/GrResolveLevelCounter.h
+++ b/src/gpu/tessellate/GrResolveLevelCounter.h
@@ -12,9 +12,9 @@
#include "src/gpu/tessellate/GrTessellationPathRenderer.h"
#include "src/gpu/tessellate/GrWangsFormula.h"
-// This class helps bin cubics by log2 "resolveLevel" when we don't use hardware tessellation. It is
-// composed of simple counters that track how many cubics we intend to draw at each resolveLevel,
-// and how many resolveLevels there are that have at least one cubic.
+// This class helps bin instances by log2 "resolveLevel" when we don't use hardware tessellation. It
+// is composed of simple counters that track how many instances we intend to draw at each
+// resolveLevel, and how many resolveLevels there are that have at least one instances.
class GrResolveLevelCounter {
public:
void reset() {
@@ -27,12 +27,15 @@
GrVectorXform xform(viewMatrix);
for (auto [verb, pts, w] : SkPathPriv::Iterate(path)) {
switch (verb) {
+ case SkPathVerb::kConic:
+ // We use the same quadratic formula for conics, ignoring w. This appears to be
+ // an upper bound on what the actual number of subdivisions would have been.
+ [[fallthrough]];
case SkPathVerb::kQuad:
- // Quadratics get converted to cubics before rendering.
- this->countCubic(GrWangsFormula::quadratic_log2(intolerance, pts, xform));
+ this->countInstance(GrWangsFormula::quadratic_log2(intolerance, pts, xform));
break;
case SkPathVerb::kCubic:
- this->countCubic(GrWangsFormula::cubic_log2(intolerance, pts, xform));
+ this->countInstance(GrWangsFormula::cubic_log2(intolerance, pts, xform));
break;
default:
break;
@@ -40,34 +43,34 @@
}
}
- void countCubic(int resolveLevel) {
+ void countInstance(int resolveLevel) {
SkASSERT(fHasCalledReset);
SkASSERT(resolveLevel >= 0);
if (resolveLevel == 0) {
- // Cubics with 2^0=1 segments are empty (zero area). We ignore them completely.
+ // Instances with 2^0=1 segments are empty (zero area). We ignore them completely.
return;
}
resolveLevel = std::min(resolveLevel, GrTessellationPathRenderer::kMaxResolveLevel);
if (!fInstanceCounts[resolveLevel]++) {
- ++fTotalCubicIndirectDrawCount;
+ ++fTotalIndirectDrawCount;
}
- ++fTotalCubicInstanceCount;
+ ++fTotalInstanceCount;
}
int operator[](int resolveLevel) const {
SkASSERT(fHasCalledReset);
- SkASSERT(resolveLevel > 0); // Empty cubics with 2^0=1 segments do not need to be drawn.
+ SkASSERT(resolveLevel > 0); // Empty instances with 2^0=1 segments do not need to be drawn.
SkASSERT(resolveLevel <= GrTessellationPathRenderer::kMaxResolveLevel);
return fInstanceCounts[resolveLevel];
}
- int totalCubicInstanceCount() const { return fTotalCubicInstanceCount; }
- int totalCubicIndirectDrawCount() const { return fTotalCubicIndirectDrawCount; }
+ int totalInstanceCount() const { return fTotalInstanceCount; }
+ int totalIndirectDrawCount() const { return fTotalIndirectDrawCount; }
private:
SkDEBUGCODE(bool fHasCalledReset = false;)
int fInstanceCounts[GrTessellationPathRenderer::kMaxResolveLevel + 1];
- int fTotalCubicInstanceCount = 0;
- int fTotalCubicIndirectDrawCount = 0;
+ int fTotalInstanceCount = 0;
+ int fTotalIndirectDrawCount = 0;
};
#endif
diff --git a/src/gpu/tessellate/GrStencilPathShader.cpp b/src/gpu/tessellate/GrStencilPathShader.cpp
index f204488..7dc6916 100644
--- a/src/gpu/tessellate/GrStencilPathShader.cpp
+++ b/src/gpu/tessellate/GrStencilPathShader.cpp
@@ -15,50 +15,79 @@
// parametric sense) line segments that are guaranteed to be within a distance of
// "MAX_LINEARIZATION_ERROR" from the actual curve.
constexpr static char kWangsFormulaCubicFn[] = R"(
- #define MAX_LINEARIZATION_ERROR 0.25 // 1/4 pixel
- float length_pow2(vec2 v) {
- return dot(v, v);
- }
- float wangs_formula_cubic(vec2 p0, vec2 p1, vec2 p2, vec2 p3) {
- float k = (3.0 * 2.0) / (8.0 * MAX_LINEARIZATION_ERROR);
- float m = max(length_pow2(-2.0*p1 + p2 + p0),
- length_pow2(-2.0*p2 + p3 + p1));
- return max(1.0, ceil(sqrt(k * sqrt(m))));
- })";
+#define MAX_LINEARIZATION_ERROR 0.25 // 1/4 pixel
+float length_pow2(vec2 v) {
+ return dot(v, v);
+}
+float wangs_formula_cubic(vec2 p0, vec2 p1, vec2 p2, vec2 p3) {
+ float k = (3.0 * 2.0) / (8.0 * MAX_LINEARIZATION_ERROR);
+ float m = max(length_pow2(-2.0*p1 + p2 + p0),
+ length_pow2(-2.0*p2 + p3 + p1));
+ return max(1.0, ceil(sqrt(k * sqrt(m))));
+})";
-// Evaluate our point of interest using numerically stable mix() operations.
-constexpr static char kEvalCubicFn[] = R"(
- vec2 eval_cubic(mat4x2 P, float T) {
- vec2 ab = mix(P[0], P[1], T);
- vec2 bc = mix(P[1], P[2], T);
- vec2 cd = mix(P[2], P[3], T);
- vec2 abc = mix(ab, bc, T);
- vec2 bcd = mix(bc, cd, T);
- return mix(abc, bcd, T);
- })";
+constexpr static char kSkSLTypeDefs[] = R"(
+#define float4x3 mat4x3
+#define float3 vec3
+#define float2 vec2
+)";
+
+// Converts a 4-point input patch into the rational cubic it intended to represent.
+constexpr static char kUnpackRationalCubicFn[] = R"(
+float4x3 unpack_rational_cubic(float2 p0, float2 p1, float2 p2, float2 p3) {
+ float4x3 P = float4x3(p0,1, p1,1, p2,1, p3,1);
+ if (isnan(P[3].y)) {
+ // This patch is actually a conic. Convert to a rational cubic.
+ float w = P[3].x;
+ float3 c = P[1] * (2/3.0 * w);
+ P = float4x3(P[0], fma(P[0], float3(1/3.0), c), fma(P[2], float3(1/3.0), c), P[2]);
+ }
+ return P;
+})";
+
+// Evaluate our point of interest using numerically stable linear interpolations. We add our own
+// "safe_mix" method to guarantee we get exactly "b" when T=1. The builtin mix() function seems
+// spec'd to behave this way, but empirical results results have shown it does not always.
+constexpr static char kEvalRationalCubicFn[] = R"(
+float3 safe_mix(float3 a, float3 b, float T, float one_minus_T) {
+ return a*one_minus_T + b*T;
+}
+float2 eval_rational_cubic(float4x3 P, float T) {
+ float one_minus_T = 1 - T;
+ float3 ab = safe_mix(P[0], P[1], T, one_minus_T);
+ float3 bc = safe_mix(P[1], P[2], T, one_minus_T);
+ float3 cd = safe_mix(P[2], P[3], T, one_minus_T);
+ float3 abc = safe_mix(ab, bc, T, one_minus_T);
+ float3 bcd = safe_mix(bc, cd, T, one_minus_T);
+ float3 abcd = safe_mix(abc, bcd, T, one_minus_T);
+ return abcd.xy / abcd.z;
+})";
class GrStencilPathShader::Impl : public GrGLSLGeometryProcessor {
protected:
void onEmitCode(EmitArgs& args, GrGPArgs* gpArgs) override {
const auto& shader = args.fGP.cast<GrStencilPathShader>();
args.fVaryingHandler->emitAttributes(shader);
+ auto v = args.fVertBuilder;
GrShaderVar vertexPos = (*shader.vertexAttributes().begin()).asShaderVar();
if (!shader.viewMatrix().isIdentity()) {
const char* viewMatrix;
fViewMatrixUniform = args.fUniformHandler->addUniform(
nullptr, kVertex_GrShaderFlag, kFloat3x3_GrSLType, "view_matrix", &viewMatrix);
- args.fVertBuilder->codeAppendf(
- "float2 vertexpos = (%s * float3(inputPoint, 1)).xy;", viewMatrix);
+ v->codeAppendf("float2 vertexpos = (%s * float3(inputPoint, 1)).xy;", viewMatrix);
+ if (shader.willUseTessellationShaders()) {
+ // If y is NaN then x is a conic weight. Don't transform.
+ v->codeAppendf("vertexpos = (isnan(vertexpos.y)) ? inputPoint : vertexpos;");
+ }
vertexPos.set(kFloat2_GrSLType, "vertexpos");
}
- if (!shader.willUseTessellationShaders()) {
+ if (!shader.willUseTessellationShaders()) { // This is the case for the triangle shader.
gpArgs->fPositionVar = vertexPos;
} else {
- args.fVertBuilder->declareGlobal(GrShaderVar(
- "P", kFloat2_GrSLType, GrShaderVar::TypeModifier::Out));
- args.fVertBuilder->codeAppendf("P = %s;", vertexPos.c_str());
+ v->declareGlobal(GrShaderVar("vsPt", kFloat2_GrSLType, GrShaderVar::TypeModifier::Out));
+ v->codeAppendf("vsPt = %s;", vertexPos.c_str());
}
// No fragment shader.
@@ -85,38 +114,52 @@
const GrShaderCaps&) const {
SkString code(versionAndExtensionDecls);
code.append(kWangsFormulaCubicFn);
+ code.append(kSkSLTypeDefs);
+ code.append(kUnpackRationalCubicFn);
code.append(R"(
- layout(vertices = 1) out;
+ layout(vertices = 1) out;
- in vec2 P[];
- out vec4 X[];
- out vec4 Y[];
+ in vec2 vsPt[];
+ out vec4 X[];
+ out vec4 Y[];
+ out float w[];
- void main() {
- // Chop the curve at T=1/2.
- vec2 ab = mix(P[0], P[1], .5);
- vec2 bc = mix(P[1], P[2], .5);
- vec2 cd = mix(P[2], P[3], .5);
- vec2 abc = mix(ab, bc, .5);
- vec2 bcd = mix(bc, cd, .5);
- vec2 abcd = mix(abc, bcd, .5);
+ void main() {
+ mat4x3 P = unpack_rational_cubic(vsPt[0], vsPt[1], vsPt[2], vsPt[3]);
- // Calculate how many triangles we need to linearize each half of the curve.
- float l0 = wangs_formula_cubic(P[0], ab, abc, abcd);
- float l1 = wangs_formula_cubic(abcd, bcd, cd, P[3]);
+ // Chop the curve at T=1/2. Here we take advantage of the fact that a uniform scalar has no
+ // effect on homogeneous coordinates in order to eval quickly at .5:
+ //
+ // mix(p0, p1, .5) / mix(w0, w1, .5)
+ // == ((p0 + p1) * .5) / ((w0 + w1) * .5)
+ // == (p0 + p1) / (w0 + w1)
+ //
+ vec3 ab = P[0] + P[1];
+ vec3 bc = P[1] + P[2];
+ vec3 cd = P[2] + P[3];
+ vec3 abc = ab + bc;
+ vec3 bcd = bc + cd;
+ vec3 abcd = abc + bcd;
- gl_TessLevelOuter[0] = l1;
- gl_TessLevelOuter[1] = 1.0;
- gl_TessLevelOuter[2] = l0;
+ // Calculate how many triangles we need to linearize each half of the curve. We simply call
+ // Wang's formula for integral cubics with the down-projected points. This appears to be an
+ // upper bound on what the actual number of subdivisions would have been.
+ float w0 = wangs_formula_cubic(P[0].xy, ab.xy/ab.z, abc.xy/abc.z, abcd.xy/abcd.z);
+ float w1 = wangs_formula_cubic(abcd.xy/abcd.z, bcd.xy/bcd.z, cd.xy/cd.z, P[3].xy);
- // Changing the inner level to 1 when l0 == l1 == 1 collapses the entire patch to a
- // single triangle. Otherwise, we need an inner level of 2 so our curve triangles
- // have an interior point to originate from.
- gl_TessLevelInner[0] = min(max(l0, l1), 2.0);
+ gl_TessLevelOuter[0] = w1;
+ gl_TessLevelOuter[1] = 1.0;
+ gl_TessLevelOuter[2] = w0;
- X[gl_InvocationID /*== 0*/] = vec4(P[0].x, P[1].x, P[2].x, P[3].x);
- Y[gl_InvocationID /*== 0*/] = vec4(P[0].y, P[1].y, P[2].y, P[3].y);
- })");
+ // Changing the inner level to 1 when w0 == w1 == 1 collapses the entire patch to a single
+ // triangle. Otherwise, we need an inner level of 2 so our curve triangles have an interior
+ // point to originate from.
+ gl_TessLevelInner[0] = min(max(w0, w1), 2.0);
+
+ X[gl_InvocationID /*== 0*/] = vec4(P[0].x, P[1].x, P[2].x, P[3].x);
+ Y[gl_InvocationID /*== 0*/] = vec4(P[0].y, P[1].y, P[2].y, P[3].y);
+ w[gl_InvocationID /*== 0*/] = P[1].z;
+ })");
return code;
}
@@ -125,31 +168,33 @@
const GrGLSLPrimitiveProcessor*, const char* versionAndExtensionDecls,
const GrGLSLUniformHandler&, const GrShaderCaps&) const {
SkString code(versionAndExtensionDecls);
- code.append(kEvalCubicFn);
+ code.append(kSkSLTypeDefs);
+ code.append(kEvalRationalCubicFn);
code.append(R"(
- layout(triangles, equal_spacing, ccw) in;
+ layout(triangles, equal_spacing, ccw) in;
- uniform vec4 sk_RTAdjust;
+ uniform vec4 sk_RTAdjust;
- in vec4 X[];
- in vec4 Y[];
+ in vec4 X[];
+ in vec4 Y[];
+ in float w[];
- void main() {
- // Locate our parametric point of interest. T ramps from [0..1/2] on the left edge
- // of the triangle, and [1/2..1] on the right. If we are the patch's interior
- // vertex, then we want T=1/2. Since the barycentric coords are (1/3, 1/3, 1/3) at
- // the interior vertex, the below fma() works in all 3 scenarios.
- float T = fma(.5, gl_TessCoord.y, gl_TessCoord.z);
+ void main() {
+ // Locate our parametric point of interest. T ramps from [0..1/2] on the left edge of the
+ // triangle, and [1/2..1] on the right. If we are the patch's interior vertex, then we want
+ // T=1/2. Since the barycentric coords are (1/3, 1/3, 1/3) at the interior vertex, the below
+ // fma() works in all 3 scenarios.
+ float T = fma(.5, gl_TessCoord.y, gl_TessCoord.z);
- mat4x2 P = transpose(mat2x4(X[0], Y[0]));
- vec2 vertexpos = eval_cubic(P, T);
- if (all(notEqual(gl_TessCoord.xz, vec2(0)))) {
- // We are the interior point of the patch; center it inside [C(0), C(.5), C(1)].
- vertexpos = (P[0] + vertexpos + P[3]) / 3.0;
- }
+ mat4x3 P = transpose(mat3x4(X[0], Y[0], 1,w[0],w[0],1));
+ vec2 vertexpos = eval_rational_cubic(P, T);
+ if (all(notEqual(gl_TessCoord.xz, vec2(0)))) {
+ // We are the interior point of the patch; center it inside [C(0), C(.5), C(1)].
+ vertexpos = (P[0].xy + vertexpos + P[3].xy) / 3.0;
+ }
- gl_Position = vec4(vertexpos * sk_RTAdjust.xz + sk_RTAdjust.yw, 0.0, 1.0);
- })");
+ gl_Position = vec4(vertexpos * sk_RTAdjust.xz + sk_RTAdjust.yw, 0.0, 1.0);
+ })");
return code;
}
@@ -160,34 +205,42 @@
const GrShaderCaps&) const {
SkString code(versionAndExtensionDecls);
code.append(kWangsFormulaCubicFn);
+ code.append(kSkSLTypeDefs);
+ code.append(kUnpackRationalCubicFn);
code.append(R"(
- layout(vertices = 1) out;
+ layout(vertices = 1) out;
- in vec2 P[];
- out vec4 X[];
- out vec4 Y[];
- out vec2 fanpoint[];
+ in vec2 vsPt[];
+ out vec4 X[];
+ out vec4 Y[];
+ out float w[];
+ out vec2 fanpoint[];
- void main() {
- // Calculate how many triangles we need to linearize the curve.
- float num_segments = wangs_formula_cubic(P[0], P[1], P[2], P[3]);
+ void main() {
+ mat4x3 P = unpack_rational_cubic(vsPt[0], vsPt[1], vsPt[2], vsPt[3]);
- // Tessellate the first side of the patch into num_segments triangles.
- gl_TessLevelOuter[0] = num_segments;
+ // Figure out how many segments to divide the curve into. To do this we simply call Wang's
+ // formula for integral cubics with the down-projected points. This appears to be an upper
+ // bound on what the actual number of subdivisions would have been.
+ float num_segments = wangs_formula_cubic(P[0].xy, P[1].xy/P[1].z, P[2].xy/P[2].z, P[3].xy);
- // Leave the other two sides of the patch as single segments.
- gl_TessLevelOuter[1] = 1.0;
- gl_TessLevelOuter[2] = 1.0;
+ // Tessellate the first side of the patch into num_segments triangles.
+ gl_TessLevelOuter[0] = num_segments;
- // Changing the inner level to 1 when num_segments == 1 collapses the entire
- // patch to a single triangle. Otherwise, we need an inner level of 2 so our curve
- // triangles have an interior point to originate from.
- gl_TessLevelInner[0] = min(num_segments, 2.0);
+ // Leave the other two sides of the patch as single segments.
+ gl_TessLevelOuter[1] = 1.0;
+ gl_TessLevelOuter[2] = 1.0;
- X[gl_InvocationID /*== 0*/] = vec4(P[0].x, P[1].x, P[2].x, P[3].x);
- Y[gl_InvocationID /*== 0*/] = vec4(P[0].y, P[1].y, P[2].y, P[3].y);
- fanpoint[gl_InvocationID /*== 0*/] = P[4];
- })");
+ // Changing the inner level to 1 when num_segments == 1 collapses the entire
+ // patch to a single triangle. Otherwise, we need an inner level of 2 so our curve
+ // triangles have an interior point to originate from.
+ gl_TessLevelInner[0] = min(num_segments, 2.0);
+
+ X[gl_InvocationID /*== 0*/] = vec4(P[0].x, P[1].x, P[2].x, P[3].x);
+ Y[gl_InvocationID /*== 0*/] = vec4(P[0].y, P[1].y, P[2].y, P[3].y);
+ w[gl_InvocationID /*== 0*/] = P[1].z;
+ fanpoint[gl_InvocationID /*== 0*/] = vsPt[4];
+ })");
return code;
}
@@ -196,35 +249,38 @@
const GrGLSLPrimitiveProcessor*, const char* versionAndExtensionDecls,
const GrGLSLUniformHandler&, const GrShaderCaps&) const {
SkString code(versionAndExtensionDecls);
- code.append(kEvalCubicFn);
+ code.append(kSkSLTypeDefs);
+ code.append(kEvalRationalCubicFn);
code.append(R"(
- layout(triangles, equal_spacing, ccw) in;
+ layout(triangles, equal_spacing, ccw) in;
- uniform vec4 sk_RTAdjust;
+ uniform vec4 sk_RTAdjust;
- in vec4 X[];
- in vec4 Y[];
- in vec2 fanpoint[];
+ in vec4 X[];
+ in vec4 Y[];
+ in float w[];
+ in vec2 fanpoint[];
- void main() {
- // Locate our parametric point of interest. It is equal to the barycentric
- // y-coordinate if we are a vertex on the tessellated edge of the triangle patch,
- // 0.5 if we are the patch's interior vertex, or N/A if we are the fan point.
- // NOTE: We are on the tessellated edge when the barycentric x-coordinate == 0.
- float T = (gl_TessCoord.x == 0.0) ? gl_TessCoord.y : 0.5;
+ void main() {
+ // Locate our parametric point of interest. It is equal to the barycentric y-coordinate if
+ // we are a vertex on the tessellated edge of the triangle patch, 0.5 if we are the patch's
+ // interior vertex, or N/A if we are the fan point.
+ // NOTE: We are on the tessellated edge when the barycentric x-coordinate == 0.
+ float T = (gl_TessCoord.x == 0.0) ? gl_TessCoord.y : 0.5;
- mat4x2 P = transpose(mat2x4(X[0], Y[0]));
- vec2 vertexpos = eval_cubic(P, T);
- if (gl_TessCoord.x == 1.0) {
- // We are the anchor point that fans from the center of the curve's contour.
- vertexpos = fanpoint[0];
- } else if (gl_TessCoord.x != 0.0) {
- // We are the interior point of the patch; center it inside [C(0), C(.5), C(1)].
- vertexpos = (P[0] + vertexpos + P[3]) / 3.0;
- }
+ mat4x3 P = transpose(mat3x4(X[0], Y[0], 1,w[0],w[0],1));
+ vec2 vertexpos = eval_rational_cubic(P, T);
- gl_Position = vec4(vertexpos * sk_RTAdjust.xz + sk_RTAdjust.yw, 0.0, 1.0);
- })");
+ if (gl_TessCoord.x == 1.0) {
+ // We are the anchor point that fans from the center of the curve's contour.
+ vertexpos = fanpoint[0];
+ } else if (gl_TessCoord.x != 0.0) {
+ // We are the interior point of the patch; center it inside [C(0), C(.5), C(1)].
+ vertexpos = (P[0].xy + vertexpos + P[3].xy) / 3.0;
+ }
+
+ gl_Position = vec4(vertexpos * sk_RTAdjust.xz + sk_RTAdjust.yw, 0.0, 1.0);
+ })");
return code;
}
@@ -297,30 +353,29 @@
args.fVertBuilder->defineConstantf("int", "kMaxVertexID", "%i", 1 << kMaxResolveLevel);
args.fVertBuilder->defineConstantf("float", "kInverseMaxVertexID", "exp2(-%i.0)",
kMaxResolveLevel);
+ args.fVertBuilder->insertFunction(kUnpackRationalCubicFn);
+ args.fVertBuilder->insertFunction(kEvalRationalCubicFn);
args.fVertBuilder->codeAppend(R"(
- float4x2 P = float4x2(inputPoints_0_1, inputPoints_2_3);
- float2 point;
- if (sk_VertexID > kMaxVertexID) {
- // This is a special index value that wants us to emit a specific point.
- point = P[sk_VertexID & 3];
- } else {
- // Evaluate the cubic at T = (sk_VertexID / 2^kMaxResolveLevel).
- float T = sk_VertexID * kInverseMaxVertexID;
- float2 ab = mix(P[0], P[1], T);
- float2 bc = mix(P[1], P[2], T);
- float2 cd = mix(P[2], P[3], T);
- float2 abc = mix(ab, bc, T);
- float2 bcd = mix(bc, cd, T);
- point = mix(abc, bcd, T);
- })");
+ float2 pos;
+ if (sk_VertexID > kMaxVertexID) {
+ // This is a special index value that instructs us to emit a specific point.
+ pos = ((sk_VertexID & 3) == 0) ? inputPoints_0_1.xy :
+ ((sk_VertexID & 2) == 0) ? inputPoints_0_1.zw : inputPoints_2_3.xy;
+ } else {
+ // Evaluate the cubic at T = (sk_VertexID / 2^kMaxResolveLevel).
+ float T = sk_VertexID * kInverseMaxVertexID;
+ float4x3 P = unpack_rational_cubic(inputPoints_0_1.xy, inputPoints_0_1.zw,
+ inputPoints_2_3.xy, inputPoints_2_3.zw);
+ pos = eval_rational_cubic(P, T);
+ })");
- GrShaderVar vertexPos("point", kFloat2_GrSLType);
+ GrShaderVar vertexPos("pos", kFloat2_GrSLType);
if (!shader.viewMatrix().isIdentity()) {
const char* viewMatrix;
fViewMatrixUniform = args.fUniformHandler->addUniform(
nullptr, kVertex_GrShaderFlag, kFloat3x3_GrSLType, "view_matrix", &viewMatrix);
args.fVertBuilder->codeAppendf(R"(
- float2 transformedPoint = (%s * float3(point, 1)).xy;)", viewMatrix);
+ float2 transformedPoint = (%s * float3(pos, 1)).xy;)", viewMatrix);
vertexPos.set(kFloat2_GrSLType, "transformedPoint");
}
gpArgs->fPositionVar = vertexPos;
diff --git a/src/gpu/tessellate/GrTessellationPathRenderer.cpp b/src/gpu/tessellate/GrTessellationPathRenderer.cpp
index a1d75f8..b8ecd53 100644
--- a/src/gpu/tessellate/GrTessellationPathRenderer.cpp
+++ b/src/gpu/tessellate/GrTessellationPathRenderer.cpp
@@ -139,11 +139,11 @@
SkPath path;
shape.asPath(&path);
- if (SkPathPriv::ConicWeightCnt(path)) {
- return CanDrawPath::kNo;
- }
if (!shape.style().isSimpleFill()) {
+ if (SkPathPriv::ConicWeightCnt(path)) {
+ return CanDrawPath::kNo;
+ }
SkPMColor4f constantColor;
// These are only temporary restrictions while we bootstrap tessellated stroking. Every one
// of them will eventually go away.