| /* | |
| * 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/render_context_impl.hpp" | |
| #include "rive/renderer/stack_vector.hpp" | |
| #include "rive/renderer/texture.hpp" | |
| #include "gradient.hpp" | |
| #include "shaders/constants.glsl" | |
| #include "rive/profiler/profiler_macros.h" | |
| #include <cmath> | |
| namespace rive::gpu | |
| { | |
| namespace | |
| { | |
| // Maximum # of outerCurve patches a curve on the path can be subdivided into. | |
| constexpr static size_t kMaxCurveSubdivisions = | |
| (kMaxParametricSegments + OuterCubicPatchSegmentSpan - 1) / | |
| OuterCubicPatchSegmentSpan; | |
| 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 / OuterCubicPatchSegmentSpan)); | |
| 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 * (GAUSSIAN_INTEGRAL_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& paintMatrix, | |
| const Mat2D* imageMatrix, | |
| BlendMode blendMode, | |
| rcp<Texture> imageTexture, | |
| ImageSampler imageSampler, | |
| Type type) : | |
| m_imageTextureRef(imageTexture.release()), | |
| m_imageSampler(imageSampler), | |
| m_pixelBounds(pixelBounds), | |
| m_paintMatrix(paintMatrix), | |
| m_imageMatrix((imageMatrix != nullptr) ? *imageMatrix : paintMatrix), | |
| m_blendMode(blendMode), | |
| m_type(type), | |
| m_clippedPixelBounds(pixelBounds) | |
| { | |
| 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 (!enums::is_flag_set(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::depthStencil || | |
| // 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::featherAtlas; | |
| } | |
| } | |
| 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::depthStencil: | |
| return CoverageType::depthStencil; | |
| } | |
| RIVE_UNREACHABLE(); | |
| } | |
| DrawUniquePtr PathDraw::Make(RenderContext* context, | |
| const Mat2D& paintMatrix, | |
| const Mat2D* imageMatrix, | |
| rcp<const RiveRenderPath> path, | |
| FillRule fillRule, | |
| const RiveRenderPaint* paint, | |
| float modulatedOpacity, | |
| std::optional<IAABB> precomputedPixelBounds) | |
| { | |
| RIVE_PROF_SCOPE_L(2); | |
| assert(path != nullptr); | |
| assert(paint != nullptr); | |
| CoverageType coverageType = | |
| SelectCoverageType(paint, | |
| paintMatrix.findMaxScale(), | |
| context->platformFeatures(), | |
| context->frameInterlockMode()); | |
| IAABB pixelBounds; | |
| // In debug mode, we want to always calculate the bounds and validate | |
| // against the pre-calculated ones so skip this | |
| #ifdef NDEBUG | |
| if (precomputedPixelBounds.has_value()) | |
| { | |
| // These were already computed so we don't need to do it again. | |
| pixelBounds = *precomputedPixelBounds; | |
| } | |
| else | |
| #endif | |
| { | |
| // We weren't given pre-computed bounds, so calculate them. | |
| AABB mappedBounds = | |
| paintMatrix.mapBoundingBox(path->getRawPath().points()); | |
| 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 = | |
| paintMatrix.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); | |
| } | |
| pixelBounds = mappedBounds.roundOut(); | |
| } | |
| // In debug mode, we always compute the pixel bounds, so validate that the | |
| // value we got was exactly the same as the precomputed bounds. | |
| assert(!precomputedPixelBounds.has_value() || | |
| pixelBounds == *precomputedPixelBounds); | |
| GrInnerFanTriangulator* triangulator = nullptr; | |
| const AABB& localBounds = path->getBounds(); | |
| if (context->isOutsideCurrentFrame(pixelBounds)) | |
| { | |
| // Anything precomputing the pixel bounds should be checking against the | |
| // screen bounds rather than letting it bubble down into here. | |
| assert(!precomputedPixelBounds.has_value()); | |
| return nullptr; | |
| } | |
| 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. | |
| auto& triController = context->triangulationController(); | |
| const size_t triVerbCount = path->getRawPath().verbs().count(); | |
| const float triArea = | |
| gpu::find_transformed_area(localBounds, paintMatrix); | |
| if (triController.isEligible(triArea, triVerbCount)) | |
| { | |
| triangulator = path->cachedTriangulator(); | |
| if (triangulator != nullptr) | |
| { | |
| // A cached triangulation is free to reuse. | |
| triController.recordCacheHit(); | |
| } | |
| else if (triController.admits(triArea, triVerbCount)) | |
| { | |
| const double triStart = context->impl()->secondsNow(); | |
| triangulator = | |
| path->createTriangulator(context->perFrameAllocator()); | |
| triController.recordBuilt(context->impl()->secondsNow() - | |
| triStart); | |
| } | |
| } | |
| } | |
| auto draw = context->make<PathDraw>(pixelBounds, | |
| paintMatrix, | |
| imageMatrix, | |
| std::move(path), | |
| fillRule, | |
| paint, | |
| modulatedOpacity, | |
| coverageType, | |
| context->frameDescriptor()); | |
| if (triangulator != nullptr) | |
| { | |
| draw->initForInteriorTriangulation(context, triangulator); | |
| } | |
| else | |
| { | |
| draw->initForMidpointFan(context, paint); | |
| } | |
| return DrawUniquePtr(draw); | |
| } | |
| PathDraw::PathDraw(IAABB pixelBounds, | |
| const Mat2D& paintMatrix, | |
| const Mat2D* imageMatrix, | |
| rcp<const RiveRenderPath> path, | |
| FillRule initialFillRule, | |
| const RiveRenderPaint* paint, | |
| float modulatedOpacity, | |
| CoverageType coverageType, | |
| const RenderContext::FrameDescriptor& frameDesc) : | |
| Draw(pixelBounds, | |
| paintMatrix, | |
| imageMatrix, | |
| paint->getBlendMode(), | |
| ref_rcp(paint->getImageTexture()), | |
| paint->getImageSampler(), | |
| Type::path), | |
| m_pathRef(path.release()), | |
| m_pathFillRule(frameDesc.clockwiseFillOverride ? FillRule::clockwise | |
| : initialFillRule), | |
| m_gradientRef(paint->getGradientWithOpacity(modulatedOpacity).release()), | |
| 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::featherAtlas) | |
| { | |
| 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 = m_paintMatrix.xx() * m_paintMatrix.yy() - | |
| m_paintMatrix.yx() * m_paintMatrix.xy(); | |
| if (det < 0) | |
| { | |
| m_contourDirections = | |
| (m_coverageType == CoverageType::depthStencil || | |
| m_coverageType == CoverageType::featherAtlas) | |
| ? gpu::ContourDirections::reverse | |
| : gpu::ContourDirections::forwardThenReverse; | |
| // Ignored by depthStencil. | |
| m_contourFlags |= NEGATE_PATH_FILL_COVERAGE_FLAG; | |
| } | |
| else | |
| { | |
| m_contourDirections = | |
| (m_coverageType == CoverageType::depthStencil || | |
| m_coverageType == CoverageType::featherAtlas) | |
| ? gpu::ContourDirections::forward | |
| : gpu::ContourDirections::reverseThenForward; | |
| } | |
| } | |
| else if (m_coverageType != CoverageType::depthStencil) | |
| { | |
| // atomic and rasterOrdering fills need reverse AND forward triangles. | |
| if (frameDesc.clockwiseFillOverride && | |
| !m_pathRef->isClockwiseDominant(m_paintMatrix)) | |
| { | |
| // For clockwiseFill, this is also our opportunity to logically | |
| // reverse the winding of the path, if it is predominantly | |
| // counterclockwise. | |
| m_contourDirections = | |
| (m_coverageType == CoverageType::featherAtlas) | |
| ? gpu::ContourDirections::reverse | |
| : gpu::ContourDirections::forwardThenReverse; | |
| m_contourFlags |= NEGATE_PATH_FILL_COVERAGE_FLAG; | |
| } | |
| else | |
| { | |
| m_contourDirections = | |
| (m_coverageType == CoverageType::featherAtlas) | |
| ? gpu::ContourDirections::forward | |
| : gpu::ContourDirections::reverseThenForward; | |
| } | |
| } | |
| else | |
| { | |
| if (initialFillRule == FillRule::nonZero || | |
| frameDesc.clockwiseFillOverride) | |
| { | |
| // Emit "nonZero" depthStencil 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(m_paintMatrix) | |
| ? gpu::ContourDirections::forward | |
| : gpu::ContourDirections::reverse; | |
| } | |
| else | |
| { | |
| // "evenOdd" depthStencil fills just get drawn twice, so any | |
| // direction is fine. | |
| m_contourDirections = gpu::ContourDirections::forward; | |
| } | |
| } | |
| m_simplePaintValue = paint->getSimpleValue(); | |
| // Apply modulated opacity to the paint value. | |
| // Gradient modulation is handled upfront in the gradient initialization. | |
| if (modulatedOpacity != 1.0f) | |
| { | |
| switch (m_paintType) | |
| { | |
| case gpu::PaintType::solidColor: | |
| m_simplePaintValue.color = | |
| colorModulateOpacity(m_simplePaintValue.color, | |
| modulatedOpacity); | |
| break; | |
| case gpu::PaintType::linearGradient: | |
| case gpu::PaintType::radialGradient: | |
| case gpu::PaintType::clipUpdate: | |
| break; | |
| } | |
| } | |
| if (m_coverageType == CoverageType::featherAtlas) | |
| { | |
| // 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_L(2) | |
| 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_L(2) | |
| // Only call init() once. | |
| assert((m_resourceCounts.midpointFanTessVertexCount | | |
| m_resourceCounts.outerCubicTessVertexCount) == 0); | |
| if (isStrokeOrFeather()) | |
| { | |
| m_strokeMatrixMaxScale = m_paintMatrix.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_paintMatrix); | |
| 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, | |
| GrInnerFanTriangulator* triangulator) | |
| { | |
| RIVE_PROF_SCOPE_L(2) | |
| 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); | |
| assert(m_triangulator == nullptr); | |
| assert(triangulator != nullptr); | |
| m_triangulator = triangulator; | |
| // The triangulator's mesh is transform- and fill-rule-independent (so it | |
| // can be cached); the fill rule and winding are fed per draw. | |
| float matrixDeterminant = m_paintMatrix[0] * m_paintMatrix[3] - | |
| m_paintMatrix[2] * m_paintMatrix[1]; | |
| m_triangulatorReverseTriangles = matrixDeterminant < 0; | |
| m_triangulatorNegateWinding = | |
| (matrixDeterminant < 0) != | |
| static_cast<bool>(m_contourFlags & NEGATE_PATH_FILL_COVERAGE_FLAG); | |
| // Every path has at least 1 (non-cubic) move. | |
| size_t originalNumChopsSize = m_pathRef->getRawPath().verbs().size() - 1; | |
| m_numChops.reset(context->numChopsAllocator(), originalNumChopsSize); | |
| iterateOuterCubics(nullptr); // Fill in m_resourceCounts. | |
| m_numChops.shrinkToFit(context->numChopsAllocator(), originalNumChopsSize); | |
| } | |
| bool PathDraw::allocateResources(RenderContext::LogicalFlush* flush) | |
| { | |
| RIVE_PROF_SCOPE_L(2) | |
| 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) | |
| { | |
| if (!flush->allocateGradient(m_gradientRef, | |
| &m_simplePaintValue.colorRampLocation)) | |
| { | |
| return false; | |
| } | |
| } | |
| // Allocate a coverage buffer range or feather atlas region if needed. | |
| if (m_coverageType == CoverageType::featherAtlas || | |
| (m_coverageType == CoverageType::clockwiseAtomic && | |
| // Outermost (i.e., non-nested) clockwiseAtomic clips render directly | |
| // to the clip buffer without using the coverage buffer. | |
| !isOutermostClipUpdate())) | |
| { | |
| 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::featherAtlas) | |
| { | |
| 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->allocateFeatherAtlasDraw(this, | |
| w, | |
| h, | |
| PADDING, | |
| &x, | |
| &y, | |
| &m_featherAtlasScissor)) | |
| { | |
| return false; // There wasn't room for our path in the atlas. | |
| } | |
| m_featherAtlasTransform.scaleFactor = scaleFactor; | |
| m_featherAtlasTransform.translateX = | |
| x - visibleBounds.left * scaleFactor; | |
| m_featherAtlasTransform.translateY = | |
| y - visibleBounds.top * scaleFactor; | |
| m_featherAtlasScissorEnabled = visibleBounds != m_pixelBounds; | |
| } | |
| else | |
| { | |
| // Round up width and height to multiples of BUFFER_IMAGE_TILE_SIZE | |
| // for tiling. | |
| uint32_t coverageWidth = | |
| math::round_up_to_multiple_of<BUFFER_IMAGE_TILE_SIZE>( | |
| visibleBounds.width() + PADDING * 2); | |
| uint32_t coverageHeight = | |
| math::round_up_to_multiple_of<BUFFER_IMAGE_TILE_SIZE>( | |
| 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(const gpu::PlatformFeatures& platformFeatures) | |
| { | |
| RIVE_PROF_SCOPE_L(2) | |
| m_subpassCount = 1; | |
| m_prepassCount = 0; | |
| switch (m_coverageType) | |
| { | |
| case CoverageType::featherAtlas: | |
| 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 (needsBorrowedCoveragePrepass()) | |
| { | |
| // Add prepasses for borrowed coverage. | |
| m_prepassCount = m_subpassCount; | |
| } | |
| break; | |
| case CoverageType::depthStencil: | |
| { | |
| if (isStroke()) | |
| { | |
| m_subpassCount = 1; // Strokes can be rendered in a single pass. | |
| } | |
| else if (enums::all_flags_set(m_drawContents, | |
| gpu::kNestedClipUpdateMask)) | |
| { | |
| // Nested clip updates only have a stencil pass. (The reset is | |
| // handled by a separate ClipReset draw.) | |
| m_subpassCount = 1; | |
| } | |
| else if (enums::is_flag_set(m_drawContents, | |
| gpu::DrawContents::evenOddFill)) | |
| { | |
| // depthStencil "slow" path: stencil-then-cover. | |
| m_subpassCount = 2; | |
| } | |
| else if (platformFeatures.supportsPipelineDynamicState) | |
| { | |
| // depthStencil "fast" path, combined: the three subpasses | |
| // (borrowed coverage, fans, stencil reset) collapse onto a | |
| // single stencilDynamicMidpointFans draw that switches between | |
| // them with dynamic color/depth/stencil/cull state. One batch | |
| // keeps the reorderer able to instance non-overlapping paths | |
| // together. | |
| m_subpassCount = 1; | |
| } | |
| else | |
| { | |
| // depthStencil "fast" path: (effectively) single pass | |
| // rendering. | |
| m_subpassCount = 3; | |
| } | |
| if (isOpaque()) | |
| { | |
| const bool usesClipping = | |
| enums::any_flag_set(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; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| gpu::DrawBatch* PathDraw::pushToRenderContext( | |
| RenderContext::LogicalFlush* flush, | |
| int subpassIndex) | |
| { | |
| RIVE_PROF_SCOPE_L(2) | |
| // 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 nullptr; | |
| } | |
| 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); | |
| return &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))); | |
| gpu::DrawBatch* batch = 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 RIVE_DEBUG_CODE( | |
| , | |
| &m_numInteriorTriangleVerticesPushed)); | |
| assert(m_numInteriorTriangleVerticesPushed <= | |
| m_triangulator->maxVertexCount(m_pathFillRule)); | |
| return batch; | |
| } | |
| RIVE_UNREACHABLE(); | |
| } | |
| case CoverageType::clockwiseAtomic: | |
| { | |
| if (m_prepassCount != 0) | |
| { | |
| // The subpass and prepass each emit half the vertices. | |
| assert(m_prepassCount == m_subpassCount); | |
| assert(tessVertexCount % 2 == 0); | |
| assert(needsBorrowedCoveragePrepass()); | |
| tessVertexCount /= 2; | |
| } | |
| switch (subpassIndex) | |
| { | |
| case -1: // Tessellation (borrowed, midpointFan or outerCubic). | |
| assert(!isStroke()); | |
| m_prepassTessLocation = | |
| allocateTessellationVertices(flush, tessVertexCount); | |
| return &pushTessellationDraw( | |
| flush, | |
| tessVertexCount, | |
| m_prepassTessLocation, | |
| gpu::ShaderMiscFlags::borrowedCoveragePass); | |
| case 0: // Tessellation (midpointFan or outerCubic). | |
| { | |
| uint32_t tessLocation = | |
| allocateTessellationVertices(flush, tessVertexCount); | |
| pushTessellationData(flush, tessVertexCount, tessLocation); | |
| return &pushTessellationDraw(flush, | |
| tessVertexCount, | |
| tessLocation); | |
| } | |
| case -2: // Interior triangles (borrowed). | |
| case 1: // Interior triangles. | |
| { | |
| assert(!isStroke()); | |
| assert(m_triangulator != nullptr); | |
| gpu::DrawBatch* batch = | |
| flush->pushInteriorTriangulationDraw( | |
| this, | |
| m_pathID, | |
| m_prepassCount == 0 ? gpu::WindingFaces::all | |
| : subpassIndex < 0 ? gpu::WindingFaces::negative | |
| : gpu::WindingFaces::positive, | |
| subpassIndex < 0 | |
| ? gpu::ShaderMiscFlags::borrowedCoveragePass | |
| : gpu::ShaderMiscFlags::none RIVE_DEBUG_CODE( | |
| , | |
| &m_numInteriorTriangleVerticesPushed)); | |
| assert(m_numInteriorTriangleVerticesPushed <= | |
| m_triangulator->maxVertexCount(m_pathFillRule)); | |
| return batch; | |
| } | |
| } | |
| RIVE_UNREACHABLE(); | |
| } | |
| case CoverageType::depthStencil: | |
| { | |
| assert(m_prepassCount == 0 || m_subpassCount == 0); | |
| int passCount = m_prepassCount | m_subpassCount; | |
| int passIdx = subpassIndex + m_prepassCount; | |
| if (passIdx == 0) | |
| { | |
| m_depthStencilTessLocation = | |
| allocateTessellationVertices(flush, tessVertexCount); | |
| pushTessellationData(flush, | |
| tessVertexCount, | |
| m_depthStencilTessLocation); | |
| } | |
| assert(1 <= passCount && passCount <= 3); | |
| assert(passIdx < passCount); | |
| if (m_triangulator != nullptr) | |
| { | |
| // depthStencil interior triangulation: the path interior is | |
| // filled by smuggling its triangles in with outerCubic patches, | |
| // rather than introducing a distinct triangle-buffer draw. | |
| gpu::DrawType outerCubicDrawType; | |
| if (passCount == 1) | |
| { | |
| if (enums::all_flags_set(m_drawContents, | |
| gpu::kNestedClipUpdateMask)) | |
| { | |
| outerCubicDrawType = | |
| gpu::DrawType::stencilOuterCubicWinding; | |
| } | |
| else | |
| { | |
| assert(flush->platformFeatures() | |
| .supportsPipelineDynamicState); | |
| outerCubicDrawType = | |
| gpu::DrawType::stencilDynamicOuterCubics; | |
| } | |
| } | |
| else | |
| { | |
| constexpr static gpu::DrawType OuterCubicFillTypes[][3] = { | |
| // Slow path (passCount == 2): stencil-then-cover. | |
| { | |
| gpu::DrawType::stencilOuterCubicWinding, | |
| gpu::DrawType::stencilOuterCubicCover, | |
| }, | |
| // Fast path (passCount == 3). | |
| { | |
| gpu::DrawType::stencilOuterCubicBorrowedCoverage, | |
| gpu::DrawType::stencilOuterCubics, | |
| gpu::DrawType::stencilOuterCubicReset, | |
| }, | |
| }; | |
| outerCubicDrawType = | |
| OuterCubicFillTypes[passCount - 2][passIdx]; | |
| } | |
| return &flush->pushOuterCubicsDraw(this, | |
| outerCubicDrawType, | |
| tessVertexCount, | |
| m_depthStencilTessLocation); | |
| } | |
| else | |
| { | |
| gpu::DrawType depthStencilDrawType; | |
| if (passCount == 1) | |
| { | |
| if (isStroke()) | |
| { | |
| depthStencilDrawType = gpu::DrawType::depthStrokes; | |
| } | |
| else if (enums::all_flags_set(m_drawContents, | |
| gpu::kNestedClipUpdateMask)) | |
| { | |
| depthStencilDrawType = | |
| gpu::DrawType::stencilMidpointFanWinding; | |
| } | |
| else | |
| { | |
| assert(flush->platformFeatures() | |
| .supportsPipelineDynamicState); | |
| depthStencilDrawType = | |
| gpu::DrawType::stencilDynamicMidpointFans; | |
| } | |
| } | |
| else | |
| { | |
| constexpr static gpu::DrawType MidpointFanFillTypes[][3] = { | |
| // Slow path (passCount == 2): stencil-then-cover | |
| { | |
| gpu::DrawType::stencilMidpointFanWinding, | |
| gpu::DrawType::stencilMidpointFanCover, | |
| }, | |
| // Fast path (passCount == 3): (mostly) single pass | |
| // rendering. | |
| { | |
| gpu::DrawType::stencilMidpointFanBorrowedCoverage, | |
| gpu::DrawType::stencilMidpointFans, | |
| gpu::DrawType::stencilMidpointFanReset, | |
| }, | |
| }; | |
| depthStencilDrawType = | |
| MidpointFanFillTypes[passCount - 2][passIdx]; | |
| } | |
| return &flush->pushMidpointFanDraw(this, | |
| depthStencilDrawType, | |
| tessVertexCount, | |
| m_depthStencilTessLocation); | |
| } | |
| } | |
| case CoverageType::featherAtlas: | |
| // 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); | |
| return &flush->pushFeatherAtlasBlit(this, m_pathID); | |
| } | |
| RIVE_UNREACHABLE(); | |
| } | |
| gpu::DrawBatch& PathDraw::pushTessellationDraw( | |
| RenderContext::LogicalFlush* flush, | |
| uint32_t tessVertexCount, | |
| uint32_t tessLocation, | |
| gpu::ShaderMiscFlags shaderMiscFlags) | |
| { | |
| if (m_triangulator != nullptr) | |
| { | |
| assert(!isStroke()); | |
| return flush->pushOuterCubicsDraw(this, | |
| gpu::DrawType::outerCurvePatches, | |
| tessVertexCount, | |
| tessLocation, | |
| shaderMiscFlags); | |
| } | |
| else | |
| { | |
| return flush->pushMidpointFanDraw( | |
| this, | |
| isFeatheredFill() ? gpu::DrawType::midpointFanCenterAAPatches | |
| : gpu::DrawType::midpointFanPatches, | |
| tessVertexCount, | |
| tessLocation, | |
| shaderMiscFlags); | |
| } | |
| } | |
| void PathDraw::pushFeatherAtlasTessellation(RenderContext::LogicalFlush* flush, | |
| uint32_t* tessVertexCount, | |
| uint32_t* tessBaseVertex) | |
| { | |
| RIVE_PROF_SCOPE_L(2) | |
| assert(m_coverageType == CoverageType::featherAtlas); | |
| 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_L(2) | |
| // 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 && | |
| m_prepassTessLocation != 0) // With padding, this will only be | |
| // zero if it's not needed. | |
| { | |
| // The tessellation for borrowed coverage was allocated at a | |
| // different location than the forward tessellation, both with | |
| // "tessVertexCount" vertices. | |
| assert(needsBorrowedCoveragePrepass()); | |
| 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 && | |
| m_prepassTessLocation != 0) // With padding, this will only be | |
| // zero if it's not needed. | |
| { | |
| // The tessellation for borrowed coverage was allocated at a | |
| // different location than the forward tessellation, both with | |
| // "tessVertexCount" vertices. | |
| assert(needsBorrowedCoveragePrepass()); | |
| 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) | |
| { | |
| iterateOuterCubics(&tessWriter); | |
| } | |
| else | |
| { | |
| pushMidpointFanTessellationData(&tessWriter); | |
| } | |
| } | |
| void PathDraw::pushMidpointFanTessellationData( | |
| RenderContext::TessellationWriter* tessWriter) | |
| { | |
| RIVE_PROF_SCOPE_L(3) | |
| 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_L(3) | |
| // 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::iterateOuterCubics(RenderContext::TessellationWriter* tessWriter) | |
| { | |
| RIVE_PROF_SCOPE_L(2) | |
| Vec2D chops[kMaxCurveSubdivisions * 3 + 1]; | |
| const RawPath& rawPath = m_pathRef->getRawPath(); | |
| assert(!rawPath.empty()); | |
| wangs_formula::VectorXform vectorXform(m_paintMatrix); | |
| size_t patchCount = 0; | |
| size_t contourCount = 0; | |
| Vec2D p0 = {0, 0}; | |
| // A straight edge produces a single-segment cubic patch. In depthStencil we | |
| // draw only the patch interior (not the AA border), and that interior is | |
| // degenerate for a single segment, so we skip these patches entirely. | |
| const bool skipFlatOuterCubics = | |
| m_coverageType == CoverageType::depthStencil; | |
| // Only used on the "emit" pass (tessWriter != nullptr). | |
| uint32_t contourIDWithFlags = 0; | |
| for (const auto [verb, pts] : rawPath) | |
| { | |
| switch (verb) | |
| { | |
| case PathVerb::move: | |
| if (contourCount != 0 && pts[-1] != p0 && !skipFlatOuterCubics) | |
| { | |
| if (tessWriter != nullptr) | |
| { | |
| tessWriter->pushCubic( | |
| convert_line_to_cubic(pts[-1], p0).data(), | |
| m_contourDirections, | |
| {0, 0}, | |
| OuterCubicPatchSegmentSpan, | |
| 1, | |
| OuterCubicPatchJoinSegmentCount, | |
| contourIDWithFlags | | |
| CULL_EXCESS_TESSELLATION_SEGMENTS_CONTOUR_FLAG); | |
| } | |
| ++patchCount; | |
| } | |
| if (tessWriter != nullptr) | |
| { | |
| contourIDWithFlags = | |
| m_contourFlags | | |
| tessWriter->pushContour({0, 0}, | |
| isStroke(), | |
| /*closed=*/true, | |
| 0); | |
| } | |
| p0 = pts[0]; | |
| ++contourCount; | |
| break; | |
| case PathVerb::line: | |
| if (!skipFlatOuterCubics) | |
| { | |
| if (tessWriter != nullptr) | |
| { | |
| tessWriter->pushCubic( | |
| convert_line_to_cubic(pts).data(), | |
| m_contourDirections, | |
| {0, 0}, | |
| OuterCubicPatchSegmentSpan, | |
| 1, | |
| OuterCubicPatchJoinSegmentCount, | |
| contourIDWithFlags | | |
| CULL_EXCESS_TESSELLATION_SEGMENTS_CONTOUR_FLAG); | |
| } | |
| ++patchCount; | |
| } | |
| break; | |
| case PathVerb::quad: | |
| RIVE_UNREACHABLE(); | |
| case PathVerb::cubic: | |
| { | |
| uint32_t numSubdivisions; | |
| if (tessWriter == nullptr) | |
| { | |
| numSubdivisions = | |
| find_outer_cubic_subdivision_count(pts, vectorXform); | |
| m_numChops.push_back(numSubdivisions); | |
| } | |
| else | |
| { | |
| numSubdivisions = m_numChops.pop_front(); | |
| } | |
| assert(numSubdivisions >= 1); | |
| const size_t numGlueTriangles = numSubdivisions - 1; | |
| const size_t numGlueTriStrips = (numGlueTriangles + 2) / 3; | |
| const size_t numPatches = numSubdivisions + numGlueTriStrips; | |
| patchCount += numPatches; | |
| if (tessWriter != nullptr) | |
| { | |
| if (numSubdivisions == 1) | |
| { | |
| tessWriter->pushCubic( | |
| pts, | |
| m_contourDirections, | |
| {0, 0}, | |
| OuterCubicPatchSegmentSpan, | |
| 1, | |
| OuterCubicPatchJoinSegmentCount, | |
| contourIDWithFlags | | |
| CULL_EXCESS_TESSELLATION_SEGMENTS_CONTOUR_FLAG); | |
| } | |
| else | |
| { | |
| RIVE_DEBUG_CODE(size_t pushedPatchCount = 0;) | |
| // 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) | |
| { | |
| tessWriter->pushCubic( | |
| chop, | |
| m_contourDirections, | |
| {0, 0}, | |
| OuterCubicPatchSegmentSpan, | |
| 1, | |
| OuterCubicPatchJoinSegmentCount, | |
| contourIDWithFlags | | |
| CULL_EXCESS_TESSELLATION_SEGMENTS_CONTOUR_FLAG); | |
| chop += 3; | |
| RIVE_DEBUG_CODE(++pushedPatchCount;) | |
| } | |
| // Fill the gap between the sub-cubics by connecting | |
| // their endpoints with a simple fan. Each fan blade is | |
| // cubic patch with RETROFIT_TRI_STRIP_CONTOUR_FLAG (a | |
| // standard strip of up to 3 triangles, retrofitted into | |
| // a cubic patch), anchored at the first vertex | |
| // (chops[0]). | |
| for (size_t a = 1; a < numSubdivisions; a += 3) | |
| { | |
| const auto segmentsRemaining = numSubdivisions - a; | |
| // Flip the triangle strip orthogonally to the fan | |
| // blade in order to generate more efficient (less | |
| // "sliver") triangles. | |
| const size_t b = segmentsRemaining == 1 ? a : a + 1; | |
| StackVector<Vec2D, 5> strip; | |
| strip.push_back(chops[b * 3]); | |
| strip.push_back(chops[(b + 1) * 3]); | |
| if (segmentsRemaining > 1) | |
| strip.push_back(chops[(b - 1) * 3]); | |
| if (segmentsRemaining > 2) | |
| strip.push_back(chops[(b + 2) * 3]); | |
| strip.push_back(chops[0]); | |
| tessWriter->pushRetrofitCubicTriStrip( | |
| strip.data(), | |
| strip.size(), | |
| m_contourDirections, | |
| contourIDWithFlags); | |
| RIVE_DEBUG_CODE(++pushedPatchCount;) | |
| } | |
| assert(pushedPatchCount == numPatches); | |
| } | |
| } | |
| break; | |
| } | |
| case PathVerb::close: | |
| break; | |
| } | |
| } | |
| Vec2D lastPt = rawPath.points().back(); | |
| if (contourCount != 0 && lastPt != p0 && !skipFlatOuterCubics) | |
| { | |
| if (tessWriter != nullptr) | |
| { | |
| tessWriter->pushCubic( | |
| convert_line_to_cubic(lastPt, p0).data(), | |
| m_contourDirections, | |
| {0, 0}, | |
| OuterCubicPatchSegmentSpan, | |
| 1, | |
| OuterCubicPatchJoinSegmentCount, | |
| contourIDWithFlags | | |
| CULL_EXCESS_TESSELLATION_SEGMENTS_CONTOUR_FLAG); | |
| } | |
| ++patchCount; | |
| } | |
| if (tessWriter == nullptr) | |
| { | |
| // We also draw each "grout" triangle using an outerCubic patch. | |
| patchCount += m_triangulator->groutList().count(); | |
| if (m_coverageType == CoverageType::depthStencil) | |
| { | |
| // depthStencil fills the path interior by smuggling its triangles | |
| // in with outerCubic patches (rather than a distinct | |
| // triangle-buffer draw). | |
| patchCount += | |
| m_triangulator->retrofitCubicPatchCount(m_pathFillRule); | |
| } | |
| 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 * OuterCubicPatchSegmentSpanPlusJoin * 2 | |
| : patchCount * OuterCubicPatchSegmentSpanPlusJoin; | |
| if (m_coverageType != CoverageType::depthStencil) | |
| { | |
| // In depthStencil, the interior triangles are smuggled in with | |
| // outerCubic patches (counted above) instead of being drawn | |
| // from the triangle buffer. | |
| m_resourceCounts.maxTriangleVertexCount += | |
| m_triangulator->maxVertexCount(m_pathFillRule); | |
| } | |
| } | |
| } | |
| else | |
| { | |
| // Submit grout triangles, retrofitted into outerCubic patches. | |
| for (auto* node = m_triangulator->groutList().head(); node; | |
| node = node->fNext) | |
| { | |
| tessWriter->pushRetrofitCubicTriStrip(node->fPts, | |
| 3, | |
| m_contourDirections, | |
| contourIDWithFlags); | |
| ++patchCount; | |
| } | |
| if (m_coverageType == CoverageType::depthStencil) | |
| { | |
| // Smuggle the interior triangles in with outerCubic patches. | |
| patchCount += m_triangulator->polysToRetrofitCubicPatches( | |
| m_pathFillRule, | |
| gpu::WindingFaces::all, | |
| [&](const Vec2D* strip, size_t cornerCount) { | |
| tessWriter->pushRetrofitCubicTriStrip(strip, | |
| cornerCount, | |
| m_contourDirections, | |
| contourIDWithFlags); | |
| }); | |
| } | |
| assert(contourCount == m_resourceCounts.contourCount); | |
| assert(patchCount == m_resourceCounts.maxTessellatedSegmentCount); | |
| assert(patchCount * OuterCubicPatchSegmentSpanPlusJoin * 2 == | |
| m_resourceCounts.outerCubicTessVertexCount || | |
| patchCount * OuterCubicPatchSegmentSpanPlusJoin == | |
| 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, | |
| nullptr, | |
| 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; | |
| } | |
| gpu::DrawBatch* ImageRectDraw::pushToRenderContext( | |
| RenderContext::LogicalFlush* flush, | |
| int subpassIndex) | |
| { | |
| assert(subpassIndex == 0); | |
| return &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, | |
| nullptr, | |
| 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; | |
| } | |
| gpu::DrawBatch* ImageMeshDraw::pushToRenderContext( | |
| RenderContext::LogicalFlush* flush, | |
| int subpassIndex) | |
| { | |
| assert(subpassIndex == 0); | |
| return &flush->pushImageMeshDraw(this); | |
| } | |
| void ImageMeshDraw::releaseRefs() | |
| { | |
| Draw::releaseRefs(); | |
| m_vertexBufferRef->unref(); | |
| m_uvBufferRef->unref(); | |
| m_indexBufferRef->unref(); | |
| } | |
| ClipReset::ClipReset(RenderContext* context, | |
| uint32_t previousClipID, | |
| gpu::DrawContents previousClipDrawContents, | |
| ResetAction resetAction) : | |
| Draw(context->getClipContentBounds(previousClipID), | |
| Mat2D{}, | |
| nullptr, | |
| 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; | |
| } | |
| gpu::DrawBatch* ClipReset::pushToRenderContext( | |
| RenderContext::LogicalFlush* flush, | |
| int subpassIndex) | |
| { | |
| assert(subpassIndex == 0); | |
| return &flush->pushClipResetDraw(this); | |
| } | |
| } // namespace rive::gpu |