PLS cleanups

The PLS code has started to devolve into chaos. This PR is a start at attempting to de-spaghettify some of it.

It also fixes a bug that got introduced in bae069339. That PR assumed that interior triangulation did not write contour records to the GPU, but that's false. "outerCurve" patches still need a contour record in order to get the pathID.

Diffs=
60296f34f PLS cleanups (#5580)

Co-authored-by: Chris Dalton <99840794+csmartdalton@users.noreply.github.com>
diff --git a/.rive_head b/.rive_head
index 2b6d57f..0362826 100644
--- a/.rive_head
+++ b/.rive_head
@@ -1 +1 @@
-094afae0b4f55e121a36981b9846ccbc9912c347
+60296f34fd95019c0dabe33b44a60d0ee6cbd5d5
diff --git a/include/rive/pls/pls_render_context.hpp b/include/rive/pls/pls_render_context.hpp
index 13108cb..54a91b3 100644
--- a/include/rive/pls/pls_render_context.hpp
+++ b/include/rive/pls/pls_render_context.hpp
@@ -138,22 +138,6 @@
         return m_clipContentID;
     }
 
-    // Returns the context's TrivialBlockAllocator, which is automatically reset at the end of every
-    // flush.
-    TrivialBlockAllocator* trivialPerFlushAllocator()
-    {
-        assert(m_didBeginFrame);
-        return &m_trivialPerFlushAllocator;
-    }
-
-    // Allocates a trivially destructible object that will be automatically deleted at the end of
-    // the current flush.
-    template <typename T, typename... Args> T* make(Args&&... args)
-    {
-        assert(m_didBeginFrame);
-        return m_trivialPerFlushAllocator.make<T>(std::forward<Args>(args)...);
-    }
-
     // Returns the number of tessellation vertices that have been pushed during the current flush.
     uint32_t currentTessVertexCount() const { return m_tessVertexCount; }
 
@@ -253,6 +237,58 @@
     // shrink if the application calls this method.)
     void shrinkGPUResourcesToFit();
 
+    // Returns the context's TrivialBlockAllocator, which is automatically reset at the end of every
+    // flush.
+    TrivialBlockAllocator* trivialPerFlushAllocator()
+    {
+        assert(m_didBeginFrame);
+        return &m_trivialPerFlushAllocator;
+    }
+
+    // Allocates a trivially destructible object that will be automatically deleted at the end of
+    // the current flush.
+    template <typename T, typename... Args> T* make(Args&&... args)
+    {
+        assert(m_didBeginFrame);
+        return m_trivialPerFlushAllocator.make<T>(std::forward<Args>(args)...);
+    }
+
+    // Simple linked list whose nodes are allocated on a context's TrivialBlockAllocator.
+    template <typename T> class PerFlushLinkedList
+    {
+    public:
+        void reset() { m_tail = m_head = nullptr; }
+
+        bool empty() const;
+        T& tail() const;
+        template <typename... Args> void emplace_back(PLSRenderContext* context, Args... args);
+
+        struct Node
+        {
+            template <typename... Args> Node(Args... args) : data(std::forward<Args>(args)...) {}
+            T data;
+            Node* next = nullptr;
+        };
+
+        class Iter
+        {
+        public:
+            Iter(Node* current) : m_current(current) {}
+            bool operator!=(const Iter& other) const { return m_current != other.m_current; }
+            void operator++() { m_current = m_current->next; }
+            T& operator*() { return m_current->data; }
+
+        private:
+            Node* m_current;
+        };
+        Iter begin() { return {m_head}; }
+        Iter end() { return {nullptr}; }
+
+    private:
+        Node* m_head = nullptr;
+        Node* m_tail = nullptr;
+    };
+
 protected:
     PLSRenderContext(const PlatformFeatures&);
 
@@ -407,9 +443,9 @@
     }
 
     // Linked list of draws to be issued by the subclass during onFlush().
-    struct DrawList
+    struct Draw
     {
-        DrawList(DrawType drawType_, uint32_t baseVertexOrInstance_) :
+        Draw(DrawType drawType_, uint32_t baseVertexOrInstance_) :
             drawType(drawType_), baseVertexOrInstance(baseVertexOrInstance_)
         {}
         const DrawType drawType;
@@ -417,11 +453,9 @@
         uint32_t vertexOrInstanceCount = 0; // Calculated during PLSRenderContext::flush().
         ShaderFeatures shaderFeatures;
         GrInnerFanTriangulator* triangulator = nullptr; // Used by "interiorTriangulation" draws.
-        DrawList* next = nullptr;
     };
 
-    DrawList* m_drawList = nullptr;
-    DrawList* m_lastDraw = nullptr;
+    PerFlushLinkedList<Draw> m_drawList;
     size_t m_drawListCount = 0;
 
     // GrTriangulator provides an upper bound on the number of vertices it will emit. Triangulations
@@ -580,4 +614,34 @@
     constexpr static size_t kPerFlushAllocatorInitialBlockSize = 1024 * 1024; // 1 MiB.
     TrivialBlockAllocator m_trivialPerFlushAllocator{kPerFlushAllocatorInitialBlockSize};
 };
+
+template <typename T> bool PLSRenderContext::PerFlushLinkedList<T>::empty() const
+{
+    assert(!!m_head == !!m_tail);
+    return m_tail == nullptr;
+}
+
+template <typename T> T& PLSRenderContext::PerFlushLinkedList<T>::tail() const
+{
+    assert(!empty());
+    return m_tail->data;
+}
+
+template <typename T>
+template <typename... Args>
+void PLSRenderContext::PerFlushLinkedList<T>::emplace_back(PLSRenderContext* context, Args... args)
+{
+    Node* node = context->make<Node>(std::forward<Args>(args)...);
+    assert(!!m_head == !!m_tail);
+    if (m_head == nullptr)
+    {
+        m_head = node;
+    }
+    else
+    {
+        m_tail->next = node;
+    }
+    m_tail = node;
+}
+
 } // namespace rive::pls
diff --git a/include/rive/pls/pls_renderer.hpp b/include/rive/pls/pls_renderer.hpp
index 48df4ac..69ff21f 100644
--- a/include/rive/pls/pls_renderer.hpp
+++ b/include/rive/pls/pls_renderer.hpp
@@ -9,6 +9,7 @@
 #include "rive/pls/aligned_buffer.hpp"
 #include "rive/pls/fixed_queue.hpp"
 #include "rive/pls/pls.hpp"
+#include "rive/pls/pls_render_context.hpp"
 #include <vector>
 
 namespace rive
@@ -65,7 +66,6 @@
                     size_t endCurveIdx_,
                     size_t endRotationIdx_,
                     Vec2D midpoint_,
-                    size_t pathIdx_,
                     bool closed_,
                     size_t strokeJoinCount_) :
             endOfContour(endOfContour_),
@@ -73,7 +73,6 @@
             endCurveIdx(endCurveIdx_),
             endRotationIdx(endRotationIdx_),
             midpoint(midpoint_),
-            pathIdx(pathIdx_),
             closed(closed_),
             strokeJoinCount(strokeJoinCount_)
         {}
@@ -82,7 +81,6 @@
         size_t endCurveIdx;
         size_t endRotationIdx; // We measure rotations on both curves and round joins.
         Vec2D midpoint;
-        size_t pathIdx;
         bool closed;
         size_t strokeJoinCount;
         uint32_t strokeCapSegmentCount = 0;
@@ -149,6 +147,8 @@
         FillRule fillRule;
         uint32_t clipID;
         GrInnerFanTriangulator* triangulator = nullptr; // Non-null if using interior triangulation.
+        size_t firstContourIdx = 0;
+        size_t contourCount = 0;
         uint32_t tessVertexCount = 0;
         uint32_t paddingVertexCount = 0;
     };
diff --git a/renderer/d3d/pls_render_context_d3d.cpp b/renderer/d3d/pls_render_context_d3d.cpp
index d70738f..40bc53c 100644
--- a/renderer/d3d/pls_render_context_d3d.cpp
+++ b/renderer/d3d/pls_render_context_d3d.cpp
@@ -793,32 +793,32 @@
     ID3D11ShaderResourceView* gradTextureView = submitted_srv(gradTexelBufferRing());
     m_gpuContext->PSSetShaderResources(kGradTextureIdx, 1, &gradTextureView);
 
-    for (const DrawList* draw = m_drawList; draw; draw = draw->next)
+    for (const Draw& draw : m_drawList)
     {
-        if (draw->vertexOrInstanceCount == 0)
+        if (draw.vertexOrInstanceCount == 0)
         {
             continue;
         }
 
-        DrawType drawType = draw->drawType;
-        setPipelineLayoutAndShaders(drawType, draw->shaderFeatures);
+        DrawType drawType = draw.drawType;
+        setPipelineLayoutAndShaders(drawType, draw.shaderFeatures);
 
         switch (drawType)
         {
             case DrawType::midpointFanPatches:
             case DrawType::outerCurvePatches:
             {
-                PerDrawUniforms uniforms(draw->baseVertexOrInstance);
+                PerDrawUniforms uniforms(draw.baseVertexOrInstance);
                 m_gpuContext->UpdateSubresource(m_perDrawUniforms.Get(), 0, NULL, &uniforms, 0, 0);
                 m_gpuContext->DrawIndexedInstanced(PatchIndexCount(drawType),
-                                                   draw->vertexOrInstanceCount,
+                                                   draw.vertexOrInstanceCount,
                                                    PatchBaseIndex(drawType),
                                                    0,
-                                                   draw->baseVertexOrInstance);
+                                                   draw.baseVertexOrInstance);
                 break;
             }
             case DrawType::interiorTriangulation:
-                m_gpuContext->Draw(draw->vertexOrInstanceCount, draw->baseVertexOrInstance);
+                m_gpuContext->Draw(draw.vertexOrInstanceCount, draw.baseVertexOrInstance);
                 break;
         }
     }
diff --git a/renderer/gl/pls_render_context_gl.cpp b/renderer/gl/pls_render_context_gl.cpp
index 8da049c..b69ded7 100644
--- a/renderer/gl/pls_render_context_gl.cpp
+++ b/renderer/gl/pls_render_context_gl.cpp
@@ -407,21 +407,14 @@
 
     // Compile the draw programs before activating pixel local storage.
     // (ANGLE_shader_pixel_local_storage doesn't allow shader compilation while active.)
-    size_t drawIdx = 0;
-    auto drawPrograms = reinterpret_cast<const DrawProgram**>(
-        trivialPerFlushAllocator()->alloc(sizeof(void*) * m_drawListCount));
-    for (DrawList* draw = m_drawList; draw; draw = draw->next, ++drawIdx)
+    for (const Draw& draw : m_drawList)
     {
         // Compile the draw program before activating pixel local storage.
         // Cache specific compilations of draw.glsl by ShaderFeatures.
-        const ShaderFeatures& shaderFeatures = draw->shaderFeatures;
         uint32_t fragmentShaderKey =
-            ShaderUniqueKey(SourceType::wholeProgram, draw->drawType, shaderFeatures);
-        drawPrograms[drawIdx] =
-            &m_drawPrograms.try_emplace(fragmentShaderKey, this, draw->drawType, shaderFeatures)
-                 .first->second;
+            ShaderUniqueKey(SourceType::wholeProgram, draw.drawType, draw.shaderFeatures);
+        m_drawPrograms.try_emplace(fragmentShaderKey, this, draw.drawType, draw.shaderFeatures);
     }
-    assert(drawIdx == m_drawListCount);
 
     // Bind the currently-submitted buffer in the triangleBufferRing to its vertex array.
     if (m_maxTriangleVertexCount > 0)
@@ -444,16 +437,17 @@
     m_plsImpl->activatePixelLocalStorage(this, renderTarget(), loadAction, needsClipBuffer);
 
     // Execute the DrawList.
-    drawIdx = 0;
-    for (const DrawList* draw = m_drawList; draw; draw = draw->next, ++drawIdx)
+    for (const Draw& draw : m_drawList)
     {
-        if (draw->vertexOrInstanceCount == 0)
+        if (draw.vertexOrInstanceCount == 0)
         {
             continue;
         }
-        const DrawProgram* drawProgram = drawPrograms[drawIdx];
-        bindProgram(drawProgram->id());
-        switch (DrawType drawType = draw->drawType)
+        uint32_t fragmentShaderKey =
+            ShaderUniqueKey(SourceType::wholeProgram, draw.drawType, draw.shaderFeatures);
+        const DrawProgram& drawProgram = m_drawPrograms.find(fragmentShaderKey)->second;
+        bindProgram(drawProgram.id());
+        switch (DrawType drawType = draw.drawType)
         {
             case DrawType::midpointFanPatches:
             case DrawType::outerCurvePatches:
@@ -470,18 +464,18 @@
                                                            indexCount,
                                                            GL_UNSIGNED_SHORT,
                                                            indexOffset,
-                                                           draw->vertexOrInstanceCount,
-                                                           draw->baseVertexOrInstance);
+                                                           draw.vertexOrInstanceCount,
+                                                           draw.baseVertexOrInstance);
                 }
                 else
                 {
-                    glUniform1i(drawProgram->baseInstancePolyfillLocation(),
-                                draw->baseVertexOrInstance);
+                    glUniform1i(drawProgram.baseInstancePolyfillLocation(),
+                                draw.baseVertexOrInstance);
                     glDrawElementsInstanced(GL_TRIANGLES,
                                             indexCount,
                                             GL_UNSIGNED_SHORT,
                                             indexOffset,
-                                            draw->vertexOrInstanceCount);
+                                            draw.vertexOrInstanceCount);
                 }
                 break;
             }
@@ -489,12 +483,11 @@
                 // Draw generic triangles.
                 m_plsImpl->ensureRasterOrderingEnabled(false);
                 bindVAO(m_interiorTrianglesVAO);
-                glDrawArrays(GL_TRIANGLES, draw->baseVertexOrInstance, draw->vertexOrInstanceCount);
+                glDrawArrays(GL_TRIANGLES, draw.baseVertexOrInstance, draw.vertexOrInstanceCount);
                 m_plsImpl->barrier();
                 break;
         }
     }
-    assert(drawIdx == m_drawListCount);
 
     m_plsImpl->deactivatePixelLocalStorage(this);
 
diff --git a/renderer/metal/pls_render_context_metal.mm b/renderer/metal/pls_render_context_metal.mm
index cf48520..ff1f01e 100644
--- a/renderer/metal/pls_render_context_metal.mm
+++ b/renderer/metal/pls_render_context_metal.mm
@@ -420,21 +420,20 @@
     }
 
     // Execute the DrawList.
-    size_t drawIdx = 0;
-    for (const DrawList* draw = m_drawList; draw; draw = draw->next, ++drawIdx)
+    for (const Draw& draw : m_drawList)
     {
-        if (draw->vertexOrInstanceCount == 0)
+        if (draw.vertexOrInstanceCount == 0)
         {
             continue;
         }
 
-        DrawType drawType = draw->drawType;
+        DrawType drawType = draw.drawType;
 
         // Setup the pipeline for this specific drawType and shaderFeatures.
         uint32_t pipelineKey =
-            ShaderUniqueKey(SourceType::wholeProgram, drawType, draw->shaderFeatures);
+            ShaderUniqueKey(SourceType::wholeProgram, drawType, draw.shaderFeatures);
         const DrawPipeline& drawPipeline =
-            m_drawPipelines.try_emplace(pipelineKey, this, drawType, draw->shaderFeatures)
+            m_drawPipelines.try_emplace(pipelineKey, this, drawType, draw.shaderFeatures)
                 .first->second;
         [encoder setRenderPipelineState:drawPipeline.pipelineState(renderTarget->pixelFormat())];
 
@@ -450,17 +449,17 @@
                                      indexType:MTLIndexTypeUInt16
                                    indexBuffer:m_pathPatchIndexBuffer
                              indexBufferOffset:PatchBaseIndex(drawType) * sizeof(uint16_t)
-                                 instanceCount:draw->vertexOrInstanceCount
+                                 instanceCount:draw.vertexOrInstanceCount
                                     baseVertex:0
-                                  baseInstance:draw->baseVertexOrInstance];
+                                  baseInstance:draw.baseVertexOrInstance];
                 break;
             }
             case DrawType::interiorTriangulation:
                 // Draw generic triangles.
                 [encoder setVertexBuffer:mtl_buffer(triangleBufferRing()) offset:0 atIndex:1];
                 [encoder drawPrimitives:MTLPrimitiveTypeTriangle
-                            vertexStart:draw->baseVertexOrInstance
-                            vertexCount:draw->vertexOrInstanceCount];
+                            vertexStart:draw.baseVertexOrInstance
+                            vertexCount:draw.vertexOrInstanceCount];
                 break;
         }
     }
diff --git a/renderer/pls_render_context.cpp b/renderer/pls_render_context.cpp
index ef4ca31..e298797 100644
--- a/renderer/pls_render_context.cpp
+++ b/renderer/pls_render_context.cpp
@@ -651,12 +651,13 @@
     // The caller is responsible to pad each path so it begins on a multiple of the patch size.
     assert(baseInstance * patchSize == baseVertexToDraw);
     pushDraw(drawType, baseInstance, fillRule, paintType, clipID, blendMode);
-    assert(m_lastDraw->baseVertexOrInstance + m_lastDraw->vertexOrInstanceCount == baseInstance);
+    assert(m_drawList.tail().baseVertexOrInstance + m_drawList.tail().vertexOrInstanceCount ==
+           baseInstance);
     uint32_t vertexCountToDraw = tessVertexCount - paddingVertexCount;
     uint32_t instanceCount = vertexCountToDraw / patchSize;
     // The caller is responsible to pad each contour so it ends on a multiple of the patch size.
     assert(instanceCount * patchSize == vertexCountToDraw);
-    m_lastDraw->vertexOrInstanceCount += instanceCount;
+    m_drawList.tail().vertexOrInstanceCount += instanceCount;
 
     // The first curve of the path will be pre-padded with 'paddingVertexCount' tessellation
     // vertices, colocated at T=0. The caller must use this argument align the beginning of the path
@@ -768,7 +769,7 @@
              blendMode);
     m_maxTriangleVertexCount += triangulator->maxVertexCount();
     triangulator->setPathID(m_currentPathID);
-    m_lastDraw->triangulator = triangulator;
+    m_drawList.tail().triangulator = triangulator;
 }
 
 void PLSRenderContext::pushDraw(DrawType drawType,
@@ -778,22 +779,12 @@
                                 uint32_t clipID,
                                 PLSBlendMode blendMode)
 {
-    if (!m_lastDraw || m_lastDraw->drawType != drawType)
+    if (m_drawList.empty() || m_drawList.tail().drawType != drawType)
     {
-        // Can't merge with the previous draw. Push a new one.
-        DrawList* nextDraw = make<DrawList>(drawType, baseVertex);
-        if (!m_lastDraw)
-        {
-            m_drawList = nextDraw;
-        }
-        else
-        {
-            m_lastDraw->next = nextDraw;
-        }
-        m_lastDraw = nextDraw;
+        m_drawList.emplace_back(this, drawType, baseVertex);
         ++m_drawListCount;
     }
-    ShaderFeatures* shaderFeatures = &m_lastDraw->shaderFeatures;
+    ShaderFeatures* shaderFeatures = &m_drawList.tail().shaderFeatures;
     if (blendMode > PLSBlendMode::srcOver)
     {
         assert(paintType != PaintType::clipReplace);
@@ -861,31 +852,30 @@
     bool needsClipBuffer = false;
     RIVE_DEBUG_CODE(size_t drawIdx = 0;)
     size_t writtenTriangleVertexCount = 0;
-    for (DrawList* draw = m_drawList; draw; draw = draw->next)
+    for (Draw& draw : m_drawList)
     {
-        switch (draw->drawType)
+        switch (draw.drawType)
         {
             case DrawType::midpointFanPatches:
             case DrawType::outerCurvePatches:
                 break;
             case DrawType::interiorTriangulation:
             {
-                size_t maxVertexCount = draw->triangulator->maxVertexCount();
+                size_t maxVertexCount = draw.triangulator->maxVertexCount();
                 assert(writtenTriangleVertexCount + maxVertexCount <= m_maxTriangleVertexCount);
                 size_t actualVertexCount = maxVertexCount;
                 if (maxVertexCount > 0)
                 {
-                    actualVertexCount = draw->triangulator->polysToTriangles(&m_triangleBuffer);
+                    actualVertexCount = draw.triangulator->polysToTriangles(&m_triangleBuffer);
                 }
                 assert(actualVertexCount <= maxVertexCount);
-                draw->baseVertexOrInstance = writtenTriangleVertexCount;
-                draw->vertexOrInstanceCount = actualVertexCount;
+                draw.baseVertexOrInstance = writtenTriangleVertexCount;
+                draw.vertexOrInstanceCount = actualVertexCount;
                 writtenTriangleVertexCount += actualVertexCount;
                 break;
             }
         }
-        needsClipBuffer =
-            needsClipBuffer || draw->shaderFeatures.programFeatures.enablePathClipping;
+        needsClipBuffer = needsClipBuffer || draw.shaderFeatures.programFeatures.enablePathClipping;
         RIVE_DEBUG_CODE(++drawIdx;)
     }
     assert(drawIdx == m_drawListCount);
@@ -973,7 +963,7 @@
 
     m_isFirstFlushOfFrame = false;
 
-    m_drawList = m_lastDraw = nullptr;
+    m_drawList.reset();
     m_drawListCount = 0;
 
     // Delete all objects that were allocted for this flush using the TrivialBlockAllocator.
diff --git a/renderer/pls_renderer.cpp b/renderer/pls_renderer.cpp
index e379969..47049c2 100644
--- a/renderer/pls_renderer.cpp
+++ b/renderer/pls_renderer.cpp
@@ -11,7 +11,6 @@
 #include "rive/math/math_types.hpp"
 #include "rive/math/simd.hpp"
 #include "rive/math/wangs_formula.hpp"
-#include "rive/pls/pls_render_context.hpp"
 
 namespace rive::pls
 {
@@ -433,10 +432,12 @@
     // we only do this for large paths, and since we're triangulating the path interior anyway,
     // adding complexity to only run Wang's formula and chop once would save about ~5% of the total
     // CPU time. (And large paths are GPU-bound anyway.)
-    void processPath(PathOp op,
-                     PLSRenderContext* context,
-                     PathDraw* path,
-                     RawPath* scratchPath = nullptr)
+    //
+    // Returns the number of contours processed.
+    size_t processPath(PathOp op,
+                       PLSRenderContext* context,
+                       PathDraw* path,
+                       RawPath* scratchPath = nullptr)
     {
         Vec2D chops[kMaxCurveSubdivisions * 3 + 1];
         const RawPath& rawPath = *path->rawPath;
@@ -576,7 +577,7 @@
         }
         else
         {
-            // Submit grout triangles, emulated by outerCubic patches.
+            // Submit grout triangles, retrofitted into outerCubic patches.
             for (auto* node = path->triangulator->groutList().head(); node; node = node->fNext)
             {
                 Vec2D triangleAsCubic[4] = {node->fPts[0], node->fPts[1], {0, 0}, node->fPts[2]};
@@ -588,12 +589,14 @@
                                    kJoinSegmentCount);
                 ++patchCount;
             }
+            assert(contourCount == path->contourCount);
             assert(path->paddingVertexCount + patchCount * kOuterCurvePatchSegmentSpan ==
                    path->tessVertexCount);
             RIVE_DEBUG_CODE(m_writtenPatchCount += patchCount;)
-            RIVE_DEBUG_CODE(m_writtenTessVertexCount +=
-                            patchCount * (kPatchSegmentCountExcludingJoin + kJoinSegmentCount);)
+            RIVE_DEBUG_CODE(m_writtenTessVertexCount += patchCount * kOuterCurvePatchSegmentSpan;)
         }
+
+        return contourCount;
     }
 
 #ifdef DEBUG
@@ -706,6 +709,7 @@
     // Iteration pass 1: Collect information on contour and curves counts for every path in the
     // batch, and begin counting tessellated vertices.
     m_contourBatch.clear();
+    size_t contourCount = 0;
     size_t lineCount = 0;
     size_t curveCount = 0;
     size_t rotationCount = 0; // We measure rotations on both curves and round joins.
@@ -717,223 +721,226 @@
             continue;
         }
 
-        // Draw this path using "interior triangulation" if it is a fill, and is sufficiently large.
-        assert(path.triangulator == nullptr);
-        if (i != strokeIdx) // Never use interior triangulation for strokes.
-        {
-            float screenSpaceArea = FindTransformedArea(path.pathBounds, *path.matrix);
-            if (screenSpaceArea > 512 * 512)
-            {
-                interiorTriHelper.processPath(
-                    InteriorTriangulationHelper::PathOp::countDataAndTriangulate,
-                    m_context,
-                    &path,
-                    &m_scratchPath);
-                continue;
-            }
-        }
-
+        size_t pathContourCount = 0;
         bool stroked = i == strokeIdx; // (Will never be true if finalPathPaint is not stroked.)
-        bool roundJoinStroked = stroked && finalPathPaint->getJoin() == StrokeJoin::round;
-        wangs_formula::VectorXform vectorXform(*path.matrix);
-        RawPath::Iter startOfContour = path.rawPath->begin();
-        RawPath::Iter end = path.rawPath->end();
-        int preChopVerbCount = 0; // Original number of lines and curves, before chopping.
-        Vec2D endpointsSum{};
-        bool closed = !stroked;
-        Vec2D lastTangent = {0, 1};
-        Vec2D firstTangent = {0, 1};
-        size_t roundJoinCount = 0;
-        auto finishAndAppendContour = [&](RawPath::Iter iter) {
-            if (closed)
-            {
-                Vec2D finalPtInContour = iter.rawPtsPtr()[-1];
-                if (startOfContour.movePt() != finalPtInContour)
-                {
-                    assert(preChopVerbCount > 0);
-                    if (roundJoinStroked)
-                    {
-                        // Round join before implicit closing line.
-                        Vec2D tangent = startOfContour.movePt() - finalPtInContour;
-                        assert(rotationCount < m_tangentPairs.capacity());
-                        m_tangentPairs[rotationCount++] = {lastTangent, tangent};
-                        lastTangent = tangent;
-                        ++roundJoinCount;
-                    }
-                    ++lineCount; // Implicit closing line.
-                    // The first point in the contour hasn't gotten counted yet.
-                    ++preChopVerbCount;
-                    endpointsSum += startOfContour.movePt();
-                }
-                if (roundJoinStroked && preChopVerbCount != 0)
-                {
-                    // Round join back to the beginning of the contour.
-                    assert(rotationCount < m_tangentPairs.capacity());
-                    m_tangentPairs[rotationCount++] = {lastTangent, firstTangent};
-                    ++roundJoinCount;
-                }
-            }
-            size_t strokeJoinCount = preChopVerbCount;
-            if (!closed)
-            {
-                strokeJoinCount = std::max<size_t>(strokeJoinCount, 1) - 1;
-            }
-            m_contourBatch.emplace_back(iter,
-                                        lineCount,
-                                        curveCount,
-                                        rotationCount,
-                                        stroked ? Vec2D() : endpointsSum * (1.f / preChopVerbCount),
-                                        i,
-                                        closed,
-                                        strokeJoinCount);
-        };
-        const int styleFlags = style_flags(stroked, roundJoinStroked);
-        for (RawPath::Iter iter = startOfContour; iter != end; ++iter)
+        assert(path.triangulator == nullptr);
+        if (!stroked && FindTransformedArea(path.pathBounds, *path.matrix) > 512 * 512)
         {
-            switch (styled_verb(iter.verb(), styleFlags))
-            {
-                case StyledVerb::roundJoinStrokedMove:
-                case StyledVerb::strokedMove:
-                case StyledVerb::filledMove:
-                    if (iter != startOfContour)
-                    {
-                        finishAndAppendContour(iter);
-                        startOfContour = iter;
-                    }
-                    preChopVerbCount = 0;
-                    endpointsSum = {0, 0};
-                    closed = !stroked;
-                    lastTangent = {0, 1};
-                    firstTangent = {0, 1};
-                    roundJoinCount = 0;
-                    break;
-                case StyledVerb::roundJoinStrokedClose:
-                case StyledVerb::strokedClose:
-                case StyledVerb::filledClose:
-                    assert(iter != startOfContour);
-                    closed = true;
-                    break;
-                case StyledVerb::roundJoinStrokedLine:
+            // This path is a sufficiently-large fill. Use interior triangulation!
+            pathContourCount = interiorTriHelper.processPath(
+                InteriorTriangulationHelper::PathOp::countDataAndTriangulate,
+                m_context,
+                &path,
+                &m_scratchPath);
+        }
+        else
+        {
+            bool roundJoinStroked = stroked && finalPathPaint->getJoin() == StrokeJoin::round;
+            wangs_formula::VectorXform vectorXform(*path.matrix);
+            RawPath::Iter startOfContour = path.rawPath->begin();
+            RawPath::Iter end = path.rawPath->end();
+            int preChopVerbCount = 0; // Original number of lines and curves, before chopping.
+            Vec2D endpointsSum{};
+            bool closed = !stroked;
+            Vec2D lastTangent = {0, 1};
+            Vec2D firstTangent = {0, 1};
+            size_t roundJoinCount = 0;
+            path.firstContourIdx = m_contourBatch.size();
+            auto finishAndAppendContour = [&](RawPath::Iter iter) {
+                if (closed)
                 {
-                    const Vec2D* p = iter.linePts();
-                    Vec2D tangent = p[1] - p[0];
-                    if (preChopVerbCount == 0)
+                    Vec2D finalPtInContour = iter.rawPtsPtr()[-1];
+                    if (startOfContour.movePt() != finalPtInContour)
                     {
-                        firstTangent = tangent;
-                    }
-                    else
-                    {
-                        assert(rotationCount < m_tangentPairs.capacity());
-                        m_tangentPairs[rotationCount++] = {lastTangent, tangent};
-                        ++roundJoinCount;
-                    }
-                    lastTangent = tangent;
-                    [[fallthrough]];
-                }
-                case StyledVerb::strokedLine:
-                case StyledVerb::filledLine:
-                {
-                    const Vec2D* p = iter.linePts();
-                    ++preChopVerbCount;
-                    endpointsSum += p[1];
-                    ++lineCount;
-                    break;
-                }
-                case StyledVerb::roundJoinStrokedQuad:
-                case StyledVerb::strokedQuad:
-                case StyledVerb::filledQuad:
-                    RIVE_UNREACHABLE();
-                    break;
-                case StyledVerb::roundJoinStrokedCubic:
-                {
-                    const Vec2D* p = iter.cubicPts();
-                    Vec2D unchoppedTangents[2];
-                    find_cubic_tangents(p, unchoppedTangents);
-                    if (preChopVerbCount == 0)
-                    {
-                        firstTangent = unchoppedTangents[0];
-                    }
-                    else
-                    {
-                        assert(rotationCount < m_tangentPairs.capacity());
-                        m_tangentPairs[rotationCount++] = {lastTangent, unchoppedTangents[0]};
-                        ++roundJoinCount;
-                    }
-                    lastTangent = unchoppedTangents[1];
-                    [[fallthrough]];
-                }
-                case StyledVerb::strokedCubic:
-                {
-                    const Vec2D* p = iter.cubicPts();
-                    ++preChopVerbCount;
-                    endpointsSum += p[3];
-                    // Chop strokes into sections that do not inflect (i.e, are convex), and do not
-                    // rotate more than 180 degrees. This is required by the GPU parametric/polar
-                    // sorter.
-                    float t[2];
-                    bool areCusps;
-                    uint8_t numChops = pathutils::FindCubicConvex180Chops(p, t, &areCusps);
-                    uint8_t chopKey = chop_key(areCusps, numChops);
-                    m_numChops.push_back(chopKey);
-                    Vec2D localChopBuffer[16];
-                    switch (chopKey)
-                    {
-                        case cusp_chop_key(2): // 2 cusps
-                        case cusp_chop_key(1): // 1 cusp
-                            // We have to chop carefully around stroked cusps in order to avoid
-                            // rendering artifacts. Luckily, cusps are extremely rare in real-world
-                            // content.
-                            m_chops.push_back() = {t[0], t[1]};
-                            chop_cubic_around_cusps(p,
-                                                    localChopBuffer,
-                                                    t,
-                                                    numChops,
-                                                    strokeMatrixMaxScale);
-                            p = localChopBuffer;
-                            numChops *= 2;
-                            break;
-                        case simple_chop_key(2): // 2 non-cusp chops
-                            m_chops.push_back() = {t[0], t[1]};
-                            pathutils::ChopCubicAt(p, localChopBuffer, t[0], t[1]);
-                            p = localChopBuffer;
-                            break;
-                        case simple_chop_key(1): // 1 non-cusp chop
+                        assert(preChopVerbCount > 0);
+                        if (roundJoinStroked)
                         {
-                            Vec2D* buff = m_chops.push_back_n(7);
-                            pathutils::ChopCubicAt(p, buff, t[0]);
-                            p = buff;
-                            break;
+                            // Round join before implicit closing line.
+                            Vec2D tangent = startOfContour.movePt() - finalPtInContour;
+                            assert(rotationCount < m_tangentPairs.capacity());
+                            m_tangentPairs[rotationCount++] = {lastTangent, tangent};
+                            lastTangent = tangent;
+                            ++roundJoinCount;
                         }
+                        ++lineCount; // Implicit closing line.
+                        // The first point in the contour hasn't gotten counted yet.
+                        ++preChopVerbCount;
+                        endpointsSum += startOfContour.movePt();
                     }
-                    // Calculate segment counts for each chopped section independently.
-                    for (const Vec2D* end = p + numChops * 3 + 3; p != end;
-                         p += 3, ++curveCount, ++rotationCount)
+                    if (roundJoinStroked && preChopVerbCount != 0)
                     {
-                        float n4 = wangs_formula::cubic_pow4(p, kParametricPrecision, vectorXform);
-                        m_parametricSegmentCounts_pow4[curveCount] = n4;
+                        // Round join back to the beginning of the contour.
                         assert(rotationCount < m_tangentPairs.capacity());
-                        find_cubic_tangents(p, m_tangentPairs[rotationCount].data());
+                        m_tangentPairs[rotationCount++] = {lastTangent, firstTangent};
+                        ++roundJoinCount;
                     }
-                    break;
                 }
-                case StyledVerb::filledCubic:
+                size_t strokeJoinCount = preChopVerbCount;
+                if (!closed)
                 {
-                    const Vec2D* p = iter.cubicPts();
-                    ++preChopVerbCount;
-                    endpointsSum += p[3];
-                    float n4 = wangs_formula::cubic_pow4(p, kParametricPrecision, vectorXform);
-                    m_parametricSegmentCounts_pow4[curveCount++] = n4;
-                    break;
+                    strokeJoinCount = std::max<size_t>(strokeJoinCount, 1) - 1;
+                }
+                m_contourBatch.emplace_back(iter,
+                                            lineCount,
+                                            curveCount,
+                                            rotationCount,
+                                            stroked ? Vec2D()
+                                                    : endpointsSum * (1.f / preChopVerbCount),
+                                            closed,
+                                            strokeJoinCount);
+                ++pathContourCount;
+            };
+            const int styleFlags = style_flags(stroked, roundJoinStroked);
+            for (RawPath::Iter iter = startOfContour; iter != end; ++iter)
+            {
+                switch (styled_verb(iter.verb(), styleFlags))
+                {
+                    case StyledVerb::roundJoinStrokedMove:
+                    case StyledVerb::strokedMove:
+                    case StyledVerb::filledMove:
+                        if (iter != startOfContour)
+                        {
+                            finishAndAppendContour(iter);
+                            startOfContour = iter;
+                        }
+                        preChopVerbCount = 0;
+                        endpointsSum = {0, 0};
+                        closed = !stroked;
+                        lastTangent = {0, 1};
+                        firstTangent = {0, 1};
+                        roundJoinCount = 0;
+                        break;
+                    case StyledVerb::roundJoinStrokedClose:
+                    case StyledVerb::strokedClose:
+                    case StyledVerb::filledClose:
+                        assert(iter != startOfContour);
+                        closed = true;
+                        break;
+                    case StyledVerb::roundJoinStrokedLine:
+                    {
+                        const Vec2D* p = iter.linePts();
+                        Vec2D tangent = p[1] - p[0];
+                        if (preChopVerbCount == 0)
+                        {
+                            firstTangent = tangent;
+                        }
+                        else
+                        {
+                            assert(rotationCount < m_tangentPairs.capacity());
+                            m_tangentPairs[rotationCount++] = {lastTangent, tangent};
+                            ++roundJoinCount;
+                        }
+                        lastTangent = tangent;
+                        [[fallthrough]];
+                    }
+                    case StyledVerb::strokedLine:
+                    case StyledVerb::filledLine:
+                    {
+                        const Vec2D* p = iter.linePts();
+                        ++preChopVerbCount;
+                        endpointsSum += p[1];
+                        ++lineCount;
+                        break;
+                    }
+                    case StyledVerb::roundJoinStrokedQuad:
+                    case StyledVerb::strokedQuad:
+                    case StyledVerb::filledQuad:
+                        RIVE_UNREACHABLE();
+                        break;
+                    case StyledVerb::roundJoinStrokedCubic:
+                    {
+                        const Vec2D* p = iter.cubicPts();
+                        Vec2D unchoppedTangents[2];
+                        find_cubic_tangents(p, unchoppedTangents);
+                        if (preChopVerbCount == 0)
+                        {
+                            firstTangent = unchoppedTangents[0];
+                        }
+                        else
+                        {
+                            assert(rotationCount < m_tangentPairs.capacity());
+                            m_tangentPairs[rotationCount++] = {lastTangent, unchoppedTangents[0]};
+                            ++roundJoinCount;
+                        }
+                        lastTangent = unchoppedTangents[1];
+                        [[fallthrough]];
+                    }
+                    case StyledVerb::strokedCubic:
+                    {
+                        const Vec2D* p = iter.cubicPts();
+                        ++preChopVerbCount;
+                        endpointsSum += p[3];
+                        // Chop strokes into sections that do not inflect (i.e, are convex), and do
+                        // not rotate more than 180 degrees. This is required by the GPU
+                        // parametric/polar sorter.
+                        float t[2];
+                        bool areCusps;
+                        uint8_t numChops = pathutils::FindCubicConvex180Chops(p, t, &areCusps);
+                        uint8_t chopKey = chop_key(areCusps, numChops);
+                        m_numChops.push_back(chopKey);
+                        Vec2D localChopBuffer[16];
+                        switch (chopKey)
+                        {
+                            case cusp_chop_key(2): // 2 cusps
+                            case cusp_chop_key(1): // 1 cusp
+                                // We have to chop carefully around stroked cusps in order to avoid
+                                // rendering artifacts. Luckily, cusps are extremely rare in
+                                // real-world content.
+                                m_chops.push_back() = {t[0], t[1]};
+                                chop_cubic_around_cusps(p,
+                                                        localChopBuffer,
+                                                        t,
+                                                        numChops,
+                                                        strokeMatrixMaxScale);
+                                p = localChopBuffer;
+                                numChops *= 2;
+                                break;
+                            case simple_chop_key(2): // 2 non-cusp chops
+                                m_chops.push_back() = {t[0], t[1]};
+                                pathutils::ChopCubicAt(p, localChopBuffer, t[0], t[1]);
+                                p = localChopBuffer;
+                                break;
+                            case simple_chop_key(1): // 1 non-cusp chop
+                            {
+                                Vec2D* buff = m_chops.push_back_n(7);
+                                pathutils::ChopCubicAt(p, buff, t[0]);
+                                p = buff;
+                                break;
+                            }
+                        }
+                        // Calculate segment counts for each chopped section independently.
+                        for (const Vec2D* end = p + numChops * 3 + 3; p != end;
+                             p += 3, ++curveCount, ++rotationCount)
+                        {
+                            float n4 =
+                                wangs_formula::cubic_pow4(p, kParametricPrecision, vectorXform);
+                            m_parametricSegmentCounts_pow4[curveCount] = n4;
+                            assert(rotationCount < m_tangentPairs.capacity());
+                            find_cubic_tangents(p, m_tangentPairs[rotationCount].data());
+                        }
+                        break;
+                    }
+                    case StyledVerb::filledCubic:
+                    {
+                        const Vec2D* p = iter.cubicPts();
+                        ++preChopVerbCount;
+                        endpointsSum += p[3];
+                        float n4 = wangs_formula::cubic_pow4(p, kParametricPrecision, vectorXform);
+                        m_parametricSegmentCounts_pow4[curveCount++] = n4;
+                        break;
+                    }
                 }
             }
+            if (startOfContour != end)
+            {
+                finishAndAppendContour(end);
+            }
         }
-        if (startOfContour != end)
-        {
-            finishAndAppendContour(end);
-        }
+        path.contourCount = pathContourCount;
+        contourCount += pathContourCount;
     }
 
-    if (m_contourBatch.empty() && interiorTriHelper.empty())
+    if (contourCount == 0)
     {
         // The entire batch is empty.
         return true;
@@ -947,8 +954,6 @@
     size_t contourFirstCurveIdx = 0;
     size_t contourFirstRotationIdx = 0;
     size_t emptyStrokeCountForCaps = 0;
-    auto contour = m_contourBatch.begin();
-    auto endContour = m_contourBatch.end();
     for (size_t currentPathIdx = 0; currentPathIdx < m_pathBatch.size(); ++currentPathIdx)
     {
         PathDraw& path = m_pathBatch[currentPathIdx];
@@ -962,9 +967,9 @@
         if (path.triangulator == nullptr)
         {
             assert(path.tessVertexCount == 0);
-            assert(contour == endContour || contour->pathIdx >= currentPathIdx);
-            for (; contour != endContour && contour->pathIdx == currentPathIdx; ++contour)
+            for (size_t i = 0; i < path.contourCount; ++i)
             {
+                ContourData* contour = &m_contourBatch[path.firstContourIdx + i];
                 size_t contourLineCount = contour->endLineIdx - contourFirstLineIdx;
                 uint32_t contourVertexCount =
                     contourLineCount * 2; // Each line tessellates to 2 vertices.
@@ -992,7 +997,7 @@
                     contourVertexCount -= m_parametricSegmentCounts[j];
                 }
 
-                bool stroked = contour->pathIdx == strokeIdx;
+                bool stroked = currentPathIdx == strokeIdx;
                 if (stroked)
                 {
                     // Finish calculating and counting polar segments for each stroked curve and
@@ -1150,13 +1155,10 @@
     assert(contourFirstRotationIdx == rotationCount);
 
     // Attempt to reserve space on the GPU for our entire batch of paths.
-    size_t pathReserveCount = m_pathBatch.size();
-    // (Interior triangulation doesn't write contour records to the GPU.)
-    size_t contourReserveCount = m_contourBatch.size();
     size_t curveReserveCount =
         curveCount + lineCount + emptyStrokeCountForCaps + interiorTriHelper.patchCount();
-    if (!m_context->reservePathData(pathReserveCount,
-                                    contourReserveCount,
+    if (!m_context->reservePathData(m_pathBatch.size(),
+                                    contourCount,
                                     curveReserveCount,
                                     batchTotalTessVertexCount))
     {
@@ -1186,19 +1188,12 @@
     size_t rotationIdx = 0;
     RawPath::Iter startOfContour;
     size_t finalPathIdx = m_pathBatch.size() - 1; // All paths are clips except the final one.
-    contour = m_contourBatch.begin();
-    endContour = m_contourBatch.end();
     for (size_t currentPathIdx = 0; currentPathIdx < m_pathBatch.size(); ++currentPathIdx)
     {
-        assert(contour == endContour || contour->pathIdx >= currentPathIdx);
-
         PathDraw& path = m_pathBatch[currentPathIdx];
         if (path.tessVertexCount == 0)
         {
-            for (; contour != endContour && contour->pathIdx == currentPathIdx; ++contour)
-            {
-                RIVE_DEBUG_CODE(++skippedContourCount;)
-            }
+            RIVE_DEBUG_CODE(skippedContourCount += path.contourCount;)
             RIVE_DEBUG_CODE(++skippedPathCount;)
             continue;
         }
@@ -1227,9 +1222,11 @@
         if (path.triangulator != nullptr)
         {
             // This path is drawn with the interior triangulation algorithm instead.
-            interiorTriHelper.processPath(InteriorTriangulationHelper::PathOp::submitOuterCubics,
-                                          m_context,
-                                          &path);
+            size_t processedContourCount RIVE_MAYBE_UNUSED = interiorTriHelper.processPath(
+                InteriorTriangulationHelper::PathOp::submitOuterCubics,
+                m_context,
+                &path);
+            RIVE_DEBUG_CODE(pushedContourCount += processedContourCount;)
             m_context->pushInteriorTriangulation(path.triangulator,
                                                  paintType,
                                                  path.clipID,
@@ -1242,27 +1239,28 @@
             RIVE_DEBUG_CODE(uint32_t contourStartingTessVertexCount =
                                 m_context->currentTessVertexCount() + path.paddingVertexCount;)
             startOfContour = path.rawPath->begin();
-            for (; contour != endContour && contour->pathIdx == currentPathIdx; ++contour)
+            for (size_t i = 0; i < path.contourCount; ++i)
             {
                 // Push a contour and curve records.
+                const ContourData& contour = m_contourBatch[path.firstContourIdx + i];
                 RIVE_DEBUG_CODE(m_pushedStrokeJoinCount = 0;)
                 RIVE_DEBUG_CODE(m_pushedStrokeCapCount = 0;)
                 pushContour(startOfContour,
-                            *contour,
+                            contour,
                             curveIdx,
                             rotationIdx,
                             strokeMatrixMaxScale,
                             currentPathIdx == strokeIdx ? finalPathPaint : nullptr);
-                assert(m_pushedCurveCount == contour->endCurveIdx);
-                assert(m_pushedRotationCount == contour->endRotationIdx);
+                assert(m_pushedCurveCount == contour.endCurveIdx);
+                assert(m_pushedRotationCount == contour.endRotationIdx);
                 assert(m_pushedStrokeJoinCount ==
-                       (currentPathIdx == strokeIdx ? contour->strokeJoinCount : 0));
-                assert(m_pushedStrokeCapCount == (contour->strokeCapSegmentCount != 0 ? 2 : 0));
+                       (currentPathIdx == strokeIdx ? contour.strokeJoinCount : 0));
+                assert(m_pushedStrokeCapCount == (contour.strokeCapSegmentCount != 0 ? 2 : 0));
                 assert(m_context->currentTessVertexCount() ==
-                       contourStartingTessVertexCount + contour->tessVertexCount);
-                curveIdx = contour->endCurveIdx;
-                rotationIdx = contour->endRotationIdx;
-                startOfContour = contour->endOfContour;
+                       contourStartingTessVertexCount + contour.tessVertexCount);
+                curveIdx = contour.endCurveIdx;
+                rotationIdx = contour.endRotationIdx;
+                startOfContour = contour.endOfContour;
                 RIVE_DEBUG_CODE(++pushedContourCount);
                 RIVE_DEBUG_CODE(contourStartingTessVertexCount =
                                     m_context->currentTessVertexCount();)
@@ -1273,8 +1271,8 @@
     }
 
     // Make sure we only pushed the amount of data we reserved.
-    assert(pushedPathCount + skippedPathCount == pathReserveCount);
-    assert(pushedContourCount + skippedContourCount == contourReserveCount);
+    assert(pushedPathCount + skippedPathCount == m_pathBatch.size());
+    assert(pushedContourCount + skippedContourCount == contourCount);
     assert(m_pushedLineCount == lineCount);
     assert(m_pushedCurveCount == curveCount);
     assert(m_pushedRotationCount == rotationCount);