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);