| /* |
| * Copyright 2022 Rive |
| */ |
| |
| #include "rive/pls/pls_renderer.hpp" |
| |
| #include "gr_inner_fan_triangulator.hpp" |
| #include "path_utils.hpp" |
| #include "pls_paint.hpp" |
| #include "pls_path.hpp" |
| #include "rive/math/math_types.hpp" |
| #include "rive/math/simd.hpp" |
| #include "rive/math/wangs_formula.hpp" |
| |
| namespace rive::pls |
| { |
| constexpr static int kNumSegmentsInMiterOrBevelJoin = 5; |
| |
| PLSRenderer::PLSRenderer(PLSRenderContext* context) : m_context(context) {} |
| |
| PLSRenderer::~PLSRenderer() {} |
| |
| void PLSRenderer::save() |
| { |
| // Copy the matrix before pushing, in case the vector grows and invalidates the reference. |
| Mat2D matrixCopy = m_stack.back().matrix; |
| m_stack.emplace_back(matrixCopy, m_clipStack.size()); |
| } |
| |
| void PLSRenderer::restore() |
| { |
| assert(!m_stack.empty()); |
| assert(m_clipStack.size() >= m_stack.back().clipStackHeight); |
| m_clipStack.resize(m_stack.back().clipStackHeight); |
| if (m_clipStack.empty()) |
| { |
| m_hasArtboardClipCandidate = false; |
| } |
| m_stack.pop_back(); |
| } |
| |
| void PLSRenderer::transform(const Mat2D& matrix) |
| { |
| m_stack.back().matrix = m_stack.back().matrix * matrix; |
| } |
| |
| bool PLSRenderer::applyClip(uint32_t* clipID) |
| { |
| if (m_clipStack.empty()) |
| { |
| *clipID = 0; |
| return true; |
| } |
| |
| // For now, only apply the final element of the clip stack. |
| ClipElement& clip = m_clipStack.back(); |
| // Ignore the first clip for now if it looks like an artboard clip. |
| if (m_clipStack.size() == 1 && m_hasArtboardClipCandidate) |
| { |
| *clipID = 0; |
| return true; |
| } |
| |
| if (clip.clipID == 0) |
| { |
| // This clip element doesn't have an ID yet. Assign one. |
| clip.clipID = m_context->generateClipID(); |
| if (clip.clipID == 0) |
| { |
| return false; // The context is out of clip IDs. We will flush and try again. |
| } |
| } |
| |
| if (m_context->getClipContentID() != clip.clipID) |
| { |
| // The clip buffer does not contain the current clip stack. Update it. |
| m_pathBatch.emplace_back(&clip.matrix, |
| &clip.path, |
| clip.pathBounds, |
| clip.fillRule, |
| clip.clipID); |
| m_context->setClipContentID(clip.clipID); |
| } |
| assert(clip.clipID != 0); |
| *clipID = clip.clipID; |
| return true; |
| } |
| |
| void PLSRenderer::drawPath(RenderPath* renderPath, RenderPaint* renderPaint) |
| { |
| PLSPath* path = static_cast<PLSPath*>(renderPath); |
| PLSPaint* paint = static_cast<PLSPaint*>(renderPaint); |
| |
| bool stroked = paint->getIsStroked(); |
| if (stroked && m_context->frameDescriptor().strokesDisabled) |
| { |
| return; |
| } |
| if (!stroked && m_context->frameDescriptor().fillsDisabled) |
| { |
| return; |
| } |
| |
| // A stroke width of zero means a path is filled in PLS. |
| if (stroked && paint->getThickness() <= 0) |
| { |
| return; |
| } |
| |
| // Make (up to) two attempts to draw the path plus any necessary clip updates in a single batch. |
| // If the first attempt fails, flush to make room and try again. |
| for (size_t i = 0; i < 2; ++i) |
| { |
| m_pathBatch.clear(); |
| uint32_t clipID; |
| if (!applyClip(&clipID)) |
| { |
| intermediateFlush(); |
| continue; |
| } |
| m_pathBatch.emplace_back(&m_stack.back().matrix, |
| &path->getRawPath(), |
| path->getBounds(), |
| path->getFillRule(), |
| clipID); |
| if (!pushInternalPathBatch(paint)) |
| { |
| intermediateFlush(); |
| continue; |
| } |
| return; |
| } |
| |
| fprintf( |
| stderr, |
| "PLSRenderer::drawPath failed. The path and/or clip stack and/or paint are too complex.\n"); |
| } |
| |
| void PLSRenderer::clipPath(RenderPath* renderPath) |
| { |
| PLSPath* path = static_cast<PLSPath*>(renderPath); |
| // If the first clip in the stack is an axis-aligned rectangle, assume it's the artboard clip. |
| if (m_clipStack.empty()) |
| { |
| m_hasArtboardClipCandidate = IsAABB(path->getRawPath()); |
| } |
| m_clipStack.push_back( |
| {m_stack.back().matrix, path->getRawPath(), path->getBounds(), path->getFillRule(), 0}); |
| } |
| |
| void PLSRenderer::drawImage(const RenderImage*, BlendMode, float opacity) {} |
| |
| void PLSRenderer::drawImageMesh(const RenderImage*, |
| rcp<RenderBuffer> vertices_f32, |
| rcp<RenderBuffer> uvCoords_f32, |
| rcp<RenderBuffer> indices_u16, |
| BlendMode, |
| float opacity) |
| {} |
| |
| namespace |
| { |
| constexpr static int kStrokeStyleFlag = 8; |
| constexpr static int kRoundJoinStyleFlag = kStrokeStyleFlag << 1; |
| RIVE_ALWAYS_INLINE constexpr int style_flags(bool stroked, bool roundJoinStroked) |
| { |
| int styleFlags = (stroked << 3) | (roundJoinStroked << 4); |
| assert(bool(styleFlags & kStrokeStyleFlag) == stroked); |
| assert(bool(styleFlags & kRoundJoinStyleFlag) == roundJoinStroked); |
| return styleFlags; |
| } |
| |
| // Switching on a StyledVerb reduces "if (stroked)" branching and makes the code cleaner. |
| enum class StyledVerb |
| { |
| filledMove = static_cast<int>(PathVerb::move), |
| strokedMove = kStrokeStyleFlag | static_cast<int>(PathVerb::move), |
| roundJoinStrokedMove = |
| kStrokeStyleFlag | kRoundJoinStyleFlag | static_cast<int>(PathVerb::move), |
| |
| filledLine = static_cast<int>(PathVerb::line), |
| strokedLine = kStrokeStyleFlag | static_cast<int>(PathVerb::line), |
| roundJoinStrokedLine = |
| kStrokeStyleFlag | kRoundJoinStyleFlag | static_cast<int>(PathVerb::line), |
| |
| filledQuad = static_cast<int>(PathVerb::quad), |
| strokedQuad = kStrokeStyleFlag | static_cast<int>(PathVerb::quad), |
| roundJoinStrokedQuad = |
| kStrokeStyleFlag | kRoundJoinStyleFlag | static_cast<int>(PathVerb::quad), |
| |
| filledCubic = static_cast<int>(PathVerb::cubic), |
| strokedCubic = kStrokeStyleFlag | static_cast<int>(PathVerb::cubic), |
| roundJoinStrokedCubic = |
| kStrokeStyleFlag | kRoundJoinStyleFlag | static_cast<int>(PathVerb::cubic), |
| |
| filledClose = static_cast<int>(PathVerb::close), |
| strokedClose = kStrokeStyleFlag | static_cast<int>(PathVerb::close), |
| roundJoinStrokedClose = |
| kStrokeStyleFlag | kRoundJoinStyleFlag | static_cast<int>(PathVerb::close), |
| }; |
| RIVE_ALWAYS_INLINE constexpr StyledVerb styled_verb(PathVerb verb, int styleFlags) |
| { |
| return static_cast<StyledVerb>(styleFlags | static_cast<int>(verb)); |
| } |
| |
| // When chopping strokes, switching on a "chop_key" reduces "if (areCusps)" branching and makes the |
| // code cleaner. |
| RIVE_ALWAYS_INLINE constexpr uint8_t chop_key(bool areCusps, uint8_t numChops) |
| { |
| return (numChops << 1) | static_cast<uint8_t>(areCusps); |
| } |
| RIVE_ALWAYS_INLINE constexpr uint8_t cusp_chop_key(uint8_t n) { return chop_key(true, n); } |
| RIVE_ALWAYS_INLINE constexpr uint8_t simple_chop_key(uint8_t n) { return chop_key(false, n); } |
| |
| // Produces a cubic equivalent to the given line, for which Wang's formula also returns 1. |
| RIVE_ALWAYS_INLINE std::array<Vec2D, 4> convert_line_to_cubic(const Vec2D line[2]) |
| { |
| float4 endPts = simd::load4f(line); |
| float4 controlPts = simd::mix(endPts, endPts.zwxy, float4(1 / 3.f)); |
| std::array<Vec2D, 4> cubic; |
| cubic[0] = line[0]; |
| simd::store(&cubic[1], controlPts); |
| cubic[3] = line[1]; |
| return cubic; |
| } |
| RIVE_ALWAYS_INLINE std::array<Vec2D, 4> convert_line_to_cubic(Vec2D p0, Vec2D p1) |
| { |
| Vec2D line[2] = {p0, p1}; |
| return convert_line_to_cubic(line); |
| } |
| |
| // Finds the tangents of the curve at T=0 and T=1 respectively. |
| RIVE_ALWAYS_INLINE Vec2D find_cubic_tan0(const Vec2D p[4]) |
| { |
| Vec2D tan0 = (p[0] != p[1] ? p[1] : p[1] != p[2] ? p[2] : p[3]) - p[0]; |
| // RawPath should have discarded empty cubics, and FindCubicConvex180Chops should have enough |
| // slop to not produce empty chops. |
| assert((tan0 != Vec2D{0, 0})); |
| return tan0; |
| } |
| RIVE_ALWAYS_INLINE Vec2D find_cubic_tan1(const Vec2D p[4]) |
| { |
| Vec2D tan1 = p[3] - (p[3] != p[2] ? p[2] : p[2] != p[1] ? p[1] : p[0]); |
| // RawPath should have discarded empty cubics, and FindCubicConvex180Chops should have enough |
| // slop to not produce empty chops. |
| assert((tan1 != Vec2D{0, 0})); |
| return tan1; |
| } |
| RIVE_ALWAYS_INLINE void find_cubic_tangents(const Vec2D p[4], Vec2D tangents[2]) |
| { |
| tangents[0] = find_cubic_tan0(p); |
| tangents[1] = find_cubic_tan1(p); |
| } |
| |
| // Chops a cubic into 2 * n + 1 segments, surrounding each cusp. The resulting cubics will be |
| // visually equivalent to the original when stroked, but the cusp won't have artifacts when rendered |
| // using the parametric/polar sorting algorithm. |
| // |
| // The size of dst[] must be 6 * n + 4 Vec2Ds. |
| static void chop_cubic_around_cusps(const Vec2D p[4], |
| Vec2D dst[/*6 * n + 4*/], |
| const float cuspT[], |
| int n, |
| float matrixMaxScale) |
| { |
| float t[4]; |
| assert(n * 2 <= std::size(t)); |
| // Generate chop points straddling each cusp with padding. This creates buffer space around the |
| // cusp that protects against fp32 precision issues. |
| for (int i = 0; i < n; ++i) |
| { |
| // If the cusps are extremely close together, don't allow the straddle points to cross. |
| float minT = i == 0 ? 0.f : (cuspT[i - 1] + cuspT[i]) * .5f; |
| float maxT = i + 1 == n ? 1.f : (cuspT[i + 1] + cuspT[i]) * .5f; |
| t[i * 2 + 0] = std::max(cuspT[i] - math::EPSILON, minT); |
| t[i * 2 + 1] = std::min(cuspT[i] + math::EPSILON, maxT); |
| } |
| pathutils::ChopCubicAt(p, dst, t, n * 2); |
| for (int i = 0; i < n; ++i) |
| { |
| // Find the three chops at this cusp. |
| Vec2D* chops = dst + i * 6; |
| // Correct the chops to fall on the actual cusp point. |
| Vec2D cusp = pathutils::EvalCubicAt(p, cuspT[i]); |
| chops[3] = chops[6] = cusp; |
| // The only purpose of the middle cubic is to capture the cusp's 180-degree rotation. |
| // Implement it as a sub-pixel 180-degree pivot. |
| Vec2D pivot = (chops[2] + chops[7]) * .5f; |
| pivot = (cusp - pivot).normalized() / (matrixMaxScale * kPolarPrecision * 2) + cusp; |
| chops[4] = chops[5] = pivot; |
| } |
| } |
| |
| // Finds the starting tangent in a contour composed of the points [pts, end). If all points are |
| // equal, generates a tangent pointing horizontally to the right. |
| static Vec2D find_starting_tangent(const Vec2D pts[], const Vec2D* end) |
| { |
| assert(end > pts); |
| const Vec2D p0 = pts[0]; |
| while (++pts < end) |
| { |
| Vec2D p = *pts; |
| if (p != p0) |
| { |
| return p - p0; |
| } |
| } |
| return {1, 0}; |
| } |
| |
| // Finds the ending tangent in a contour composed of the points [pts, end). If all points are equal, |
| // generates a tangent pointing horizontally to the left. |
| static Vec2D find_ending_tangent(const Vec2D pts[], const Vec2D* end) |
| { |
| assert(end > pts); |
| const Vec2D endpoint = end[-1]; |
| while (--end > pts) |
| { |
| Vec2D p = end[-1]; |
| if (p != endpoint) |
| { |
| return endpoint - p; |
| } |
| } |
| return {-1, 0}; |
| } |
| |
| static Vec2D find_join_tangent_full_impl(const Vec2D* joinPoint, |
| const Vec2D* end, |
| bool closed, |
| const Vec2D* p0) |
| { |
| // Find the first point in the contour not equal to *joinPoint and return the difference. |
| // RawPath should have discarded empty verbs, so this should be a fast operation. |
| for (const Vec2D* p = joinPoint + 1; p != end; ++p) |
| { |
| if (*p != *joinPoint) |
| { |
| return *p - *joinPoint; |
| } |
| } |
| if (closed) |
| { |
| for (const Vec2D* p = p0; p != joinPoint; ++p) |
| { |
| if (*p != *joinPoint) |
| { |
| return *p - *joinPoint; |
| } |
| } |
| } |
| // This should never be reached because RawPath discards empty verbs. |
| RIVE_UNREACHABLE(); |
| } |
| |
| RIVE_ALWAYS_INLINE Vec2D find_join_tangent(const Vec2D* joinPoint, |
| const Vec2D* end, |
| bool closed, |
| const Vec2D* p0) |
| { |
| // Quick early out for inlining and branch prediction: The next point in the contour is almost |
| // always the point that determines the join tangent. |
| const Vec2D* nextPoint = joinPoint + 1; |
| nextPoint = nextPoint != end ? nextPoint : p0; |
| Vec2D tangent = *nextPoint - *joinPoint; |
| return tangent != Vec2D{0, 0} ? tangent |
| : find_join_tangent_full_impl(joinPoint, end, closed, p0); |
| } |
| |
| // Should an empty stroke emit round caps, square caps, or none? |
| // |
| // Just pick the cap type that makes the most sense for a contour that animates from non-empty to |
| // empty: |
| // |
| // * A non-closed contour with round caps and a CLOSED contour with round JOINS both converge to a |
| // circle when animated to empty. |
| // => round caps on the empty contour. |
| // |
| // * A non-closed contour with square caps converges to a square (albeit with potential rotation |
| // that is lost when the contour becomes empty). |
| // => square caps on the empty contour. |
| // |
| // * A closed contour with miter JOINS converges to... some sort of polygon with pointy corners. |
| // ~=> square caps on the empty contour. |
| // |
| // * All other contours converge to nothing. |
| // => butt caps on the empty contour, which are ignored. |
| // |
| static StrokeCap empty_stroke_cap(const PLSPaint* paint, bool closed) |
| { |
| if (closed) |
| { |
| switch (paint->getJoin()) |
| { |
| case StrokeJoin::round: |
| return StrokeCap::round; |
| case StrokeJoin::miter: |
| return StrokeCap::square; |
| case StrokeJoin::bevel: |
| return StrokeCap::butt; |
| } |
| } |
| return paint->getCap(); |
| } |
| |
| RIVE_ALWAYS_INLINE bool is_final_verb_of_contour(const RawPath::Iter& iter, |
| const RawPath::Iter& end) |
| { |
| return iter.rawVerbsPtr() + 1 == end.rawVerbsPtr(); |
| } |
| } // namespace |
| |
| // Helps count required resources for, and submit data to the render context that will be used to |
| // render paths with the "interior triangulation" algorithm. |
| class PLSRenderer::InteriorTriangulationHelper |
| { |
| public: |
| size_t patchCount() const { return m_patchCount; } |
| bool empty() const { return m_patchCount == 0; } |
| |
| enum class PathOp : bool |
| { |
| countDataAndTriangulate, |
| submitOuterCubics |
| }; |
| |
| // For now, we just iterate and subdivide the path twice (once for each enum in PathOp). Since |
| // 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.) |
| // |
| // 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; |
| assert(!rawPath.empty()); |
| wangs_formula::VectorXform vectorXform(*path->matrix); |
| size_t patchCount = 0; |
| size_t contourCount = 0; |
| Vec2D p0 = {0, 0}; |
| if (op == PathOp::countDataAndTriangulate) |
| { |
| scratchPath->rewind(); |
| } |
| for (const auto [verb, pts] : rawPath) |
| { |
| switch (verb) |
| { |
| case PathVerb::move: |
| if (contourCount != 0 && pts[-1] != p0) |
| { |
| if (op == PathOp::submitOuterCubics) |
| { |
| context->pushCubic(convert_line_to_cubic(pts[-1], p0).data(), |
| {0, 0}, |
| flags::kCullExcessTessellationSegments, |
| kPatchSegmentCountExcludingJoin, |
| 1, |
| kJoinSegmentCount); |
| } |
| ++patchCount; |
| } |
| if (op == PathOp::countDataAndTriangulate) |
| { |
| scratchPath->move(pts[0]); |
| } |
| else |
| { |
| context->pushContour({0, 0}, true, 0); |
| } |
| p0 = pts[0]; |
| ++contourCount; |
| break; |
| case PathVerb::line: |
| if (op == PathOp::countDataAndTriangulate) |
| { |
| scratchPath->line(pts[1]); |
| } |
| else |
| { |
| context->pushCubic(convert_line_to_cubic(pts).data(), |
| {0, 0}, |
| flags::kCullExcessTessellationSegments, |
| kPatchSegmentCountExcludingJoin, |
| 1, |
| kJoinSegmentCount); |
| } |
| ++patchCount; |
| break; |
| case PathVerb::quad: |
| RIVE_UNREACHABLE(); |
| case PathVerb::cubic: |
| { |
| size_t numSubdivisions = FindSubdivisionCount(pts, vectorXform); |
| if (numSubdivisions == 1) |
| { |
| if (op == PathOp::countDataAndTriangulate) |
| { |
| scratchPath->line(pts[3]); |
| } |
| else |
| { |
| context->pushCubic(pts, |
| {0, 0}, |
| flags::kCullExcessTessellationSegments, |
| kPatchSegmentCountExcludingJoin, |
| 1, |
| kJoinSegmentCount); |
| } |
| } |
| else |
| { |
| // Passing nullptr for the 'tValues' causes it to chop the cubic uniformly |
| // in T. |
| pathutils::ChopCubicAt(pts, chops, nullptr, numSubdivisions - 1); |
| const Vec2D* chop = chops; |
| for (size_t i = 0; i < numSubdivisions; ++i) |
| { |
| if (op == PathOp::countDataAndTriangulate) |
| { |
| scratchPath->line(chop[3]); |
| } |
| else |
| { |
| context->pushCubic(chop, |
| {0, 0}, |
| flags::kCullExcessTessellationSegments, |
| kPatchSegmentCountExcludingJoin, |
| 1, |
| kJoinSegmentCount); |
| } |
| chop += 3; |
| } |
| } |
| patchCount += numSubdivisions; |
| break; |
| } |
| case PathVerb::close: |
| break; |
| } |
| } |
| Vec2D lastPt = rawPath.points().back(); |
| if (contourCount != 0 && lastPt != p0) |
| { |
| if (op == PathOp::submitOuterCubics) |
| { |
| context->pushCubic(convert_line_to_cubic(lastPt, p0).data(), |
| {0, 0}, |
| flags::kCullExcessTessellationSegments, |
| kPatchSegmentCountExcludingJoin, |
| 1, |
| kJoinSegmentCount); |
| } |
| ++patchCount; |
| } |
| |
| if (op == PathOp::countDataAndTriangulate) |
| { |
| assert(!path->triangulator); |
| path->triangulator = |
| context->make<GrInnerFanTriangulator>(*scratchPath, |
| *path->matrix, |
| path->pathBounds, |
| path->fillRule, |
| context->trivialPerFlushAllocator()); |
| // We also draw each "grout" triangle using an outerCubic patch. |
| patchCount += path->triangulator->groutList().count(); |
| path->tessVertexCount = patchCount * kOuterCurvePatchSegmentSpan; |
| m_patchCount += patchCount; |
| } |
| else |
| { |
| // 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]}; |
| context->pushCubic(triangleAsCubic, |
| {0, 0}, |
| flags::kRetrofittedTriangle, |
| kPatchSegmentCountExcludingJoin, |
| 1, |
| 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 * kOuterCurvePatchSegmentSpan;) |
| } |
| |
| return contourCount; |
| } |
| |
| #ifdef DEBUG |
| bool didSubmitAllData() |
| { |
| return m_writtenPatchCount == m_patchCount && |
| m_writtenTessVertexCount == m_patchCount * kOuterCurvePatchSegmentSpan; |
| } |
| #endif |
| |
| private: |
| // The final segment in an outerCurve patch is a bowtie join. |
| constexpr static size_t kJoinSegmentCount = 1; |
| constexpr static size_t kPatchSegmentCountExcludingJoin = |
| kOuterCurvePatchSegmentSpan - kJoinSegmentCount; |
| |
| // Maximum # of outerCurve patches a curve on the path can be subdivided into. |
| constexpr static size_t kMaxCurveSubdivisions = |
| (kMaxParametricSegments + kPatchSegmentCountExcludingJoin - 1) / |
| kPatchSegmentCountExcludingJoin; |
| |
| static size_t FindSubdivisionCount(const Vec2D pts[], |
| const wangs_formula::VectorXform& vectorXform) |
| { |
| size_t numSubdivisions = |
| ceilf(wangs_formula::cubic(pts, kParametricPrecision, vectorXform) * |
| (1.f / kPatchSegmentCountExcludingJoin)); |
| return std::clamp<size_t>(numSubdivisions, 1, kMaxCurveSubdivisions); |
| } |
| |
| size_t m_patchCount = 0; |
| RIVE_DEBUG_CODE(size_t m_writtenPatchCount = 0;) |
| RIVE_DEBUG_CODE(size_t m_writtenTessVertexCount = 0;) |
| }; |
| |
| bool PLSRenderer::pushInternalPathBatch(PLSPaint* finalPathPaint) |
| { |
| // Only the final path in the batch uses 'finalPathPaint', which may or may not be stroked. |
| size_t strokeIdx = finalPathPaint->getIsStroked() ? m_pathBatch.size() - 1 |
| : std::numeric_limits<size_t>::max(); |
| float strokeMatrixMaxScale = |
| finalPathPaint->getIsStroked() ? m_pathBatch.back().matrix->findMaxScale() : 0; |
| float strokeRadius = finalPathPaint->getIsStroked() ? finalPathPaint->getThickness() * .5f : 0; |
| |
| // Count up how much temporary storage this function will need to reserve in CPU buffers. |
| size_t maxStrokedCurvesBeforeChops = 0; |
| size_t maxStrokedCurvesAfterChops = 0; |
| size_t maxTotalCurvesAfterChops = 0; |
| PLSPaint clipPaint; |
| for (size_t i = 0; i < m_pathBatch.size(); ++i) |
| { |
| const RawPath* rawPath = m_pathBatch[i].rawPath; |
| if (rawPath->empty()) |
| { |
| continue; |
| } |
| bool stroked = i == strokeIdx; // (Will never be true if finalPathPaint is not stroked.) |
| // Reserve enough space to record all the info we might need for this path. |
| assert(rawPath->verbs()[0] == PathVerb::move); |
| // Every path has at least 1 (non-curve) move. |
| size_t maxCurves = rawPath->verbs().size() - 1; |
| // Stroked cubics can be chopped into a maximum of 5 segments. |
| size_t maxCurvesAfterChops = stroked ? maxCurves * 5 : maxCurves; |
| if (stroked) |
| { |
| maxStrokedCurvesBeforeChops += maxCurves; |
| maxStrokedCurvesAfterChops += maxCurvesAfterChops; |
| } |
| maxTotalCurvesAfterChops += maxCurvesAfterChops; |
| } |
| |
| // Reserve temporary CPU storage for the loops that follow. |
| // (+3 because we process these values in SIMD batches of 4, an may begin at n - 1.) |
| m_parametricSegmentCounts_pow4.resize( |
| std::max(maxTotalCurvesAfterChops + 3, m_parametricSegmentCounts_pow4.capacity())); |
| m_parametricSegmentCounts.resize( |
| std::max(maxTotalCurvesAfterChops + 3, m_parametricSegmentCounts.capacity())); |
| size_t maxTangentPairs = 0; |
| if (maxStrokedCurvesAfterChops != 0) |
| { |
| assert(finalPathPaint->getIsStroked()); |
| // Each stroked curve will record the number of chops it requires (either 0, 1, or 2). |
| m_numChops.resizeAndRewind(std::max(maxStrokedCurvesBeforeChops, m_numChops.capacity())); |
| // We only chop into this queue if a cubic has one chop. More chops in a single cubic |
| // are rare and require a lot of memory, so if a cubic needs more chops we just re-chop |
| // the second time around. The maximum size this queue would need is therefore enough to |
| // chop each cubic once, or 7 points per. |
| m_chops.resizeAndRewind(std::max(maxStrokedCurvesBeforeChops * 7, m_chops.capacity())); |
| // After chopping, each stroked curve will also record its beginning and ending tangents |
| // (4 floats) so we can measure its rotation. |
| maxTangentPairs += maxStrokedCurvesAfterChops; |
| } |
| if (finalPathPaint->getIsStroked()) |
| { |
| // If the stroke has round joins, we also record the tangents between (pre-chopped) joins in |
| // order to calculate how many vertices are in each round join. |
| if (finalPathPaint->getJoin() == StrokeJoin::round) |
| { |
| maxTangentPairs += maxStrokedCurvesBeforeChops; |
| } |
| // Reserve temporary CPU storage for the loops that follow. |
| // (+3 because we process these values in SIMD batches of 4, an may begin at n - 1.) |
| m_tangentPairs.resize(std::max(maxTangentPairs + 3, m_tangentPairs.capacity())); |
| m_polarSegmentCounts.resize( |
| std::max(maxStrokedCurvesAfterChops + 3, m_polarSegmentCounts.capacity())); |
| } |
| |
| InteriorTriangulationHelper interiorTriHelper; |
| |
| // 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. |
| for (size_t i = 0; i < m_pathBatch.size(); ++i) |
| { |
| PathDraw& path = m_pathBatch[i]; |
| if (path.rawPath->empty()) |
| { |
| continue; |
| } |
| |
| size_t pathContourCount = 0; |
| bool stroked = i == strokeIdx; // (Will never be true if finalPathPaint is not stroked.) |
| assert(path.triangulator == nullptr); |
| if (!stroked && FindTransformedArea(path.pathBounds, *path.matrix) > 512 * 512) |
| { |
| // 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) |
| { |
| 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), |
| 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); |
| } |
| } |
| path.contourCount = pathContourCount; |
| contourCount += pathContourCount; |
| } |
| |
| if (contourCount == 0) |
| { |
| // The entire batch is empty. |
| return true; |
| } |
| |
| // Iteration pass 2: Finish calculating the numbers of tessellation segments in each contour, |
| // using SIMD. |
| size_t contourFirstLineIdx = 0; |
| size_t contourFirstCurveIdx = 0; |
| size_t contourFirstRotationIdx = 0; |
| size_t emptyStrokeCountForCaps = 0; |
| PLSRenderContext::TessVertexCounter tessVertexCounter(m_context); |
| for (size_t currentPathIdx = 0; currentPathIdx < m_pathBatch.size(); ++currentPathIdx) |
| { |
| PathDraw& path = m_pathBatch[currentPathIdx]; |
| if (path.rawPath->empty()) |
| { |
| continue; |
| } |
| |
| bool stroked = currentPathIdx == strokeIdx; |
| |
| // (If we used interior triangulation, interiorTriHelper already counted the path's vertices |
| // for us.) |
| if (path.triangulator != nullptr) |
| { |
| assert(path.paddingVertexCount == 0); |
| // If the path has a nonzero number of tessellation vertices, pad them so they align on |
| // a multiple of the patch size. |
| if (path.tessVertexCount > 0) |
| { |
| assert(!stroked); |
| path.paddingVertexCount = |
| tessVertexCounter.countPath<kOuterCurvePatchSegmentSpan>(path.tessVertexCount, |
| false); |
| path.tessVertexCount += path.paddingVertexCount; |
| } |
| } |
| else |
| { |
| assert(path.tessVertexCount == 0); |
| 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. |
| uint4 mergedTessVertexSums4 = 0; |
| |
| // Finish calculating and counting parametric segments for each curve. |
| size_t j; |
| for (j = contourFirstCurveIdx; j < contour->endCurveIdx; j += 4) |
| { |
| assert(j + 4 <= m_parametricSegmentCounts_pow4.capacity()); |
| // Curves recorded their segment counts raised to the 4th power. Now find their |
| // roots and convert to integers in batches of 4. |
| float4 n = simd::load4f(m_parametricSegmentCounts_pow4.get() + j); |
| n = simd::ceil(simd::sqrt(simd::sqrt(n))); |
| n = simd::clamp(n, float4(1), float4(kMaxParametricSegments)); |
| uint4 n_ = simd::cast<uint32_t>(n); |
| assert(j + 4 <= m_parametricSegmentCounts.capacity()); |
| simd::store(m_parametricSegmentCounts.get() + j, n_); |
| mergedTessVertexSums4 += n_; |
| } |
| // We counted in batches of 4. Undo the values we counted from beyond the end of the |
| // path. |
| while (j-- > contour->endCurveIdx) |
| { |
| contourVertexCount -= m_parametricSegmentCounts[j]; |
| } |
| |
| if (stroked) |
| { |
| // Finish calculating and counting polar segments for each stroked curve and |
| // round join. |
| const float r_ = strokeRadius * strokeMatrixMaxScale; |
| const float polarSegmentsPerRad = |
| pathutils::CalcPolarSegmentsPerRadian<kPolarPrecision>(r_); |
| for (j = contourFirstRotationIdx; j < contour->endRotationIdx; j += 4) |
| { |
| // Measure the rotations of curves in batches of 4. |
| assert(j + 4 <= m_tangentPairs.capacity()); |
| auto [tx0, ty0, tx1, ty1] = simd::load4x4f(&m_tangentPairs[j][0].x); |
| float4 numer = tx0 * tx1 + ty0 * ty1; |
| float4 denom_pow2 = (tx0 * tx0 + ty0 * ty0) * (tx1 * tx1 + ty1 * ty1); |
| float4 cosTheta = numer / simd::sqrt(denom_pow2); |
| cosTheta = simd::clamp(cosTheta, float4(-1), float4(1)); |
| float4 theta = simd::fast_acos(cosTheta); |
| // Find polar segment counts from the rotation angles. |
| float4 n = simd::ceil(theta * polarSegmentsPerRad); |
| n = simd::clamp(n, float4(1), float4(kMaxPolarSegments)); |
| uint4 n_ = simd::cast<uint32_t>(n); |
| assert(j + 4 <= m_polarSegmentCounts.capacity()); |
| simd::store(m_polarSegmentCounts.get() + j, n_); |
| // Polar and parametric segments share the first and final vertices. |
| // Therefore: |
| // |
| // parametricVertexCount = parametricSegmentCount + 1 |
| // |
| // polarVertexCount = polarVertexCount + 1 |
| // |
| // mergedVertexCount = parametricVertexCount + polarVertexCount - 2 |
| // = parametricSegmentCount + 1 + polarSegmentCount + 1 |
| // - 2 = parametricSegmentCount + polarSegmentCount |
| // |
| mergedTessVertexSums4 += n_; |
| } |
| |
| // We counted in batches of 4. Undo the values we counted from beyond the end of |
| // the path. |
| while (j-- > contour->endRotationIdx) |
| { |
| contourVertexCount -= m_polarSegmentCounts[j]; |
| } |
| |
| // Count joins. |
| if (finalPathPaint->getJoin() == StrokeJoin::round) |
| { |
| // Round joins share their beginning and ending vertices with the curve on |
| // either side. Therefore, the number of vertices we need to allocate for a |
| // round join is "joinSegmentCount - 1". Do all the -1's here. |
| contourVertexCount -= contour->strokeJoinCount; |
| } |
| else |
| { |
| // The shader needs 3 segments for each miter and bevel join (which |
| // translates to two interior vertices, since joins share their beginning |
| // and ending vertices with the curve on either side). |
| contourVertexCount += |
| contour->strokeJoinCount * (kNumSegmentsInMiterOrBevelJoin - 1); |
| } |
| |
| // Count stroke caps, if any. |
| bool empty = contour->endLineIdx == contourFirstLineIdx && |
| contour->endCurveIdx == contourFirstCurveIdx; |
| StrokeCap cap; |
| bool needsCaps; |
| if (!empty) |
| { |
| cap = finalPathPaint->getCap(); |
| needsCaps = !contour->closed; |
| } |
| else |
| { |
| cap = empty_stroke_cap(finalPathPaint, contour->closed); |
| needsCaps = |
| cap != StrokeCap::butt; // Ignore butt caps when the contour is empty. |
| } |
| if (needsCaps) |
| { |
| // We emulate stroke caps as 180-degree joins. |
| if (cap == StrokeCap::round) |
| { |
| // Round caps rotate 180 degrees. |
| contour->strokeCapSegmentCount = ceilf(polarSegmentsPerRad * math::PI); |
| // +2 because round caps emulated as joins need to emit vertices at T=0 |
| // and T=1, unlike normal round joins. |
| contour->strokeCapSegmentCount += 2; |
| // Make sure not to exceed kMaxPolarSegments. |
| contour->strokeCapSegmentCount = |
| std::min(contour->strokeCapSegmentCount, kMaxPolarSegments); |
| } |
| else |
| { |
| contour->strokeCapSegmentCount = kNumSegmentsInMiterOrBevelJoin; |
| } |
| // pushContour() uses "strokeCapSegmentCount != 0" to tell if it needs |
| // stroke caps. |
| assert(contour->strokeCapSegmentCount != 0); |
| // As long as a contour isn't empty, we can tack the end cap onto the join |
| // section of the final curve in the stroke. Otherwise, we need to introduce |
| // 0-tessellation-segment curves with non-empty joins to carry the caps. |
| emptyStrokeCountForCaps += empty ? 2 : 1; |
| contourVertexCount += (contour->strokeCapSegmentCount - 1) * 2; |
| } |
| } |
| else |
| { |
| // Fills don't have polar segments: |
| // |
| // mergedVertexCount = parametricVertexCount = parametricSegmentCount + 1 |
| // |
| // Just collect the +1 for each non-stroked curve. |
| size_t contourCurveCount = contour->endCurveIdx - contourFirstCurveIdx; |
| contourVertexCount += contourCurveCount; |
| } |
| contourVertexCount += simd::sum(mergedTessVertexSums4); |
| |
| // Add padding vertices until the number of tessellation vertices in the contour is |
| // an exact multiple of kMidpointFanPatchSegmentSpan. This ensures that patch |
| // boundaries align with contour boundaries. |
| contour->paddingVertexCount = |
| PaddingToAlignUp<kMidpointFanPatchSegmentSpan>(contourVertexCount); |
| contourVertexCount += contour->paddingVertexCount; |
| assert(contourVertexCount % kMidpointFanPatchSegmentSpan == 0); |
| RIVE_DEBUG_CODE(contour->tessVertexCount = contourVertexCount;) |
| |
| path.tessVertexCount += contourVertexCount; |
| contourFirstLineIdx = contour->endLineIdx; |
| contourFirstCurveIdx = contour->endCurveIdx; |
| contourFirstRotationIdx = contour->endRotationIdx; |
| } |
| assert(path.paddingVertexCount == 0); |
| // If the path has a nonzero number of tessellation vertices, pad them so they align on |
| // a multiple of the patch size. |
| if (path.tessVertexCount > 0) |
| { |
| path.paddingVertexCount = |
| tessVertexCounter.countPath<kMidpointFanPatchSegmentSpan>(path.tessVertexCount, |
| stroked); |
| path.tessVertexCount += path.paddingVertexCount; |
| } |
| } |
| } |
| assert(contourFirstLineIdx == lineCount); |
| assert(contourFirstCurveIdx == curveCount); |
| assert(contourFirstRotationIdx == rotationCount); |
| |
| // Attempt to reserve space on the GPU for our entire batch of paths. |
| size_t curveReserveCount = |
| curveCount + lineCount + emptyStrokeCountForCaps + interiorTriHelper.patchCount(); |
| if (!m_context->reservePathData(m_pathBatch.size(), |
| contourCount, |
| curveReserveCount, |
| tessVertexCounter)) |
| { |
| // The paths don't fit. Give up and let the caller flush and try again. |
| return false; |
| } |
| |
| // Attempt to push 'finalPathPaint' to the GPU buffers. |
| PaintData paintData; |
| if (!m_context->pushPaint(finalPathPaint, &paintData)) |
| { |
| // The paint doesn't fit. Give up and let the caller flush and try again. |
| return false; |
| } |
| |
| // Iteration pass 3: Now that we have space reserved, push the whole batch of paths to the GPU. |
| RIVE_DEBUG_CODE(size_t pushedPathCount = 0;) |
| RIVE_DEBUG_CODE(size_t skippedPathCount = 0;) |
| RIVE_DEBUG_CODE(size_t pushedContourCount = 0;) |
| RIVE_DEBUG_CODE(size_t skippedContourCount = 0;) |
| RIVE_DEBUG_CODE(m_pushedLineCount = 0;) |
| RIVE_DEBUG_CODE(m_pushedCurveCount = 0;) |
| RIVE_DEBUG_CODE(m_pushedRotationCount = 0;) |
| RIVE_DEBUG_CODE(m_pushedEmptyStrokeCountForCaps = 0;) |
| size_t curveIdx = 0; |
| size_t rotationIdx = 0; |
| RawPath::Iter startOfContour; |
| size_t finalPathIdx = m_pathBatch.size() - 1; // All paths are clips except the final one. |
| for (size_t currentPathIdx = 0; currentPathIdx < m_pathBatch.size(); ++currentPathIdx) |
| { |
| PathDraw& path = m_pathBatch[currentPathIdx]; |
| if (path.tessVertexCount == 0) |
| { |
| RIVE_DEBUG_CODE(skippedContourCount += path.contourCount;) |
| RIVE_DEBUG_CODE(++skippedPathCount;) |
| continue; |
| } |
| assert(!path.rawPath->empty()); |
| |
| // Push a path record. |
| bool isClipPath = currentPathIdx != finalPathIdx; |
| PaintType paintType = isClipPath ? PaintType::clipReplace : finalPathPaint->getType(); |
| PLSBlendMode blendMode = |
| isClipPath ? PLSBlendMode::srcOver : finalPathPaint->getBlendMode(); |
| |
| m_context->pushPath(path.triangulator ? PatchType::outerCurves : PatchType::midpointFan, |
| *path.matrix, |
| isClipPath ? 0 : strokeRadius, |
| path.fillRule, |
| paintType, |
| path.clipID, |
| blendMode, |
| isClipPath ? PaintData{} : paintData, |
| path.tessVertexCount, |
| path.paddingVertexCount); |
| RIVE_DEBUG_CODE(++pushedPathCount;) |
| |
| if (path.triangulator != nullptr) |
| { |
| // This path is drawn with the interior triangulation algorithm instead. |
| 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, |
| blendMode); |
| } |
| else |
| { |
| startOfContour = path.rawPath->begin(); |
| 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, |
| curveIdx, |
| rotationIdx, |
| strokeMatrixMaxScale, |
| currentPathIdx == strokeIdx ? finalPathPaint : nullptr); |
| 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)); |
| curveIdx = contour.endCurveIdx; |
| rotationIdx = contour.endRotationIdx; |
| startOfContour = contour.endOfContour; |
| RIVE_DEBUG_CODE(++pushedContourCount); |
| } |
| } |
| } |
| |
| // Make sure we only pushed the amount of data we reserved. |
| assert(pushedPathCount + skippedPathCount == m_pathBatch.size()); |
| assert(pushedContourCount + skippedContourCount == contourCount); |
| assert(m_pushedLineCount == lineCount); |
| assert(m_pushedCurveCount == curveCount); |
| assert(m_pushedRotationCount == rotationCount); |
| assert(m_pushedEmptyStrokeCountForCaps == emptyStrokeCountForCaps); |
| assert(interiorTriHelper.didSubmitAllData()); |
| assert(m_pushedLineCount + m_pushedCurveCount + m_pushedEmptyStrokeCountForCaps + |
| interiorTriHelper.patchCount() == |
| curveReserveCount); |
| return true; |
| } |
| |
| void PLSRenderer::pushContour(RawPath::Iter iter, |
| const ContourData& contour, |
| size_t curveIdx, |
| size_t rotationIdx, |
| float matrixMaxScale, |
| const PLSPaint* strokePaint) |
| { |
| assert(iter.verb() == PathVerb::move); |
| assert(strokePaint != nullptr || contour.closed); // Fills are always closed. |
| RIVE_DEBUG_CODE(const size_t startingCurveIdx = curveIdx;) |
| RIVE_DEBUG_CODE(const size_t startingRotationIdx = rotationIdx;) |
| |
| const Vec2D* pts = iter.rawPtsPtr(); |
| const RawPath::Iter end = contour.endOfContour; |
| uint32_t joinTypeFlags = 0; |
| bool roundJoinStroked = false; |
| bool needsFirstEmulatedCapAsJoin = false; // Emit a starting cap before the next cubic? |
| uint32_t emulatedCapAsJoinFlags = 0; |
| if (strokePaint != nullptr) |
| { |
| joinTypeFlags = flags::JoinTypeFlags(strokePaint->getJoin()); |
| roundJoinStroked = joinTypeFlags == 0; |
| if (contour.strokeCapSegmentCount != 0) |
| { |
| StrokeCap cap = !contour.closed ? strokePaint->getCap() |
| : empty_stroke_cap(strokePaint, contour.closed); |
| emulatedCapAsJoinFlags = flags::kEmulatedStrokeCap; |
| if (cap == StrokeCap::square) |
| { |
| emulatedCapAsJoinFlags |= flags::kMiterClipJoin; |
| } |
| else if (cap == StrokeCap::butt) |
| { |
| emulatedCapAsJoinFlags |= flags::kBevelJoin; |
| } |
| needsFirstEmulatedCapAsJoin = true; |
| } |
| } |
| |
| // Make a data record for this current contour on the GPU. |
| m_context->pushContour(contour.midpoint, contour.closed, contour.paddingVertexCount); |
| |
| // Convert all curves in the contour to cubics and push them to the GPU. |
| const int styleFlags = style_flags(strokePaint != nullptr, roundJoinStroked); |
| Vec2D joinTangent = {0, 1}; |
| int joinSegmentCount = 1; |
| Vec2D implicitClose[2]; // In case we need an implicit closing line. |
| for (; iter != end; ++iter) |
| { |
| StyledVerb styledVerb = styled_verb(iter.verb(), styleFlags); |
| switch (styledVerb) |
| { |
| case StyledVerb::filledMove: |
| case StyledVerb::strokedMove: |
| case StyledVerb::roundJoinStrokedMove: |
| implicitClose[1] = iter.movePt(); // In case we need an implicit closing line. |
| break; |
| case StyledVerb::filledClose: |
| case StyledVerb::strokedClose: |
| case StyledVerb::roundJoinStrokedClose: |
| assert(contour.closed); |
| break; |
| case StyledVerb::roundJoinStrokedLine: |
| { |
| if (contour.closed || !is_final_verb_of_contour(iter, end)) |
| { |
| joinTangent = m_tangentPairs[rotationIdx][1]; |
| joinSegmentCount = m_polarSegmentCounts[rotationIdx]; |
| ++rotationIdx; |
| RIVE_DEBUG_CODE(++m_pushedStrokeJoinCount;) |
| } |
| else |
| { |
| // End with a 180-degree join that looks like the stroke cap. |
| joinTangent = -find_ending_tangent(pts, end.rawPtsPtr()); |
| joinTypeFlags = emulatedCapAsJoinFlags; |
| joinSegmentCount = contour.strokeCapSegmentCount; |
| RIVE_DEBUG_CODE(++m_pushedStrokeCapCount;) |
| } |
| goto line_common; |
| } |
| case StyledVerb::strokedLine: |
| if (contour.closed || !is_final_verb_of_contour(iter, end)) |
| { |
| joinTangent = |
| find_join_tangent(iter.linePts() + 1, end.rawPtsPtr(), contour.closed, pts); |
| joinSegmentCount = kNumSegmentsInMiterOrBevelJoin; |
| RIVE_DEBUG_CODE(++m_pushedStrokeJoinCount;) |
| } |
| else |
| { |
| // End with a 180-degree join that looks like the stroke cap. |
| joinTangent = -find_ending_tangent(pts, end.rawPtsPtr()); |
| joinTypeFlags = emulatedCapAsJoinFlags; |
| joinSegmentCount = contour.strokeCapSegmentCount; |
| RIVE_DEBUG_CODE(++m_pushedStrokeCapCount;) |
| } |
| [[fallthrough]]; |
| case StyledVerb::filledLine: |
| line_common: |
| { |
| std::array<Vec2D, 4> cubic = convert_line_to_cubic(iter.linePts()); |
| if (needsFirstEmulatedCapAsJoin) |
| { |
| // Emulate the start cap as a 180-degree join before the first stroke. |
| pushEmulatedStrokeCapAsJoinBeforeCubic(cubic.data(), |
| emulatedCapAsJoinFlags, |
| contour.strokeCapSegmentCount); |
| needsFirstEmulatedCapAsJoin = false; |
| } |
| m_context |
| ->pushCubic(cubic.data(), joinTangent, joinTypeFlags, 1, 1, joinSegmentCount); |
| RIVE_DEBUG_CODE(++m_pushedLineCount;) |
| break; |
| } |
| case StyledVerb::roundJoinStrokedQuad: |
| case StyledVerb::strokedQuad: |
| case StyledVerb::filledQuad: |
| RIVE_UNREACHABLE(); |
| break; |
| case StyledVerb::roundJoinStrokedCubic: |
| case StyledVerb::strokedCubic: |
| { |
| const Vec2D* p = iter.cubicPts(); |
| uint8_t chopKey = m_numChops.pop_front(); |
| uint8_t numChops = 0; |
| 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. |
| chop_cubic_around_cusps(p, |
| localChopBuffer, |
| &m_chops.pop_front().x, |
| chopKey >> 1, |
| matrixMaxScale); |
| p = localChopBuffer; |
| // The bottom bit of chopKey is 1, meaning "areCusps". Clearing the bottom |
| // bit leaves "numChops * 2", which is the number of chops a cusp needs! |
| numChops = chopKey ^ 1; |
| break; |
| |
| case simple_chop_key(2): // 2 non-cusp chops |
| { |
| // Curves that need 2 chops are rare in real-world content. Just re-chop the |
| // curve this time around as well. |
| auto [t0, t1] = m_chops.pop_front(); |
| pathutils::ChopCubicAt(p, localChopBuffer, t0, t1); |
| p = localChopBuffer; |
| numChops = 2; |
| break; |
| } |
| case simple_chop_key(1): // 1 non-cusp chop |
| // Single-chop curves were saved in the m_chops queue. |
| p = m_chops.pop_front_n(7); |
| numChops = 1; |
| break; |
| } |
| if (needsFirstEmulatedCapAsJoin) |
| { |
| // Emulate the start cap as a 180-degree join before the first stroke. |
| pushEmulatedStrokeCapAsJoinBeforeCubic(p, |
| emulatedCapAsJoinFlags, |
| contour.strokeCapSegmentCount); |
| needsFirstEmulatedCapAsJoin = false; |
| } |
| // Push chops before the final one. |
| for (size_t end = curveIdx + numChops; curveIdx != end; |
| ++curveIdx, ++rotationIdx, p += 3) |
| { |
| uint32_t parametricSegmentCount = m_parametricSegmentCounts[curveIdx]; |
| uint32_t polarSegmentCount = m_polarSegmentCounts[rotationIdx]; |
| m_context->pushCubic(p, |
| joinTangent, |
| joinTypeFlags, |
| parametricSegmentCount, |
| polarSegmentCount, |
| 1); |
| } |
| // Push the final chop, with a join. |
| uint32_t parametricSegmentCount = m_parametricSegmentCounts[curveIdx++]; |
| uint32_t polarSegmentCount = m_polarSegmentCounts[rotationIdx++]; |
| if (contour.closed || !is_final_verb_of_contour(iter, end)) |
| { |
| if (styledVerb == StyledVerb::roundJoinStrokedCubic) |
| { |
| joinTangent = m_tangentPairs[rotationIdx][1]; |
| joinSegmentCount = m_polarSegmentCounts[rotationIdx]; |
| ++rotationIdx; |
| } |
| else |
| { |
| joinTangent = find_join_tangent(iter.cubicPts() + 3, |
| end.rawPtsPtr(), |
| contour.closed, |
| pts); |
| joinSegmentCount = kNumSegmentsInMiterOrBevelJoin; |
| } |
| RIVE_DEBUG_CODE(++m_pushedStrokeJoinCount;) |
| } |
| else |
| { |
| // End with a 180-degree join that looks like the stroke cap. |
| joinTangent = -find_ending_tangent(pts, end.rawPtsPtr()); |
| joinTypeFlags = emulatedCapAsJoinFlags; |
| joinSegmentCount = contour.strokeCapSegmentCount; |
| RIVE_DEBUG_CODE(++m_pushedStrokeCapCount;) |
| } |
| m_context->pushCubic(p, |
| joinTangent, |
| joinTypeFlags, |
| parametricSegmentCount, |
| polarSegmentCount, |
| joinSegmentCount); |
| break; |
| } |
| case StyledVerb::filledCubic: |
| { |
| uint32_t parametricSegmentCount = m_parametricSegmentCounts[curveIdx++]; |
| m_context->pushCubic(iter.cubicPts(), Vec2D{}, 0, parametricSegmentCount, 1, 1); |
| break; |
| } |
| } |
| } |
| |
| if (needsFirstEmulatedCapAsJoin) |
| { |
| // The contour was empty. Emit both caps on p0. |
| Vec2D p0 = pts[0], left = {p0.x - 1, p0.y}, right = {p0.x + 1, p0.y}; |
| pushEmulatedStrokeCapAsJoinBeforeCubic(std::array{p0, right, right, right}.data(), |
| emulatedCapAsJoinFlags, |
| contour.strokeCapSegmentCount); |
| pushEmulatedStrokeCapAsJoinBeforeCubic(std::array{p0, left, left, left}.data(), |
| emulatedCapAsJoinFlags, |
| contour.strokeCapSegmentCount); |
| } |
| else if (contour.closed) |
| { |
| implicitClose[0] = iter.rawPtsPtr()[-1]; |
| if (implicitClose[0] != implicitClose[1]) |
| { |
| // Draw a line back to the beginning of the contour. |
| std::array<Vec2D, 4> cubic = convert_line_to_cubic(implicitClose); |
| // Closing join back to the beginning of the contour. |
| if (roundJoinStroked) |
| { |
| joinTangent = m_tangentPairs[rotationIdx][1]; |
| joinSegmentCount = m_polarSegmentCounts[rotationIdx]; |
| ++rotationIdx; |
| RIVE_DEBUG_CODE(++m_pushedStrokeJoinCount;) |
| } |
| else if (strokePaint != nullptr) |
| { |
| joinTangent = find_starting_tangent(pts, end.rawPtsPtr()); |
| joinSegmentCount = kNumSegmentsInMiterOrBevelJoin; |
| RIVE_DEBUG_CODE(++m_pushedStrokeJoinCount;) |
| } |
| m_context->pushCubic(cubic.data(), joinTangent, joinTypeFlags, 1, 1, joinSegmentCount); |
| RIVE_DEBUG_CODE(++m_pushedLineCount;) |
| } |
| } |
| |
| RIVE_DEBUG_CODE(m_pushedCurveCount += curveIdx - startingCurveIdx;) |
| RIVE_DEBUG_CODE(m_pushedRotationCount += rotationIdx - startingRotationIdx;) |
| } |
| |
| void PLSRenderer::pushEmulatedStrokeCapAsJoinBeforeCubic(const Vec2D cubic[], |
| uint32_t emulatedCapAsJoinFlags, |
| uint32_t strokeCapSegmentCount) |
| { |
| // Reverse the cubic and push it with zero parametric and polar segments, and a 180-degree join |
| // tangent. This results in a solitary join, positioned immediately before the provided cubic, |
| // that looks like the desired stroke cap. |
| m_context->pushCubic(std::array{cubic[3], cubic[2], cubic[1], cubic[0]}.data(), |
| find_cubic_tan0(cubic), |
| emulatedCapAsJoinFlags, |
| 0, |
| 0, |
| strokeCapSegmentCount); |
| RIVE_DEBUG_CODE(++m_pushedStrokeCapCount;) |
| RIVE_DEBUG_CODE(++m_pushedEmptyStrokeCountForCaps;) |
| } |
| |
| void PLSRenderer::intermediateFlush() |
| { |
| m_context->flush(PLSRenderContext::FlushType::intermediate); |
| |
| // Reset clip IDs, since these get reset by the context on flush. |
| for (ClipElement& clip : m_clipStack) |
| { |
| clip.clipID = 0; |
| } |
| } |
| |
| bool PLSRenderer::IsAABB(const RawPath& path) |
| { |
| constexpr static size_t kAABBVerbCount = 5; |
| constexpr static PathVerb aabbVerbs[kAABBVerbCount] = {PathVerb::move, |
| PathVerb::line, |
| PathVerb::line, |
| PathVerb::line, |
| PathVerb::close}; |
| Span<const PathVerb> verbs = path.verbs(); |
| if (verbs.count() != kAABBVerbCount || memcmp(verbs.data(), aabbVerbs, sizeof(aabbVerbs)) != 0) |
| { |
| return false; |
| } |
| Span<const Vec2D> pts = path.points(); |
| assert(pts.count() == 4); |
| float4 corners = {pts[0].x, pts[0].y, pts[2].x, pts[2].y}; |
| float4 oppositeCorners = {pts[1].x, pts[1].y, pts[3].x, pts[3].y}; |
| return simd::all(corners == oppositeCorners.zyxw) || simd::all(corners == oppositeCorners.xwzy); |
| } |
| } // namespace rive::pls |