Move tessellation-specific functions out of GrPathUtils

Bug: skia:12524
Change-Id: I2664c8ea707a40724bcf916e907fe28d1fa276d6
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/474357
Reviewed-by: Michael Ludwig <michaelludwig@google.com>
Commit-Queue: Chris Dalton <csmartdalton@google.com>
diff --git a/bench/FindCubicConvex180ChopsBench.cpp b/bench/FindCubicConvex180ChopsBench.cpp
new file mode 100644
index 0000000..78e7d42
--- /dev/null
+++ b/bench/FindCubicConvex180ChopsBench.cpp
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2020 Google Inc.
+ *
+ * Use of this source code is governed by a BSD-style license that can be
+ * found in the LICENSE file.
+ */
+
+#include "bench/Benchmark.h"
+#include "src/gpu/tessellate/Tessellation.h"
+
+class FindCubicConvex180ChopsBench : public Benchmark {
+public:
+    FindCubicConvex180ChopsBench(const std::array<SkPoint,4>& pts, const char* suffix) : fPts(pts) {
+        fName.printf("FindCubicConvex180Chops%s", suffix);
+    }
+
+private:
+    const char* onGetName() override { return fName.c_str(); }
+    bool isSuitableFor(Backend backend) final { return backend == kNonRendering_Backend; }
+    void onDraw(int loops, SkCanvas*) final {
+        float T[2] = {0};
+        bool areCusps;
+        int iters = 50000 * loops;
+        for (int i = 0; i < iters; ++i) {
+            int count = skgpu::FindCubicConvex180Chops(fPts.data(), T, &areCusps);
+            if (T[0] == 200.7f) {
+                // This will never happen. Pretend to use the result to keep the compiler honest.
+                SkDebugf("%i%f%f", count, T[0], T[1]);
+            }
+        }
+    }
+
+    SkString fName;
+    std::array<SkPoint,4> fPts;
+};
+
+DEF_BENCH(return new FindCubicConvex180ChopsBench({{{0,0}, {100,0}, {50,100}, {100,100}}},
+                                                  "_inflect1");)
+DEF_BENCH(return new FindCubicConvex180ChopsBench({{{0,0}, {50,0}, {100,50}, {100,100}}},
+                                                  "_loop");)
diff --git a/bench/GrPathUtilsBench.cpp b/bench/GrPathUtilsBench.cpp
deleted file mode 100644
index 6e28a2e..0000000
--- a/bench/GrPathUtilsBench.cpp
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * Copyright 2020 Google Inc.
- *
- * Use of this source code is governed by a BSD-style license that can be
- * found in the LICENSE file.
- */
-
-#include "bench/Benchmark.h"
-#include "src/gpu/geometry/GrPathUtils.h"
-
-class FindCubicConvex180Chops : public Benchmark {
-public:
-    FindCubicConvex180Chops(const std::array<SkPoint,4>& pts, const char* suffix) : fPts(pts) {
-        fName.printf("GrPathUtils_findCubicConvex180Chops%s", suffix);
-    }
-
-private:
-    const char* onGetName() override { return fName.c_str(); }
-    bool isSuitableFor(Backend backend) final { return backend == kNonRendering_Backend; }
-    void onDraw(int loops, SkCanvas*) final {
-        float T[2] = {0};
-        bool areCusps;
-        int iters = 50000 * loops;
-        for (int i = 0; i < iters; ++i) {
-            int count = GrPathUtils::findCubicConvex180Chops(fPts.data(), T, &areCusps);
-            if (T[0] == 200.7f) {
-                // This will never happen. Pretend to use the result to keep the compiler honest.
-                SkDebugf("%i%f%f", count, T[0], T[1]);
-            }
-        }
-    }
-
-    SkString fName;
-    std::array<SkPoint,4> fPts;
-};
-
-DEF_BENCH(return new FindCubicConvex180Chops({{{0,0}, {100,0}, {50,100}, {100,100}}}, "_inflect1");)
-DEF_BENCH(return new FindCubicConvex180Chops({{{0,0}, {50,0}, {100,50}, {100,100}}}, "_loop");)
diff --git a/gn/bench.gni b/gn/bench.gni
index 76da063..6372521 100644
--- a/gn/bench.gni
+++ b/gn/bench.gni
@@ -44,6 +44,7 @@
   "$_bench/EncodeBench.cpp",
   "$_bench/FSRectBench.cpp",
   "$_bench/FilteringBench.cpp",
+  "$_bench/FindCubicConvex180ChopsBench.cpp",
   "$_bench/FontCacheBench.cpp",
   "$_bench/GMBench.cpp",
   "$_bench/GameBench.cpp",
@@ -51,7 +52,6 @@
   "$_bench/GlyphQuadFillBench.cpp",
   "$_bench/GrMemoryPoolBench.cpp",
   "$_bench/GrMipmapBench.cpp",
-  "$_bench/GrPathUtilsBench.cpp",
   "$_bench/GrQuadBench.cpp",
   "$_bench/GrResourceCacheBench.cpp",
   "$_bench/GradientBench.cpp",
diff --git a/gn/tests.gni b/gn/tests.gni
index c01c3d7..1985933 100644
--- a/gn/tests.gni
+++ b/gn/tests.gni
@@ -72,6 +72,7 @@
   "$_tests/F16StagesTest.cpp",
   "$_tests/FakeStreams.h",
   "$_tests/FillPathTest.cpp",
+  "$_tests/FindCubicConvex180ChopsTest.cpp",
   "$_tests/FitsInTest.cpp",
   "$_tests/FlattenDrawableTest.cpp",
   "$_tests/FlattenableFactoryToName.cpp",
@@ -98,7 +99,6 @@
   "$_tests/GrFinishedFlushTest.cpp",
   "$_tests/GrMemoryPoolTest.cpp",
   "$_tests/GrOpListFlushTest.cpp",
-  "$_tests/GrPathUtilsTest.cpp",
   "$_tests/GrPorterDuffTest.cpp",
   "$_tests/GrQuadBufferTest.cpp",
   "$_tests/GrQuadCropTest.cpp",
diff --git a/src/gpu/geometry/GrPathUtils.cpp b/src/gpu/geometry/GrPathUtils.cpp
index 9e0bd7f..c68f53d 100644
--- a/src/gpu/geometry/GrPathUtils.cpp
+++ b/src/gpu/geometry/GrPathUtils.cpp
@@ -549,140 +549,3 @@
         convert_noninflect_cubic_to_quads_with_constraint(cubic, tolSqd, dir, quads);
     }
 }
-
-int GrPathUtils::findCubicConvex180Chops(const SkPoint pts[], float T[2], bool* areCusps) {
-    using grvx::float2;
-    SkASSERT(pts);
-    SkASSERT(T);
-    SkASSERT(areCusps);
-
-    // If a chop falls within a distance of "kEpsilon" from 0 or 1, throw it out. Tangents become
-    // unstable when we chop too close to the boundary. This works out because the tessellation
-    // shaders don't allow more than 2^10 parametric segments, and they snap the beginning and
-    // ending edges at 0 and 1. So if we overstep an inflection or point of 180-degree rotation by a
-    // fraction of a tessellation segment, it just gets snapped.
-    constexpr static float kEpsilon = 1.f / (1 << 11);
-    // Floating-point representation of "1 - 2*kEpsilon".
-    constexpr static uint32_t kIEEE_one_minus_2_epsilon = (127 << 23) - 2 * (1 << (24 - 11));
-    // Unfortunately we don't have a way to static_assert this, but we can runtime assert that the
-    // kIEEE_one_minus_2_epsilon bits are correct.
-    SkASSERT(sk_bit_cast<float>(kIEEE_one_minus_2_epsilon) == 1 - 2*kEpsilon);
-
-    float2 p0 = skvx::bit_pun<float2>(pts[0]);
-    float2 p1 = skvx::bit_pun<float2>(pts[1]);
-    float2 p2 = skvx::bit_pun<float2>(pts[2]);
-    float2 p3 = skvx::bit_pun<float2>(pts[3]);
-
-    // Find the cubic's power basis coefficients. These define the bezier curve as:
-    //
-    //                                    |T^3|
-    //     Cubic(T) = x,y = |A  3B  3C| * |T^2| + P0
-    //                      |.   .   .|   |T  |
-    //
-    // And the tangent direction (scaled by a uniform 1/3) will be:
-    //
-    //                                                 |T^2|
-    //     Tangent_Direction(T) = dx,dy = |A  2B  C| * |T  |
-    //                                    |.   .  .|   |1  |
-    //
-    float2 C = p1 - p0;
-    float2 D = p2 - p1;
-    float2 E = p3 - p0;
-    float2 B = D - C;
-    float2 A = -3*D + E;
-
-    // Now find the cubic's inflection function. There are inflections where F' x F'' == 0.
-    // We formulate this as a quadratic equation:  F' x F'' == aT^2 + bT + c == 0.
-    // See: https://www.microsoft.com/en-us/research/wp-content/uploads/2005/01/p1000-loop.pdf
-    // NOTE: We only need the roots, so a uniform scale factor does not affect the solution.
-    float a = grvx::cross(A,B);
-    float b = grvx::cross(A,C);
-    float c = grvx::cross(B,C);
-    float b_over_minus_2 = -.5f * b;
-    float discr_over_4 = b_over_minus_2*b_over_minus_2 - a*c;
-
-    // If -cuspThreshold <= discr_over_4 <= cuspThreshold, it means the two roots are within
-    // kEpsilon of one another (in parametric space). This is close enough for our purposes to
-    // consider them a single cusp.
-    float cuspThreshold = a * (kEpsilon/2);
-    cuspThreshold *= cuspThreshold;
-
-    if (discr_over_4 < -cuspThreshold) {
-        // The curve does not inflect or cusp. This means it might rotate more than 180 degrees
-        // instead. Chop were rotation == 180 deg. (This is the 2nd root where the tangent is
-        // parallel to tan0.)
-        //
-        //      Tangent_Direction(T) x tan0 == 0
-        //      (AT^2 x tan0) + (2BT x tan0) + (C x tan0) == 0
-        //      (A x C)T^2 + (2B x C)T + (C x C) == 0  [[because tan0 == P1 - P0 == C]]
-        //      bT^2 + 2cT + 0 == 0  [[because A x C == b, B x C == c]]
-        //      T = [0, -2c/b]
-        //
-        // NOTE: if C == 0, then C != tan0. But this is fine because the curve is definitely
-        // convex-180 if any points are colocated, and T[0] will equal NaN which returns 0 chops.
-        *areCusps = false;
-        float root = sk_ieee_float_divide(c, b_over_minus_2);
-        // Is "root" inside the range [kEpsilon, 1 - kEpsilon)?
-        if (sk_bit_cast<uint32_t>(root - kEpsilon) < kIEEE_one_minus_2_epsilon) {
-            T[0] = root;
-            return 1;
-        }
-        return 0;
-    }
-
-    *areCusps = (discr_over_4 <= cuspThreshold);
-    if (*areCusps) {
-        // The two roots are close enough that we can consider them a single cusp.
-        if (a != 0 || b_over_minus_2 != 0 || c != 0) {
-            // Pick the average of both roots.
-            float root = sk_ieee_float_divide(b_over_minus_2, a);
-            // Is "root" inside the range [kEpsilon, 1 - kEpsilon)?
-            if (sk_bit_cast<uint32_t>(root - kEpsilon) < kIEEE_one_minus_2_epsilon) {
-                T[0] = root;
-                return 1;
-            }
-            return 0;
-        }
-
-        // The curve is a flat line. The standard inflection function doesn't detect cusps from flat
-        // lines. Find cusps by searching instead for points where the tangent is perpendicular to
-        // tan0. This will find any cusp point.
-        //
-        //     dot(tan0, Tangent_Direction(T)) == 0
-        //
-        //                         |T^2|
-        //     tan0 * |A  2B  C| * |T  | == 0
-        //            |.   .  .|   |1  |
-        //
-        float2 tan0 = skvx::if_then_else(C != 0, C, p2 - p0);
-        a = grvx::dot(tan0, A);
-        b_over_minus_2 = -grvx::dot(tan0, B);
-        c = grvx::dot(tan0, C);
-        discr_over_4 = std::max(b_over_minus_2*b_over_minus_2 - a*c, 0.f);
-    }
-
-    // Solve our quadratic equation to find where to chop. See the quadratic formula from
-    // Numerical Recipes in C.
-    float q = sqrtf(discr_over_4);
-    q = copysignf(q, b_over_minus_2);
-    q = q + b_over_minus_2;
-    float2 roots = float2{q,c} / float2{a,q};
-
-    auto inside = (roots > kEpsilon) & (roots < (1 - kEpsilon));
-    if (inside[0]) {
-        if (inside[1] && roots[0] != roots[1]) {
-            if (roots[0] > roots[1]) {
-                roots = skvx::shuffle<1,0>(roots);  // Sort.
-            }
-            roots.store(T);
-            return 2;
-        }
-        T[0] = roots[0];
-        return 1;
-    }
-    if (inside[1]) {
-        T[0] = roots[1];
-        return 1;
-    }
-    return 0;
-}
diff --git a/src/gpu/geometry/GrPathUtils.h b/src/gpu/geometry/GrPathUtils.h
index c9ccc21..24ae1bb 100644
--- a/src/gpu/geometry/GrPathUtils.h
+++ b/src/gpu/geometry/GrPathUtils.h
@@ -133,53 +133,6 @@
                                             SkPathFirstDirection dir,
                                             SkTArray<SkPoint, true>* quads);
 
-// Converts the given line to a cubic bezier.
-// NOTE: This method interpolates at 1/3 and 2/3, but if suitable in context, the cubic
-// {p0, p0, p1, p1} may also work.
-inline void writeLineAsCubic(SkPoint startPt, SkPoint endPt, skgpu::VertexWriter* writer) {
-    using grvx::float2, skvx::bit_pun;
-    float2 p0 = bit_pun<float2>(startPt);
-    float2 p1 = bit_pun<float2>(endPt);
-    float2 v = (p1 - p0) * (1/3.f);
-    *writer << p0 << (p0 + v) << (p1 - v) << p1;
-}
-
-// Converts the given quadratic bezier to a cubic.
-inline void writeQuadAsCubic(const SkPoint p[3], skgpu::VertexWriter* writer) {
-    using grvx::float2, skvx::bit_pun;
-    float2 p0 = bit_pun<float2>(p[0]);
-    float2 p1 = bit_pun<float2>(p[1]);
-    float2 p2 = bit_pun<float2>(p[2]);
-    float2 c = p1 * (2/3.f);
-    *writer << p0 << (p0*(1/3.f) + c) << (p2 * (1/3.f) + c) << p2;
-}
-inline void convertQuadToCubic(const SkPoint p[3], SkPoint out[4]) {
-    skgpu::VertexWriter writer(out);
-    writeQuadAsCubic(p, &writer);
-}
-
-// Finds 0, 1, or 2 T values at which to chop the given curve in order to guarantee the resulting
-// cubics are convex and rotate no more than 180 degrees.
-//
-//   - If the cubic is "serpentine", then the T values are any inflection points in [0 < T < 1].
-//   - If the cubic is linear, then the T values are any 180-degree cusp points in [0 < T < 1].
-//   - Otherwise the T value is the point at which rotation reaches 180 degrees, iff in [0 < T < 1].
-//
-// 'areCusps' is set to true if the chop point occurred at a cusp (within tolerance), or if the chop
-// point(s) occurred at 180-degree turnaround points on a degenerate flat line.
-int findCubicConvex180Chops(const SkPoint[], float T[2], bool* areCusps);
-
-// Returns true if the given conic (or quadratic) has a cusp point. The w value is not necessary in
-// determining this. If there is a cusp, it can be found at the midtangent.
-inline bool conicHasCusp(const SkPoint p[3]) {
-    SkVector a = p[1] - p[0];
-    SkVector b = p[2] - p[1];
-    // A conic of any class can only have a cusp if it is a degenerate flat line with a 180 degree
-    // turnarund. To detect this, the beginning and ending tangents must be parallel
-    // (a.cross(b) == 0) and pointing in opposite directions (a.dot(b) < 0).
-    return a.cross(b) == 0 && a.dot(b) < 0;
-}
-
 }  // namespace GrPathUtils
 
 #endif
diff --git a/src/gpu/tessellate/PatchWriter.h b/src/gpu/tessellate/PatchWriter.h
index 780293a..4f2c01d 100644
--- a/src/gpu/tessellate/PatchWriter.h
+++ b/src/gpu/tessellate/PatchWriter.h
@@ -182,6 +182,9 @@
 
 // Converts a quadratic to a cubic when being output via '<<' to a VertexWriter.
 struct QuadToCubic {
+    QuadToCubic(float2 p0, float2 p1, float2 p2) : fP0(p0), fP1(p1), fP2(p2) {}
+    QuadToCubic(const SkPoint p[3])
+            : QuadToCubic(float2::Load(p), float2::Load(p+1), float2::Load(p+2)) {}
     float2 fP0, fP1, fP2;
 };
 
@@ -191,6 +194,11 @@
     return vertexWriter << p0 << mix(float4(p0,p2), p1.xyxy(), 2/3.f) << p2;
 }
 
+SK_MAYBE_UNUSED SK_ALWAYS_INLINE VertexWriter& operator<<(VertexWriter&& vertexWriter,
+                                                          const QuadToCubic& quadratic) {
+    return vertexWriter << quadratic;
+}
+
 SK_MAYBE_UNUSED SK_ALWAYS_INLINE void operator<<(
         PatchWriter& w, MiddleOutPolygonTriangulator::PoppedTriangleStack&& stack) {
     for (auto [p0, p1, p2] : stack) {
diff --git a/src/gpu/tessellate/PathCurveTessellator.cpp b/src/gpu/tessellate/PathCurveTessellator.cpp
index aae0ac1..4feea87 100644
--- a/src/gpu/tessellate/PathCurveTessellator.cpp
+++ b/src/gpu/tessellate/PathCurveTessellator.cpp
@@ -7,7 +7,6 @@
 
 #include "src/gpu/tessellate/PathCurveTessellator.h"
 
-#include "src/gpu/geometry/GrPathUtils.h"
 #include "src/gpu/tessellate/AffineMatrix.h"
 #include "src/gpu/tessellate/MiddleOutPolygonTriangulator.h"
 #include "src/gpu/tessellate/PatchWriter.h"
diff --git a/src/gpu/tessellate/StrokeFixedCountTessellator.cpp b/src/gpu/tessellate/StrokeFixedCountTessellator.cpp
index c160e08..d5312f3 100644
--- a/src/gpu/tessellate/StrokeFixedCountTessellator.cpp
+++ b/src/gpu/tessellate/StrokeFixedCountTessellator.cpp
@@ -8,7 +8,6 @@
 #include "src/gpu/tessellate/StrokeFixedCountTessellator.h"
 
 #include "src/core/SkGeometry.h"
-#include "src/gpu/geometry/GrPathUtils.h"
 #include "src/gpu/tessellate/PatchWriter.h"
 #include "src/gpu/tessellate/StrokeIterator.h"
 #include "src/gpu/tessellate/WangsFormula.h"
@@ -56,7 +55,7 @@
             return;
         }
         SkPoint cubic[4];
-        GrPathUtils::convertQuadToCubic(p, cubic);
+        VertexWriter(cubic) << QuadToCubic(p);
         SkPoint endControlPoint = cubic[2];
         this->writeStroke(cubic, endControlPoint, GrTessellationShader::kCubicCurveType);
         fMaxParametricSegments_pow4 = std::max(numParametricSegments_pow4,
@@ -259,7 +258,7 @@
                     instanceWriter.lineTo(p[0], p[1]);
                     break;
                 case Verb::kQuad:
-                    if (GrPathUtils::conicHasCusp(p)) {
+                    if (ConicHasCusp(p)) {
                         // The cusp is always at the midtandent.
                         SkPoint cusp = SkEvalQuadAt(p, SkFindQuadMidTangent(p));
                         instanceWriter.writeCircle(cusp);
@@ -271,7 +270,7 @@
                     }
                     break;
                 case Verb::kConic:
-                    if (GrPathUtils::conicHasCusp(p)) {
+                    if (ConicHasCusp(p)) {
                         // The cusp is always at the midtandent.
                         SkConic conic(p, strokeIter.w());
                         SkPoint cusp = conic.evalAt(conic.findMidTangent());
@@ -287,7 +286,7 @@
                     SkPoint chops[10];
                     float T[2];
                     bool areCusps;
-                    numChops = GrPathUtils::findCubicConvex180Chops(p, T, &areCusps);
+                    numChops = FindCubicConvex180Chops(p, T, &areCusps);
                     if (numChops == 0) {
                         instanceWriter.cubicConvex180To(p);
                     } else if (numChops == 1) {
diff --git a/src/gpu/tessellate/StrokeHardwareTessellator.cpp b/src/gpu/tessellate/StrokeHardwareTessellator.cpp
index ba3e3fa..1189ab1 100644
--- a/src/gpu/tessellate/StrokeHardwareTessellator.cpp
+++ b/src/gpu/tessellate/StrokeHardwareTessellator.cpp
@@ -7,8 +7,8 @@
 
 #include "src/gpu/tessellate/StrokeHardwareTessellator.h"
 
+#include "src/core/SkGeometry.h"
 #include "src/core/SkPathPriv.h"
-#include "src/gpu/geometry/GrPathUtils.h"
 #include "src/gpu/tessellate/PatchWriter.h"
 #include "src/gpu/tessellate/WangsFormula.h"
 #include "src/gpu/tessellate/shaders/GrTessellationShader.h"
@@ -163,7 +163,7 @@
         SkPoint chops[10];
         float chopT[2];
         bool areCusps;
-        int numChops = GrPathUtils::findCubicConvex180Chops(p, chopT, &areCusps);
+        int numChops = FindCubicConvex180Chops(p, chopT, &areCusps);
         if (numChops == 0) {
             // The curve is already convex and rotates no more than 180 degrees.
             this->internalCubicConvex180PatchesTo(fStrokeJoinType, p);
@@ -349,7 +349,7 @@
         // Convert to a patch.
         SkPoint asPatch[4];
         if (w == 1) {
-            GrPathUtils::convertQuadToCubic(p, asPatch);
+            VertexWriter(asPatch) << QuadToCubic(p);
         } else {
             GrTessellationShader::WriteConicPatch(p, w, asPatch);
         }
@@ -634,7 +634,7 @@
     float2 p2 = skvx::bit_pun<float2>(p[2]);
     float2 p3 = skvx::bit_pun<float2>(p[3]);
 
-    // See GrPathUtils::findCubicConvex180Chops() for the math.
+    // See FindCubicConvex180Chops() for the math.
     float2 C = p1 - p0;
     float2 D = p2 - p1;
     float2 E = p3 - p0;
@@ -747,7 +747,7 @@
                         hwPatchWriter.writeLineTo(p[0], p[2]);
                         continue;
                     }
-                    if (GrPathUtils::conicHasCusp(p)) {
+                    if (ConicHasCusp(p)) {
                         // Cusps are rare, but the tessellation shader can't handle them. Chop the
                         // curve into segments that the shader can handle.
                         SkPoint cusp = SkEvalQuadAt(p, SkFindQuadMidTangent(p));
@@ -767,7 +767,7 @@
                     // Write it out directly.
                     prevJoinFitsInPatch = hwPatchWriter.stroke180FitsInPatch_withJoin(
                             numParametricSegments_pow4);
-                    GrPathUtils::convertQuadToCubic(p, scratchPts);
+                    VertexWriter(scratchPts) << QuadToCubic(p);
                     patchPts = scratchPts;
                     endControlPoint = patchPts[2];
                     break;
@@ -782,7 +782,7 @@
                         hwPatchWriter.writeLineTo(p[0], p[2]);
                         continue;
                     }
-                    if (GrPathUtils::conicHasCusp(p)) {
+                    if (ConicHasCusp(p)) {
                         // Cusps are rare, but the tessellation shader can't handle them. Chop the
                         // curve into segments that the shader can handle.
                         SkConic conic(p, *w);
diff --git a/src/gpu/tessellate/Tessellation.cpp b/src/gpu/tessellate/Tessellation.cpp
index cf5c4e3..18abf6e 100644
--- a/src/gpu/tessellate/Tessellation.cpp
+++ b/src/gpu/tessellate/Tessellation.cpp
@@ -10,6 +10,7 @@
 #include "include/core/SkPath.h"
 #include "src/core/SkGeometry.h"
 #include "src/core/SkPathPriv.h"
+#include "src/core/SkUtils.h"
 #include "src/gpu/BufferWriter.h"
 #include "src/gpu/tessellate/CullTest.h"
 #include "src/gpu/tessellate/MiddleOutPolygonTriangulator.h"
@@ -191,4 +192,139 @@
     return chopper.path();
 }
 
+int FindCubicConvex180Chops(const SkPoint pts[], float T[2], bool* areCusps) {
+    SkASSERT(pts);
+    SkASSERT(T);
+    SkASSERT(areCusps);
+
+    // If a chop falls within a distance of "kEpsilon" from 0 or 1, throw it out. Tangents become
+    // unstable when we chop too close to the boundary. This works out because the tessellation
+    // shaders don't allow more than 2^10 parametric segments, and they snap the beginning and
+    // ending edges at 0 and 1. So if we overstep an inflection or point of 180-degree rotation by a
+    // fraction of a tessellation segment, it just gets snapped.
+    constexpr static float kEpsilon = 1.f / (1 << 11);
+    // Floating-point representation of "1 - 2*kEpsilon".
+    constexpr static uint32_t kIEEE_one_minus_2_epsilon = (127 << 23) - 2 * (1 << (24 - 11));
+    // Unfortunately we don't have a way to static_assert this, but we can runtime assert that the
+    // kIEEE_one_minus_2_epsilon bits are correct.
+    SkASSERT(sk_bit_cast<float>(kIEEE_one_minus_2_epsilon) == 1 - 2*kEpsilon);
+
+    float2 p0 = skvx::bit_pun<float2>(pts[0]);
+    float2 p1 = skvx::bit_pun<float2>(pts[1]);
+    float2 p2 = skvx::bit_pun<float2>(pts[2]);
+    float2 p3 = skvx::bit_pun<float2>(pts[3]);
+
+    // Find the cubic's power basis coefficients. These define the bezier curve as:
+    //
+    //                                    |T^3|
+    //     Cubic(T) = x,y = |A  3B  3C| * |T^2| + P0
+    //                      |.   .   .|   |T  |
+    //
+    // And the tangent direction (scaled by a uniform 1/3) will be:
+    //
+    //                                                 |T^2|
+    //     Tangent_Direction(T) = dx,dy = |A  2B  C| * |T  |
+    //                                    |.   .  .|   |1  |
+    //
+    float2 C = p1 - p0;
+    float2 D = p2 - p1;
+    float2 E = p3 - p0;
+    float2 B = D - C;
+    float2 A = -3*D + E;
+
+    // Now find the cubic's inflection function. There are inflections where F' x F'' == 0.
+    // We formulate this as a quadratic equation:  F' x F'' == aT^2 + bT + c == 0.
+    // See: https://www.microsoft.com/en-us/research/wp-content/uploads/2005/01/p1000-loop.pdf
+    // NOTE: We only need the roots, so a uniform scale factor does not affect the solution.
+    float a = cross(A,B);
+    float b = cross(A,C);
+    float c = cross(B,C);
+    float b_over_minus_2 = -.5f * b;
+    float discr_over_4 = b_over_minus_2*b_over_minus_2 - a*c;
+
+    // If -cuspThreshold <= discr_over_4 <= cuspThreshold, it means the two roots are within
+    // kEpsilon of one another (in parametric space). This is close enough for our purposes to
+    // consider them a single cusp.
+    float cuspThreshold = a * (kEpsilon/2);
+    cuspThreshold *= cuspThreshold;
+
+    if (discr_over_4 < -cuspThreshold) {
+        // The curve does not inflect or cusp. This means it might rotate more than 180 degrees
+        // instead. Chop were rotation == 180 deg. (This is the 2nd root where the tangent is
+        // parallel to tan0.)
+        //
+        //      Tangent_Direction(T) x tan0 == 0
+        //      (AT^2 x tan0) + (2BT x tan0) + (C x tan0) == 0
+        //      (A x C)T^2 + (2B x C)T + (C x C) == 0  [[because tan0 == P1 - P0 == C]]
+        //      bT^2 + 2cT + 0 == 0  [[because A x C == b, B x C == c]]
+        //      T = [0, -2c/b]
+        //
+        // NOTE: if C == 0, then C != tan0. But this is fine because the curve is definitely
+        // convex-180 if any points are colocated, and T[0] will equal NaN which returns 0 chops.
+        *areCusps = false;
+        float root = sk_ieee_float_divide(c, b_over_minus_2);
+        // Is "root" inside the range [kEpsilon, 1 - kEpsilon)?
+        if (sk_bit_cast<uint32_t>(root - kEpsilon) < kIEEE_one_minus_2_epsilon) {
+            T[0] = root;
+            return 1;
+        }
+        return 0;
+    }
+
+    *areCusps = (discr_over_4 <= cuspThreshold);
+    if (*areCusps) {
+        // The two roots are close enough that we can consider them a single cusp.
+        if (a != 0 || b_over_minus_2 != 0 || c != 0) {
+            // Pick the average of both roots.
+            float root = sk_ieee_float_divide(b_over_minus_2, a);
+            // Is "root" inside the range [kEpsilon, 1 - kEpsilon)?
+            if (sk_bit_cast<uint32_t>(root - kEpsilon) < kIEEE_one_minus_2_epsilon) {
+                T[0] = root;
+                return 1;
+            }
+            return 0;
+        }
+
+        // The curve is a flat line. The standard inflection function doesn't detect cusps from flat
+        // lines. Find cusps by searching instead for points where the tangent is perpendicular to
+        // tan0. This will find any cusp point.
+        //
+        //     dot(tan0, Tangent_Direction(T)) == 0
+        //
+        //                         |T^2|
+        //     tan0 * |A  2B  C| * |T  | == 0
+        //            |.   .  .|   |1  |
+        //
+        float2 tan0 = skvx::if_then_else(C != 0, C, p2 - p0);
+        a = dot(tan0, A);
+        b_over_minus_2 = -dot(tan0, B);
+        c = dot(tan0, C);
+        discr_over_4 = std::max(b_over_minus_2*b_over_minus_2 - a*c, 0.f);
+    }
+
+    // Solve our quadratic equation to find where to chop. See the quadratic formula from
+    // Numerical Recipes in C.
+    float q = sqrtf(discr_over_4);
+    q = copysignf(q, b_over_minus_2);
+    q = q + b_over_minus_2;
+    float2 roots = float2{q,c} / float2{a,q};
+
+    auto inside = (roots > kEpsilon) & (roots < (1 - kEpsilon));
+    if (inside[0]) {
+        if (inside[1] && roots[0] != roots[1]) {
+            if (roots[0] > roots[1]) {
+                roots = skvx::shuffle<1,0>(roots);  // Sort.
+            }
+            roots.store(T);
+            return 2;
+        }
+        T[0] = roots[0];
+        return 1;
+    }
+    if (inside[1]) {
+        T[0] = roots[1];
+        return 1;
+    }
+    return 0;
+}
 }  // namespace skgpu
diff --git a/src/gpu/tessellate/Tessellation.h b/src/gpu/tessellate/Tessellation.h
index 127d28d..d1e52e2 100644
--- a/src/gpu/tessellate/Tessellation.h
+++ b/src/gpu/tessellate/Tessellation.h
@@ -140,6 +140,28 @@
                          const SkMatrix&,
                          const SkRect& viewport);
 
+// Finds 0, 1, or 2 T values at which to chop the given curve in order to guarantee the resulting
+// cubics are convex and rotate no more than 180 degrees.
+//
+//   - If the cubic is "serpentine", then the T values are any inflection points in [0 < T < 1].
+//   - If the cubic is linear, then the T values are any 180-degree cusp points in [0 < T < 1].
+//   - Otherwise the T value is the point at which rotation reaches 180 degrees, iff in [0 < T < 1].
+//
+// 'areCusps' is set to true if the chop point occurred at a cusp (within tolerance), or if the chop
+// point(s) occurred at 180-degree turnaround points on a degenerate flat line.
+int FindCubicConvex180Chops(const SkPoint[], float T[2], bool* areCusps);
+
+// Returns true if the given conic (or quadratic) has a cusp point. The w value is not necessary in
+// determining this. If there is a cusp, it can be found at the midtangent.
+inline bool ConicHasCusp(const SkPoint p[3]) {
+    SkVector a = p[1] - p[0];
+    SkVector b = p[2] - p[1];
+    // A conic of any class can only have a cusp if it is a degenerate flat line with a 180 degree
+    // turnarund. To detect this, the beginning and ending tangents must be parallel
+    // (a.cross(b) == 0) and pointing in opposite directions (a.dot(b) < 0).
+    return a.cross(b) == 0 && a.dot(b) < 0;
+}
+
 }  // namespace skgpu
 
 #endif  // tessellate_Tessellation_DEFINED
diff --git a/tests/GrPathUtilsTest.cpp b/tests/FindCubicConvex180ChopsTest.cpp
similarity index 73%
rename from tests/GrPathUtilsTest.cpp
rename to tests/FindCubicConvex180ChopsTest.cpp
index 7fd0dc4..955d07f 100644
--- a/tests/GrPathUtilsTest.cpp
+++ b/tests/FindCubicConvex180ChopsTest.cpp
@@ -7,9 +7,11 @@
 
 #include "include/utils/SkRandom.h"
 #include "src/core/SkGeometry.h"
-#include "src/gpu/geometry/GrPathUtils.h"
+#include "src/gpu/tessellate/Tessellation.h"
 #include "tests/Test.h"
 
+namespace skgpu {
+
 static bool is_linear(SkPoint p0, SkPoint p1, SkPoint p2) {
     return SkScalarNearlyZero((p0 - p1).cross(p2 - p1));
 }
@@ -22,9 +24,9 @@
     bool areCusps = false;
     float inflectT[2], convex180T[2];
     if (int inflectN = SkFindCubicInflections(p, inflectT)) {
-        // The curve has inflections. findCubicConvex180Chops should return the inflection
+        // The curve has inflections. FindCubicConvex180Chops should return the inflection
         // points.
-        int convex180N = GrPathUtils::findCubicConvex180Chops(p, convex180T, &areCusps);
+        int convex180N = FindCubicConvex180Chops(p, convex180T, &areCusps);
         REPORTER_ASSERT(r, inflectN == convex180N);
         if (!areCusps) {
             REPORTER_ASSERT(r, inflectN == 1 ||
@@ -35,7 +37,7 @@
         }
     } else {
         float totalRotation = SkMeasureNonInflectCubicRotation(p);
-        int convex180N = GrPathUtils::findCubicConvex180Chops(p, convex180T, &areCusps);
+        int convex180N = FindCubicConvex180Chops(p, convex180T, &areCusps);
         SkPoint chops[10];
         SkChopCubicAt(p, chops, convex180T, convex180N);
         float radsSum = 0;
@@ -65,7 +67,7 @@
     }
 }
 
-DEF_TEST(GrPathUtils_findCubicConvex180Chops, r) {
+DEF_TEST(FindCubicConvex180Chops, r) {
     // Test all combinations of corners from the square [0,0,1,1]. This covers every cubic type as
     // well as a wide variety of special cases for cusps, lines, loops, and inflections.
     for (int i = 0; i < (1 << 8); ++i) {
@@ -89,11 +91,11 @@
     SkPoint quad[4] = {{0,0}, {2,2}, {4,2}, {6,0}};
     float T[2];
     bool areCusps;
-    REPORTER_ASSERT(r, GrPathUtils::findCubicConvex180Chops(quad, T, &areCusps) == 0);
+    REPORTER_ASSERT(r, FindCubicConvex180Chops(quad, T, &areCusps) == 0);
 
     // Now test that cusps and near-cusps get flagged as cusps.
     SkPoint cusp[4] = {{0,0}, {1,1}, {1,0}, {0,1}};
-    REPORTER_ASSERT(r, GrPathUtils::findCubicConvex180Chops(cusp, T, &areCusps) == 1);
+    REPORTER_ASSERT(r, FindCubicConvex180Chops(cusp, T, &areCusps) == 1);
     REPORTER_ASSERT(r, areCusps == true);
 
     // Find the height of the right side of "cusp" at which the distance between its inflection
@@ -110,33 +112,14 @@
     // Ensure two inflection points barely more than kEpsilon apart do not get flagged as cusps.
     cusp[1].fY = (float)(1 - 1.1 * dy);
     cusp[2].fY = (float)(0 + 1.1 * dy);
-    REPORTER_ASSERT(r, GrPathUtils::findCubicConvex180Chops(cusp, T, &areCusps) == 2);
+    REPORTER_ASSERT(r, FindCubicConvex180Chops(cusp, T, &areCusps) == 2);
     REPORTER_ASSERT(r, areCusps == false);
 
     // Ensure two inflection points barely less than kEpsilon apart do get flagged as cusps.
     cusp[1].fY = (float)(1 - .9 * dy);
     cusp[2].fY = (float)(0 + .9 * dy);
-    REPORTER_ASSERT(r, GrPathUtils::findCubicConvex180Chops(cusp, T, &areCusps) == 1);
+    REPORTER_ASSERT(r, FindCubicConvex180Chops(cusp, T, &areCusps) == 1);
     REPORTER_ASSERT(r, areCusps == true);
 }
 
-DEF_TEST(GrPathUtils_convertToCubic, r) {
-    SkPoint cubic[4];
-    skgpu::VertexWriter cubicWriter(cubic);
-    GrPathUtils::writeLineAsCubic({0,0}, {3,6}, &cubicWriter);
-    REPORTER_ASSERT(r, cubic[0] == SkPoint::Make(0,0));
-    REPORTER_ASSERT(r, SkScalarNearlyEqual(cubic[1].fX, 1));
-    REPORTER_ASSERT(r, SkScalarNearlyEqual(cubic[1].fY, 2));
-    REPORTER_ASSERT(r, SkScalarNearlyEqual(cubic[2].fX, 2));
-    REPORTER_ASSERT(r, SkScalarNearlyEqual(cubic[2].fY, 4));
-    REPORTER_ASSERT(r, cubic[3] == SkPoint::Make(3,6));
-
-    SkPoint quad[3] = {{0,0}, {3,3}, {6,0}};
-    GrPathUtils::convertQuadToCubic(quad, cubic);
-    REPORTER_ASSERT(r, cubic[0] == SkPoint::Make(0,0));
-    REPORTER_ASSERT(r, SkScalarNearlyEqual(cubic[1].fX, 2));
-    REPORTER_ASSERT(r, SkScalarNearlyEqual(cubic[1].fY, 2));
-    REPORTER_ASSERT(r, SkScalarNearlyEqual(cubic[2].fX, 4));
-    REPORTER_ASSERT(r, SkScalarNearlyEqual(cubic[2].fY, 2));
-    REPORTER_ASSERT(r, cubic[3] == SkPoint::Make(6,0));
-}
+}  // namespace skgpu