/*
 * Copyright 2023 Rive
 */

#include "rive/renderer/draw.hpp"

#include "gr_inner_fan_triangulator.hpp"
#include "rive_render_path.hpp"
#include "rive_render_paint.hpp"
#include "rive/math/bezier_utils.hpp"
#include "rive/math/wangs_formula.hpp"
#include "rive/renderer/texture.hpp"
#include "gradient.hpp"
#include "shaders/constants.glsl"
#include "rive/profiler/profiler_macros.h"

namespace rive::gpu
{
namespace
{
// 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 uint32_t find_outer_cubic_subdivision_count(
    const Vec2D pts[],
    const wangs_formula::VectorXform& vectorXform)
{
    float numSubdivisions =
        ceilf(wangs_formula::cubic(pts, kParametricPrecision, vectorXform) *
              (1.f / kPatchSegmentCountExcludingJoin));
    return static_cast<uint32_t>(
        math::clamp(numSubdivisions, 1, kMaxCurveSubdivisions));
}

constexpr static int NUM_SEGMENTS_IN_MITER_OR_BEVEL_JOIN = 5;
constexpr static int STROKE_OR_FEATHER_STYLE_FLAG = 8;
constexpr static int ROUND_JOIN_STYLE_FLAG = STROKE_OR_FEATHER_STYLE_FLAG << 1;
RIVE_ALWAYS_INLINE constexpr int style_flags(bool isStrokeOrFeather,
                                             bool roundJoinStroked)
{
    int styleFlags = (isStrokeOrFeather << 3) | (roundJoinStroked << 4);
    assert(bool(styleFlags & STROKE_OR_FEATHER_STYLE_FLAG) ==
           isStrokeOrFeather);
    assert(bool(styleFlags & ROUND_JOIN_STYLE_FLAG) == 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 =
        STROKE_OR_FEATHER_STYLE_FLAG | static_cast<int>(PathVerb::move),
    roundJoinStrokedMove = STROKE_OR_FEATHER_STYLE_FLAG |
                           ROUND_JOIN_STYLE_FLAG |
                           static_cast<int>(PathVerb::move),

    filledLine = static_cast<int>(PathVerb::line),
    strokedLine =
        STROKE_OR_FEATHER_STYLE_FLAG | static_cast<int>(PathVerb::line),
    roundJoinStrokedLine = STROKE_OR_FEATHER_STYLE_FLAG |
                           ROUND_JOIN_STYLE_FLAG |
                           static_cast<int>(PathVerb::line),

    filledQuad = static_cast<int>(PathVerb::quad),
    strokedQuad =
        STROKE_OR_FEATHER_STYLE_FLAG | static_cast<int>(PathVerb::quad),
    roundJoinStrokedQuad = STROKE_OR_FEATHER_STYLE_FLAG |
                           ROUND_JOIN_STYLE_FLAG |
                           static_cast<int>(PathVerb::quad),

    filledCubic = static_cast<int>(PathVerb::cubic),
    strokedCubic =
        STROKE_OR_FEATHER_STYLE_FLAG | static_cast<int>(PathVerb::cubic),
    roundJoinStrokedCubic = STROKE_OR_FEATHER_STYLE_FLAG |
                            ROUND_JOIN_STYLE_FLAG |
                            static_cast<int>(PathVerb::cubic),

    filledClose = static_cast<int>(PathVerb::close),
    strokedClose =
        STROKE_OR_FEATHER_STYLE_FLAG | static_cast<int>(PathVerb::close),
    roundJoinStrokedClose = STROKE_OR_FEATHER_STYLE_FLAG |
                            ROUND_JOIN_STYLE_FLAG |
                            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);
}

// 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] = fmaxf(cuspT[i] - math::EPSILON, minT);
        t[i * 2 + 1] = fminf(cuspT[i] + math::EPSILON, maxT);
    }
    math::chop_cubic_at(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 = math::eval_cubic_at(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(bool closed, StrokeJoin join, StrokeCap cap)
{
    if (closed)
    {
        switch (join)
        {
            case StrokeJoin::round:
                return StrokeCap::round;
            case StrokeJoin::miter:
                return StrokeCap::square;
            case StrokeJoin::bevel:
                return StrokeCap::butt;
        }
    }
    return cap;
}

RIVE_ALWAYS_INLINE bool is_final_verb_of_contour(const RawPath::Iter& iter,
                                                 const RawPath::Iter& end)
{
    return iter.rawVerbsPtr() + 1 == end.rawVerbsPtr();
}

RIVE_ALWAYS_INLINE uint32_t join_type_flags(StrokeJoin join)
{
    switch (join)
    {
        case StrokeJoin::miter:
            return MITER_REVERT_JOIN_CONTOUR_FLAG;
        case StrokeJoin::round:
            return ROUND_JOIN_CONTOUR_FLAG;
        case StrokeJoin::bevel:
            return BEVEL_JOIN_CONTOUR_FLAG;
    }
    RIVE_UNREACHABLE();
}

inline float find_feather_radius(float paintFeather)
{
    // Blur magnitudes in design tools are customarily the width of two standard
    // deviations, or, the length of the range -1stddev .. +1stddev.
    return paintFeather * (FEATHER_TEXTURE_STDDEVS / 2);
}

inline float find_atlas_feather_scale_factor(float featherRadius,
                                             float matrixMaxScale)
{
    // In practice, and with "gaussian -> linear -> gaussian" filtering, we
    // never need to render a blur with a radius larger than 16 pixels. After
    // this point, we can just scale it up to whatever resolution it needs to be
    // displayed at.
    return 16.f / fmaxf(featherRadius * matrixMaxScale, 16);
}

static uint32_t feather_join_segment_count(float polarSegmentsPerRadian)
{
    uint32_t n =
        static_cast<uint32_t>(ceilf(polarSegmentsPerRadian * math::PI)) +
        FEATHER_JOIN_HELPER_SEGMENT_COUNT;
    n = std::max(n, FEATHER_JOIN_MIN_SEGMENT_COUNT);
    // FEATHER_POLAR_SEGMENT_MIN_ANGLE should limit n long before we reach
    // kMaxPolarSegments.
    assert(n < gpu::kMaxPolarSegments);
    return n;
}
} // namespace

Draw::Draw(IAABB pixelBounds,
           const Mat2D& matrix,
           BlendMode blendMode,
           rcp<Texture> imageTexture,
           ImageSampler imageSampler,
           Type type) :
    m_imageTextureRef(imageTexture.release()),
    m_imageSampler(imageSampler),
    m_pixelBounds(pixelBounds),
    m_matrix(matrix),
    m_blendMode(blendMode),
    m_type(type)

{
    if (m_blendMode != BlendMode::srcOver)
    {
        m_drawContents |= gpu::DrawContents::advancedBlend;
    }
}

void Draw::setClipID(uint32_t clipID)
{
    m_clipID = clipID;

    // For clipUpdates, m_clipID refers to the ID we are writing to the stencil
    // buffer (NOT the ID we are clipping against). It therefore doesn't affect
    // the activeClip flag in that case.
    if (!(m_drawContents & gpu::DrawContents::clipUpdate))
    {
        if (m_clipID != 0)
        {
            m_drawContents |= gpu::DrawContents::activeClip;
        }
        else
        {
            m_drawContents &= ~gpu::DrawContents::activeClip;
        }
    }
}

void Draw::releaseRefs() { safe_unref(m_imageTextureRef); }

PathDraw::CoverageType PathDraw::SelectCoverageType(
    const RiveRenderPaint* paint,
    float matrixMaxScale,
    const gpu::PlatformFeatures& platformFeatures,
    gpu::InterlockMode interlockMode)
{
    if (paint->getFeather() != 0)
    {
        if (platformFeatures.alwaysFeatherToAtlas ||
            interlockMode == gpu::InterlockMode::msaa ||
            // Always switch to the atlas once we can render quarter-resultion.
            find_atlas_feather_scale_factor(
                find_feather_radius(paint->getFeather()),
                matrixMaxScale) <= .5f)
        {
            return CoverageType::atlas;
        }
    }
    switch (interlockMode)
    {
        case gpu::InterlockMode::rasterOrdering:
        case gpu::InterlockMode::atomics:
            return CoverageType::pixelLocalStorage;
        case gpu::InterlockMode::clockwise:
            return CoverageType::clockwise;
        case gpu::InterlockMode::clockwiseAtomic:
            return CoverageType::clockwiseAtomic;
        case gpu::InterlockMode::msaa:
            return CoverageType::msaa;
    }
    RIVE_UNREACHABLE();
}

DrawUniquePtr PathDraw::Make(RenderContext* context,
                             const Mat2D& matrix,
                             rcp<const RiveRenderPath> path,
                             FillRule fillRule,
                             const RiveRenderPaint* paint,
                             RawPath* scratchPath)
{
    RIVE_PROF_SCOPE();
    assert(path != nullptr);
    assert(paint != nullptr);

    CoverageType coverageType =
        SelectCoverageType(paint,
                           matrix.findMaxScale(),
                           context->platformFeatures(),
                           context->frameInterlockMode());

    // Compute the screen-space bounding box.
    AABB mappedBounds;
    if (context->frameInterlockMode() == gpu::InterlockMode::rasterOrdering &&
        coverageType != CoverageType::atlas)
    {
        // In rasterOrdering mode we can use a looser bounding box since we
        // don't do reordering.
        mappedBounds = matrix.mapBoundingBox(path->getBounds());
    }
    else
    {
        // Otherwise find a tight bounding box in order to maximize reordering.
        mappedBounds =
            matrix.mapBoundingBox(path->getRawPath().points().data(),
                                  path->getRawPath().points().count());
    }
    assert(mappedBounds.width() >= 0);
    assert(mappedBounds.height() >= 0);
    if (paint->getIsStroked() || paint->getFeather() != 0)
    {
        // Outset the path's bounding box to account for stroking & feathering.
        float outset = 0;
        if (paint->getIsStroked())
        {
            outset = paint->getThickness() * .5f;
            if (paint->getJoin() == StrokeJoin::miter)
            {
                // Miter joins may be longer than the stroke radius.
                outset *= RIVE_MITER_LIMIT;
            }
            else if (paint->getCap() == StrokeCap::square)
            {
                // The diagonal of a square cap is longer than the stroke
                // radius.
                outset *= math::SQRT2;
            }
        }
        if (paint->getFeather() != 0)
        {
            outset += find_feather_radius(paint->getFeather());
        }
        AABB strokePixelOutset = matrix.mapBoundingBox({0, 0, outset, outset});
        // Add an extra pixel to the stroke outset radius to account for:
        //   * Butt caps and bevel joins bleed out 1/2 AA width.
        //   * With Manhattan sytle AA, an AA width can be as large as sqrt(2).
        //   * The diagonal of that sqrt(2)/2 bleed is 1px in length.
        mappedBounds = mappedBounds.outset(strokePixelOutset.width() + 1,
                                           strokePixelOutset.height() + 1);
    }

    IAABB pixelBounds = mappedBounds.roundOut();
    bool doTriangulation = false;
    const AABB& localBounds = path->getBounds();
    if (context->isOutsideCurrentFrame(pixelBounds))
    {
        return DrawUniquePtr();
    }
    if (!paint->getIsStroked() && paint->getFeather() == 0)
    {
        // Use interior triangulation to draw filled paths if they're large
        // enough to benefit from it.
        //
        // FIXME! Implement interior triangulation for feathers.
        //
        // FIXME! Implement interior triangulation in msaa mode.
        if (context->frameInterlockMode() != gpu::InterlockMode::msaa &&
            path->getRawPath().verbs().count() < 1000 &&
            gpu::find_transformed_area(localBounds, matrix) > 512.f * 512.f)
        {
            doTriangulation = true;
        }
    }

    auto draw = context->make<PathDraw>(pixelBounds,
                                        matrix,
                                        std::move(path),
                                        fillRule,
                                        paint,
                                        coverageType,
                                        context->frameDescriptor());
    if (doTriangulation)
    {
        draw->initForInteriorTriangulation(
            context,
            scratchPath,
            localBounds.width() > localBounds.height()
                ? PathDraw::TriangulatorAxis::horizontal
                : PathDraw::TriangulatorAxis::vertical);
    }
    else
    {
        draw->initForMidpointFan(context, paint);
    }

    return DrawUniquePtr(draw);
}

PathDraw::PathDraw(IAABB pixelBounds,
                   const Mat2D& matrix,
                   rcp<const RiveRenderPath> path,
                   FillRule initialFillRule,
                   const RiveRenderPaint* paint,
                   CoverageType coverageType,
                   const RenderContext::FrameDescriptor& frameDesc) :
    Draw(pixelBounds,
         matrix,
         paint->getBlendMode(),
         ref_rcp(paint->getImageTexture()),
         paint->getImageSampler(),
         Type::path),
    m_pathRef(path.release()),
    m_pathFillRule(frameDesc.clockwiseFillOverride ? FillRule::clockwise
                                                   : initialFillRule),
    m_gradientRef(safe_ref(paint->getGradient())),
    m_paintType(paint->getType()),
    m_coverageType(coverageType)
{
    assert(m_pathRef != nullptr);
    assert(!m_pathRef->getRawPath().empty());
    assert(paint != nullptr);

    if (paint->getIsOpaque())
    {
        m_drawContents |= gpu::DrawContents::opaquePaint;
    }

    if (paint->getFeather() != 0)
    {
        m_featherRadius = find_feather_radius(paint->getFeather());
        assert(!std::isnan(m_featherRadius)); // These should get culled in
                                              // RiveRenderer::drawPath().
        assert(m_featherRadius > 0);
    }

    if (paint->getIsStroked())
    {
        m_strokeRadius = paint->getThickness() * .5f;
        // Ensure stroke radius is nonzero. (In PLS, zero radius means the path
        // is filled.)
        m_strokeRadius =
            fmaxf(m_strokeRadius, std::numeric_limits<float>::min());
        assert(!std::isnan(m_strokeRadius)); // These should get culled in
                                             // RiveRenderer::drawPath().
        assert(m_strokeRadius > 0);
    }

    // For atlased paths, m_drawContents refers to the rectangle being drawn
    // into the main render target, not the step that generates the atlas mask.
    if (m_coverageType != CoverageType::atlas)
    {
        if (isStroke())
        {
            m_drawContents |= gpu::DrawContents::stroke;
        }
        else
        {
            if (m_featherRadius)
            {
                m_drawContents |= gpu::DrawContents::featheredFill;
            }
            if (initialFillRule == FillRule::clockwise ||
                frameDesc.clockwiseFillOverride)
            {
                m_drawContents |= gpu::DrawContents::clockwiseFill;
            }
            else if (initialFillRule == FillRule::nonZero)
            {
                m_drawContents |= gpu::DrawContents::nonZeroFill;
            }
            else if (initialFillRule == FillRule::evenOdd)
            {
                m_drawContents |= gpu::DrawContents::evenOddFill;
            }
        }
    }

    if (paint->getType() == gpu::PaintType::clipUpdate)
    {
        m_drawContents |= gpu::DrawContents::clipUpdate;
        if (paint->getSimpleValue().outerClipID != 0)
        {
            m_drawContents |= gpu::DrawContents::activeClip;
        }
    }

    if (isStroke())
    {
        // Stroke triangles are always forward.
        m_contourDirections = gpu::ContourDirections::forward;
    }
    else if (initialFillRule == FillRule::clockwise)
    {
        // Clockwise paths need to be reversed when the matrix is left-handed,
        // so that the intended forward triangles remain clockwise.
        float det = matrix.xx() * matrix.yy() - matrix.yx() * matrix.xy();
        if (det < 0)
        {
            m_contourDirections =
                m_coverageType == CoverageType::msaa
                    ? gpu::ContourDirections::reverse
                    : gpu::ContourDirections::forwardThenReverse;
            m_contourFlags |= NEGATE_PATH_FILL_COVERAGE_FLAG; // ignored by msaa
        }
        else
        {
            m_contourDirections =
                m_coverageType == CoverageType::msaa
                    ? gpu::ContourDirections::forward
                    : gpu::ContourDirections::reverseThenForward;
        }
    }
    else if (m_coverageType != CoverageType::msaa)
    {
        // atomic and rasterOrdering fills need reverse AND forward triangles.
        if (frameDesc.clockwiseFillOverride &&
            !m_pathRef->isClockwiseDominant(matrix))
        {
            // For clockwiseFill, this is also our opportunity to logically
            // reverse the winding of the path, if it is predominantly
            // counterclockwise.
            m_contourDirections = gpu::ContourDirections::forwardThenReverse;
            m_contourFlags |= NEGATE_PATH_FILL_COVERAGE_FLAG;
        }
        else
        {
            m_contourDirections = gpu::ContourDirections::reverseThenForward;
        }
    }
    else
    {
        if (initialFillRule == FillRule::nonZero ||
            frameDesc.clockwiseFillOverride)
        {
            // Emit "nonZero" msaa fills in a direction such that the dominant
            // triangle winding area is always clockwise. This maximizes pixel
            // throughput since we will draw counterclockwise triangles twice
            // and clockwise only once.
            m_contourDirections = m_pathRef->isClockwiseDominant(matrix)
                                      ? gpu::ContourDirections::forward
                                      : gpu::ContourDirections::reverse;
        }
        else
        {
            // "evenOdd" msaa fills just get drawn twice, so any direction is
            // fine.
            m_contourDirections = gpu::ContourDirections::forward;
        }
    }

    m_simplePaintValue = paint->getSimpleValue();

    if (m_coverageType == CoverageType::atlas)
    {
        // Reserve two triangles for our on-screen rectangle that reads coverage
        // from the atlas.
        m_resourceCounts.maxTriangleVertexCount = 6;
    }

    RIVE_DEBUG_CODE(m_pathRef->lockRawPathMutations();)
    RIVE_DEBUG_CODE(m_rawPathMutationID = m_pathRef->getRawPathMutationID();)
    assert(isStroke() == (strokeRadius() > 0));
    assert(isFeatheredFill() == (!isStroke() && featherRadius() > 0));
    assert(!isFeatheredFill() || featherRadius() > 0);
}

void PathDraw::releaseRefs()
{
    RIVE_PROF_SCOPE()
    Draw::releaseRefs();
    RIVE_DEBUG_CODE(m_pathRef->unlockRawPathMutations();)
    m_pathRef->unref();
    safe_unref(m_gradientRef);
}

void PathDraw::initForMidpointFan(RenderContext* context,
                                  const RiveRenderPaint* paint)
{
    RIVE_PROF_SCOPE()
    // Only call init() once.
    assert((m_resourceCounts.midpointFanTessVertexCount |
            m_resourceCounts.outerCubicTessVertexCount) == 0);

    if (isStrokeOrFeather())
    {
        m_strokeMatrixMaxScale = m_matrix.findMaxScale();

        float r_ = 0;
        if (m_featherRadius != 0)
        {
            r_ = m_featherRadius * m_strokeMatrixMaxScale;

            // Inverse of 1/calc_polar_segments_per_radian at
            // FEATHER_MIN_POLAR_SEGMENT_ANGLE.
            //
            // i.e., 1 / calc_polar_segments_per_radian<kPolarPrecision>(
            //           FEATHER_MIN_POLAR_SEGMENT_ANGLE) ==
            //       FEATHER_MAX_SCREEN_SPACE_RADIUS
            //
            constexpr static float FEATHER_MAX_SCREEN_SPACE_RADIUS =
                1 / (kPolarPrecision *
                     (1 - COS_FEATHER_POLAR_SEGMENT_MIN_ANGLE_OVER_2));
            assert(math::nearly_equal(
                1 / math::calc_polar_segments_per_radian<kPolarPrecision>(
                        FEATHER_MAX_SCREEN_SPACE_RADIUS),
                gpu::FEATHER_POLAR_SEGMENT_MIN_ANGLE));

            // r_ is used to calculate how many polar segments are needed. Limit
            // r_ for large feathers. (See FEATHER_POLAR_SEGMENT_MIN_ANGLE.)
            r_ = std::min(r_, FEATHER_MAX_SCREEN_SPACE_RADIUS);
        }
        if (isStroke())
        {
            r_ += m_strokeRadius * m_strokeMatrixMaxScale;
        }
        m_polarSegmentsPerRadian =
            math::calc_polar_segments_per_radian<kPolarPrecision>(r_);

        m_strokeJoin = paint->getJoin();
        m_strokeCap = paint->getCap();
    }

    // Count up how much temporary storage this function will need to reserve in
    // CPU buffers.
    const RawPath& rawPath = m_pathRef->getRawPath();
    size_t contourCount = rawPath.countMoveTos();
    assert(contourCount != 0);

    m_contours = reinterpret_cast<ContourInfo*>(
        context->perFrameAllocator().alloc(sizeof(ContourInfo) * contourCount));

    size_t maxStrokedCurvesBeforeChops = 0;
    size_t maxCurves = 0;
    size_t maxRotations = 0;
    // 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-cubic) move.
    size_t pathMaxLinesOrCurvesBeforeChops = rawPath.verbs().size() - 1;
    // Stroked cubics can be chopped into a maximum of 5 segments.
    size_t pathMaxLinesOrCurvesAfterChops =
        isStrokeOrFeather() ? pathMaxLinesOrCurvesBeforeChops * 5
                            : pathMaxLinesOrCurvesBeforeChops;
    maxCurves += pathMaxLinesOrCurvesAfterChops;
    if (isStrokeOrFeather())
    {
        maxStrokedCurvesBeforeChops += pathMaxLinesOrCurvesBeforeChops;
        maxRotations += pathMaxLinesOrCurvesAfterChops;
        if (m_strokeJoin == StrokeJoin::round)
        {
            // If the stroke has round joins, we also record the rotations
            // between (pre-chopped) joins in order to calculate how many
            // vertices are in each round join.
            maxRotations += pathMaxLinesOrCurvesBeforeChops;
        }
    }

    // Each stroked curve will record the number of chops it requires (either 0,
    // 1, or 2).
    size_t maxChops = maxStrokedCurvesBeforeChops;
    // 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 5
    // internal points per chop.
    size_t maxChopVertices = maxStrokedCurvesBeforeChops * 5;
    // +3 for each contour because we align each contour's curves and rotations
    // on multiples of 4.
    size_t maxPaddedRotations =
        isStrokeOrFeather() ? maxRotations + contourCount * 3 : 0;
    size_t maxPaddedCurves = maxCurves + contourCount * 3;

    // Reserve intermediate space for the polar segment counts of each curve and
    // round join.
    if (isStrokeOrFeather())
    {
        m_numChops.reset(context->numChopsAllocator(), maxChops);
        m_chopVertices.reset(context->chopVerticesAllocator(), maxChopVertices);
        m_tangentPairs =
            context->tangentPairsAllocator().alloc(maxPaddedRotations);
        m_polarSegmentCounts =
            context->polarSegmentCountsAllocator().alloc(maxPaddedRotations);
    }
    m_parametricSegmentCounts =
        context->parametricSegmentCountsAllocator().alloc(maxPaddedCurves);

    float parametricPrecision = gpu::kParametricPrecision;
    if (m_featherRadius > 1)
    {
        // Once the blur radius is above ~50 pixels, we don't have to tessellate
        // within 1/4px of the edge anymore.
        // At this point, tessellate within strokeRadius/200 pixels of the edge.
        // (parametricPrecision == 1/tolerance.)
        parametricPrecision =
            std::min(parametricPrecision * 100.f /
                         (m_featherRadius * m_strokeMatrixMaxScale),
                     parametricPrecision);
    }

    size_t lineCount = 0;
    size_t unpaddedCurveCount = 0;
    size_t unpaddedRotationCount = 0;
    size_t emptyStrokeCountForCaps = 0;

    // Iteration pass 1: Collect information on contour and curves counts for
    // every path in the batch, and begin counting tessellated vertices.
    size_t contourIdx = 0;
    size_t curveIdx = 0;
    // We measure rotations on both curves and round joins.
    size_t rotationIdx = 0;
    bool roundJoinStroked = isStroke() && m_strokeJoin == StrokeJoin::round;
    wangs_formula::VectorXform vectorXform(m_matrix);
    RawPath::Iter startOfContour = rawPath.begin();
    RawPath::Iter end = rawPath.end();
    // Original number of lines and curves, before chopping.
    int preChopVerbCount = 0;
    Vec2D endpointsSum{};
    bool closed = !isStroke();
    Vec2D lastTangent = {0, 1};
    Vec2D firstTangent = {0, 1};
    size_t roundJoinCount = 0;
    size_t contourFirstCurveIdx = curveIdx;
    assert(contourFirstCurveIdx % 4 == 0);
    size_t contourFirstRotationIdx = rotationIdx;
    assert(contourFirstRotationIdx % 4 == 0);
    auto finishAndAppendContour = [&](RawPath::Iter iter) {
        if (closed)
        {
            Vec2D finalPtInContour = iter.rawPtsPtr()[-1];
            // Bit-cast to uint64_t because we don't want the special equality
            // rules for NaN. If we're empty or otherwise return back to p0, we
            // want to detect this, regardless of whether there are NaN values.
            if (math::bit_cast<uint64_t>(startOfContour.movePt()) !=
                math::bit_cast<uint64_t>(finalPtInContour))
            {
                assert(preChopVerbCount > 0);
                if (roundJoinStroked)
                {
                    // Round join before implicit closing line.
                    Vec2D tangent = startOfContour.movePt() - finalPtInContour;
                    assert(rotationIdx < maxPaddedRotations);
                    m_tangentPairs[rotationIdx++] = {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(rotationIdx < maxPaddedRotations);
                m_tangentPairs[rotationIdx++] = {lastTangent, firstTangent};
                ++roundJoinCount;
            }
        }
        size_t strokeJoinCount = preChopVerbCount;
        if (!closed)
        {
            strokeJoinCount = std::max<size_t>(strokeJoinCount, 1) - 1;
        }
        assert(contourIdx < contourCount);
        m_contours[contourIdx++] = {
            iter,
            lineCount,
            contourFirstCurveIdx,
            curveIdx,
            contourFirstRotationIdx,
            rotationIdx,
            isStroke() ? Vec2D() : endpointsSum * (1.f / preChopVerbCount),
            closed,
            strokeJoinCount,
            0,                 // strokeCapSegmentCount
            0,                 // paddingVertexCount
            RIVE_DEBUG_CODE(0) // tessVertexCount
        };
        unpaddedCurveCount += curveIdx - contourFirstCurveIdx;
        contourFirstCurveIdx = curveIdx =
            math::round_up_to_multiple_of<4>(curveIdx);
        unpaddedRotationCount += rotationIdx - contourFirstRotationIdx;
        contourFirstRotationIdx = rotationIdx =
            math::round_up_to_multiple_of<4>(rotationIdx);
    };
    const int styleFlags = style_flags(isStrokeOrFeather(), 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 = !isStroke();
                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(rotationIdx < maxPaddedRotations);
                    m_tangentPairs[rotationIdx++] = {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];
                math::find_cubic_tangents(p, unchoppedTangents);
                if (preChopVerbCount == 0)
                {
                    firstTangent = unchoppedTangents[0];
                }
                else
                {
                    assert(rotationIdx < maxPaddedRotations);
                    m_tangentPairs[rotationIdx++] = {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 = false;
                uint8_t numChops =
                    isStroke()
                        ? math::find_cubic_convex_180_chops(p, t, &areCusps)
                        : 0; // Feathers already got chopped.
                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_chopVertices.push_back() = {t[0], t[1]};
                        chop_cubic_around_cusps(p,
                                                localChopBuffer,
                                                t,
                                                numChops,
                                                m_strokeMatrixMaxScale);
                        p = localChopBuffer;
                        numChops *= 2;
                        break;
                    case simple_chop_key(2): // 2 non-cusp chops
                        m_chopVertices.push_back() = {t[0], t[1]};
                        math::chop_cubic_at(p, localChopBuffer, t[0], t[1]);
                        p = localChopBuffer;
                        break;
                    case simple_chop_key(1): // 1 non-cusp chop
                    {
                        math::chop_cubic_at(p, localChopBuffer, t[0]);
                        p = localChopBuffer;
                        memcpy(m_chopVertices.push_back_n(5),
                               p + 1,
                               sizeof(Vec2D) * 5);
                        break;
                    }
                }
                // Calculate segment counts for each chopped section
                // independently.
                for (const Vec2D* end = p + numChops * 3 + 3; p != end;
                     p += 3, ++curveIdx, ++rotationIdx)
                {
                    float n4 = wangs_formula::cubic_pow4(p,
                                                         parametricPrecision,
                                                         vectorXform);
                    // Record n^4 for now. This will get resolved later.
                    assert(curveIdx < maxPaddedCurves);
                    RIVE_INLINE_MEMCPY(m_parametricSegmentCounts + curveIdx,
                                       &n4,
                                       sizeof(uint32_t));
                    assert(rotationIdx < maxPaddedRotations);
                    if (isStroke())
                    {
                        math::find_cubic_tangents(
                            p,
                            m_tangentPairs[rotationIdx].data());
                    }
                    else
                    {
                        // FIXME: Feathered fills don't have polar segments for
                        // now, but we're leaving space for them in
                        // m_tangentPairs because we will convert them to use a
                        // similar concept of "polar joins" once we move them
                        // onto the GPU.
                        m_tangentPairs[rotationIdx] = {Vec2D{0, 1},
                                                       Vec2D{0, 1}};
                    }
                }
                break;
            }
            case StyledVerb::filledCubic:
            {
                const Vec2D* p = iter.cubicPts();
                ++preChopVerbCount;
                endpointsSum += p[3];
                float n4 = wangs_formula::cubic_pow4(p,
                                                     parametricPrecision,
                                                     vectorXform);
                // Record n^4 for now. This will get resolved later.
                assert(curveIdx < maxPaddedCurves);
                RIVE_INLINE_MEMCPY(m_parametricSegmentCounts + curveIdx++,
                                   &n4,
                                   sizeof(uint32_t));
                break;
            }
        }
    }
    if (startOfContour != end)
    {
        finishAndAppendContour(end);
    }
    assert(contourIdx == contourCount);
    assert(contourCount > 0);
    assert(curveIdx <= maxPaddedCurves);
    assert(rotationIdx <= maxPaddedRotations);
    // Because we write parametric segment counts in batches of 4.
    assert(curveIdx % 4 == 0);
    // Because we write polar segment counts in batches of 4.
    assert(rotationIdx % 4 == 0);
    assert(isStrokeOrFeather() || maxPaddedRotations == 0);
    assert(isStrokeOrFeather() || rotationIdx == 0);

    // Return any data we conservatively allocated but did not use.
    if (isStrokeOrFeather())
    {
        m_numChops.shrinkToFit(context->numChopsAllocator(), maxChops);
        m_chopVertices.shrinkToFit(context->chopVerticesAllocator(),
                                   maxChopVertices);
        context->tangentPairsAllocator().rewindLastAllocation(
            maxPaddedRotations - rotationIdx);
        context->polarSegmentCountsAllocator().rewindLastAllocation(
            maxPaddedRotations - rotationIdx);
    }
    context->parametricSegmentCountsAllocator().rewindLastAllocation(
        maxPaddedCurves - curveIdx);

    // Iteration pass 2: Finish calculating the numbers of tessellation segments
    // in each contour, using SIMD.
    size_t contourFirstLineIdx = 0;
    size_t tessVertexCount = 0;
    for (size_t i = 0; i < contourCount; ++i)
    {
        ContourInfo* contour = &m_contours[i];
        size_t contourLineCount = contour->endLineIdx - contourFirstLineIdx;
        uint32_t contourVertexCount = math::lossless_numeric_cast<uint32_t>(
            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 = contour->firstCurveIdx; j < contour->endCurveIdx; j += 4)
        {
            // Curves recorded their segment counts raised to the 4th power. Now
            // find their roots and convert to integers in batches of 4.
            assert(j + 4 <= curveIdx);
            float4 n = simd::load4f(m_parametricSegmentCounts + 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 <= curveIdx);
            simd::store(m_parametricSegmentCounts + 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 (isStrokeOrFeather())
        {
            // Finish calculating and counting polar segments for each stroked
            // curve and round join.
            for (j = contour->firstRotationIdx; j < contour->endRotationIdx;
                 j += 4)
            {
                // Measure the rotations of curves in batches of 4.
                assert(j + 4 <= rotationIdx);

                float4 tx0, ty0, tx1, ty1;
                std::tie(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 * m_polarSegmentsPerRadian);
                n = simd::clamp(n, float4(1), float4(kMaxPolarSegments));
                uint4 n_ = simd::cast<uint32_t>(n);
                assert(j + 4 <= rotationIdx);
                simd::store(m_polarSegmentCounts + 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 (!isStroke())
            {
                assert(isFeatheredFill());
                uint32_t numSegmentsInFeatherJoin =
                    feather_join_segment_count(m_polarSegmentsPerRadian);
                contourVertexCount +=
                    contour->strokeJoinCount * (numSegmentsInFeatherJoin - 1);
            }
            else if (m_strokeJoin == 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 *
                                      (NUM_SEGMENTS_IN_MITER_OR_BEVEL_JOIN - 1);
            }

            // Count stroke caps, if any.
            bool empty = contour->endLineIdx == contourFirstLineIdx &&
                         contour->endCurveIdx == contour->firstCurveIdx;
            StrokeCap cap;
            bool needsCaps = false;
            if (!empty)
            {
                cap = m_strokeCap;
                needsCaps = !contour->closed;
            }
            else if (isStroke())
            {
                cap = empty_stroke_cap(contour->closed,
                                       m_strokeJoin,
                                       m_strokeCap);
                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.
                    float strokeCapSegmentCount =
                        ceilf(m_polarSegmentsPerRadian * math::PI);
                    // +2 because round caps emulated as joins need to emit
                    // vertices at T=0 and T=1, unlike normal round joins.
                    strokeCapSegmentCount += 2;
                    // Make sure not to exceed kMaxPolarSegments.
                    strokeCapSegmentCount =
                        fminf(strokeCapSegmentCount, kMaxPolarSegments);
                    contour->strokeCapSegmentCount =
                        static_cast<uint32_t>(strokeCapSegmentCount);
                }
                else
                {
                    contour->strokeCapSegmentCount =
                        NUM_SEGMENTS_IN_MITER_OR_BEVEL_JOIN;
                }
                // PLS expects all patches to have >0 tessellation vertices, so
                // for the case of an empty patch with a stroke cap,
                // contour->strokeCapSegmentCount can't be zero. Also,
                // pushContourToRenderContext() uses "strokeCapSegmentCount !=
                // 0" to tell if it needs stroke caps.
                assert(contour->strokeCapSegmentCount >= 2);
                // 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 - contour->firstCurveIdx;
            contourVertexCount += contourCurveCount;
        }
        contourVertexCount += simd::reduce_add(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 =
            math::padding_to_align_up<kMidpointFanPatchSegmentSpan>(
                contourVertexCount);
        contourVertexCount += contour->paddingVertexCount;
        assert(contourVertexCount % kMidpointFanPatchSegmentSpan == 0);
        RIVE_DEBUG_CODE(contour->tessVertexCount = contourVertexCount;)

        tessVertexCount += contourVertexCount;
        contourFirstLineIdx = contour->endLineIdx;
    }

    assert(contourFirstLineIdx == lineCount);
    RIVE_DEBUG_CODE(m_pendingLineCount = lineCount);
    RIVE_DEBUG_CODE(m_pendingCurveCount = unpaddedCurveCount);
    RIVE_DEBUG_CODE(m_pendingRotationCount = unpaddedRotationCount);
    RIVE_DEBUG_CODE(m_pendingEmptyStrokeCountForCaps = emptyStrokeCountForCaps);

    if (tessVertexCount > 0)
    {
        m_resourceCounts.pathCount = 1;
        m_resourceCounts.contourCount = contourCount;
        // maxTessellatedSegmentCount does not get doubled when we emit both
        // forward and mirrored contours because the forward and mirrored pair
        // both get packed into a single gpu::TessVertexSpan.
        m_resourceCounts.maxTessellatedSegmentCount =
            lineCount + unpaddedCurveCount + emptyStrokeCountForCaps;
        m_resourceCounts.midpointFanTessVertexCount =
            gpu::ContourDirectionsAreDoubleSided(m_contourDirections)
                ? tessVertexCount * 2
                : tessVertexCount;
    }
}

void PathDraw::initForInteriorTriangulation(RenderContext* context,
                                            RawPath* scratchPath,
                                            TriangulatorAxis triangulatorAxis)
{
    RIVE_PROF_SCOPE()
    PUSH_DISABLE_CLANG_SIMD_ABI_WARNING()
    assert(simd::all(m_resourceCounts.toVec() == 0)); // Only call init() once.
    POP_DISABLE_CLANG_SIMD_ABI_WARNING()
    assert(!isStrokeOrFeather());
    assert(m_strokeRadius == 0);

    // Every path has at least 1 (non-cubic) move.
    size_t originalNumChopsSize = m_pathRef->getRawPath().verbs().size() - 1;
    m_numChops.reset(context->numChopsAllocator(), originalNumChopsSize);
    iterateInteriorTriangulation(
        InteriorTriangulationOp::countDataAndTriangulate,
        &context->perFrameAllocator(),
        scratchPath,
        triangulatorAxis,
        nullptr);
    m_numChops.shrinkToFit(context->numChopsAllocator(), originalNumChopsSize);
}

bool PathDraw::allocateResources(RenderContext::LogicalFlush* flush)
{
    RIVE_PROF_SCOPE()
    const RenderContext::FrameDescriptor& frameDesc = flush->frameDescriptor();

    // Allocate a gradient if needed. Do this first since it's more expensive to
    // fail after setting up an atlas draw than a gradient draw.
    if (m_gradientRef != nullptr &&
        !flush->allocateGradient(m_gradientRef,
                                 &m_simplePaintValue.colorRampLocation))
    {
        return false;
    }

    // Allocate a coverage buffer range or atlas region if needed.
    if (m_coverageType == CoverageType::atlas ||
        m_coverageType == CoverageType::clockwiseAtomic)
    {
        constexpr static int PADDING = 2;

        // We don't need any coverage space for areas outside the viewport.
        // TODO: Account for active clips as well once we have the info.
        IAABB renderTargetBounds = {
            0,
            0,
            static_cast<int32_t>(frameDesc.renderTargetWidth),
            static_cast<int32_t>(frameDesc.renderTargetHeight),
        };
        IAABB visibleBounds = renderTargetBounds.intersect(m_pixelBounds);

        if (m_coverageType == CoverageType::atlas)
        {
            const float scaleFactor =
                find_atlas_feather_scale_factor(m_featherRadius,
                                                m_strokeMatrixMaxScale);
            auto w = static_cast<uint16_t>(
                ceilf(visibleBounds.width() * scaleFactor));
            auto h = static_cast<uint16_t>(
                ceilf(visibleBounds.height() * scaleFactor));
            uint16_t x, y;
            if (!flush->allocateAtlasDraw(this,
                                          w,
                                          h,
                                          PADDING,
                                          &x,
                                          &y,
                                          &m_atlasScissor))
            {
                return false; // There wasn't room for our path in the atlas.
            }
            m_atlasTransform.scaleFactor = scaleFactor;
            m_atlasTransform.translateX = x - visibleBounds.left * scaleFactor;
            m_atlasTransform.translateY = y - visibleBounds.top * scaleFactor;
            m_atlasScissorEnabled = visibleBounds != m_pixelBounds;
        }
        else
        {
            // Round up width and height to multiples of 32 for tiling.
            uint32_t coverageWidth = math::round_up_to_multiple_of<32>(
                visibleBounds.width() + PADDING * 2);
            uint32_t coverageHeight = math::round_up_to_multiple_of<32>(
                visibleBounds.height() + PADDING * 2);

            // Get our coverage allocation.
            size_t offset = flush->allocateCoverageBufferRange(coverageHeight *
                                                               coverageWidth);
            if (offset == -1)
            {
                return false; // There wasn't room for our coverage buffer.
            }
            m_coverageBufferRange.offset =
                math::lossless_numeric_cast<uint32_t>(offset);
            m_coverageBufferRange.pitch = coverageWidth;
            m_coverageBufferRange.offsetX = -visibleBounds.left + PADDING;
            m_coverageBufferRange.offsetY = -visibleBounds.top + PADDING;
        }
    }
    return true;
}

void PathDraw::countSubpasses()
{
    RIVE_PROF_SCOPE()
    m_subpassCount = 1;
    m_prepassCount = 0;

    switch (m_coverageType)
    {
        case CoverageType::atlas:
            assert(m_triangulator == nullptr);
            m_subpassCount = 1;
            break;

        case CoverageType::pixelLocalStorage:
            m_subpassCount = (m_triangulator != nullptr)
                                 ? 2 // outer cubics, interior triangles
                                 : 1;
            break;

        case CoverageType::clockwise:
            m_subpassCount =
                (m_triangulator != nullptr)
                    ? 3 // ccw interior tris, outer cubics, cw interior tris
                    : 1;
            break;

        case CoverageType::clockwiseAtomic:
            m_subpassCount = (m_triangulator != nullptr) ? 2 : 1;
            if (!isStroke())
            {
                m_prepassCount = m_subpassCount; // Borrowed coverage.
            }
            break;

        case CoverageType::msaa:
        {
            if (isStroke())
            {
                m_subpassCount = 1; // Strokes can be rendered in a single pass.
            }
            else if ((m_drawContents & gpu::kNestedClipUpdateMask) ==
                     gpu::kNestedClipUpdateMask)
            {
                // Nested clip updates only have a stencil pass. (The reset is
                // handled by a separate msaaStencilClipReset draw.)
                m_subpassCount = 1;
            }
            else if (m_drawContents & gpu::DrawContents::evenOddFill)
            {
                m_subpassCount = 2; // MSAA "slow" path: stencil-then-cover.
            }
            else
            {
                // MSAA "fast" path: (effectively) single pass rendering.
                m_subpassCount = 3;
            }
            if (isOpaque())
            {
                const bool usesClipping =
                    m_drawContents & (gpu::DrawContents::activeClip |
                                      gpu::DrawContents::clipUpdate);
                if (!usesClipping)
                {
                    // Render this path front-to-back instead of back-to-front.
                    assert(m_prepassCount == 0);
                    m_prepassCount = m_subpassCount;
                    m_subpassCount = 0;
                }
            }
        }
    }
}

void PathDraw::pushToRenderContext(RenderContext::LogicalFlush* flush,
                                   int subpassIndex)
{
    RIVE_PROF_SCOPE()
    // Make sure the rawPath in our path reference hasn't changed since we began
    // holding!
    assert(m_rawPathMutationID == m_pathRef->getRawPathMutationID());
    assert(!m_pathRef->getRawPath().empty());

    assert(m_resourceCounts.outerCubicTessVertexCount == 0 ||
           m_resourceCounts.midpointFanTessVertexCount == 0);
    uint32_t tessVertexCount = math::lossless_numeric_cast<uint32_t>(
        m_resourceCounts.outerCubicTessVertexCount |
        m_resourceCounts.midpointFanTessVertexCount);
    if (tessVertexCount == 0)
    {
        return;
    }

    if (m_pathID == 0)
    {
        // Reserve our pathID and write out a path record.
        m_pathID = flush->pushPath(this);
    }

    switch (m_coverageType)
    {
        case CoverageType::pixelLocalStorage:
        case CoverageType::clockwise:
        {
            const int mainSubpassIdx =
                (m_coverageType == CoverageType::clockwise &&
                 m_triangulator != nullptr)
                    ? 1
                    : 0;
            if (subpassIndex == mainSubpassIdx)
            {
                // Tessellation (midpoint fan or outer cubic).
                uint32_t tessLocation =
                    allocateTessellationVertices(flush, tessVertexCount);
                pushTessellationData(flush, tessVertexCount, tessLocation);
                pushTessellationDraw(flush, tessVertexCount, tessLocation);
            }
            else
            {
                // Interior triangles.
                assert(m_triangulator != nullptr);
                assert((m_coverageType == CoverageType::pixelLocalStorage &&
                        subpassIndex == 1) ||
                       (m_coverageType == CoverageType::clockwise &&
                        (subpassIndex == 0 || subpassIndex == 2)));
                RIVE_DEBUG_CODE(m_numInteriorTriangleVerticesPushed +=)
                flush->pushInteriorTriangulationDraw(
                    this,
                    m_pathID,
                    (m_coverageType == CoverageType::clockwise)
                        // Clockwise mode renders counterclockwise (borrowed
                        // coverage) interior triangles in a separate pass.
                        ? (subpassIndex == 0) ? gpu::WindingFaces::negative
                                              : gpu::WindingFaces::positive
                        : gpu::WindingFaces::all,
                    (subpassIndex == 0) // => CoverageType::clockwise
                        ? gpu::ShaderMiscFlags::borrowedCoveragePass
                        : gpu::ShaderMiscFlags::none);
                assert(m_numInteriorTriangleVerticesPushed <=
                       m_triangulator->maxVertexCount());
            }
            break;
        }

        case CoverageType::clockwiseAtomic:
            if (!isStroke())
            {
                // The subpass and prepass each emit half the vertices.
                assert(m_prepassCount == m_subpassCount);
                assert(tessVertexCount % 2 == 0);
                tessVertexCount /= 2;
            }
            switch (subpassIndex)
            {
                case -1: // Tessellation (borrowed, midpointFan or outerCubic).
                    assert(!isStroke());
                    m_prepassTessLocation =
                        allocateTessellationVertices(flush, tessVertexCount);
                    pushTessellationDraw(
                        flush,
                        tessVertexCount,
                        m_prepassTessLocation,
                        gpu::ShaderMiscFlags::borrowedCoveragePass);
                    break;

                case 0: // Tessellation (midpointFan or outerCubic).
                {
                    uint32_t tessLocation =
                        allocateTessellationVertices(flush, tessVertexCount);
                    pushTessellationData(flush, tessVertexCount, tessLocation);
                    pushTessellationDraw(flush, tessVertexCount, tessLocation);
                    break;
                }

                case -2: // Interior triangles (borrowed).
                case 1:  // Interior triangles.
                    assert(!isStroke());
                    assert(m_triangulator != nullptr);
                    RIVE_DEBUG_CODE(m_numInteriorTriangleVerticesPushed +=)
                    flush->pushInteriorTriangulationDraw(
                        this,
                        m_pathID,
                        subpassIndex < 0 ? gpu::WindingFaces::negative
                                         : gpu::WindingFaces::positive,
                        subpassIndex < 0
                            ? gpu::ShaderMiscFlags::borrowedCoveragePass
                            : gpu::ShaderMiscFlags::none);
                    assert(m_numInteriorTriangleVerticesPushed <=
                           m_triangulator->maxVertexCount());
                    break;

                default:
                    RIVE_UNREACHABLE();
            }
            break;

        case CoverageType::msaa:
        {
            assert(m_prepassCount == 0 || m_subpassCount == 0);
            int passCount = m_prepassCount | m_subpassCount;
            int passIdx = subpassIndex + m_prepassCount;
            if (passIdx == 0)
            {
                m_msaaTessLocation =
                    allocateTessellationVertices(flush, tessVertexCount);
                pushTessellationData(flush,
                                     tessVertexCount,
                                     m_msaaTessLocation);
            }
            constexpr static gpu::DrawType MSAA_FILL_TYPES[][3] = {
                // Nested clip update (passCount == 1; the reset is handled by a
                // separate msaaStencilClipReset draw.)
                {
                    gpu::DrawType::msaaMidpointFanPathsStencil,
                },

                // Slow path (passCount == 2): stencil-then-cover
                {
                    gpu::DrawType::msaaMidpointFanPathsStencil,
                    gpu::DrawType::msaaMidpointFanPathsCover,
                },

                // Fast path (passCount == 3): (mostly) single pass rendering.
                {
                    gpu::DrawType::msaaMidpointFanBorrowedCoverage,
                    gpu::DrawType::msaaMidpointFans,
                    gpu::DrawType::msaaMidpointFanStencilReset,
                },
            };
            assert(passCount <= 3);
            assert(passIdx < passCount);
            gpu::DrawType msaaDrawType =
                isStroke() ? gpu::DrawType::msaaStrokes
                           : MSAA_FILL_TYPES[passCount - 1][passIdx];
            flush->pushMidpointFanDraw(this,
                                       msaaDrawType,
                                       tessVertexCount,
                                       m_msaaTessLocation);
            break;
        }

        case CoverageType::atlas:
            // Atlas draws only have one subpass -- the rectangular blit from
            // the atlas to the screen. The step that renders coverage to the
            // offscreen atlas is handled separately, outside the subpass
            // system.
            assert(subpassIndex == 0);
            flush->pushAtlasBlit(this, m_pathID);
            break;
    }
}

void PathDraw::pushTessellationDraw(RenderContext::LogicalFlush* flush,
                                    uint32_t tessVertexCount,
                                    uint32_t tessLocation,
                                    gpu::ShaderMiscFlags shaderMiscFlags)
{
    if (m_triangulator != nullptr)
    {
        assert(!isStroke());
        flush->pushOuterCubicsDraw(this,
                                   gpu::DrawType::outerCurvePatches,
                                   tessVertexCount,
                                   tessLocation,
                                   shaderMiscFlags);
    }
    else
    {
        flush->pushMidpointFanDraw(
            this,
            isFeatheredFill() ? gpu::DrawType::midpointFanCenterAAPatches
                              : gpu::DrawType::midpointFanPatches,
            tessVertexCount,
            tessLocation,
            shaderMiscFlags);
    }
}

void PathDraw::pushAtlasTessellation(RenderContext::LogicalFlush* flush,
                                     uint32_t* tessVertexCount,
                                     uint32_t* tessBaseVertex)
{
    RIVE_PROF_SCOPE()
    assert(m_coverageType == CoverageType::atlas);
    assert(m_resourceCounts.outerCubicTessVertexCount == 0 ||
           m_resourceCounts.midpointFanTessVertexCount == 0);

    *tessVertexCount = math::lossless_numeric_cast<uint32_t>(
        m_resourceCounts.outerCubicTessVertexCount |
        m_resourceCounts.midpointFanTessVertexCount);

    if (*tessVertexCount == 0)
    {
        assert(m_pathID == 0);
        return;
    }

    *tessBaseVertex = allocateTessellationVertices(flush, *tessVertexCount);
    pushTessellationData(flush, *tessVertexCount, *tessBaseVertex);
}

void PathDraw::pushTessellationData(RenderContext::LogicalFlush* flush,
                                    uint32_t tessVertexCount,
                                    uint32_t tessLocation)
{
    RIVE_PROF_SCOPE()
    // Determine where to fill in forward and mirrored tessellations.
    uint32_t forwardTessVertexCount, forwardTessLocation,
        mirroredTessVertexCount, mirroredTessLocation;
    switch (m_contourDirections)
    {
        case gpu::ContourDirections::forward:
            forwardTessVertexCount = tessVertexCount;
            forwardTessLocation = tessLocation;
            mirroredTessLocation = mirroredTessVertexCount = 0;
            break;
        case gpu::ContourDirections::reverse:
            forwardTessVertexCount = forwardTessLocation = 0;
            mirroredTessVertexCount = tessVertexCount;
            mirroredTessLocation = tessLocation + tessVertexCount;
            break;
        case gpu::ContourDirections::reverseThenForward:
            if (m_coverageType == CoverageType::clockwiseAtomic && !isStroke())
            {
                // The tessellation for borrowed coverage was allocated at a
                // different location than the forward tessellation, both with
                // "tessVertexCount" vertices.
                assert(m_prepassTessLocation != 0); // With padding, this will
                                                    // only be zero if it wasn't
                                                    // initialized.
                forwardTessVertexCount = mirroredTessVertexCount =
                    tessVertexCount;
                forwardTessLocation = tessLocation;
                mirroredTessLocation = m_prepassTessLocation + tessVertexCount;
            }
            else
            {
                // The reverse and forward tessellations are allocated
                // contiguously, with a combined vertex count of
                // "tessVertexCount". (tessVertexCount/2 vertices each.)
                assert(tessVertexCount % 2 == 0);
                forwardTessVertexCount = mirroredTessVertexCount =
                    tessVertexCount / 2;
                forwardTessLocation = mirroredTessLocation =
                    tessLocation + tessVertexCount / 2;
            }
            break;
        case gpu::ContourDirections::forwardThenReverse:
            if (m_coverageType == CoverageType::clockwiseAtomic && !isStroke())
            {
                // The tessellation for borrowed coverage was allocated at a
                // different location than the forward tessellation, both with
                // "tessVertexCount" vertices.
                assert(m_prepassTessLocation != 0); // With padding, this will
                                                    // only be zero if it wasn't
                                                    // initialized.
                forwardTessVertexCount = mirroredTessVertexCount =
                    tessVertexCount;
                forwardTessLocation = m_prepassTessLocation;
                mirroredTessLocation = tessLocation + tessVertexCount;
            }
            else
            {
                // The reverse and forward tessellations are allocated
                // contiguously, with a combined vertex count of
                // "tessVertexCount". (tessVertexCount/2 vertices each.)
                assert(tessVertexCount % 2 == 0);
                forwardTessVertexCount = mirroredTessVertexCount =
                    tessVertexCount / 2;
                forwardTessLocation = tessLocation;
                mirroredTessLocation = tessLocation + tessVertexCount;
            }
            break;
    }

    // Write out the TessVertexSpans and path contours.
    RenderContext::TessellationWriter tessWriter(flush,
                                                 m_pathID,
                                                 m_contourDirections,
                                                 forwardTessVertexCount,
                                                 forwardTessLocation,
                                                 mirroredTessVertexCount,
                                                 mirroredTessLocation);

    if (m_triangulator != nullptr)
    {
        iterateInteriorTriangulation(
            InteriorTriangulationOp::pushOuterCubicTessellationData,
            nullptr,
            nullptr,
            TriangulatorAxis::dontCare,
            &tessWriter);
    }
    else
    {
        pushMidpointFanTessellationData(&tessWriter);
    }
}

void PathDraw::pushMidpointFanTessellationData(
    RenderContext::TessellationWriter* tessWriter)
{
    RIVE_PROF_SCOPE()
    const RawPath& rawPath = m_pathRef->getRawPath();
    RawPath::Iter startOfContour = rawPath.begin();
    for (size_t i = 0; i < m_resourceCounts.contourCount; ++i)
    {
        // Push a contour and curve records.
        const ContourInfo& contour = m_contours[i];
        assert(startOfContour.verb() == PathVerb::move);
        assert(isStroke() || contour.closed); // Fills are always closed.
        RIVE_DEBUG_CODE(m_pendingStrokeJoinCount =
                            isStrokeOrFeather() ? contour.strokeJoinCount : 0;)
        RIVE_DEBUG_CODE(m_pendingStrokeCapCount =
                            contour.strokeCapSegmentCount != 0 ? 2 : 0;)

        const Vec2D* pts = startOfContour.rawPtsPtr();
        size_t curveIdx = contour.firstCurveIdx;
        size_t rotationIdx = contour.firstRotationIdx;
        const RawPath::Iter end = contour.endOfContour;
        uint32_t joinTypeFlags = 0;
        bool roundJoinStroked = false;
        // Emit a starting cap before the next cubic?
        bool needsFirstEmulatedCapAsJoin = false;
        uint32_t emulatedCapAsJoinFlags = 0;
        if (isStrokeOrFeather())
        {
            joinTypeFlags = isStroke() ? join_type_flags(m_strokeJoin)
                                       : FEATHER_JOIN_CONTOUR_FLAG;
            roundJoinStroked = joinTypeFlags == ROUND_JOIN_CONTOUR_FLAG;
            if (contour.strokeCapSegmentCount != 0)
            {
                StrokeCap cap =
                    !contour.closed
                        ? m_strokeCap
                        : empty_stroke_cap(true, m_strokeJoin, m_strokeCap);
                switch (cap)
                {
                    case StrokeCap::butt:
                        emulatedCapAsJoinFlags = BEVEL_JOIN_CONTOUR_FLAG;
                        break;
                    case StrokeCap::square:
                        emulatedCapAsJoinFlags = MITER_CLIP_JOIN_CONTOUR_FLAG;
                        break;
                    case StrokeCap::round:
                        emulatedCapAsJoinFlags = ROUND_JOIN_CONTOUR_FLAG;
                        break;
                }
                emulatedCapAsJoinFlags |= EMULATED_STROKE_CAP_CONTOUR_FLAG;
                needsFirstEmulatedCapAsJoin = true;
            }
        }

        // Make a data record for this current contour on the GPU.
        uint32_t contourIDWithFlags =
            m_contourFlags |
            tessWriter->pushContour(contour.midpoint,
                                    isStroke(),
                                    contour.closed,
                                    contour.paddingVertexCount);

        // When we don't have round joins, the number of segments per join is
        // constant. (Round joins have a variable number of segments per join,
        // depending on the angle.)
        uint32_t numSegmentsInNotRoundJoin;
        if (isFeatheredFill())
        {
            numSegmentsInNotRoundJoin =
                feather_join_segment_count(m_polarSegmentsPerRadian);
        }
        else
        {
            numSegmentsInNotRoundJoin = NUM_SEGMENTS_IN_MITER_OR_BEVEL_JOIN;
        }

        // Convert all curves in the contour to cubics and push them to the GPU.
        const int styleFlags =
            style_flags(isStrokeOrFeather(), roundJoinStroked);
        Vec2D joinTangent = {0, 1};
        int joinSegmentCount = 1;
        Vec2D implicitClose[2]; // In case we need an implicit closing line.
        for (auto iter = startOfContour; 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_pendingRotationCount;)
                        RIVE_DEBUG_CODE(--m_pendingStrokeJoinCount;)
                    }
                    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_pendingStrokeCapCount;)
                    }
                    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 = numSegmentsInNotRoundJoin;
                        RIVE_DEBUG_CODE(--m_pendingStrokeJoinCount;)
                    }
                    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_pendingStrokeCapCount;)
                    }
                    [[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(
                            tessWriter,
                            cubic.data(),
                            contour.strokeCapSegmentCount,
                            contourIDWithFlags | emulatedCapAsJoinFlags);
                        needsFirstEmulatedCapAsJoin = false;
                    }
                    tessWriter->pushCubic(cubic.data(),
                                          m_contourDirections,
                                          joinTangent,
                                          1,
                                          1,
                                          joinSegmentCount,
                                          contourIDWithFlags | joinTypeFlags);
                    RIVE_DEBUG_CODE(--m_pendingLineCount;)
                    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_chopVertices.pop_front().x,
                                chopKey >> 1,
                                m_strokeMatrixMaxScale);
                            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_chopVertices.pop_front();
                            math::chop_cubic_at(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_chopVertices queue.
                            localChopBuffer[0] = p[0];
                            memcpy(localChopBuffer + 1,
                                   m_chopVertices.pop_front_n(5),
                                   sizeof(Vec2D) * 5);
                            localChopBuffer[6] = p[3];
                            p = localChopBuffer;
                            numChops = 1;
                            break;
                    }
                    if (needsFirstEmulatedCapAsJoin)
                    {
                        // Emulate the start cap as a 180-degree join before the
                        // first stroke.
                        pushEmulatedStrokeCapAsJoinBeforeCubic(
                            tessWriter,
                            p,
                            contour.strokeCapSegmentCount,
                            contourIDWithFlags | emulatedCapAsJoinFlags);
                        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];
                        tessWriter->pushCubic(p,
                                              m_contourDirections,
                                              joinTangent,
                                              parametricSegmentCount,
                                              polarSegmentCount,
                                              1,
                                              contourIDWithFlags |
                                                  joinTypeFlags);
                        RIVE_DEBUG_CODE(--m_pendingCurveCount;)
                        RIVE_DEBUG_CODE(--m_pendingRotationCount;)
                    }
                    // Push the final chop, with a join.
                    uint32_t parametricSegmentCount =
                        m_parametricSegmentCounts[curveIdx++];
                    uint32_t polarSegmentCount =
                        m_polarSegmentCounts[rotationIdx++];
                    RIVE_DEBUG_CODE(--m_pendingRotationCount;)
                    if (contour.closed || !is_final_verb_of_contour(iter, end))
                    {
                        if (styledVerb == StyledVerb::roundJoinStrokedCubic)
                        {
                            joinTangent = m_tangentPairs[rotationIdx][1];
                            joinSegmentCount =
                                m_polarSegmentCounts[rotationIdx];
                            ++rotationIdx;
                            RIVE_DEBUG_CODE(--m_pendingRotationCount;)
                        }
                        else
                        {
                            joinTangent = find_join_tangent(iter.cubicPts() + 3,
                                                            end.rawPtsPtr(),
                                                            contour.closed,
                                                            pts);
                            joinSegmentCount = numSegmentsInNotRoundJoin;
                        }
                        RIVE_DEBUG_CODE(--m_pendingStrokeJoinCount;)
                    }
                    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_pendingStrokeCapCount;)
                    }
                    tessWriter->pushCubic(p,
                                          m_contourDirections,
                                          joinTangent,
                                          parametricSegmentCount,
                                          polarSegmentCount,
                                          joinSegmentCount,
                                          contourIDWithFlags | joinTypeFlags);
                    RIVE_DEBUG_CODE(--m_pendingCurveCount;)
                    break;
                }
                case StyledVerb::filledCubic:
                {
                    uint32_t parametricSegmentCount =
                        m_parametricSegmentCounts[curveIdx++];
                    tessWriter->pushCubic(iter.cubicPts(),
                                          m_contourDirections,
                                          Vec2D{},
                                          parametricSegmentCount,
                                          1,
                                          1,
                                          contourIDWithFlags);
                    RIVE_DEBUG_CODE(--m_pendingCurveCount;)
                    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(
                tessWriter,
                std::array{p0, right, right, right}.data(),
                contour.strokeCapSegmentCount,
                contourIDWithFlags | emulatedCapAsJoinFlags);
            pushEmulatedStrokeCapAsJoinBeforeCubic(
                tessWriter,
                std::array{p0, left, left, left}.data(),
                contour.strokeCapSegmentCount,
                contourIDWithFlags | emulatedCapAsJoinFlags);
        }
        else if (contour.closed)
        {
            implicitClose[0] = end.rawPtsPtr()[-1];
            // Bit-cast to uint64_t because we don't want the special equality
            // rules for NaN. If we're empty or otherwise return back to p0, we
            // want to detect this, regardless of whether there are NaN values.
            if (math::bit_cast<uint64_t>(implicitClose[0]) !=
                math::bit_cast<uint64_t>(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_pendingRotationCount;)
                    RIVE_DEBUG_CODE(--m_pendingStrokeJoinCount;)
                }
                else if (isStrokeOrFeather())
                {
                    joinTangent = find_starting_tangent(pts, end.rawPtsPtr());
                    joinSegmentCount = numSegmentsInNotRoundJoin;
                    RIVE_DEBUG_CODE(--m_pendingStrokeJoinCount;)
                }
                tessWriter->pushCubic(cubic.data(),
                                      m_contourDirections,
                                      joinTangent,
                                      1,
                                      1,
                                      joinSegmentCount,
                                      contourIDWithFlags | joinTypeFlags);
                RIVE_DEBUG_CODE(--m_pendingLineCount;)
            }
        }

        assert(curveIdx == contour.endCurveIdx);
        assert(rotationIdx == contour.endRotationIdx);

        assert(m_pendingStrokeJoinCount == 0);
        assert(m_pendingStrokeCapCount == 0);
        startOfContour = contour.endOfContour;
    }

    // Make sure we only pushed the amount of data we reserved.
    assert(m_pendingLineCount == 0);
    assert(m_pendingCurveCount == 0);
    assert(m_pendingRotationCount == 0);
    assert(m_pendingEmptyStrokeCountForCaps == 0);
}

void PathDraw::pushEmulatedStrokeCapAsJoinBeforeCubic(
    RenderContext::TessellationWriter* tessWriter,
    const Vec2D cubic[],
    uint32_t strokeCapSegmentCount,
    uint32_t contourIDWithFlags)
{
    RIVE_PROF_SCOPE()
    // 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.
    assert(strokeCapSegmentCount >= 2);
    tessWriter->pushCubic(
        std::array{cubic[3], cubic[2], cubic[1], cubic[0]}.data(),
        m_contourDirections,
        math::find_cubic_tan0(cubic),
        0,
        0,
        strokeCapSegmentCount,
        contourIDWithFlags);
    RIVE_DEBUG_CODE(--m_pendingStrokeCapCount;)
    RIVE_DEBUG_CODE(--m_pendingEmptyStrokeCountForCaps;)
}

void PathDraw::iterateInteriorTriangulation(
    InteriorTriangulationOp op,
    TrivialBlockAllocator* allocator,
    RawPath* scratchPath,
    TriangulatorAxis triangulatorAxis,
    RenderContext::TessellationWriter* tessWriter)
{
    RIVE_PROF_SCOPE()
    Vec2D chops[kMaxCurveSubdivisions * 3 + 1];
    const RawPath& rawPath = m_pathRef->getRawPath();
    assert(!rawPath.empty());
    wangs_formula::VectorXform vectorXform(m_matrix);
    size_t patchCount = 0;
    size_t contourCount = 0;
    Vec2D p0 = {0, 0};
    if (op == InteriorTriangulationOp::countDataAndTriangulate)
    {
        scratchPath->rewind();
    }
    // Used with InteriorTriangulationOp::pushOuterCubicData.
    uint32_t contourIDWithFlags = 0;
    for (const auto [verb, pts] : rawPath)
    {
        switch (verb)
        {
            case PathVerb::move:
                if (contourCount != 0 && pts[-1] != p0)
                {
                    if (op ==
                        InteriorTriangulationOp::pushOuterCubicTessellationData)
                    {
                        tessWriter->pushCubic(
                            convert_line_to_cubic(pts[-1], p0).data(),
                            m_contourDirections,
                            {0, 0},
                            kPatchSegmentCountExcludingJoin,
                            1,
                            kJoinSegmentCount,
                            contourIDWithFlags |
                                CULL_EXCESS_TESSELLATION_SEGMENTS_CONTOUR_FLAG);
                    }
                    ++patchCount;
                }
                if (op == InteriorTriangulationOp::countDataAndTriangulate)
                {
                    scratchPath->move(pts[0]);
                }
                else
                {
                    contourIDWithFlags =
                        m_contourFlags |
                        tessWriter->pushContour({0, 0},
                                                isStroke(),
                                                /*closed=*/true,
                                                0);
                }
                p0 = pts[0];
                ++contourCount;
                break;
            case PathVerb::line:
                if (op == InteriorTriangulationOp::countDataAndTriangulate)
                {
                    scratchPath->line(pts[1]);
                }
                else
                {
                    tessWriter->pushCubic(
                        convert_line_to_cubic(pts).data(),
                        m_contourDirections,
                        {0, 0},
                        kPatchSegmentCountExcludingJoin,
                        1,
                        kJoinSegmentCount,
                        contourIDWithFlags |
                            CULL_EXCESS_TESSELLATION_SEGMENTS_CONTOUR_FLAG);
                }
                ++patchCount;
                break;
            case PathVerb::quad:
                RIVE_UNREACHABLE();
            case PathVerb::cubic:
            {
                uint32_t numSubdivisions;
                if (op == InteriorTriangulationOp::countDataAndTriangulate)
                {
                    numSubdivisions =
                        find_outer_cubic_subdivision_count(pts, vectorXform);
                    m_numChops.push_back(numSubdivisions);
                }
                else
                {
                    numSubdivisions = m_numChops.pop_front();
                }
                if (numSubdivisions == 1)
                {
                    if (op == InteriorTriangulationOp::countDataAndTriangulate)
                    {
                        scratchPath->line(pts[3]);
                    }
                    else
                    {
                        tessWriter->pushCubic(
                            pts,
                            m_contourDirections,
                            {0, 0},
                            kPatchSegmentCountExcludingJoin,
                            1,
                            kJoinSegmentCount,
                            contourIDWithFlags |
                                CULL_EXCESS_TESSELLATION_SEGMENTS_CONTOUR_FLAG);
                    }
                }
                else
                {
                    // Passing nullptr for the 'tValues' causes it to chop the
                    // cubic uniformly in T.
                    math::chop_cubic_at(pts,
                                        chops,
                                        nullptr,
                                        numSubdivisions - 1);
                    const Vec2D* chop = chops;
                    for (size_t i = 0; i < numSubdivisions; ++i)
                    {
                        if (op ==
                            InteriorTriangulationOp::countDataAndTriangulate)
                        {
                            scratchPath->line(chop[3]);
                        }
                        else
                        {
                            tessWriter->pushCubic(
                                chop,
                                m_contourDirections,
                                {0, 0},
                                kPatchSegmentCountExcludingJoin,
                                1,
                                kJoinSegmentCount,
                                contourIDWithFlags |
                                    CULL_EXCESS_TESSELLATION_SEGMENTS_CONTOUR_FLAG);
                        }
                        chop += 3;
                    }
                }
                patchCount += numSubdivisions;
                break;
            }
            case PathVerb::close:
                break;
        }
    }
    Vec2D lastPt = rawPath.points().back();
    if (contourCount != 0 && lastPt != p0)
    {
        if (op == InteriorTriangulationOp::pushOuterCubicTessellationData)
        {
            tessWriter->pushCubic(
                convert_line_to_cubic(lastPt, p0).data(),
                m_contourDirections,
                {0, 0},
                kPatchSegmentCountExcludingJoin,
                1,
                kJoinSegmentCount,
                contourIDWithFlags |
                    CULL_EXCESS_TESSELLATION_SEGMENTS_CONTOUR_FLAG);
        }
        ++patchCount;
    }

    if (op == InteriorTriangulationOp::countDataAndTriangulate)
    {
        assert(m_triangulator == nullptr);
        assert(triangulatorAxis != TriangulatorAxis::dontCare);
        m_triangulator = allocator->make<GrInnerFanTriangulator>(
            *scratchPath,
            m_matrix,
            triangulatorAxis == TriangulatorAxis::horizontal
                ? GrTriangulator::Comparator::Direction::kHorizontal
                : GrTriangulator::Comparator::Direction::kVertical,
            // clockwise and nonZero paths both get triangulated as nonZero,
            // because clockwise fill still needs the backwards triangles for
            // borrowed coverage.
            m_pathFillRule == FillRule::evenOdd ? FillRule::evenOdd
                                                : FillRule::nonZero,
            allocator);
        float matrixDeterminant =
            m_matrix[0] * m_matrix[3] - m_matrix[2] * m_matrix[1];
        if ((matrixDeterminant < 0) !=
            static_cast<bool>(m_contourFlags & NEGATE_PATH_FILL_COVERAGE_FLAG))
        {
            m_triangulator->negateWinding();
        }
        // We also draw each "grout" triangle using an outerCubic patch.
        patchCount += m_triangulator->groutList().count();

        if (patchCount > 0)
        {
            m_resourceCounts.pathCount = 1;
            m_resourceCounts.contourCount = contourCount;
            // maxTessellatedSegmentCount does not get doubled when we emit both
            // forward and mirrored contours because the forward and mirrored
            // pair both get packed into a single gpu::TessVertexSpan.
            m_resourceCounts.maxTessellatedSegmentCount = patchCount;
            // outerCubic patches emit their tessellated geometry twice: once
            // forward and once mirrored.
            m_resourceCounts.outerCubicTessVertexCount =
                gpu::ContourDirectionsAreDoubleSided(m_contourDirections)
                    ? patchCount * kOuterCurvePatchSegmentSpan * 2
                    : patchCount * kOuterCurvePatchSegmentSpan;
            m_resourceCounts.maxTriangleVertexCount +=
                m_triangulator->maxVertexCount();
        }
    }
    else
    {
        assert(m_triangulator != nullptr);
        // Submit grout triangles, retrofitted into outerCubic patches.
        for (auto* node = m_triangulator->groutList().head(); node;
             node = node->fNext)
        {
            Vec2D triangleAsCubic[4] = {node->fPts[0],
                                        node->fPts[1],
                                        {0, 0},
                                        node->fPts[2]};
            tessWriter->pushCubic(triangleAsCubic,
                                  m_contourDirections,
                                  {0, 0},
                                  kPatchSegmentCountExcludingJoin,
                                  1,
                                  kJoinSegmentCount,
                                  contourIDWithFlags |
                                      RETROFITTED_TRIANGLE_CONTOUR_FLAG);
            ++patchCount;
        }
        assert(contourCount == m_resourceCounts.contourCount);
        assert(patchCount == m_resourceCounts.maxTessellatedSegmentCount);
        assert(patchCount * kOuterCurvePatchSegmentSpan * 2 ==
                   m_resourceCounts.outerCubicTessVertexCount ||
               patchCount * kOuterCurvePatchSegmentSpan ==
                   m_resourceCounts.outerCubicTessVertexCount);
    }
}

ImageRectDraw::ImageRectDraw(RenderContext* context,
                             IAABB pixelBounds,
                             const Mat2D& matrix,
                             BlendMode blendMode,
                             rcp<Texture> imageTexture,
                             const ImageSampler imageSampler,
                             float opacity) :
    Draw(pixelBounds,
         matrix,
         blendMode,
         std::move(imageTexture),
         imageSampler,
         Type::imageRect),
    m_opacity(opacity)
{
    // If we support image paints for paths, the client should draw a
    // rectangular path with an image paint instead of using this draw.
    assert(!context->frameSupportsImagePaintForPaths());
    m_resourceCounts.imageDrawCount = 1;
}

void ImageRectDraw::pushToRenderContext(RenderContext::LogicalFlush* flush,
                                        int subpassIndex)
{
    assert(subpassIndex == 0);
    flush->pushImageRectDraw(this);
}

ImageMeshDraw::ImageMeshDraw(IAABB pixelBounds,
                             const Mat2D& matrix,
                             BlendMode blendMode,
                             rcp<Texture> imageTexture,
                             const ImageSampler imageSampler,
                             rcp<RenderBuffer> vertexBuffer,
                             rcp<RenderBuffer> uvBuffer,
                             rcp<RenderBuffer> indexBuffer,
                             uint32_t indexCount,
                             float opacity) :
    Draw(pixelBounds,
         matrix,
         blendMode,
         std::move(imageTexture),

         imageSampler,
         Type::imageMesh),
    m_vertexBufferRef(vertexBuffer.release()),
    m_uvBufferRef(uvBuffer.release()),
    m_indexBufferRef(indexBuffer.release()),
    m_indexCount(indexCount),
    m_opacity(opacity)
{
    assert(m_vertexBufferRef != nullptr);
    assert(m_uvBufferRef != nullptr);
    assert(m_indexBufferRef != nullptr);
    m_resourceCounts.imageDrawCount = 1;
}

void ImageMeshDraw::pushToRenderContext(RenderContext::LogicalFlush* flush,
                                        int subpassIndex)
{
    assert(subpassIndex == 0);
    flush->pushImageMeshDraw(this);
}

void ImageMeshDraw::releaseRefs()
{
    Draw::releaseRefs();
    m_vertexBufferRef->unref();
    m_uvBufferRef->unref();
    m_indexBufferRef->unref();
}

StencilClipReset::StencilClipReset(RenderContext* context,
                                   uint32_t previousClipID,
                                   gpu::DrawContents previousClipDrawContents,
                                   ResetAction resetAction) :
    Draw(context->getClipContentBounds(previousClipID),
         Mat2D(),
         BlendMode::srcOver,
         nullptr,
         ImageSampler::LinearClamp(),
         Type::stencilClipReset),
    m_previousClipID(previousClipID)
{
    constexpr static gpu::DrawContents FILL_RULE_FLAGS =
        gpu::DrawContents::nonZeroFill | gpu::DrawContents::evenOddFill |
        gpu::DrawContents::clockwiseFill;
    m_drawContents |= previousClipDrawContents & FILL_RULE_FLAGS;
    switch (resetAction)
    {
        case ResetAction::intersectPreviousClip:
            m_drawContents |= gpu::DrawContents::activeClip;
            [[fallthrough]];
        case ResetAction::clearPreviousClip:
            m_drawContents |= gpu::DrawContents::clipUpdate;
            break;
    }
    m_resourceCounts.maxTriangleVertexCount = 6;
}

void StencilClipReset::pushToRenderContext(RenderContext::LogicalFlush* flush,
                                           int subpassIndex)
{
    assert(subpassIndex == 0);
    flush->pushStencilClipResetDraw(this);
}
} // namespace rive::gpu
