| /* | |
| * Copyright 2023 Rive | |
| */ | |
| #include "rive/renderer/draw.hpp" | |
| #include "gr_inner_fan_triangulator.hpp" | |
| #include "rive_render_path.hpp" | |
| #include "rive/math/bezier_utils.hpp" | |
| #include "rive/math/wangs_formula.hpp" | |
| #include "rive/renderer/render_context_impl.hpp" | |
| #include "rive/renderer/rive_renderer.hpp" | |
| #include "rive/renderer/rive_render_paint.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 NumSpokesInMiterOrBevelJoin = 4; | |
| // depthStencil bevels need 2 internal spokes: both colocated the ends, but with | |
| // AA outsets in the direction of the bevel. | |
| constexpr static int NumSpokesInDepthStencilBevelJoin = 2; | |
| // depthStencil miters need 3 internal spokes: all colocated on the miter | |
| // corner, with different AA outsets. | |
| constexpr static int NumSpokesInDepthStencilMiterJoin = 3; | |
| // depthStencil miterClip joins need 4 internal spokes: 2 colocated per corner, | |
| // with different AA outsets. | |
| constexpr static int NumSpokesInDepthStencilMiterClipJoin = 4; | |
| // "Fixed" join types, i.e., miter or bevel (not round), all have a fixed number | |
| // of spokes regardless of angle. | |
| constexpr static int numSpokesInFixedJoin( | |
| PathDraw::CoverageType coverageType, | |
| StrokeJoin join) | |
| { | |
| assert(join != StrokeJoin::round); | |
| if (coverageType == PathDraw::CoverageType::depthStencil) | |
| { | |
| switch (join) | |
| { | |
| case StrokeJoin::bevel: | |
| return NumSpokesInDepthStencilBevelJoin; | |
| case StrokeJoin::miter: | |
| return NumSpokesInDepthStencilMiterJoin; | |
| case StrokeJoin::round: | |
| RIVE_UNREACHABLE(); | |
| } | |
| } | |
| return NumSpokesInMiterOrBevelJoin; | |
| } | |
| constexpr static int numSegmentsInFixedJoin( | |
| PathDraw::CoverageType coverageType, | |
| StrokeJoin join) | |
| { | |
| // Since a join sits between 2 cubics, it already has an implicit spoke at | |
| // either end, shared with its cubic neighbor. A join SEGMENT count is | |
| // therefore its internal spoke count plus 1. | |
| return numSpokesInFixedJoin(coverageType, join) + 1; | |
| } | |
| constexpr static int numSegmentsInMiterClipJoin( | |
| PathDraw::CoverageType coverageType) | |
| { | |
| return (coverageType != PathDraw::CoverageType::depthStencil | |
| ? NumSpokesInMiterOrBevelJoin | |
| : NumSpokesInDepthStencilMiterClipJoin) + | |
| 1; | |
| } | |
| 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 "chopKey" reduces "if (areChopsCusps)" | |
| // branching and makes the code cleaner. | |
| RIVE_ALWAYS_INLINE constexpr uint8_t chopKey( | |
| bool areChopsCusps, | |
| uint8_t numChops) | |
| { | |
| return (numChops << 1) | static_cast<uint8_t>(areChopsCusps); | |
| } | |
| RIVE_ALWAYS_INLINE constexpr uint8_t cuspChopKey(uint8_t n) | |
| { | |
| return chopKey(true, n); | |
| } | |
| RIVE_ALWAYS_INLINE constexpr uint8_t simpleChopKey(uint8_t n) | |
| { | |
| return chopKey(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); | |
| } | |
| // Room for the pieces of any chopped cubic. Two cusp chops require storage for | |
| // 16 points. This is because when chopping around a cusp, we temporarily | |
| // produce three cubics (one before the cusp, a tiny one at the cusp, and one | |
| // after the cusp), then remove the tiny middle one (it's replaced with a cusp | |
| // join). So performing two cusp chops temporarily produces: cubic A, cusp | |
| // cubic, cubic B, cusp cubic, cubic C. Each cubic requires three points plus | |
| // the last cubic's end control point, bringing us to 16 points. (After removing | |
| // the two cusp cubics, we're back down to 10 points.) | |
| using ChopBuffer = std::array<Vec2D, 16>; | |
| // Chops a cubic at each of its n cusps into n + 1 pieces that meet exactly on | |
| // the cusp points. Strokes draw a cusp join across each cusp. | |
| // | |
| // To avoid zero tangents that occur at cusps, for a cusp at T, two chops are | |
| // performed at T - EPSILON and T + EPSILON and the middle cubic is collapsed by | |
| // snapping the surrounding two cubics' endpoints to the cusp's position. | |
| // | |
| // dst needs room for 6 * n + 4 points because it has to hold an intermediate | |
| // result. The result is the first 3 * n + 4 points. | |
| RIVE_ALWAYS_INLINE static void chopCubicAtCusps( | |
| const Vec2D p[4], | |
| ChopBuffer* dst, | |
| const float cuspT[], | |
| int n) | |
| { | |
| float t[4]; | |
| assert(n * 2 <= std::size(t)); | |
| assert(6 * n + 4 <= std::tuple_size<ChopBuffer>::value); | |
| // 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); | |
| } | |
| // chopCubicAt() will produce 2n + 1 pieces, with piece j at dst[3 * j]. | |
| // Each odd piece is a sliver around a cusp. | |
| math::chopCubicAt(p, dst->data(), t, n * 2); | |
| // Collapse each sliver to the cusp by placing the previous piece's last | |
| // control point (which is shared with the next piece's first control | |
| // point) directly at the cusp's position and shifting the next piece down | |
| // in the list. | |
| ChopBuffer& pts = *dst; | |
| for (int i = 0; i < n; ++i) | |
| { | |
| pts[3 * i + 3] = math::evalCubicAt(p, cuspT[i]); | |
| pts[3 * i + 4] = pts[6 * i + 7]; | |
| pts[3 * i + 5] = pts[6 * i + 8]; | |
| } | |
| // Copy the cubic's original endpoint. | |
| pts[3 * n + 3] = pts[6 * n + 3]; | |
| } | |
| // Chops a stroked cubic at the T values its chop key calls for, writing the | |
| // pieces end to end into dst. The key must have at least one chop. | |
| RIVE_ALWAYS_INLINE static void chopCubic( | |
| const Vec2D p[4], | |
| uint8_t chopKey, | |
| const float t[2], | |
| ChopBuffer* dst) | |
| { | |
| switch (chopKey) | |
| { | |
| case cuspChopKey(2): // 2 cusps | |
| case cuspChopKey(1): // 1 cusp | |
| chopCubicAtCusps(p, dst, t, chopKey >> 1); | |
| break; | |
| case simpleChopKey(2): // 2 non-cusp chops | |
| math::chopCubicAt(p, dst->data(), t[0], t[1]); | |
| break; | |
| case simpleChopKey(1): // 1 non-cusp chop | |
| math::chopCubicAt(p, dst->data(), t[0]); | |
| break; | |
| default: | |
| RIVE_UNREACHABLE(); | |
| } | |
| } | |
| // 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_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; | |
| } | |
| // Finds where each of a stroke's batched cubics gets chopped, four at a time. | |
| // Returns the total number of chops. | |
| RIVE_NEVER_INLINE static size_t findCubicChops( | |
| RenderContext::CubicBatch* batch, | |
| size_t cubicCount) | |
| { | |
| // Most cubics need no chops. The candidates' keys get filled in below. | |
| batch->chopKeys.assign( | |
| math::round_up_to_multiple_of<4>(cubicCount), | |
| chopKey(false, 0)); | |
| batch->chopCandidates.clear(); | |
| const float4* blocks = batch->points.data(); | |
| for (size_t blockStart = 0; blockStart < cubicCount; blockStart += 4) | |
| { | |
| // Padding lanes hold copies of the last cubic, so whole blocks are safe | |
| // to process. | |
| const float4* block = blocks + blockStart * 2; | |
| auto [p0x, p0y, p1x, p1y] = simd::load4x4f( | |
| reinterpret_cast<const float*>(block)); | |
| auto [p2x, p2y, p3x, p3y] = simd::load4x4f( | |
| reinterpret_cast<const float*>(block + 4)); | |
| // The early-out finds cubics that need no chops four at a time. The | |
| // rest become candidates for the chop finder. | |
| uint32_t candidateLanes = ~simd::bitmask(math::cubicsNeedNoChops( | |
| p0x, | |
| p0y, | |
| p1x, | |
| p1y, | |
| p2x, | |
| p2y, | |
| p3x, | |
| p3y)) & | |
| 0xf; | |
| if (candidateLanes == 0) | |
| { | |
| continue; | |
| } | |
| if (cubicCount - blockStart < 4) | |
| { | |
| // Padding lanes are never candidates. | |
| candidateLanes &= (1u << (cubicCount - blockStart)) - 1; | |
| } | |
| for (uint32_t lane = 0; lane < 4; ++lane) | |
| { | |
| if (candidateLanes & (1u << lane)) | |
| { | |
| batch->chopCandidates.push_back( | |
| math::lossless_numeric_cast<uint32_t>(blockStart + lane)); | |
| } | |
| } | |
| } | |
| // Find the candidates' chops four at a time. | |
| size_t chopCount = 0; | |
| const uint32_t* candidates = batch->chopCandidates.data(); | |
| size_t candidateCount = batch->chopCandidates.size(); | |
| // Room to store whole groups of 4. | |
| size_t paddedCandidateCount = math::round_up_to_multiple_of<4>( | |
| candidateCount); | |
| if (batch->chopT0.size() < paddedCandidateCount) | |
| { | |
| batch->chopT0.resize(paddedCandidateCount); | |
| batch->chopT1.resize(paddedCandidateCount); | |
| } | |
| for (size_t first = 0; first < candidateCount; first += 4) | |
| { | |
| // Gather up to four candidates' rows, repeating the last to fill a | |
| // partial group. | |
| float4 p01[4]; | |
| float4 p23[4]; | |
| for (size_t lane = 0; lane < 4; ++lane) | |
| { | |
| uint32_t i = candidates[std::min(first + lane, candidateCount - 1)]; | |
| const float4* block = blocks + (i / 4) * 8; | |
| p01[lane] = block[i % 4]; | |
| p23[lane] = block[4 + i % 4]; | |
| } | |
| auto [p0x, p0y, p1x, p1y] = simd::transpose4x4f( | |
| p01[0], | |
| p01[1], | |
| p01[2], | |
| p01[3]); | |
| auto [p2x, p2y, p3x, p3y] = simd::transpose4x4f( | |
| p23[0], | |
| p23[1], | |
| p23[2], | |
| p23[3]); | |
| math::Convex180Chops4 chops = math::findCubicConvex180Chops4( | |
| p0x, | |
| p0y, | |
| p1x, | |
| p1y, | |
| p2x, | |
| p2y, | |
| p3x, | |
| p3y); | |
| // A group's candidates have consecutive slots, so store all 4 lanes at | |
| // once. The scalar lanes below overwrite theirs. | |
| simd::store(batch->chopT0.data() + first, chops.T0); | |
| simd::store(batch->chopT1.data() + first, chops.T1); | |
| for (size_t lane = 0; lane < 4 && first + lane < candidateCount; ++lane) | |
| { | |
| uint32_t i = candidates[first + lane]; | |
| uint8_t numChops; | |
| if (chops.possibleCusps[lane]) | |
| { | |
| // Cusps are rare enough to handle one at a time. | |
| Vec2D p[4]; | |
| simd::store(&p[0].x, p01[lane]); | |
| simd::store(&p[2].x, p23[lane]); | |
| float t[2] = {0, 0}; | |
| bool areChopsCusps = false; | |
| // Note: calling the full findCubicConvex180Chops() is doing a | |
| // bit of redundant work (e.g. the early-out check, and tests | |
| // for non-cusp cases). I haven't optimized for this case | |
| // because the possibleCusps branch is fairly rare. | |
| numChops = math::findCubicConvex180Chops(p, t, &areChopsCusps); | |
| batch->chopKeys[i] = chopKey(areChopsCusps, numChops); | |
| batch->chopT0[first + lane] = t[0]; | |
| batch->chopT1[first + lane] = t[1]; | |
| } | |
| else | |
| { | |
| numChops = static_cast<uint8_t>(chops.numChops[lane]); | |
| batch->chopKeys[i] = chopKey(false, numChops); | |
| } | |
| chopCount += numChops; | |
| } | |
| } | |
| return chopCount; | |
| } | |
| // Finds the polar segment count of every rotation from its tangent pair, four | |
| // at a time. | |
| RIVE_NEVER_INLINE static void countPolarSegments( | |
| std::array<Vec2D, 2>* tangentPairs, | |
| size_t rotationCount, | |
| float polarSegmentsPerRadian, | |
| uint32_t* polarSegmentCounts) | |
| { | |
| size_t paddedRotationCount = math::round_up_to_multiple_of<4>(rotationCount); | |
| for (size_t j = rotationCount; j < paddedRotationCount; ++j) | |
| { | |
| tangentPairs[j] = {Vec2D{0, 1}, Vec2D{0, 1}}; | |
| } | |
| for (size_t j = 0; j < paddedRotationCount; j += 4) | |
| { | |
| auto [tx0, ty0, tx1, ty1] = simd::load4x4f(&tangentPairs[j][0].x); | |
| float4 numer = tx0 * tx1 + ty0 * ty1; | |
| float4 denomPow2 = (tx0 * tx0 + ty0 * ty0) * (tx1 * tx1 + ty1 * ty1); | |
| #ifdef RIVE_HAS_FAST_INVERSE_SQRT | |
| float4 cosTheta = numer * simd::inverseSqrtFast(denomPow2); | |
| #else | |
| float4 cosTheta = numer / simd::sqrt(denomPow2); | |
| #endif | |
| cosTheta = simd::clamp(cosTheta, float4(-1), float4(1)); | |
| float4 theta = simd::fast_acos(cosTheta); | |
| float4 n = simd::ceil(theta * polarSegmentsPerRadian); | |
| n = simd::clamp(n, float4(1), float4(kMaxPolarSegments)); | |
| simd::store(polarSegmentCounts + j, simd::cast<uint32_t>(n)); | |
| } | |
| } | |
| // Finds the parametric segment count of every piece, four at a time, and, when | |
| // given tangentPairs, the tangents at both ends of each piece, which go to the | |
| // piece's rotation slot. Returns the total parametric segment count. | |
| static uint32_t processPieces( | |
| const float4* blocks, | |
| size_t pieceCount, | |
| float parametricPrecision, | |
| const wangs_formula::VectorXform& vectorXform, | |
| uint32_t* parametricSegmentCounts, | |
| std::array<Vec2D, 2>* tangentPairs, | |
| const uint32_t* rotationSlots) | |
| { | |
| uint4 total = 0; | |
| for (size_t first = 0; first < pieceCount; first += 4) | |
| { | |
| const float4* block = blocks + first * 2; | |
| auto [p0x, p0y, p1x, p1y] = simd::load4x4f( | |
| reinterpret_cast<const float*>(block)); | |
| auto [p2x, p2y, p3x, p3y] = simd::load4x4f( | |
| reinterpret_cast<const float*>(block + 4)); | |
| uint4 counts = wangs_formula::cubicSegmentCounts<kMaxParametricSegments>( | |
| p0x, | |
| p0y, | |
| p1x, | |
| p1y, | |
| p2x, | |
| p2y, | |
| p3x, | |
| p3y, | |
| parametricPrecision, | |
| vectorXform); | |
| // parametricSegmentCounts has room for whole blocks. | |
| simd::store(parametricSegmentCounts + first, counts); | |
| // Padding lanes don't count toward the total. | |
| total += simd::if_then_else( | |
| uint4{0, 1, 2, 3} + uint32_t(first) < uint32_t(pieceCount), | |
| counts, | |
| uint4(0)); | |
| if (tangentPairs == nullptr) | |
| { | |
| continue; | |
| } | |
| auto [tan0x, tan0y, tan1x, tan1y] = math::findCubicTangents4( | |
| p0x, | |
| p0y, | |
| p1x, | |
| p1y, | |
| p2x, | |
| p2y, | |
| p3x, | |
| p3y); | |
| // Each column is one piece's tangent pair. | |
| simd::gvec4x4<float> pairs = simd::transpose4x4f( | |
| tan0x, | |
| tan0y, | |
| tan1x, | |
| tan1y); | |
| size_t laneCount = std::min<size_t>(pieceCount - first, 4); | |
| for (size_t lane = 0; lane < laneCount; ++lane) | |
| { | |
| simd::store( | |
| tangentPairs[rotationSlots[first + lane]].data(), | |
| pairs.cols[lane]); | |
| } | |
| } | |
| return simd::reduce_add(total); | |
| } | |
| } // namespace | |
| Draw::Draw( | |
| IAABB pixelBounds, | |
| const Mat2D& paintMatrix, | |
| const Mat2D* imageMatrix, | |
| BlendMode blendMode, | |
| float additiveness, | |
| 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_additiveness(math::clamp(additiveness, 0.f, 1.f)) | |
| , m_type(type) | |
| , m_clippedPixelBounds(pixelBounds) | |
| { | |
| if (m_blendMode != BlendMode::srcOver) | |
| { | |
| m_combinedDrawContents |= 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_combinedDrawContents, | |
| gpu::DrawContents::clipUpdate)) | |
| { | |
| if (m_clipID != 0) | |
| { | |
| m_combinedDrawContents |= gpu::DrawContents::activeClip; | |
| } | |
| else | |
| { | |
| m_combinedDrawContents &= ~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( | |
| featherRadiusFromFeather(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(); | |
| } | |
| IAABB PathDraw::calculatePixelBounds( | |
| const Mat2D& paintMatrix, | |
| const RiveRenderPath* path, | |
| const RiveRenderPaint* paint, | |
| bool inkbleed) | |
| { | |
| std::optional<StrokeParams> stroke; | |
| if (paint->getIsStroked()) | |
| { | |
| stroke = paint->getStrokeParams(); | |
| switch (stroke->position) | |
| { | |
| case StrokePosition::inside: | |
| // Measure these as if not stroked | |
| stroke = std::nullopt; | |
| break; | |
| case StrokePosition::center: | |
| // center is already good | |
| break; | |
| case StrokePosition::outside: | |
| // Measure these with a double-width centered stroke | |
| stroke->position = StrokePosition::center; | |
| stroke->thickness *= 2.0f; | |
| break; | |
| } | |
| } | |
| return path->calculatePixelBounds( | |
| paintMatrix, | |
| stroke, | |
| paint->getFeather(), | |
| inkbleed && paint->getType() != gpu::PaintType::clipUpdate); | |
| } | |
| DrawUniquePtr PathDraw::Make( | |
| RenderContext* context, | |
| const Mat2D& paintMatrix, | |
| const Mat2D* imageMatrix, | |
| rcp<const RiveRenderPath> path, | |
| FillRule fillRule, | |
| const RiveRenderPaint* paint, | |
| float modulatedOpacity, | |
| ColorInt modulatedColor, | |
| 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. | |
| pixelBounds = calculatePixelBounds( | |
| paintMatrix, | |
| path.get(), | |
| paint, | |
| context->frameDescriptor().inkbleedOverride); | |
| } | |
| // 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; | |
| } | |
| // Skip interior triangulation on inkbleed until we have a compute-based | |
| // index generator. We're vertex bound on Android now, and outerCubic | |
| // patches generate a lot of vertex waste, which then gets compounded by the | |
| // multiple lanes and multiple passes of inkbleed hairlines. | |
| // We also don't allow interior triangulation for draws with an additional | |
| // outset. | |
| if (!paint->getIsStroked() && paint->getFeather() == 0 && | |
| !context->frameDescriptor().inkbleedOverride && | |
| paint->getAdditionalOutset() == 0.0f) | |
| { | |
| // 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 = PathDrawUniquePtr(context->make<PathDraw>( | |
| pixelBounds, | |
| paintMatrix, | |
| imageMatrix, | |
| std::move(path), | |
| fillRule, | |
| paint, | |
| modulatedOpacity, | |
| modulatedColor, | |
| coverageType, | |
| context->frameDescriptor())); | |
| if (triangulator != nullptr) | |
| { | |
| draw->initForInteriorTriangulation(context, triangulator); | |
| } | |
| else | |
| { | |
| draw->initForMidpointFan(context, paint); | |
| } | |
| // Drop empty draws before they get sorted into batches. | |
| if (draw->isEmpty()) | |
| { | |
| return nullptr; | |
| } | |
| return draw; | |
| } | |
| PathDraw::PathDraw( | |
| IAABB pixelBounds, | |
| const Mat2D& paintMatrix, | |
| const Mat2D* imageMatrix, | |
| rcp<const RiveRenderPath> path, | |
| FillRule initialFillRule, | |
| const RiveRenderPaint* paint, | |
| float modulatedOpacity, | |
| ColorInt modulatedColor, | |
| CoverageType coverageType, | |
| const RenderContext::FrameDescriptor& frameDesc) | |
| : Draw( | |
| pixelBounds, | |
| paintMatrix, | |
| imageMatrix, | |
| paint->getBlendMode(), | |
| paint->getAdditiveness(), | |
| ref_rcp(paint->getImageTexture()), | |
| paint->getImageSampler(), | |
| Type::path) | |
| , m_pathRef(path.release()) | |
| , m_pathFillRule( | |
| frameDesc.clockwiseFillOverride ? FillRule::clockwise | |
| : initialFillRule) | |
| , m_gradientRef( | |
| paint->getModulatedGradient(modulatedOpacity, modulatedColor) | |
| .release()) | |
| , m_inverseGradientTransform(paint->getInverseGradientTransform()) | |
| , m_paintType(paint->getType()) | |
| , m_coverageType(coverageType) | |
| , m_additionalOutset(paint->getAdditionalOutset()) | |
| { | |
| assert(m_pathRef != nullptr); | |
| assert(!m_pathRef->getRawPath().empty()); | |
| assert(paint != nullptr); | |
| if (paint->getIsOpaque() && | |
| // Inkbleed strokes introduce coverage, which is no longer opaque. | |
| !(frameDesc.inkbleedOverride && paint->getIsStroked())) | |
| { | |
| m_combinedDrawContents |= gpu::DrawContents::opaquePaint; | |
| } | |
| m_hasHairlinePass = frameDesc.inkbleedOverride && | |
| m_coverageType == CoverageType::depthStencil && | |
| m_pathFillRule == FillRule::clockwise && | |
| !paint->getIsStroked() && paint->getFeather() == 0 && | |
| paint->getType() != gpu::PaintType::clipUpdate; | |
| if (paint->getIsLayerMask()) | |
| { | |
| m_isLayerMask = true; | |
| m_layerMaskMode = paint->getLayerMaskMode(); | |
| } | |
| if (paint->getFeather() != 0) | |
| { | |
| m_featherRadius = featherRadiusFromFeather(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 = std::max( | |
| std::numeric_limits<float>::min(), | |
| m_strokeRadius); | |
| assert(!std::isnan(m_strokeRadius)); // These should get culled in | |
| // RiveRenderer::drawPath(). | |
| assert(m_strokeRadius > 0); | |
| } | |
| // For atlased paths, m_combinedDrawContents refer to the rectangle being | |
| // drawn into the main render target (not the step that generates the atlas | |
| // mask, which uses, e.g., fill rules, etc.). | |
| if (m_coverageType != CoverageType::featherAtlas) | |
| { | |
| if (isStroke()) | |
| { | |
| m_combinedDrawContents |= gpu::DrawContents::stroke; | |
| } | |
| else | |
| { | |
| if (m_featherRadius) | |
| { | |
| m_combinedDrawContents |= gpu::DrawContents::featheredFill; | |
| } | |
| if (initialFillRule == FillRule::clockwise || | |
| frameDesc.clockwiseFillOverride) | |
| { | |
| m_combinedDrawContents |= gpu::DrawContents::clockwiseFill; | |
| } | |
| else if (initialFillRule == FillRule::nonZero) | |
| { | |
| m_combinedDrawContents |= gpu::DrawContents::nonZeroFill; | |
| } | |
| else if (initialFillRule == FillRule::evenOdd) | |
| { | |
| m_combinedDrawContents |= gpu::DrawContents::evenOddFill; | |
| } | |
| } | |
| } | |
| if (paint->getType() == gpu::PaintType::clipUpdate) | |
| { | |
| m_combinedDrawContents |= gpu::DrawContents::clipUpdate; | |
| if (paint->getSimpleValue().outerClipID != 0) | |
| { | |
| m_combinedDrawContents |= 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 modulation to the paint value. | |
| // Gradient modulation is handled upfront in the gradient initialization. | |
| if (modulatedOpacity != 1.0f || modulatedColor != 0xFFFFFFFF) | |
| { | |
| switch (m_paintType) | |
| { | |
| case gpu::PaintType::solidColor: | |
| m_simplePaintValue.color = colorModulate( | |
| m_simplePaintValue.color, | |
| modulatedColor, | |
| modulatedOpacity); | |
| break; | |
| case gpu::PaintType::linearGradient: | |
| case gpu::PaintType::radialGradient: | |
| case gpu::PaintType::angularGradient: | |
| 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); | |
| } | |
| // The cubics start out as one list, with one curve slot and one rotation slot | |
| // each, as if none get chopped. For each cubic that does get chopped, its first | |
| // piece replaces the unchopped entry in-place and its other pieces are appended | |
| // to the end of the curve list. Their rotations, plus a cusp join before each | |
| // piece that follows a cusp, are appended to the end of the rotation list. | |
| // Returns the number of rotations added. | |
| size_t PathDraw::appendChoppedPieces( | |
| RenderContext::CubicBatch* batch, | |
| size_t cubicCount, | |
| [[maybe_unused]] size_t contourCount, | |
| size_t mainRotationCount) | |
| { | |
| float4* blocks = batch->points.data(); | |
| uint32_t* rotationSlots = batch->rotationSlots.data(); | |
| batch->cuspJoins.clear(); | |
| auto writePiece = [blocks](size_t curveIdx, const Vec2D* p) { | |
| float4* block = blocks + (curveIdx / 4) * 8; | |
| block[curveIdx % 4] = simd::load4f(p); | |
| block[4 + curveIdx % 4] = simd::load4f(p + 2); | |
| }; | |
| // The extra chopped entries start after all of the unchopped entries in the | |
| // curve and rotation lists | |
| size_t extraCurveIdx = cubicCount; | |
| size_t extraRotationIdx = mainRotationCount; | |
| ContourInfo* contour = m_contours; | |
| for (size_t candidateIdx = 0; candidateIdx < batch->chopCandidates.size(); | |
| ++candidateIdx) | |
| { | |
| uint32_t k = batch->chopCandidates[candidateIdx]; | |
| uint8_t chopKey = batch->chopKeys[k]; | |
| uint8_t numChops = chopKey >> 1; | |
| if (numChops == 0) | |
| { | |
| continue; | |
| } | |
| bool areChopsCusps = (chopKey & 1) != 0; | |
| // Advance to the contour that owns this cubic, skipping any with no | |
| // cubics (e.g., only lines). A contour's curve slots are the indices | |
| // of its cubics (extraCurveCount tracks the appended pieces). | |
| while (k >= contour->endCurveIdx) | |
| { | |
| ++contour; | |
| assert(contour < m_contours + contourCount); | |
| } | |
| // Chop from the RawPath (batch->rawPathCubicPoints rather than | |
| // batch->points). The chop functions need the points in one contiguous | |
| // array, which the batch's block layout doesn't keep, and copying them | |
| // out of the block just to read them back would stall on store | |
| // forwarding. | |
| float t[2] = {batch->chopT0[candidateIdx], batch->chopT1[candidateIdx]}; | |
| ChopBuffer localChopBuffer; | |
| chopCubic(batch->rawPathCubicPoints[k], chopKey, t, &localChopBuffer); | |
| // pushMidpointFanTessellationData() only gets the T values and chops | |
| // the cubic again, which measured cheaper than saving these pieces for | |
| // it to read back. (The pieces here only feed processPieces().) | |
| m_chopVertices.push_back() = {t[0], t[1]}; | |
| const Vec2D* p = localChopBuffer.data(); | |
| writePiece(k, p); | |
| uint32_t lastPieceRotationIdx = rotationSlots[k]; | |
| for (int piece = 1; piece <= numChops; ++piece) | |
| { | |
| p += 3; | |
| if (areChopsCusps) | |
| { | |
| // A cusp join across the cusp before this piece. Its tangents | |
| // come from the pieces on either side. | |
| uint32_t joinIdx = math::lossless_numeric_cast<uint32_t>( | |
| extraRotationIdx++); | |
| uint32_t nextPieceRotationIdx = math::lossless_numeric_cast<uint32_t>( | |
| extraRotationIdx); | |
| batch->cuspJoins.push_back( | |
| {joinIdx, lastPieceRotationIdx, nextPieceRotationIdx}); | |
| ++contour->cuspJoinCount; | |
| ++contour->extraRotationCount; | |
| } | |
| writePiece(extraCurveIdx, p); | |
| lastPieceRotationIdx = math::lossless_numeric_cast<uint32_t>( | |
| extraRotationIdx++); | |
| rotationSlots[extraCurveIdx++] = lastPieceRotationIdx; | |
| ++contour->extraCurveCount; | |
| ++contour->extraRotationCount; | |
| } | |
| } | |
| return extraRotationIdx - mainRotationCount; | |
| } | |
| RIVE_NEVER_INLINE uint32_t PathDraw::countStrokeContourVertices( | |
| ContourInfo* contour, | |
| bool empty, | |
| size_t* extraRotationIdx, | |
| size_t* emptyStrokeCountForCaps) | |
| { | |
| uint32_t vertexCount = 0; | |
| // 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 | |
| // | |
| for (size_t j = contour->firstRotationIdx; j < contour->endRotationIdx; ++j) | |
| { | |
| vertexCount += m_polarSegmentCounts[j]; | |
| } | |
| size_t extraIdx = *extraRotationIdx; | |
| for (size_t end = extraIdx + contour->extraRotationCount; extraIdx < end; | |
| ++extraIdx) | |
| { | |
| vertexCount += m_polarSegmentCounts[extraIdx]; | |
| } | |
| *extraRotationIdx = extraIdx; | |
| // Count joins. | |
| if (!isStroke()) | |
| { | |
| assert(isFeatheredFill()); | |
| uint32_t numSegmentsInFeatherJoin = feather_join_segment_count( | |
| m_polarSegmentsPerRadian); | |
| vertexCount += contour->strokeJoinCount * (numSegmentsInFeatherJoin - 1); | |
| } | |
| else if (m_strokeJoin == StrokeJoin::round) | |
| { | |
| // Round joins share their beginning and ending spokes with the curve | |
| // on either side. Therefore, the number of spokes we need to allocate | |
| // for a round join is "joinSegmentCount - 1". Do all the -1's here. | |
| vertexCount -= contour->strokeJoinCount; | |
| } | |
| else | |
| { | |
| vertexCount += contour->strokeJoinCount * | |
| numSpokesInFixedJoin(m_coverageType, m_strokeJoin); | |
| } | |
| // Cusp joins share both end vertices (like round joins). Their polar | |
| // segments were summed above, with the other rotations. | |
| vertexCount -= contour->cuspJoinCount; | |
| // Count stroke caps, if any. | |
| 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); | |
| // Ignore butt caps when the contour is empty. | |
| needsCaps = cap != StrokeCap::butt; | |
| } | |
| 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); | |
| // Make sure not to exceed kMaxPolarSegments. | |
| strokeCapSegmentCount = std::min<float>( | |
| kMaxPolarSegments, | |
| strokeCapSegmentCount); | |
| contour->strokeCapSegmentCount = static_cast<uint32_t>( | |
| strokeCapSegmentCount); | |
| } | |
| else | |
| { | |
| // Butt caps are emulated as bevel joins, square caps as | |
| // miterClip joins. | |
| contour->strokeCapSegmentCount = | |
| (cap == StrokeCap::square | |
| ? numSegmentsInMiterClipJoin(m_coverageType) | |
| : numSegmentsInFixedJoin(m_coverageType, StrokeJoin::bevel)); | |
| } | |
| // +2 because caps emulated as joins need to emit spokes at T=0 and | |
| // T=1, unlike normal joins, which always have a cubic neighbor to | |
| // share with on both sides. | |
| contour->strokeCapSegmentCount = std::min( | |
| contour->strokeCapSegmentCount + 2, | |
| kMaxPolarSegments); | |
| // 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; | |
| vertexCount += (contour->strokeCapSegmentCount - 1) * 2; | |
| } | |
| return vertexCount; | |
| } | |
| 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/calcPolarSegmentsPerRadian at | |
| // FEATHER_MIN_POLAR_SEGMENT_ANGLE. | |
| // | |
| // i.e., 1 / calcPolarSegmentsPerRadian<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::calcPolarSegmentsPerRadian<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::calcPolarSegmentsPerRadian< | |
| 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(); | |
| assert(rawPath.verbs()[0] == PathVerb::move); | |
| size_t contourCount = rawPath.verbCount(PathVerb::move); | |
| assert(contourCount != 0); | |
| m_contours = reinterpret_cast<ContourInfo*>( | |
| context->perFrameAllocator().alloc(sizeof(ContourInfo) * contourCount)); | |
| assert(rawPath.verbCount(PathVerb::quad) == 0); | |
| size_t cubicCount = rawPath.verbCount(PathVerb::cubic); | |
| // Only cubics take curve slots. A stroked cubic chops into at most 3 | |
| // pieces. | |
| size_t maxCurves = isStrokeOrFeather() ? cubicCount * 3 : cubicCount; | |
| size_t maxRotations = 0; | |
| if (isStrokeOrFeather()) | |
| { | |
| // Each piece of a stroked cubic takes a rotation slot, as does the cusp | |
| // join across each cusp. | |
| maxRotations += cubicCount * 5; | |
| 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. Each line or cubic has at most | |
| // one join before it, and closing a contour can add an additional | |
| // join. Note that we append `contourCount` rather than the number | |
| // of explicit `close` verbs to account for implicit closes. | |
| maxRotations += rawPath.verbCount(PathVerb::line) + cubicCount + | |
| contourCount; | |
| } | |
| } | |
| // Each stroked cubic will record the number of chops it requires (either | |
| // 0, 1, or 2). | |
| size_t maxChops = isStrokeOrFeather() ? cubicCount : 0; | |
| // Each chopped cubic saves its T values for | |
| // pushMidpointFanTessellationData() to chop it again. | |
| size_t maxChopVertices = maxChops; | |
| // Curves and rotations are processed in whole blocks of 4. | |
| size_t maxPaddedRotations = isStrokeOrFeather() | |
| ? math::round_up_to_multiple_of<4>( | |
| maxRotations) | |
| : 0; | |
| size_t maxPaddedCurves = math::round_up_to_multiple_of<4>(maxCurves); | |
| // 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 emptyStrokeCountForCaps = 0; | |
| // Iteration pass 1: Collect contours, round joins and line counts, and copy | |
| // the cubics into the batch buffer. Each cubic gets one curve and rotation | |
| // slot. appendChoppedPieces() will later add any slots for the extra pieces | |
| // that result from chopping. | |
| 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); | |
| RenderContext::CubicBatch& cubicBatch = context->cubicBatch(); | |
| cubicBatch.resize(cubicCount, maxCurves, isStroke()); | |
| float4* cubicBlocks = cubicBatch.points.data(); | |
| // Where the next cubic's [p0 p1] goes. Its [p2 p3] goes 4 lanes later. | |
| float4* nextCubicLane = cubicBlocks; | |
| // Copies a cubic into its block and gives it the next curve slot. In pass 1 | |
| // the curve slots are the cubics' indices. | |
| auto gatherCubic = [&](const Vec2D* p) { | |
| assert(curveIdx < cubicCount); | |
| nextCubicLane[0] = simd::load4f(p); | |
| nextCubicLane[4] = simd::load4f(p + 2); | |
| ++curveIdx; | |
| // Skip the [p2 p3] half after a block's last lane. | |
| nextCubicLane += (curveIdx % 4 != 0) ? 1 : 5; | |
| }; | |
| 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() || paint->getForceClosed(); | |
| Vec2D lastTangent = {0, 1}; | |
| Vec2D firstTangent = {0, 1}; | |
| size_t roundJoinCount = 0; | |
| size_t contourFirstCurveIdx = curveIdx; | |
| size_t contourFirstRotationIdx = rotationIdx; | |
| // Inlined so the counters it shares with the loop can stay in registers. | |
| auto finishAndAppendContour = [&](RawPath::Iter iter) RIVE_INLINE_LAMBDA { | |
| 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, // cuspJoinCount (see appendChoppedPieces()) | |
| 0, // extraCurveCount | |
| 0, // extraRotationCount | |
| 0, // strokeCapSegmentCount | |
| 0, // paddingVertexCount | |
| RIVE_DEBUG_CODE(0) // tessVertexCount | |
| }; | |
| contourFirstCurveIdx = curveIdx; | |
| contourFirstRotationIdx = 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() || paint->getForceClosed(); | |
| 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::findCubicTangents(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]; | |
| assert(curveIdx < maxPaddedCurves); | |
| assert(rotationIdx < maxPaddedRotations); | |
| if (isStroke()) | |
| { | |
| // Where this cubic's tangents go. processPieces() fills | |
| // them in after chopping, once the slot holds the cubic's | |
| // first piece. | |
| cubicBatch | |
| .rotationSlots[curveIdx] = math::lossless_numeric_cast<uint32_t>( | |
| rotationIdx); | |
| cubicBatch.rawPathCubicPoints[curveIdx] = p; | |
| } | |
| 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}}; | |
| } | |
| gatherCubic(p); | |
| ++rotationIdx; | |
| break; | |
| } | |
| case StyledVerb::filledCubic: | |
| { | |
| const Vec2D* p = iter.cubicPts(); | |
| ++preChopVerbCount; | |
| endpointsSum += p[3]; | |
| assert(curveIdx < maxPaddedCurves); | |
| gatherCubic(p); | |
| break; | |
| } | |
| } | |
| } | |
| if (startOfContour != end) | |
| { | |
| finishAndAppendContour(end); | |
| } | |
| assert(curveIdx == cubicCount); | |
| assert(contourIdx == contourCount); | |
| assert(contourCount > 0); | |
| assert(rotationIdx <= maxPaddedRotations); | |
| assert(isStrokeOrFeather() || maxPaddedRotations == 0); | |
| assert(isStrokeOrFeather() || rotationIdx == 0); | |
| // Fill the final block's unused lanes with copies of the last cubic, so | |
| // SIMD code never sees uninitialized data. | |
| if (size_t lastLane = cubicCount % 4; lastLane != 0) | |
| { | |
| float4* block = cubicBlocks + (cubicCount / 4) * 8; | |
| for (size_t lane = lastLane; lane < 4; ++lane) | |
| { | |
| block[lane] = block[lastLane - 1]; | |
| block[4 + lane] = block[4 + lastLane - 1]; | |
| } | |
| } | |
| // 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. (Feathered fills come here already chopped.) | |
| size_t chopCount = 0; | |
| if (isStroke() && cubicCount != 0) | |
| { | |
| chopCount = findCubicChops(&cubicBatch, cubicCount); | |
| // pushMidpointFanTessellationData() chops each cubic again from its | |
| // key and T values. | |
| memcpy( | |
| m_numChops.push_back_n(cubicCount), | |
| cubicBatch.chopKeys.data(), | |
| cubicCount); | |
| } | |
| else if (isStrokeOrFeather()) | |
| { | |
| memset(m_numChops.push_back_n(cubicCount), chopKey(false, 0), cubicCount); | |
| } | |
| m_firstExtraCurveIdx = curveIdx; | |
| m_firstExtraRotationIdx = rotationIdx; | |
| if (chopCount != 0) | |
| { | |
| rotationIdx += appendChoppedPieces( | |
| &cubicBatch, | |
| cubicCount, | |
| contourCount, | |
| rotationIdx); | |
| curveIdx += chopCount; | |
| } | |
| assert(curveIdx <= maxPaddedCurves); | |
| assert(rotationIdx <= maxPaddedRotations); | |
| // Count every curve's parametric segments and, for strokes, find its | |
| // tangents. | |
| uint32_t parametricSegmentTotal = processPieces( | |
| cubicBatch.points.data(), | |
| curveIdx, | |
| parametricPrecision, | |
| vectorXform, | |
| m_parametricSegmentCounts, | |
| isStroke() ? m_tangentPairs : nullptr, | |
| cubicBatch.rotationSlots.data()); | |
| if (chopCount != 0) | |
| { | |
| // Fill in the tangents for cusp joins that were added for cusp chops. | |
| // These just bridge the surrounding cubics' tangents. | |
| for (auto [joinIdx, beforeIdx, afterIdx] : cubicBatch.cuspJoins) | |
| { | |
| m_tangentPairs[joinIdx] = { | |
| m_tangentPairs[beforeIdx][1], | |
| m_tangentPairs[afterIdx][0]}; | |
| } | |
| } | |
| // Return any data we conservatively allocated but did not use. | |
| size_t paddedCurveCount = math::round_up_to_multiple_of<4>(curveIdx); | |
| size_t paddedRotationCount = math::round_up_to_multiple_of<4>(rotationIdx); | |
| if (isStrokeOrFeather()) | |
| { | |
| m_numChops.shrinkToFit(context->numChopsAllocator(), maxChops); | |
| m_chopVertices.shrinkToFit( | |
| context->chopVerticesAllocator(), | |
| maxChopVertices); | |
| context->tangentPairsAllocator().rewindLastAllocation( | |
| maxPaddedRotations - paddedRotationCount); | |
| context->polarSegmentCountsAllocator().rewindLastAllocation( | |
| maxPaddedRotations - paddedRotationCount); | |
| } | |
| context->parametricSegmentCountsAllocator().rewindLastAllocation( | |
| maxPaddedCurves - paddedCurveCount); | |
| if (isStrokeOrFeather()) | |
| { | |
| countPolarSegments( | |
| m_tangentPairs, | |
| rotationIdx, | |
| m_polarSegmentsPerRadian, | |
| m_polarSegmentCounts); | |
| } | |
| // Iteration pass 2: Count the tessellation vertices in each contour. | |
| size_t contourFirstLineIdx = 0; | |
| size_t tessVertexCount = 0; | |
| size_t extraCurveIdx = m_firstExtraCurveIdx; | |
| size_t extraRotationIdx = m_firstExtraRotationIdx; | |
| 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. | |
| if (contourCount == 1) | |
| { | |
| // A single contour has every curve. | |
| contourVertexCount += parametricSegmentTotal; | |
| } | |
| else | |
| { | |
| for (size_t c = contour->firstCurveIdx; c < contour->endCurveIdx; ++c) | |
| { | |
| contourVertexCount += m_parametricSegmentCounts[c]; | |
| } | |
| for (size_t end = extraCurveIdx + contour->extraCurveCount; | |
| extraCurveIdx < end; | |
| ++extraCurveIdx) | |
| { | |
| contourVertexCount += m_parametricSegmentCounts[extraCurveIdx]; | |
| } | |
| } | |
| if (isStrokeOrFeather()) | |
| { | |
| bool empty = contour->endLineIdx == contourFirstLineIdx && | |
| contour->endCurveIdx == contour->firstCurveIdx; | |
| contourVertexCount += countStrokeContourVertices( | |
| contour, | |
| empty, | |
| &extraRotationIdx, | |
| &emptyStrokeCountForCaps); | |
| } | |
| 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; | |
| } | |
| // 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 = curveIdx); | |
| RIVE_DEBUG_CODE(m_pendingRotationCount = rotationIdx); | |
| 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 + curveIdx + | |
| 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_combinedDrawContents, | |
| 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_combinedDrawContents, | |
| 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_combinedDrawContents, | |
| 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; | |
| } | |
| } | |
| if (m_hasHairlinePass) | |
| { | |
| // The hairline is always a subpass (never a prepass because the | |
| // AA makes it non-opaque). | |
| ++m_subpassCount; | |
| } | |
| } | |
| } | |
| const int passCount = m_prepassCount + m_subpassCount; | |
| assert(passCount <= MaxSubpassCount); | |
| std::fill_n(m_passDrawContents.begin(), passCount, m_combinedDrawContents); | |
| if (m_hasHairlinePass) | |
| { | |
| // The hairline is never opaque because it has AA. | |
| m_passDrawContents[passCount - 1] &= ~gpu::DrawContents::opaquePaint; | |
| } | |
| } | |
| gpu::DrawBatch* PathDraw::pushToRenderContext( | |
| RenderContext::LogicalFlush* flush, | |
| int signedSubpassIdx, | |
| uint32_t zIndex) | |
| { | |
| 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(!isEmpty()); | |
| assert( | |
| m_resourceCounts.outerCubicTessVertexCount == 0 || | |
| m_resourceCounts.midpointFanTessVertexCount == 0); | |
| uint32_t tessVertexCount = math::lossless_numeric_cast<uint32_t>( | |
| m_resourceCounts.outerCubicTessVertexCount | | |
| m_resourceCounts.midpointFanTessVertexCount); | |
| const gpu::DrawContents subpassDrawContents = this->subpassDrawContents( | |
| signedSubpassIdx); | |
| if (m_pathID == 0) | |
| { | |
| // Reserve our pathID and write out a path record. | |
| m_pathID = flush->pushPath(this, zIndex); | |
| } | |
| switch (m_coverageType) | |
| { | |
| case CoverageType::pixelLocalStorage: | |
| case CoverageType::clockwise: | |
| { | |
| const int mainSubpassIdx = (m_coverageType == CoverageType::clockwise && | |
| m_triangulator != nullptr) | |
| ? 1 | |
| : 0; | |
| if (signedSubpassIdx == 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 && | |
| signedSubpassIdx == 1) || | |
| (m_coverageType == CoverageType::clockwise && | |
| (signedSubpassIdx == 0 || signedSubpassIdx == 2))); | |
| gpu::DrawBatch* batch = flush->pushInteriorTriangulationDraw( | |
| this, | |
| subpassDrawContents, | |
| m_pathID, | |
| (m_coverageType == CoverageType::clockwise) | |
| // Clockwise mode renders counterclockwise (borrowed | |
| // coverage) interior triangles in a separate pass. | |
| ? (signedSubpassIdx == 0) ? gpu::WindingFaces::negative | |
| : gpu::WindingFaces::positive | |
| : gpu::WindingFaces::all, | |
| (signedSubpassIdx == 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 (signedSubpassIdx) | |
| { | |
| 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, | |
| subpassDrawContents, | |
| m_pathID, | |
| m_prepassCount == 0 ? gpu::WindingFaces::all | |
| : signedSubpassIdx < 0 ? gpu::WindingFaces::negative | |
| : gpu::WindingFaces::positive, | |
| signedSubpassIdx < 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: | |
| { | |
| // The fill passes are either all opaque prepasses or else all | |
| // subpasses. The hairline adds one extra subpass on top of either. | |
| assert(m_prepassCount == 0 || m_subpassCount == int(m_hasHairlinePass)); | |
| int fillPassCount = m_prepassCount + m_subpassCount - | |
| int(m_hasHairlinePass); | |
| int passIdx = signedSubpassIdx + m_prepassCount; | |
| if (passIdx == 0) | |
| { | |
| m_depthStencilTessLocation = allocateTessellationVertices( | |
| flush, | |
| tessVertexCount); | |
| pushTessellationData( | |
| flush, | |
| tessVertexCount, | |
| m_depthStencilTessLocation); | |
| } | |
| if (m_hasHairlinePass && passIdx == fillPassCount) | |
| { | |
| // Hairline pass: unlike polar strokes, hairlines have no polar | |
| // spokes, no join spokes, and tessellate identically to | |
| // midpointFans. | |
| assert(m_triangulator == nullptr); | |
| return &flush->pushMidpointFanDraw( | |
| this, | |
| gpu::DrawType::depthAAOuterHairline, | |
| subpassDrawContents, | |
| tessVertexCount, | |
| m_depthStencilTessLocation); | |
| } | |
| assert(1 <= fillPassCount && fillPassCount <= 3); | |
| assert(passIdx < fillPassCount); | |
| 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 (fillPassCount == 1) | |
| { | |
| if (enums::all_flags_set( | |
| subpassDrawContents, | |
| gpu::kNestedClipUpdateMask)) | |
| { | |
| outerCubicDrawType = gpu::DrawType::stencilOuterCubicWinding; | |
| } | |
| else | |
| { | |
| assert(flush->platformFeatures().supportsPipelineDynamicState); | |
| outerCubicDrawType = gpu::DrawType::stencilDynamicOuterCubics; | |
| } | |
| } | |
| else | |
| { | |
| constexpr static gpu::DrawType OuterCubicFillTypes[][3] = { | |
| // Slow path (fillPassCount == 2): stencil-then-cover. | |
| { | |
| gpu::DrawType::stencilOuterCubicWinding, | |
| gpu::DrawType::stencilOuterCubicCover, | |
| }, | |
| // Fast path (fillPassCount == 3). | |
| { | |
| gpu::DrawType::stencilOuterCubicBorrowedCoverage, | |
| gpu::DrawType::stencilOuterCubics, | |
| gpu::DrawType::stencilOuterCubicReset, | |
| }, | |
| }; | |
| outerCubicDrawType = OuterCubicFillTypes[fillPassCount - 2][passIdx]; | |
| } | |
| return &flush->pushOuterCubicsDraw( | |
| this, | |
| outerCubicDrawType, | |
| subpassDrawContents, | |
| tessVertexCount, | |
| m_depthStencilTessLocation); | |
| } | |
| else | |
| { | |
| gpu::DrawType depthStencilDrawType; | |
| if (fillPassCount == 1) | |
| { | |
| if (isStroke()) | |
| { | |
| depthStencilDrawType = flush->frameDescriptor().inkbleedOverride | |
| ? gpu::DrawType::depthAAStrokes | |
| : gpu::DrawType::depthStrokes; | |
| } | |
| else if (enums::all_flags_set( | |
| subpassDrawContents, | |
| gpu::kNestedClipUpdateMask)) | |
| { | |
| depthStencilDrawType = gpu::DrawType::stencilMidpointFanWinding; | |
| } | |
| else | |
| { | |
| assert(flush->platformFeatures().supportsPipelineDynamicState); | |
| depthStencilDrawType = gpu::DrawType::stencilDynamicMidpointFans; | |
| } | |
| } | |
| else | |
| { | |
| constexpr static gpu::DrawType MidpointFanFillTypes[][3] = { | |
| // Slow path (fillPassCount == 2): stencil-then-cover | |
| { | |
| gpu::DrawType::stencilMidpointFanWinding, | |
| gpu::DrawType::stencilMidpointFanCover, | |
| }, | |
| // Fast path (fillPassCount == 3): (mostly) single pass | |
| // rendering. | |
| { | |
| gpu::DrawType::stencilMidpointFanBorrowedCoverage, | |
| gpu::DrawType::stencilMidpointFans, | |
| gpu::DrawType::stencilMidpointFanReset, | |
| }, | |
| }; | |
| depthStencilDrawType = MidpointFanFillTypes[fillPassCount - 2] | |
| [passIdx]; | |
| } | |
| return &flush->pushMidpointFanDraw( | |
| this, | |
| depthStencilDrawType, | |
| subpassDrawContents, | |
| 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(signedSubpassIdx == 0); | |
| return &flush->pushFeatherAtlasBlit(this, subpassDrawContents, 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, | |
| m_combinedDrawContents, | |
| tessVertexCount, | |
| tessLocation, | |
| shaderMiscFlags); | |
| } | |
| else | |
| { | |
| return flush->pushMidpointFanDraw( | |
| this, | |
| isFeatheredFill() ? gpu::DrawType::midpointFanCenterAAPatches | |
| : gpu::DrawType::midpointFanPatches, | |
| m_combinedDrawContents, | |
| 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(!isEmpty()); | |
| assert( | |
| m_resourceCounts.outerCubicTessVertexCount == 0 || | |
| m_resourceCounts.midpointFanTessVertexCount == 0); | |
| *tessVertexCount = math::lossless_numeric_cast<uint32_t>( | |
| m_resourceCounts.outerCubicTessVertexCount | | |
| m_resourceCounts.midpointFanTessVertexCount); | |
| *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(); | |
| // The second and later pieces of chopped cubics, and their cusp joins, come | |
| // after all the path's other curves and rotations, in order. | |
| size_t extraCurveIdx = m_firstExtraCurveIdx; | |
| size_t extraRotationIdx = m_firstExtraRotationIdx; | |
| 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; | |
| RIVE_DEBUG_CODE(size_t contourFirstExtraCurveIdx = extraCurveIdx;) | |
| RIVE_DEBUG_CODE(size_t contourFirstExtraRotationIdx = extraRotationIdx;) | |
| 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 fixedJoinSegmentCount = 0; | |
| if (isFeatheredFill()) | |
| { | |
| fixedJoinSegmentCount = feather_join_segment_count( | |
| m_polarSegmentsPerRadian); | |
| } | |
| else if (isStroke() && !roundJoinStroked) | |
| { | |
| fixedJoinSegmentCount = numSegmentsInFixedJoin( | |
| m_coverageType, | |
| m_strokeJoin); | |
| } | |
| // 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 = fixedJoinSegmentCount; | |
| 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 = chopKey >> 1; | |
| ChopBuffer localChopBuffer; | |
| if (numChops != 0) | |
| { | |
| auto [t0, t1] = m_chopVertices.pop_front(); | |
| const float t[2] = {t0, t1}; | |
| chopCubic(p, chopKey, t, &localChopBuffer); | |
| p = localChopBuffer.data(); | |
| } | |
| 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. The first piece is in | |
| // the cubic's own slots. | |
| const bool areChopsCusps = (chopKey & 1) != 0; | |
| size_t pieceCurveIdx = curveIdx++; | |
| size_t pieceRotationIdx = rotationIdx++; | |
| for (uint8_t piece = 0; piece < numChops; ++piece, p += 3) | |
| { | |
| uint32_t parametricSegmentCount = m_parametricSegmentCounts | |
| [pieceCurveIdx]; | |
| uint32_t polarSegmentCount = m_polarSegmentCounts[pieceRotationIdx]; | |
| RIVE_DEBUG_CODE(--m_pendingRotationCount;) | |
| Vec2D chopJoinTangent; | |
| uint32_t chopJoinSegmentCount; | |
| uint32_t chopJoinTypeFlags; | |
| if (areChopsCusps) | |
| { | |
| // A cusp turns 180 degrees which gets drawn with a | |
| // cusp join. | |
| size_t cuspJoinIdx = extraRotationIdx++; | |
| chopJoinTangent = m_tangentPairs[cuspJoinIdx][1]; | |
| chopJoinSegmentCount = m_polarSegmentCounts[cuspJoinIdx]; | |
| chopJoinTypeFlags = CUSP_JOIN_CONTOUR_FLAG; | |
| RIVE_DEBUG_CODE(--m_pendingRotationCount;) | |
| } | |
| else | |
| { | |
| // A chop at an inflection or at 180 degrees | |
| // continues smoothly, so it needs no join. | |
| chopJoinTangent = joinTangent; | |
| chopJoinSegmentCount = 1; | |
| chopJoinTypeFlags = joinTypeFlags; | |
| } | |
| tessWriter->pushCubic( | |
| p, | |
| m_contourDirections, | |
| chopJoinTangent, | |
| parametricSegmentCount, | |
| polarSegmentCount, | |
| chopJoinSegmentCount, | |
| contourIDWithFlags | chopJoinTypeFlags); | |
| RIVE_DEBUG_CODE(--m_pendingCurveCount;) | |
| pieceCurveIdx = extraCurveIdx++; | |
| pieceRotationIdx = extraRotationIdx++; | |
| } | |
| // Push the final chop, with a join. | |
| uint32_t | |
| parametricSegmentCount = m_parametricSegmentCounts[pieceCurveIdx]; | |
| uint32_t polarSegmentCount = m_polarSegmentCounts[pieceRotationIdx]; | |
| 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 = fixedJoinSegmentCount; | |
| } | |
| 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 = fixedJoinSegmentCount; | |
| 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( | |
| extraCurveIdx - contourFirstExtraCurveIdx == contour.extraCurveCount); | |
| assert( | |
| extraRotationIdx - contourFirstExtraRotationIdx == | |
| contour.extraRotationCount); | |
| 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::findCubicTan0(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, | |
| /*bowtieSegmentCount=*/1, | |
| 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, | |
| /*bowtieSegmentCount=*/1, | |
| 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, | |
| /*bowtieSegmentCount=*/1, | |
| 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::chopCubicAt(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, | |
| /*bowtieSegmentCount=*/1, | |
| 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, | |
| /*bowtieSegmentCount=*/1, | |
| 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 * OuterCubicPatchSegmentSpanPlusBowtie * 2 | |
| : patchCount * OuterCubicPatchSegmentSpanPlusBowtie; | |
| 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 * OuterCubicPatchSegmentSpanPlusBowtie * 2 == | |
| m_resourceCounts.outerCubicTessVertexCount || | |
| patchCount * OuterCubicPatchSegmentSpanPlusBowtie == | |
| m_resourceCounts.outerCubicTessVertexCount); | |
| } | |
| } | |
| ImageRectDraw::ImageRectDraw( | |
| RenderContext* context, | |
| IAABB pixelBounds, | |
| const Mat2D& matrix, | |
| BlendMode blendMode, | |
| float additiveness, | |
| rcp<Texture> imageTexture, | |
| rcp<const Gradient> gradient, | |
| const ImageSampler imageSampler, | |
| ColorInt modulatedColor, | |
| const Mat2D& imageMatrix, | |
| const Mat2D& gradientMatrix, | |
| const Mat2D& inverseGradientTransform) | |
| : Draw( | |
| pixelBounds, | |
| matrix, | |
| &imageMatrix, | |
| blendMode, | |
| additiveness, | |
| std::move(imageTexture), | |
| imageSampler, | |
| Type::imageRect) | |
| , m_modulatedColor(modulatedColor) | |
| , m_gradientMatrix(gradientMatrix) | |
| , m_inverseGradientTransform(inverseGradientTransform) | |
| , m_gradientRef(gradient.release()) | |
| { | |
| // 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.imageRectCount = 1; | |
| } | |
| bool ImageRectDraw::allocateResources(RenderContext::LogicalFlush* flush) | |
| { | |
| if (!Draw::allocateResources(flush)) | |
| { | |
| return false; | |
| } | |
| if (m_gradientRef != nullptr) | |
| { | |
| if (!flush->allocateGradient(m_gradientRef, &m_rampLocation)) | |
| { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| void ImageRectDraw::releaseRefs() | |
| { | |
| Draw::releaseRefs(); | |
| safe_unref(m_gradientRef); | |
| } | |
| gpu::DrawBatch* ImageRectDraw::pushToRenderContext( | |
| RenderContext::LogicalFlush* flush, | |
| int signedSubpassIdx, | |
| uint32_t zIndex) | |
| { | |
| assert(signedSubpassIdx == 0); | |
| return &flush->pushImageRectDraw(this, subpassDrawContents(0), zIndex); | |
| } | |
| ImageMeshDrawBase::ImageMeshDrawBase( | |
| IAABB pixelBounds, | |
| const Mat2D& matrix, | |
| BlendMode blendMode, | |
| float additiveness, | |
| rcp<Texture> imageTexture, | |
| const ImageSampler imageSampler, | |
| rcp<RenderBuffer> vertexBuffer, | |
| rcp<RenderBuffer> uvBuffer, | |
| rcp<RenderBuffer> indexBuffer, | |
| uint32_t indexCount) | |
| : Draw( | |
| pixelBounds, | |
| matrix, | |
| nullptr, | |
| blendMode, | |
| additiveness, | |
| std::move(imageTexture), | |
| imageSampler, | |
| Type::imageMesh) | |
| , m_vertexBufferRef(vertexBuffer.release()) | |
| , m_uvBufferRef(uvBuffer.release()) | |
| , m_indexBufferRef(indexBuffer.release()) | |
| , m_indexCount(indexCount) | |
| { | |
| assert(m_vertexBufferRef != nullptr); | |
| assert(m_uvBufferRef != nullptr); | |
| assert(m_indexBufferRef != nullptr); | |
| } | |
| void ImageMeshDrawBase::releaseRefs() | |
| { | |
| Draw::releaseRefs(); | |
| m_vertexBufferRef->unref(); | |
| m_uvBufferRef->unref(); | |
| m_indexBufferRef->unref(); | |
| } | |
| ImageMeshDraw::ImageMeshDraw( | |
| IAABB pixelBounds, | |
| const Mat2D& matrix, | |
| BlendMode blendMode, | |
| float additiveness, | |
| rcp<Texture> imageTexture, | |
| const ImageSampler imageSampler, | |
| rcp<RenderBuffer> vertexBuffer, | |
| rcp<RenderBuffer> uvBuffer, | |
| rcp<RenderBuffer> indexBuffer, | |
| uint32_t indexCount, | |
| ColorInt modulatedColor, | |
| const Vec2D& uvTranslate, | |
| const Vec2D& uvScale) | |
| : ImageMeshDrawBase( | |
| pixelBounds, | |
| matrix, | |
| blendMode, | |
| additiveness, | |
| std::move(imageTexture), | |
| imageSampler, | |
| std::move(vertexBuffer), | |
| std::move(uvBuffer), | |
| std::move(indexBuffer), | |
| indexCount) | |
| , m_modulatedColor(modulatedColor) | |
| , m_uvTranslate(uvTranslate) | |
| , m_uvScale(uvScale) | |
| { | |
| m_resourceCounts.imageMeshCount = 1; | |
| } | |
| gpu::DrawBatch* ImageMeshDraw::pushToRenderContext( | |
| RenderContext::LogicalFlush* flush, | |
| int signedSubpassIdx, | |
| uint32_t zIndex) | |
| { | |
| assert(signedSubpassIdx == 0); | |
| return &flush->pushImageMeshDraw(this, subpassDrawContents(0), zIndex); | |
| } | |
| ImageMeshInstancedDraw::ImageMeshInstancedDraw( | |
| IAABB pixelBounds, | |
| const Mat2D& matrix, | |
| rcp<Texture> imageTexture, | |
| const ImageSampler imageSampler, | |
| rcp<RenderBuffer> vertexBuffer, | |
| rcp<RenderBuffer> uvBuffer, | |
| rcp<RenderBuffer> indexBuffer, | |
| uint32_t indexCount, | |
| rcp<ImageMeshInstances> instances, | |
| ColorInt modulatedColor, | |
| float modulatedOpacity) | |
| : ImageMeshDrawBase( | |
| pixelBounds, | |
| matrix, | |
| BlendMode::srcOver, | |
| 0.0f, // Additiveness is per instance here. | |
| std::move(imageTexture), | |
| imageSampler, | |
| std::move(vertexBuffer), | |
| std::move(uvBuffer), | |
| std::move(indexBuffer), | |
| indexCount) | |
| , m_instancesRef(instances.release()) | |
| , m_modulatedColor(modulatedColor) | |
| , m_modulatedOpacity(modulatedOpacity) | |
| { | |
| assert(m_instancesRef != nullptr); | |
| assert(m_instancesRef->count() > 0); | |
| assert(!m_instancesRef->isEditing()); | |
| m_resourceCounts.imageMeshCount = m_instancesRef->count(); | |
| RIVE_DEBUG_CODE(m_editCount = m_instancesRef->editCount();) | |
| } | |
| gpu::DrawBatch* ImageMeshInstancedDraw::pushToRenderContext( | |
| RenderContext::LogicalFlush* flush, | |
| int signedSubpassIdx, | |
| uint32_t zIndex) | |
| { | |
| assert(signedSubpassIdx == 0); | |
| return &flush->pushImageMeshInstancedDraw( | |
| this, | |
| subpassDrawContents(0), | |
| zIndex); | |
| } | |
| void ImageMeshInstancedDraw::releaseRefs() | |
| { | |
| ImageMeshDrawBase::releaseRefs(); | |
| m_instancesRef->unref(); | |
| } | |
| ClipReset::ClipReset( | |
| RenderContext* context, | |
| uint32_t previousClipID, | |
| gpu::DrawContents previousClipDrawContents, | |
| ResetAction resetAction) | |
| : Draw( | |
| context->getClipContentBounds(previousClipID), | |
| Mat2D{}, | |
| nullptr, | |
| BlendMode::srcOver, | |
| /*additiveness =*/0, | |
| 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_combinedDrawContents |= previousClipDrawContents & FILL_RULE_FLAGS; | |
| switch (resetAction) | |
| { | |
| case ResetAction::intersectPreviousClip: | |
| m_combinedDrawContents |= gpu::DrawContents::activeClip; | |
| [[fallthrough]]; | |
| case ResetAction::clearPreviousClip: | |
| m_combinedDrawContents |= gpu::DrawContents::clipUpdate; | |
| break; | |
| } | |
| m_resourceCounts.maxTriangleVertexCount = 6; | |
| } | |
| gpu::DrawBatch* ClipReset::pushToRenderContext( | |
| RenderContext::LogicalFlush* flush, | |
| int signedSubpassIdx, | |
| uint32_t zIndex) | |
| { | |
| assert(signedSubpassIdx == 0); | |
| return &flush->pushClipResetDraw(this, subpassDrawContents(0), zIndex); | |
| } | |
| } // namespace rive::gpu |