/*
 * Copyright 2022 Rive
 */

#pragma once

#include "rive/math/vec2d.hpp"
#include "rive/renderer/gpu.hpp"
#include "rive/renderer/rive_render_factory.hpp"
#include "rive/renderer/render_target.hpp"
#include "rive/renderer/sk_rectanizer_skyline.hpp"
#include "rive/renderer/trivial_block_allocator.hpp"
#include "rive/shapes/paint/color.hpp"
#include <array>
#include <unordered_map>

class PushRetrofittedTrianglesGMDraw;
class RenderContextTest;

namespace rive
{
class RawPath;
class RiveRenderPaint;
class RiveRenderPath;
} // namespace rive

namespace rive::gpu
{
class GradientLibrary;
class IntersectionBoard;
class ImageMeshDraw;
class ImageRectDraw;
class StencilClipReset;
class Draw;
class Gradient;
class RenderContextImpl;
class PathDraw;

enum class ShaderCompilationMode
{
    allowAsynchronous,
    alwaysSynchronous,
    onlyUbershaders,

    // The default mode is to allow asynchronous compilation where available.
    standard = allowAsynchronous,
};

// Used as a key for complex gradients.
class GradientContentKey
{
public:
    inline GradientContentKey(rcp<const Gradient> gradient);
    inline GradientContentKey(GradientContentKey&& other);
    bool operator==(const GradientContentKey&) const;
    const Gradient* gradient() const { return m_gradient.get(); }

private:
    rcp<const Gradient> m_gradient;
};

// Hashes all stops and all colors in a complex gradient.
class DeepHashGradient
{
public:
    size_t operator()(const GradientContentKey&) const;
};

// Even though Draw is block-allocated, we still need to call releaseRefs() on
// each individual instance before releasing the block. This smart pointer
// guarantees we always call releaseRefs() (implementation in pls_draw.hpp).
struct DrawReleaseRefs
{
    void operator()(Draw* draw);
};
using DrawUniquePtr = std::unique_ptr<Draw, DrawReleaseRefs>;

// Top-level, API agnostic rendering context for RiveRenderer. This class
// manages all the GPU buffers, context state, and other resources required for
// Rive's pixel local storage path rendering algorithm.
class RenderContext : public RiveRenderFactory
{
public:
    RenderContext(std::unique_ptr<RenderContextImpl>);
    ~RenderContext();

    RenderContextImpl* impl() { return m_impl.get(); }
    template <typename T> T* static_impl_cast()
    {
        return static_cast<T*>(m_impl.get());
    }

    const gpu::PlatformFeatures& platformFeatures() const;

    // Options for controlling how and where a frame is rendered.
    struct FrameDescriptor
    {
        uint32_t renderTargetWidth = 0;
        uint32_t renderTargetHeight = 0;
        LoadAction loadAction = LoadAction::clear;
        ColorInt clearColor = 0;
        // If nonzero, the number of MSAA samples to use.
        // Setting this to a nonzero value forces msaa mode.
        uint32_t msaaSampleCount = 0;
        // Use atomic mode (preferred) or msaa instead of rasterOrdering.
        bool disableRasterOrdering = false;

        // Testing flags.
        bool wireframe = false;
        bool fillsDisabled = false;
        bool strokesDisabled = false;
        // Override all paths' fill rules (winding or even/odd) to emulate
        // clockwiseAtomic mode.
        bool clockwiseFillOverride = false;
#ifdef WITH_RIVE_TOOLS
        // Synthesize compilation failures to make sure the device handles them
        // gracefully. (e.g., by falling back on an uber shader or at least not
        // crashing.) Valid compilations may fail in the real world if the
        // device is pressed for resources or in a bad state.
        gpu::SynthesizedFailureType synthesizedFailureType =
            gpu::SynthesizedFailureType::none;
#endif
    };

    // Called at the beginning of a frame and establishes where and how it will
    // be rendered.
    //
    // All rendering related calls must be made between beginFrame() and
    // flush().
    void beginFrame(const FrameDescriptor&);

    const FrameDescriptor& frameDescriptor() const
    {
        assert(m_didBeginFrame);
        return m_frameDescriptor;
    }

    // True if bounds is empty or outside [0, 0, renderTargetWidth,
    // renderTargetHeight].
    bool isOutsideCurrentFrame(const IAABB& pixelBounds);

    // True if the current frame supports draws with clipRects
    // (clipRectInverseMatrix != null). If false, all clipping must be done with
    // clipPaths.
    bool frameSupportsClipRects() const;

    // If the frame doesn't support image paints, the client must draw images
    // with pushImageRect(). If it DOES support image paints, the client CANNOT
    // use pushImageRect(); it should draw images as rectangular paths with an
    // image paint.
    bool frameSupportsImagePaintForPaths() const;

    const gpu::InterlockMode frameInterlockMode() const
    {
        return m_frameInterlockMode;
    }

    // Generates a unique clip ID that is guaranteed to not exist in the current
    // clip buffer, and assigns a contentBounds to it.
    //
    // Returns 0 if a unique ID could not be generated, at which point the
    // caller must issue a logical flush and try again.
    uint32_t generateClipID(const IAABB& contentBounds);

    // Screen-space bounding box of the region inside the given clip.
    const IAABB& getClipContentBounds(uint32_t clipID)
    {
        assert(m_didBeginFrame);
        assert(!m_logicalFlushes.empty());
        return m_logicalFlushes.back()->getClipInfo(clipID).contentBounds;
    }

    // Mark the given clip as being read from within a screen-space bounding
    // box.
    void addClipReadBounds(uint32_t clipID, const IAABB& bounds)
    {
        assert(m_didBeginFrame);
        assert(!m_logicalFlushes.empty());
        return m_logicalFlushes.back()->addClipReadBounds(clipID, bounds);
    }

    // Union of screen-space bounding boxes from all draws that read the given
    // clip element.
    const IAABB& getClipReadBounds(uint32_t clipID)
    {
        assert(m_didBeginFrame);
        assert(!m_logicalFlushes.empty());
        return m_logicalFlushes.back()->getClipInfo(clipID).readBounds;
    }

    // Get/set a "clip content ID" that uniquely identifies the current contents
    // of the clip buffer. This ID is reset to 0 on every logical flush.
    void setClipContentID(uint32_t clipID)
    {
        assert(m_didBeginFrame);
        m_clipContentID = clipID;
    }

    uint32_t getClipContentID()
    {
        assert(m_didBeginFrame);
        return m_clipContentID;
    }

    // Appends a list of high-level Draws to the current frame.
    // Returns false if the draws don't fit within the current resource
    // constraints, at which point the caller must issue a logical flush and try
    // again.
    [[nodiscard]] bool pushDraws(DrawUniquePtr draws[], size_t drawCount);

    // Records a "logical" flush, in that it builds up commands to break up the
    // render pass and re-render the resource textures, but it won't submit any
    // command buffers or rotate/synchronize the buffer rings.
    void logicalFlush();

    // GPU resources required to execute the GPU commands for a frame.
    struct FlushResources
    {
        RenderTarget* renderTarget = nullptr;

        // Command buffer that rendering commands will be added to.
        //  - VkCommandBuffer on Vulkan.
        //  - id<MTLCommandBuffer> on Metal.
        //  - WGPUCommandEncoder on WebGPU.
        //  - Unused otherwise.
        void* externalCommandBuffer = nullptr;

        // Resource lifetime counters. Resources used during the upcoming flush
        // will belong to 'currentFrameNumber'. Resources last used on or before
        // 'safeFrameNumber' are safe to be released or recycled.
        uint64_t currentFrameNumber = 0;
        uint64_t safeFrameNumber = 0;
    };

    // Submits all GPU commands that have been built up since beginFrame().
    void flush(const FlushResources&);

    // Called when the client will stop rendering. Releases all CPU and GPU
    // resources associated with this render context.
    void releaseResources();

    // Returns the context's TrivialBlockAllocator, which is automatically reset
    // at the end of every frame. (Memory in this allocator is preserved between
    // logical flushes.)
    TrivialBlockAllocator& perFrameAllocator()
    {
        assert(m_didBeginFrame);
        return m_perFrameAllocator;
    }

    // Allocators for intermediate path processing buffers.
    TrivialArrayAllocator<uint8_t>& numChopsAllocator()
    {
        return m_numChopsAllocator;
    }
    TrivialArrayAllocator<Vec2D>& chopVerticesAllocator()
    {
        return m_chopVerticesAllocator;
    }
    TrivialArrayAllocator<std::array<Vec2D, 2>>& tangentPairsAllocator()
    {
        return m_tangentPairsAllocator;
    }
    TrivialArrayAllocator<uint32_t, alignof(float4)>&
    polarSegmentCountsAllocator()
    {
        return m_polarSegmentCountsAllocator;
    }
    TrivialArrayAllocator<uint32_t, alignof(float4)>&
    parametricSegmentCountsAllocator()
    {
        return m_parametricSegmentCountsAllocator;
    }

    // Allocates a trivially destructible object that will be automatically
    // dropped at the end of the current frame.
    template <typename T, typename... Args> T* make(Args&&... args)
    {
        assert(m_didBeginFrame);
        return m_perFrameAllocator.make<T>(std::forward<Args>(args)...);
    }

    // Backend-specific RiveRenderFactory implementation.
    rcp<RenderBuffer> makeRenderBuffer(RenderBufferType,
                                       RenderBufferFlags,
                                       size_t) override;
    rcp<RenderImage> decodeImage(Span<const uint8_t>) override;

private:
    friend class Draw;
    friend class PathDraw;
    friend class ImageRectDraw;
    friend class ImageMeshDraw;
    friend class StencilClipReset;
    friend class ::PushRetrofittedTrianglesGMDraw; // For testing.
    friend class ::RenderContextTest;              // For testing.

    // Resets the CPU-side STL containers so they don't have unbounded growth.
    void resetContainers();

    // Throttled width/height of the atlas texture. If drawing to a render
    // target larger than this, we may create a larger atlas anyway.
    uint32_t atlasMaxSize() const
    {
        constexpr static uint32_t MAX_ATLAS_MAX_SIZE = 4096;
        return std::min(platformFeatures().maxTextureSize, MAX_ATLAS_MAX_SIZE);
    }

    // Defines the exact size of each of our GPU resources. Computed during
    // flush(), based on LogicalFlush::ResourceCounters and
    // LogicalFlush::LayoutCounters.
    struct ResourceAllocationCounts
    {
        constexpr static int NUM_ELEMENTS = 19;
        using VecType = simd::gvec<size_t, NUM_ELEMENTS>;

        RIVE_ALWAYS_INLINE VecType toVec() const
        {
            static_assert(sizeof(*this) == sizeof(size_t) * NUM_ELEMENTS);
            static_assert(sizeof(VecType) >= sizeof(*this));
            VecType vec;
            RIVE_INLINE_MEMCPY(&vec, this, sizeof(*this));
            return vec;
        }

        static RIVE_ALWAYS_INLINE ResourceAllocationCounts
        FromVec(const VecType& vec)
        {
            ResourceAllocationCounts allocs;
            static_assert(sizeof(allocs) == sizeof(size_t) * NUM_ELEMENTS);
            static_assert(sizeof(VecType) >= sizeof(allocs));
            RIVE_INLINE_MEMCPY(&allocs, &vec, sizeof(allocs));
            return allocs;
        }

        size_t flushUniformBufferCount = 0;
        size_t imageDrawUniformBufferCount = 0;
        size_t pathBufferCount = 0;
        size_t paintBufferCount = 0;
        size_t paintAuxBufferCount = 0;
        size_t contourBufferCount = 0;
        size_t gradSpanBufferCount = 0;
        size_t tessSpanBufferCount = 0;
        size_t triangleVertexBufferCount = 0;
        size_t gradTextureHeight = 0;
        size_t tessTextureHeight = 0;
        size_t atlasTextureWidth = 0;
        size_t atlasTextureHeight = 0;
        size_t plsTransientBackingWidth = 0;
        size_t plsTransientBackingHeight = 0;
        size_t plsTransientBackingPlaneCount = 0;
        size_t plsAtomicCoverageBackingWidth = 0;  // atomic mode only.
        size_t plsAtomicCoverageBackingHeight = 0; // atomic mode only.
        size_t coverageBufferLength = 0;           // clockwiseAtomic mode only.
    };

    // Reallocates GPU resources and updates m_currentResourceAllocations.
    // If forceRealloc is true, every GPU resource is allocated, even if the
    // size would not change.
    void setResourceSizes(ResourceAllocationCounts, bool forceRealloc = false);

    void mapResourceBuffers(const ResourceAllocationCounts&);
    void unmapResourceBuffers(const ResourceAllocationCounts&);

    // Returns the next coverage buffer prefix to use in a logical flush.
    // Sets needsCoverageBufferClear if the coverage buffer must be cleared in
    // order to support the returned coverage buffer prefix.
    // (clockwiseAtomic mode only.)
    uint32_t incrementCoverageBufferPrefix(bool* needsCoverageBufferClear);

    const std::unique_ptr<RenderContextImpl> m_impl;
    const size_t m_maxPathID;

    ResourceAllocationCounts m_currentResourceAllocations;
    ResourceAllocationCounts m_maxRecentResourceRequirements;
    double m_lastResourceTrimTimeInSeconds;

    // Per-frame state.
    FrameDescriptor m_frameDescriptor;
    gpu::InterlockMode m_frameInterlockMode;
    gpu::ShaderFeatures m_frameShaderFeaturesMask;
    RIVE_DEBUG_CODE(bool m_didBeginFrame = false;)

    // Clipping state.
    uint32_t m_clipContentID = 0;

    // Monotonically increasing prefix that gets appended to the most
    // significant "32 - CLOCKWISE_COVERAGE_BIT_COUNT" bits of coverage buffer
    // values.
    //
    // Increasing this prefix implicitly clears the entire coverage buffer to
    // zero.
    //
    // (clockwiseAtomic mode only.)
    uint32_t m_coverageBufferPrefix = 0;

    // Used by LogicalFlushes for re-ordering high level draws.
    std::vector<int64_t> m_indirectDrawList;
    std::unique_ptr<IntersectionBoard> m_intersectionBoard;

    WriteOnlyMappedMemory<gpu::FlushUniforms> m_flushUniformData;
    WriteOnlyMappedMemory<gpu::PathData> m_pathData;
    WriteOnlyMappedMemory<gpu::PaintData> m_paintData;
    WriteOnlyMappedMemory<gpu::PaintAuxData> m_paintAuxData;
    WriteOnlyMappedMemory<gpu::ContourData> m_contourData;
    WriteOnlyMappedMemory<gpu::GradientSpan> m_gradSpanData;
    WriteOnlyMappedMemory<gpu::TessVertexSpan> m_tessSpanData;
    WriteOnlyMappedMemory<gpu::TriangleVertex> m_triangleVertexData;
    WriteOnlyMappedMemory<gpu::ImageDrawUniforms> m_imageDrawUniformData;

    // Simple allocator for trivially-destructible data that needs to persist
    // until the current frame has completed. All memory in this allocator is
    // dropped at the end of the every frame.
    constexpr static size_t kPerFlushAllocatorInitialBlockSize =
        1024 * 1024; // 1 MiB.
    TrivialBlockAllocator m_perFrameAllocator{
        kPerFlushAllocatorInitialBlockSize};

    // Allocators for intermediate path processing buffers.
    constexpr static size_t kIntermediateDataInitialStrokes =
        8192; // * 84 == 688 KiB.
    constexpr static size_t kIntermediateDataInitialFillCurves =
        32768; // * 4 == 128 KiB.
    TrivialArrayAllocator<uint8_t> m_numChopsAllocator{
        kIntermediateDataInitialStrokes * 4}; // 4 byte per stroke curve.
    TrivialArrayAllocator<Vec2D> m_chopVerticesAllocator{
        kIntermediateDataInitialStrokes * 4}; // 32 bytes per stroke curve.
    TrivialArrayAllocator<std::array<Vec2D, 2>> m_tangentPairsAllocator{
        kIntermediateDataInitialStrokes * 2}; // 32 bytes per stroke curve.
    TrivialArrayAllocator<uint32_t, alignof(float4)>
        m_polarSegmentCountsAllocator{kIntermediateDataInitialStrokes *
                                      4}; // 16 bytes per stroke curve.
    TrivialArrayAllocator<uint32_t, alignof(float4)>
        m_parametricSegmentCountsAllocator{
            kIntermediateDataInitialFillCurves}; // 4 bytes per fill curve.

    class TessellationWriter;

    // Manages a list of high-level Draws and their required resources.
    //
    // Since textures have hard size limits, we can't always fit an entire frame
    // into one flush. It's rare for us to require more than one flush in a
    // single frame, but for the times that we do, this flush logic is
    // encapsulated in a nested class that can be built up into a list and
    // executed the end of a frame.
    class LogicalFlush
    {
    public:
        LogicalFlush(RenderContext* parent);

        // Rewinds this flush object back to an empty state without shrinking
        // any internal allocations held by CPU-side STL containers.
        void rewind();

        // Resets the CPU-side STL containers so they don't have unbounded
        // growth.
        void resetContainers();

        const FrameDescriptor& frameDescriptor() const
        {
            return m_ctx->frameDescriptor();
        }
        gpu::InterlockMode interlockMode() const
        {
            return m_ctx->frameInterlockMode();
        }

        // Access this flush's gpu::FlushDescriptor (which is not valid until
        // layoutResources()). NOTE: Some fields in the FlushDescriptor
        // (tessVertexSpanCount, hasTriangleVertices, drawList, and
        // combinedShaderFeatures) do not become valid until after
        // writeResources().
        const gpu::FlushDescriptor& desc()
        {
            assert(m_hasDoneLayout);
            return m_flushDesc;
        }

        // Generates a unique clip ID that is guaranteed to not exist in the
        // current clip buffer.
        //
        // Returns 0 if a unique ID could not be generated, at which point the
        // caller must issue a logical flush and try again.
        uint32_t generateClipID(const IAABB& contentBounds);

        struct ClipInfo
        {
            ClipInfo(const IAABB& contentBounds_) :
                contentBounds(contentBounds_)
            {}

            // Screen-space bounding box of the region inside the clip.
            const IAABB contentBounds;

            // Union of screen-space bounding boxes from all draws that read the
            // clip.
            //
            // (Initialized with a maximally negative rectangle whose union with
            // any other rectangle will be equal to that same rectangle.)
            IAABB readBounds = {std::numeric_limits<int32_t>::max(),
                                std::numeric_limits<int32_t>::max(),
                                std::numeric_limits<int32_t>::min(),
                                std::numeric_limits<int32_t>::min()};
        };

        const ClipInfo& getClipInfo(uint32_t clipID)
        {
            return getWritableClipInfo(clipID);
        }

        // Mark the given clip as being read from within a screen-space bounding
        // box.
        void addClipReadBounds(uint32_t clipID, const IAABB& bounds);

        // Appends a list of high-level Draws to the flush.
        // Returns false if the draws don't fit within the current resource
        // constraints, at which point the context must append a new logical
        // flush and try again.
        [[nodiscard]] bool pushDraws(DrawUniquePtr draws[], size_t drawCount);

        // Running counts of data records required by Draws that need to be
        // allocated in the render context's various GPU buffers.
        struct ResourceCounters
        {
            constexpr static int NUM_ELEMENTS = 7;
            using VecType = simd::gvec<size_t, NUM_ELEMENTS>;

            VecType toVec() const
            {
                static_assert(sizeof(*this) == sizeof(size_t) * NUM_ELEMENTS);
                static_assert(sizeof(VecType) >= sizeof(*this));
                VecType vec;
                RIVE_INLINE_MEMCPY(&vec, this, sizeof(*this));
                return vec;
            }

            ResourceCounters(const VecType& vec)
            {
                static_assert(sizeof(*this) == sizeof(size_t) * NUM_ELEMENTS);
                static_assert(sizeof(VecType) >= sizeof(*this));
                RIVE_INLINE_MEMCPY(this, &vec, sizeof(*this));
            }

            ResourceCounters() = default;

            size_t midpointFanTessVertexCount = 0;
            size_t outerCubicTessVertexCount = 0;
            size_t pathCount = 0;
            size_t contourCount = 0;
            // lines, curves, lone joins, emulated caps, etc.
            size_t maxTessellatedSegmentCount = 0;
            size_t maxTriangleVertexCount = 0;
            size_t imageDrawCount = 0; // imageRect or imageMesh.
        };

        // Additional counters for layout state that don't need to be tracked by
        // individual draws.
        struct LayoutCounters
        {
            uint32_t pathPaddingCount = 0;
            uint32_t paintPaddingCount = 0;
            uint32_t paintAuxPaddingCount = 0;
            uint32_t contourPaddingCount = 0;
            uint32_t gradSpanCount = 0;
            uint32_t gradSpanPaddingCount = 0;
            uint32_t maxGradTextureHeight = 0;
            uint32_t maxTessTextureHeight = 0;
            uint32_t maxAtlasWidth = 0;
            uint32_t maxAtlasHeight = 0;
            uint32_t maxPLSTransientBackingPlaneCount = 0;
            size_t maxCoverageBufferLength = 0;
        };

        // Allocates a horizontal span of texels in the gradient texture and
        // schedules either a texture upload or a draw that fills it with the
        // given gradient's color ramp.
        //
        // Fills out a ColorRampLocation record that tells the shader how to
        // access the gradient.
        //
        // Returns false if the gradient texture is out of space, at which point
        // the caller must issue a logical flush and try again.
        [[nodiscard]] bool allocateGradient(const Gradient*,
                                            gpu::ColorRampLocation*);

        // Allocates a rectangular region in the atlas for this draw to use, and
        // registers a future callback to PathDraw::pushAtlasTessellation()
        // where it will render its coverage data to this same region in the
        // atlas.
        //
        // Attempts to leave a border of "desiredPadding" pixels surrounding the
        // rectangular region, but the allocation may not be padded if the path
        // is up against an edge.
        bool allocateAtlasDraw(PathDraw*,
                               uint16_t drawWidth,
                               uint16_t drawHeight,
                               uint16_t desiredPadding,
                               uint16_t* x,
                               uint16_t* y,
                               TAABB<uint16_t>* paddedRegion);

        // Reserves a range within the coverage buffer for a path to use in
        // clockwiseAtomic mode.
        //
        // "length" is the length in pixels of this allocation and must be a
        // multiple of 32*32, in order to support 32x32 tiling.
        //
        // Returns the offset of the allocated range within the coverage buffer,
        // or -1 if there was not room.
        size_t allocateCoverageBufferRange(size_t length);

        // Carves out space for this specific flush within the total frame's
        // resource buffers and lays out the flush-specific resource textures.
        // Updates the total frame running conters based on layout.
        void layoutResources(const FlushResources&,
                             size_t logicalFlushIdx,
                             ResourceCounters* runningFrameResourceCounts,
                             LayoutCounters* runningFrameLayoutCounts);

        // Called after all flushes in a frame have done their layout and the
        // render context has allocated and mapped its resource buffers. Writes
        // the GPU data for this flush to the context's actively mapped resource
        // buffers.
        void writeResources();

        // Reserves a span of "count" vertices from the "midpointFanPatches"
        // section of the tessellation texture.
        //
        // This method must be called for a total count of precisely
        // "m_resourceCounts.midpointFanTessVertexCount" vertices.
        //
        // The caller must fill these vertices in with TessellationWriter.
        //
        // Returns the index of the first vertex in the newly allocated span.
        uint32_t allocateMidpointFanTessVertices(uint32_t count);

        // Reserves a span of "count" vertices from the "outerCurvePatches"
        // section of the tessellation texture.
        //
        // This method must be called for a total count of precisely
        // "m_resourceCounts.outerCubicTessVertexCount" vertices.
        //
        // The caller must fill these vertices in with TessellationWriter.
        //
        // Returns the index of the first vertex in the newly allocated span.
        uint32_t allocateOuterCubicTessVertices(uint32_t count);

        // Allocates and initializes a record on the GPU for the given path.
        //
        // Returns a unique 16-bit "pathID" handle for this specific record.
        //
        // This method does not add the path to the draw list. The caller must
        // define that draw specifically with a separate call to
        // pushMidpointFanDraw() or pushOuterCubicsDraw().
        [[nodiscard]] uint32_t pushPath(const PathDraw* draw);

        // Pushes a contour record to the GPU that references the given path.
        //
        // "vertexIndex0" is the index within the tessellation where the first
        // vertex of the contour resides. Shaders need this when the contour is
        // closed.
        //
        // Returns a unique 16-bit "contourID" handle for this specific record.
        // This ID may be or-ed with '*_CONTOUR_FLAG' bits from constants.glsl.
        [[nodiscard]] uint32_t pushContour(uint32_t pathID,
                                           Vec2D midpoint,
                                           bool isStroke,
                                           bool closed,
                                           uint32_t vertexIndex0);

        // Writes padding vertices to the tessellation texture, with an invalid
        // contour ID that is guaranteed to not be the same ID as any neighbors.
        void pushPaddingVertices(uint32_t count, uint32_t tessLocation);

        // Pushes a "midpointFanPatches" draw to the list. Path, contour, and
        // cubic data are pushed separately.
        //
        // Also adds the PathDraw to a dstRead list if one is
        // required, and if this is the path's first subpass.
        void pushMidpointFanDraw(
            const PathDraw*,
            gpu::DrawType,
            uint32_t tessVertexCount,
            uint32_t tessLocation,
            gpu::ShaderMiscFlags = gpu::ShaderMiscFlags::none);

        // Pushes an "outerCurvePatches" draw to the list. Path, contour, and
        // cubic data are pushed separately.
        //
        // Also adds the PathDraw to a dstRead list if one is
        // required, and if this is the path's first subpass.
        void pushOuterCubicsDraw(
            const PathDraw*,
            gpu::DrawType,
            uint32_t tessVertexCount,
            uint32_t tessLocation,
            gpu::ShaderMiscFlags = gpu::ShaderMiscFlags::none);

        // Writes out triangle verties for the desired WindingFaces and pushes
        // an "interiorTriangulation" draw to the list.
        // Returns the number of vertices actually written.
        size_t pushInteriorTriangulationDraw(
            const PathDraw*,
            uint32_t pathID,
            gpu::WindingFaces,
            gpu::ShaderMiscFlags = gpu::ShaderMiscFlags::none);

        // Pushes a screen-space rectangle to the draw list, whose pixel
        // coverage is determined by the atlas region associated with the given
        // pathID.
        void pushAtlasBlit(PathDraw*, uint32_t pathID);

        // Pushes an "imageRect" to the draw list.
        // This should only be used when we in atomic mode. Otherwise, images
        // should be drawn as rectangular paths with an image paint.
        void pushImageRectDraw(ImageRectDraw*);

        // Pushes an "imageMesh" draw to the list.
        void pushImageMeshDraw(ImageMeshDraw*);

        // Pushes a "stencilClipReset" draw to the list.
        void pushStencilClipResetDraw(StencilClipReset*);

    private:
        friend class TessellationWriter;

        ClipInfo& getWritableClipInfo(uint32_t clipID);

        // Either appends a new drawBatch to m_drawList or merges into
        // m_drawList.tail(). Updates the batch's ShaderFeatures according to
        // the passed parameters.
        DrawBatch& pushPathDraw(const PathDraw*,
                                DrawType,
                                gpu::ShaderMiscFlags,
                                uint32_t vertexCount,
                                uint32_t baseVertex);
        DrawBatch& pushDraw(const Draw*,
                            DrawType,
                            gpu::ShaderMiscFlags,
                            gpu::PaintType,
                            uint32_t elementCount,
                            uint32_t baseElement);

        // Instance pointer to the outer parent class.
        RenderContext* const m_ctx;

        // Running counts of GPU data records that need to be allocated for
        // draws.
        ResourceCounters m_resourceCounts;

        // Running count of combined prepasses and subpasses from every draw in
        // m_draws.
        int m_drawPassCount;

        // Simple gradients have one stop at t=0 and one stop at t=1. They're
        // implemented with 2 texels.
        std::unordered_map<uint64_t, uint32_t>
            m_simpleGradients; // [color0, color1] -> texelsIdx.
        std::vector<gpu::TwoTexelRamp> m_pendingSimpleGradDraws;

        // Complex gradients have stop(s) between t=0 and t=1. In theory they
        // should be scaled to a ramp where every stop lands exactly on a pixel
        // center, but for now we just always scale them to the entire gradient
        // texture width.
        std::unordered_map<GradientContentKey, uint16_t, DeepHashGradient>
            m_complexGradients; // [colors[0..n], stops[0..n]] -> rowIdx
        std::vector<const Gradient*> m_pendingComplexGradDraws;

        // Simple and complex gradients both get uploaded to the GPU as sets of
        // "GradientSpan" instances.
        size_t m_pendingGradSpanCount;

        std::vector<ClipInfo> m_clips;

        // High-level draw list. These get built into a low-level list of
        // gpu::DrawBatch objects during writeResources().
        std::vector<DrawUniquePtr> m_draws;
        IAABB m_combinedDrawBounds;
        gpu::DrawContents m_combinedDrawContents;

        // State computed during layout.
        uint32_t m_pathPaddingCount;
        uint32_t m_paintPaddingCount;
        uint32_t m_paintAuxPaddingCount;
        uint32_t m_contourPaddingCount;
        uint32_t m_gradSpanPaddingCount;
        uint32_t m_midpointFanTessEndLocation;
        uint32_t m_outerCubicTessEndLocation;
        uint32_t m_outerCubicTessVertexIdx;
        uint32_t m_midpointFanTessVertexIdx;
        gpu::GradTextureLayout m_gradTextureLayout;
        gpu::ShaderMiscFlags m_baselineShaderMiscFlags;

        gpu::FlushDescriptor m_flushDesc;

        BlockAllocatedLinkedList<DrawBatch> m_drawList;
        const DrawBatch* m_firstDstBlendBarrier;
        // Final "next" pointer in the list of DrawBatches that have dstBlend
        // barriers.
        const DrawBatch** m_dstBlendBarrierListTail;

        gpu::ShaderFeatures m_combinedShaderFeatures;

        // Most recent path and contour state.
        uint32_t m_currentPathID;
        uint32_t m_currentContourID;

        // Atlas for offscreen feathering.
        std::unique_ptr<skgpu::RectanizerSkyline> m_atlasRectanizer;
        uint32_t m_atlasMaxX = 0;
        uint32_t m_atlasMaxY = 0;
        std::vector<PathDraw*> m_pendingAtlasDraws;

        // Total coverage allocated via allocateCoverageBufferRange().
        // (clockwiseAtomic mode only.)
        uint32_t m_coverageBufferLength = 0;

        // Barriers that must execute before pushing the next DrawBatch
        // (pushPathDraw()/pushDraw()). If any barriers are pending, this also
        // prevents DrawBatches from being combined with the existing drawList.
        BarrierFlags m_pendingBarriers;

        // Stateful Z index of the current draw being pushed. Used by msaa mode
        // to avoid double hits and to reverse-sort opaque paths front to back.
        uint32_t m_currentZIndex;

        RIVE_DEBUG_CODE(bool m_hasDoneLayout = false;)
    };

    std::vector<std::unique_ptr<LogicalFlush>> m_logicalFlushes;

    // Writes out TessVertexSpans that are used to tessellate the vertices
    // in a path.
    class TessellationWriter
    {
    public:
        // forwardTessLocation & mirroredTessLocation are allocated by
        // allocate*TessVertices().
        //
        // forwardTessLocation starts at the beginning of the vertex span
        // and advances forward.
        //
        // mirroredTessLocation starts at the end of the vertex span and
        // advances backward.
        //
        // If the ContourDirections are double sided, forwardTessVertexCount
        // & mirroredTessVertexCount must both be equal, and
        // forwardTessLocation & mirroredTessLocation must both be valid.
        // Otherwise, one span or the other may be empty.
        TessellationWriter(LogicalFlush* flush,
                           uint32_t pathID,
                           gpu::ContourDirections,
                           uint32_t forwardTessVertexCount,
                           uint32_t forwardTessLocation,
                           uint32_t mirroredTessVertexCount = 0,
                           uint32_t mirroredTessLocation = 0);

        ~TessellationWriter();

        // Returns the index of the next vertex to be written.
        //
        // In the case of double-sided tessellations the next vertex gets
        // tessellated twice, and either index will be identical. So we just
        // return the next *forward* tessellation index when it's double sided.
        uint32_t nextVertexIndex()
        {
            return m_contourDirections != gpu::ContourDirections::reverse
                       ? m_pathTessLocation
                       : m_pathMirroredTessLocation - 1;
        }

        // Wrapper around LogicalFlush::pushContour(), with an additional
        // padding option.
        //
        // The first curve of the contour will be pre-padded with
        // 'paddingVertexCount' tessellation vertices, colocated at T=0. The
        // caller must use this argument to align the end of the contour on
        // a boundary of the patch size. (See gpu::PaddingToAlignUp().)
        [[nodiscard]] uint32_t pushContour(Vec2D midpoint,
                                           bool isStroke,
                                           bool closed,
                                           uint32_t paddingVertexCount);

        // Wites out (potentially wrapped) TessVertexSpan(s) that tessellate
        // a cubic curve & join at the current tessellation location(s).
        // Advances the tessellation location(s).
        //
        // The bottom 16 bits of contourIDWithFlags must match the most
        // recent contourID returned by pushContour(), but it may also have
        // extra '*_CONTOUR_FLAG' bits from constants.glsl
        //
        // An instance consists of a cubic curve with
        // "parametricSegmentCount + polarSegmentCount" segments, followed
        // by a join with "joinSegmentCount" segments, for a grand total of
        // "parametricSegmentCount + polarSegmentCount + joinSegmentCount -
        // 1" vertices.
        //
        // If a cubic has already been pushed to the current contour, pts[0]
        // must be equal to the former cubic's pts[3].
        //
        // "joinTangent" is the ending tangent of the join that follows the
        // cubic.
        void pushCubic(const Vec2D pts[4],
                       gpu::ContourDirections,
                       Vec2D joinTangent,
                       uint32_t parametricSegmentCount,
                       uint32_t polarSegmentCount,
                       uint32_t joinSegmentCount,
                       uint32_t contourIDWithFlags);

        // pushCubic() impl for forward tessellations.
        RIVE_ALWAYS_INLINE void pushTessellationSpans(
            const Vec2D pts[4],
            Vec2D joinTangent,
            uint32_t totalVertexCount,
            uint32_t parametricSegmentCount,
            uint32_t polarSegmentCount,
            uint32_t joinSegmentCount,
            uint32_t contourIDWithFlags);

        // pushCubic() impl for mirrored tessellations.
        RIVE_ALWAYS_INLINE void pushMirroredTessellationSpans(
            const Vec2D pts[4],
            Vec2D joinTangent,
            uint32_t totalVertexCount,
            uint32_t parametricSegmentCount,
            uint32_t polarSegmentCount,
            uint32_t joinSegmentCount,
            uint32_t contourIDWithFlags);

        // Functionally equivalent to "pushMirroredTessellationSpans();
        // pushTessellationSpans();", but packs each forward and mirrored
        // pair into a single gpu::TessVertexSpan.
        RIVE_ALWAYS_INLINE void pushDoubleSidedTessellationSpans(
            const Vec2D pts[4],
            Vec2D joinTangent,
            uint32_t totalVertexCount,
            uint32_t parametricSegmentCount,
            uint32_t polarSegmentCount,
            uint32_t joinSegmentCount,
            uint32_t contourIDWithFlags);

    private:
        LogicalFlush* const m_flush;
        WriteOnlyMappedMemory<gpu::TessVertexSpan>& m_tessSpanData;
        const uint32_t m_pathID;
        const gpu::ContourDirections m_contourDirections;
        uint32_t m_pathTessLocation;
        uint32_t m_pathMirroredTessLocation;
        // Padding to add to the next curve.
        uint32_t m_nextCubicPaddingVertexCount = 0;
        RIVE_DEBUG_CODE(uint32_t m_expectedPathTessEndLocation;)
        RIVE_DEBUG_CODE(uint32_t m_expectedPathMirroredTessEndLocation;)
    };
};
} // namespace rive::gpu
