| /* |
| * Copyright 2023 Rive |
| */ |
| |
| #include "rive/pls/pls_draw.hpp" |
| |
| #include "gr_inner_fan_triangulator.hpp" |
| #include "path_utils.hpp" |
| #include "pls_path.hpp" |
| #include "pls_paint.hpp" |
| #include "rive/math/wangs_formula.hpp" |
| #include "rive/pls/pls_image.hpp" |
| #include "shaders/constants.glsl" |
| |
| namespace rive::pls |
| { |
| namespace |
| { |
| constexpr static int kNumSegmentsInMiterOrBevelJoin = 5; |
| constexpr static int kStrokeStyleFlag = 8; |
| constexpr static int kRoundJoinStyleFlag = kStrokeStyleFlag << 1; |
| RIVE_ALWAYS_INLINE constexpr int style_flags(bool isStroked, bool roundJoinStroked) |
| { |
| int styleFlags = (isStroked << 3) | (roundJoinStroked << 4); |
| assert(bool(styleFlags & kStrokeStyleFlag) == isStroked); |
| assert(bool(styleFlags & kRoundJoinStyleFlag) == roundJoinStroked); |
| return styleFlags; |
| } |
| |
| // Switching on a StyledVerb reduces "if (stroked)" branching and makes the code cleaner. |
| enum class StyledVerb |
| { |
| filledMove = static_cast<int>(PathVerb::move), |
| strokedMove = kStrokeStyleFlag | static_cast<int>(PathVerb::move), |
| roundJoinStrokedMove = |
| kStrokeStyleFlag | kRoundJoinStyleFlag | static_cast<int>(PathVerb::move), |
| |
| filledLine = static_cast<int>(PathVerb::line), |
| strokedLine = kStrokeStyleFlag | static_cast<int>(PathVerb::line), |
| roundJoinStrokedLine = |
| kStrokeStyleFlag | kRoundJoinStyleFlag | static_cast<int>(PathVerb::line), |
| |
| filledQuad = static_cast<int>(PathVerb::quad), |
| strokedQuad = kStrokeStyleFlag | static_cast<int>(PathVerb::quad), |
| roundJoinStrokedQuad = |
| kStrokeStyleFlag | kRoundJoinStyleFlag | static_cast<int>(PathVerb::quad), |
| |
| filledCubic = static_cast<int>(PathVerb::cubic), |
| strokedCubic = kStrokeStyleFlag | static_cast<int>(PathVerb::cubic), |
| roundJoinStrokedCubic = |
| kStrokeStyleFlag | kRoundJoinStyleFlag | static_cast<int>(PathVerb::cubic), |
| |
| filledClose = static_cast<int>(PathVerb::close), |
| strokedClose = kStrokeStyleFlag | static_cast<int>(PathVerb::close), |
| roundJoinStrokedClose = |
| kStrokeStyleFlag | kRoundJoinStyleFlag | static_cast<int>(PathVerb::close), |
| }; |
| RIVE_ALWAYS_INLINE constexpr StyledVerb styled_verb(PathVerb verb, int styleFlags) |
| { |
| return static_cast<StyledVerb>(styleFlags | static_cast<int>(verb)); |
| } |
| |
| // When chopping strokes, switching on a "chop_key" reduces "if (areCusps)" branching and makes the |
| // code cleaner. |
| RIVE_ALWAYS_INLINE constexpr uint8_t chop_key(bool areCusps, uint8_t numChops) |
| { |
| return (numChops << 1) | static_cast<uint8_t>(areCusps); |
| } |
| RIVE_ALWAYS_INLINE constexpr uint8_t cusp_chop_key(uint8_t n) { return chop_key(true, n); } |
| RIVE_ALWAYS_INLINE constexpr uint8_t simple_chop_key(uint8_t n) { return chop_key(false, n); } |
| |
| // Produces a cubic equivalent to the given line, for which Wang's formula also returns 1. |
| RIVE_ALWAYS_INLINE std::array<Vec2D, 4> convert_line_to_cubic(const Vec2D line[2]) |
| { |
| float4 endPts = simd::load4f(line); |
| float4 controlPts = simd::mix(endPts, endPts.zwxy, float4(1 / 3.f)); |
| std::array<Vec2D, 4> cubic; |
| cubic[0] = line[0]; |
| simd::store(&cubic[1], controlPts); |
| cubic[3] = line[1]; |
| return cubic; |
| } |
| RIVE_ALWAYS_INLINE std::array<Vec2D, 4> convert_line_to_cubic(Vec2D p0, Vec2D p1) |
| { |
| Vec2D line[2] = {p0, p1}; |
| return convert_line_to_cubic(line); |
| } |
| |
| // Finds the tangents of the curve at T=0 and T=1 respectively. |
| RIVE_ALWAYS_INLINE Vec2D find_cubic_tan0(const Vec2D p[4]) |
| { |
| Vec2D tan0 = (p[0] != p[1] ? p[1] : p[1] != p[2] ? p[2] : p[3]) - p[0]; |
| return tan0; |
| } |
| RIVE_ALWAYS_INLINE Vec2D find_cubic_tan1(const Vec2D p[4]) |
| { |
| Vec2D tan1 = p[3] - (p[3] != p[2] ? p[2] : p[2] != p[1] ? p[1] : p[0]); |
| return tan1; |
| } |
| RIVE_ALWAYS_INLINE void find_cubic_tangents(const Vec2D p[4], Vec2D tangents[2]) |
| { |
| tangents[0] = find_cubic_tan0(p); |
| tangents[1] = find_cubic_tan1(p); |
| } |
| |
| // Chops a cubic into 2 * n + 1 segments, surrounding each cusp. The resulting cubics will be |
| // visually equivalent to the original when stroked, but the cusp won't have artifacts when rendered |
| // using the parametric/polar sorting algorithm. |
| // |
| // The size of dst[] must be 6 * n + 4 Vec2Ds. |
| static void chop_cubic_around_cusps(const Vec2D p[4], |
| Vec2D dst[/*6 * n + 4*/], |
| const float cuspT[], |
| int n, |
| float matrixMaxScale) |
| { |
| float t[4]; |
| assert(n * 2 <= std::size(t)); |
| // Generate chop points straddling each cusp with padding. This creates buffer space around the |
| // cusp that protects against fp32 precision issues. |
| for (int i = 0; i < n; ++i) |
| { |
| // If the cusps are extremely close together, don't allow the straddle points to cross. |
| float minT = i == 0 ? 0.f : (cuspT[i - 1] + cuspT[i]) * .5f; |
| float maxT = i + 1 == n ? 1.f : (cuspT[i + 1] + cuspT[i]) * .5f; |
| t[i * 2 + 0] = std::max(cuspT[i] - math::EPSILON, minT); |
| t[i * 2 + 1] = std::min(cuspT[i] + math::EPSILON, maxT); |
| } |
| pathutils::ChopCubicAt(p, dst, t, n * 2); |
| for (int i = 0; i < n; ++i) |
| { |
| // Find the three chops at this cusp. |
| Vec2D* chops = dst + i * 6; |
| // Correct the chops to fall on the actual cusp point. |
| Vec2D cusp = pathutils::EvalCubicAt(p, cuspT[i]); |
| chops[3] = chops[6] = cusp; |
| // The only purpose of the middle cubic is to capture the cusp's 180-degree rotation. |
| // Implement it as a sub-pixel 180-degree pivot. |
| Vec2D pivot = (chops[2] + chops[7]) * .5f; |
| pivot = (cusp - pivot).normalized() / (matrixMaxScale * kPolarPrecision * 2) + cusp; |
| chops[4] = chops[5] = pivot; |
| } |
| } |
| |
| // Finds the starting tangent in a contour composed of the points [pts, end). If all points are |
| // equal, generates a tangent pointing horizontally to the right. |
| static Vec2D find_starting_tangent(const Vec2D pts[], const Vec2D* end) |
| { |
| assert(end > pts); |
| const Vec2D p0 = pts[0]; |
| while (++pts < end) |
| { |
| Vec2D p = *pts; |
| if (p != p0) |
| { |
| return p - p0; |
| } |
| } |
| return {1, 0}; |
| } |
| |
| // Finds the ending tangent in a contour composed of the points [pts, end). If all points are equal, |
| // generates a tangent pointing horizontally to the left. |
| static Vec2D find_ending_tangent(const Vec2D pts[], const Vec2D* end) |
| { |
| assert(end > pts); |
| const Vec2D endpoint = end[-1]; |
| while (--end > pts) |
| { |
| Vec2D p = end[-1]; |
| if (p != endpoint) |
| { |
| return endpoint - p; |
| } |
| } |
| return {-1, 0}; |
| } |
| |
| static Vec2D find_join_tangent_full_impl(const Vec2D* joinPoint, |
| const Vec2D* end, |
| bool closed, |
| const Vec2D* p0) |
| { |
| // Find the first point in the contour not equal to *joinPoint and return the difference. |
| // RawPath should have discarded empty verbs, so this should be a fast operation. |
| for (const Vec2D* p = joinPoint + 1; p != end; ++p) |
| { |
| if (*p != *joinPoint) |
| { |
| return *p - *joinPoint; |
| } |
| } |
| if (closed) |
| { |
| for (const Vec2D* p = p0; p != joinPoint; ++p) |
| { |
| if (*p != *joinPoint) |
| { |
| return *p - *joinPoint; |
| } |
| } |
| } |
| // This should never be reached because RawPath discards empty verbs. |
| RIVE_UNREACHABLE(); |
| } |
| |
| RIVE_ALWAYS_INLINE Vec2D find_join_tangent(const Vec2D* joinPoint, |
| const Vec2D* end, |
| bool closed, |
| const Vec2D* p0) |
| { |
| // Quick early out for inlining and branch prediction: The next point in the contour is almost |
| // always the point that determines the join tangent. |
| const Vec2D* nextPoint = joinPoint + 1; |
| nextPoint = nextPoint != end ? nextPoint : p0; |
| Vec2D tangent = *nextPoint - *joinPoint; |
| return tangent != Vec2D{0, 0} ? tangent |
| : find_join_tangent_full_impl(joinPoint, end, closed, p0); |
| } |
| |
| // Should an empty stroke emit round caps, square caps, or none? |
| // |
| // Just pick the cap type that makes the most sense for a contour that animates from non-empty to |
| // empty: |
| // |
| // * A non-closed contour with round caps and a CLOSED contour with round JOINS both converge to a |
| // circle when animated to empty. |
| // => round caps on the empty contour. |
| // |
| // * A non-closed contour with square caps converges to a square (albeit with potential rotation |
| // that is lost when the contour becomes empty). |
| // => square caps on the empty contour. |
| // |
| // * A closed contour with miter JOINS converges to... some sort of polygon with pointy corners. |
| // ~=> square caps on the empty contour. |
| // |
| // * All other contours converge to nothing. |
| // => butt caps on the empty contour, which are ignored. |
| // |
| static StrokeCap empty_stroke_cap(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 0; |
| case StrokeJoin::bevel: |
| return BEVEL_JOIN_CONTOUR_FLAG; |
| } |
| RIVE_UNREACHABLE(); |
| } |
| } // namespace |
| |
| PLSDraw::PLSDraw(IAABB pixelBounds, |
| const Mat2D& matrix, |
| BlendMode blendMode, |
| rcp<const PLSTexture> imageTexture, |
| Type type) : |
| m_imageTextureRef(imageTexture.release()), |
| m_pixelBounds(pixelBounds), |
| m_matrix(matrix), |
| m_blendMode(blendMode), |
| m_type(type) |
| { |
| if (m_blendMode != BlendMode::srcOver) |
| { |
| m_drawContents |= pls::DrawContents::advancedBlend; |
| } |
| } |
| |
| void PLSDraw::setClipID(uint32_t clipID) |
| { |
| m_clipID = clipID; |
| |
| // For clipUpdates, m_clipID refers to the ID we are writing to the stencil buffer (NOT the ID |
| // we are clipping against). It therefore doesn't affect the activeClip flag in that case. |
| if (!(m_drawContents & pls::DrawContents::clipUpdate)) |
| { |
| if (m_clipID != 0) |
| { |
| m_drawContents |= pls::DrawContents::activeClip; |
| } |
| else |
| { |
| m_drawContents &= ~pls::DrawContents::activeClip; |
| } |
| } |
| } |
| |
| bool PLSDraw::allocateGradientIfNeeded(PLSRenderContext::LogicalFlush* flush, |
| ResourceCounters* counters) |
| { |
| return m_gradientRef == nullptr || |
| flush->allocateGradient(m_gradientRef, counters, &m_simplePaintValue.colorRampLocation); |
| } |
| |
| void PLSDraw::releaseRefs() |
| { |
| safe_unref(m_imageTextureRef); |
| safe_unref(m_gradientRef); |
| } |
| |
| PLSDrawUniquePtr PLSPathDraw::Make(PLSRenderContext* context, |
| const Mat2D& matrix, |
| rcp<const PLSPath> path, |
| FillRule fillRule, |
| const PLSPaint* paint, |
| RawPath* scratchPath) |
| { |
| assert(path != nullptr); |
| assert(paint != nullptr); |
| AABB mappedBounds; |
| if (context->frameInterlockMode() == pls::InterlockMode::atomics) |
| { |
| // In atomic mode, find a tighter bounding box in order to maximize reordering. |
| mappedBounds = matrix.mapBoundingBox(path->getRawPath().points().data(), |
| path->getRawPath().points().count()); |
| } |
| else |
| { |
| // Otherwise we can get away with just mapping the path's bounding box. |
| mappedBounds = matrix.mapBoundingBox(path->getBounds()); |
| } |
| assert(mappedBounds.width() >= 0); |
| assert(mappedBounds.height() >= 0); |
| if (paint->getIsStroked()) |
| { |
| // Outset the path's bounding box to account for stroking. |
| float strokeOutset = paint->getThickness() * .5f; |
| if (paint->getJoin() == StrokeJoin::miter) |
| { |
| strokeOutset *= 4; |
| } |
| else if (paint->getCap() == StrokeCap::square) |
| { |
| strokeOutset *= math::SQRT2; |
| } |
| AABB strokePixelOutset = matrix.mapBoundingBox({0, 0, strokeOutset, strokeOutset}); |
| mappedBounds = mappedBounds.inset(-strokePixelOutset.width(), -strokePixelOutset.height()); |
| } |
| IAABB pixelBounds = mappedBounds.roundOut(); |
| if (!paint->getIsStroked()) |
| { |
| // Use interior triangulation to draw filled paths if they're large enough to benefit from |
| // it. |
| const AABB& localBounds = path->getBounds(); |
| // FIXME! Implement interior triangulation in depthStencil mode. |
| if (context->frameInterlockMode() != pls::InterlockMode::depthStencil && |
| pls::FindTransformedArea(localBounds, matrix) > 512 * 512) |
| { |
| return PLSDrawUniquePtr(context->make<InteriorTriangulationDraw>( |
| context, |
| pixelBounds, |
| matrix, |
| std::move(path), |
| fillRule, |
| paint, |
| scratchPath, |
| localBounds.width() > localBounds.height() |
| ? InteriorTriangulationDraw::TriangulatorAxis::horizontal |
| : InteriorTriangulationDraw::TriangulatorAxis::vertical)); |
| } |
| } |
| return PLSDrawUniquePtr(context->make<MidpointFanPathDraw>(context, |
| pixelBounds, |
| matrix, |
| std::move(path), |
| fillRule, |
| paint)); |
| } |
| |
| PLSPathDraw::PLSPathDraw(IAABB pixelBounds, |
| const Mat2D& matrix, |
| rcp<const PLSPath> path, |
| FillRule fillRule, |
| const PLSPaint* paint, |
| Type type, |
| pls::InterlockMode frameInterlockMode) : |
| PLSDraw(pixelBounds, matrix, paint->getBlendMode(), ref_rcp(paint->getImageTexture()), type), |
| m_pathRef(path.release()), |
| m_fillRule(paint->getIsStroked() ? FillRule::nonZero : fillRule), |
| m_paintType(paint->getType()) |
| { |
| assert(m_pathRef != nullptr); |
| assert(paint != nullptr); |
| if (m_blendMode == BlendMode::srcOver && paint->getIsOpaque()) |
| { |
| m_drawContents |= pls::DrawContents::opaquePaint; |
| } |
| if (paint->getIsStroked()) |
| { |
| m_drawContents |= pls::DrawContents::stroke; |
| m_strokeRadius = paint->getThickness() * .5f; |
| // Ensure stroke radius is nonzero. (In PLS, zero radius means the path is filled.) |
| m_strokeRadius = std::max(m_strokeRadius, std::numeric_limits<float>::min()); |
| assert(!std::isnan(m_strokeRadius)); // These should get culled in PLSRenderer::drawPath(). |
| assert(m_strokeRadius > 0); |
| } |
| else if (m_fillRule == FillRule::evenOdd) |
| { |
| m_drawContents |= pls::DrawContents::evenOddFill; |
| } |
| if (paint->getType() == pls::PaintType::clipUpdate) |
| { |
| m_drawContents |= pls::DrawContents::clipUpdate; |
| if (paint->getSimpleValue().outerClipID != 0) |
| { |
| m_drawContents |= pls::DrawContents::activeClip; |
| } |
| } |
| |
| if (isStroked()) |
| { |
| // Stroke triangles are always forward. |
| m_contourDirections = pls::ContourDirections::forward; |
| } |
| else if (frameInterlockMode != pls::InterlockMode::depthStencil) |
| { |
| // atomic and rasterOrdering fills need reverse AND forward triangles. |
| m_contourDirections = pls::ContourDirections::reverseAndForward; |
| } |
| else if (m_fillRule != FillRule::evenOdd) |
| { |
| // 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. |
| float matrixDeterminant = matrix[0] * matrix[3] - matrix[2] * matrix[1]; |
| m_contourDirections = m_pathRef->getCoarseArea() * matrixDeterminant >= 0 |
| ? pls::ContourDirections::forward |
| : pls::ContourDirections::reverse; |
| } |
| else |
| { |
| // "evenOdd" depthStencil fils just get drawn twice, so any direction is fine. |
| m_contourDirections = pls::ContourDirections::forward; |
| } |
| |
| m_simplePaintValue = paint->getSimpleValue(); |
| m_gradientRef = safe_ref(paint->getGradient()); |
| RIVE_DEBUG_CODE(m_pathRef->lockRawPathMutations();) |
| RIVE_DEBUG_CODE(m_rawPathMutationID = m_pathRef->getRawPathMutationID();) |
| assert(isStroked() == (strokeRadius() > 0)); |
| } |
| |
| void PLSPathDraw::pushToRenderContext(PLSRenderContext::LogicalFlush* flush) |
| { |
| // Make sure the rawPath in our path reference hasn't changed since we began holding! |
| assert(m_rawPathMutationID == m_pathRef->getRawPathMutationID()); |
| |
| size_t tessVertexCount = m_type == Type::midpointFanPath |
| ? m_resourceCounts.midpointFanTessVertexCount |
| : m_resourceCounts.outerCubicTessVertexCount; |
| if (tessVertexCount == 0) |
| { |
| return; |
| } |
| assert(!m_pathRef->getRawPath().empty()); |
| |
| // Push a path record. |
| flush->pushPath(this, |
| m_type == Type::midpointFanPath ? PatchType::midpointFan |
| : PatchType::outerCurves, |
| tessVertexCount); |
| |
| onPushToRenderContext(flush); |
| } |
| |
| void PLSPathDraw::releaseRefs() |
| { |
| PLSDraw::releaseRefs(); |
| RIVE_DEBUG_CODE(m_pathRef->unlockRawPathMutations();) |
| m_pathRef->unref(); |
| } |
| |
| MidpointFanPathDraw::MidpointFanPathDraw(PLSRenderContext* context, |
| IAABB pixelBounds, |
| const Mat2D& matrix, |
| rcp<const PLSPath> path, |
| FillRule fillRule, |
| const PLSPaint* paint) : |
| PLSPathDraw(pixelBounds, |
| matrix, |
| std::move(path), |
| fillRule, |
| paint, |
| Type::midpointFanPath, |
| context->frameInterlockMode()) |
| { |
| if (isStroked()) |
| { |
| m_strokeMatrixMaxScale = m_matrix.findMaxScale(); |
| m_strokeJoin = paint->getJoin(); |
| m_strokeCap = paint->getCap(); |
| } |
| |
| // Count up how much temporary storage this function will need to reserve in CPU buffers. |
| const RawPath& rawPath = m_pathRef->getRawPath(); |
| size_t contourCount = rawPath.countMoveTos(); |
| if (contourCount == 0) |
| { |
| // The entire batch is empty. |
| return; |
| } |
| |
| m_contours = reinterpret_cast<ContourInfo*>( |
| context->perFrameAllocator().alloc(sizeof(ContourInfo) * contourCount)); |
| |
| size_t maxStrokedCurvesBeforeChops = 0; |
| size_t maxCurves = 0; |
| size_t maxRotations = 0; |
| // Reserve enough space to record all the info we might need for this path. |
| assert(rawPath.verbs()[0] == PathVerb::move); |
| // Every path has at least 1 (non-curve) move. |
| size_t pathMaxLinesOrCurvesBeforeChops = rawPath.verbs().size() - 1; |
| // Stroked cubics can be chopped into a maximum of 5 segments. |
| size_t pathMaxLinesOrCurvesAfterChops = |
| isStroked() ? pathMaxLinesOrCurvesBeforeChops * 5 : pathMaxLinesOrCurvesBeforeChops; |
| maxCurves += pathMaxLinesOrCurvesAfterChops; |
| if (isStroked()) |
| { |
| maxStrokedCurvesBeforeChops += pathMaxLinesOrCurvesBeforeChops; |
| maxRotations += pathMaxLinesOrCurvesAfterChops; |
| if (m_strokeJoin == StrokeJoin::round) |
| { |
| // If the stroke has round joins, we also record the rotations between (pre-chopped) |
| // joins in order to calculate how many vertices are in each round join. |
| maxRotations += pathMaxLinesOrCurvesBeforeChops; |
| } |
| } |
| |
| // Each stroked curve will record the number of chops it requires (either 0, 1, or 2). |
| size_t maxChops = maxStrokedCurvesBeforeChops; |
| // We only chop into this queue if a cubic has one chop. More chops in a single cubic |
| // are rare and require a lot of memory, so if a cubic needs more chops we just re-chop |
| // the second time around. The maximum size this queue would need is therefore enough to |
| // chop each cubic once, or 5 internal points per chop. |
| size_t maxChopVertices = maxStrokedCurvesBeforeChops * 5; |
| // +3 for each contour because we align each contour's curves and rotations on multiples of 4. |
| size_t maxPaddedRotations = isStroked() ? maxRotations + contourCount * 3 : 0; |
| size_t maxPaddedCurves = maxCurves + contourCount * 3; |
| |
| // Reserve intermediate space for the polar segment counts of each curve and round join. |
| if (isStroked()) |
| { |
| 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); |
| |
| size_t lineCount = 0; |
| size_t unpaddedCurveCount = 0; |
| size_t unpaddedRotationCount = 0; |
| size_t emptyStrokeCountForCaps = 0; |
| |
| // Iteration pass 1: Collect information on contour and curves counts for every path in the |
| // batch, and begin counting tessellated vertices. |
| size_t contourIdx = 0; |
| size_t curveIdx = 0; |
| size_t rotationIdx = 0; // We measure rotations on both curves and round joins. |
| bool roundJoinStroked = isStroked() && m_strokeJoin == StrokeJoin::round; |
| wangs_formula::VectorXform vectorXform(m_matrix); |
| RawPath::Iter startOfContour = rawPath.begin(); |
| RawPath::Iter end = rawPath.end(); |
| int preChopVerbCount = 0; // Original number of lines and curves, before chopping. |
| Vec2D endpointsSum{}; |
| bool closed = !isStroked(); |
| Vec2D lastTangent = {0, 1}; |
| Vec2D firstTangent = {0, 1}; |
| size_t roundJoinCount = 0; |
| size_t contourFirstCurveIdx = curveIdx; |
| assert(contourFirstCurveIdx % 4 == 0); |
| size_t contourFirstRotationIdx = rotationIdx; |
| assert(contourFirstRotationIdx % 4 == 0); |
| auto finishAndAppendContour = [&](RawPath::Iter iter) { |
| if (closed) |
| { |
| Vec2D finalPtInContour = iter.rawPtsPtr()[-1]; |
| // Bit-cast to uint64_t because we don't want the special equality rules for NaN inside |
| // of Vec2D::operator==. 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, |
| isStroked() ? Vec2D() : endpointsSum * (1.f / preChopVerbCount), |
| closed, |
| strokeJoinCount, |
| 0, // strokeCapSegmentCount |
| 0, // paddingVertexCount |
| RIVE_DEBUG_CODE(0) // tessVertexCount |
| }; |
| unpaddedCurveCount += curveIdx - contourFirstCurveIdx; |
| contourFirstCurveIdx = curveIdx = math::round_up_to_multiple_of<4>(curveIdx); |
| unpaddedRotationCount += rotationIdx - contourFirstRotationIdx; |
| contourFirstRotationIdx = rotationIdx = math::round_up_to_multiple_of<4>(rotationIdx); |
| }; |
| const int styleFlags = style_flags(isStroked(), 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 = !isStroked(); |
| 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]; |
| find_cubic_tangents(p, unchoppedTangents); |
| if (preChopVerbCount == 0) |
| { |
| firstTangent = unchoppedTangents[0]; |
| } |
| else |
| { |
| assert(rotationIdx < maxPaddedRotations); |
| m_tangentPairs[rotationIdx++] = {lastTangent, unchoppedTangents[0]}; |
| ++roundJoinCount; |
| } |
| lastTangent = unchoppedTangents[1]; |
| [[fallthrough]]; |
| } |
| case StyledVerb::strokedCubic: |
| { |
| const Vec2D* p = iter.cubicPts(); |
| ++preChopVerbCount; |
| endpointsSum += p[3]; |
| // Chop strokes into sections that do not inflect (i.e, are convex), and do |
| // not rotate more than 180 degrees. This is required by the GPU |
| // parametric/polar sorter. |
| float t[2]; |
| bool areCusps; |
| uint8_t numChops = pathutils::FindCubicConvex180Chops(p, t, &areCusps); |
| uint8_t chopKey = chop_key(areCusps, numChops); |
| m_numChops.push_back(chopKey); |
| Vec2D localChopBuffer[16]; |
| switch (chopKey) |
| { |
| case cusp_chop_key(2): // 2 cusps |
| case cusp_chop_key(1): // 1 cusp |
| // We have to chop carefully around stroked cusps in |
| // order to avoid rendering artifacts. Luckily, cusps |
| // are extremely rare in real-world content. |
| m_chopVertices.push_back() = {t[0], t[1]}; |
| chop_cubic_around_cusps(p, |
| localChopBuffer, |
| t, |
| numChops, |
| m_strokeMatrixMaxScale); |
| p = localChopBuffer; |
| numChops *= 2; |
| break; |
| case simple_chop_key(2): // 2 non-cusp chops |
| m_chopVertices.push_back() = {t[0], t[1]}; |
| pathutils::ChopCubicAt(p, localChopBuffer, t[0], t[1]); |
| p = localChopBuffer; |
| break; |
| case simple_chop_key(1): // 1 non-cusp chop |
| { |
| pathutils::ChopCubicAt(p, localChopBuffer, t[0]); |
| p = localChopBuffer; |
| memcpy(m_chopVertices.push_back_n(5), p + 1, sizeof(Vec2D) * 5); |
| break; |
| } |
| } |
| // Calculate segment counts for each chopped section independently. |
| for (const Vec2D* end = p + numChops * 3 + 3; p != end; |
| p += 3, ++curveIdx, ++rotationIdx) |
| { |
| float n4 = wangs_formula::cubic_pow4(p, kParametricPrecision, vectorXform); |
| // Record n^4 for now. This will get resolved later. |
| assert(curveIdx < maxPaddedCurves); |
| RIVE_INLINE_MEMCPY(m_parametricSegmentCounts + curveIdx, &n4, sizeof(uint32_t)); |
| assert(rotationIdx < maxPaddedRotations); |
| find_cubic_tangents(p, m_tangentPairs[rotationIdx].data()); |
| } |
| break; |
| } |
| case StyledVerb::filledCubic: |
| { |
| const Vec2D* p = iter.cubicPts(); |
| ++preChopVerbCount; |
| endpointsSum += p[3]; |
| float n4 = wangs_formula::cubic_pow4(p, kParametricPrecision, vectorXform); |
| // Record n^4 for now. This will get resolved later. |
| assert(curveIdx < maxPaddedCurves); |
| RIVE_INLINE_MEMCPY(m_parametricSegmentCounts + curveIdx++, &n4, sizeof(uint32_t)); |
| break; |
| } |
| } |
| } |
| if (startOfContour != end) |
| { |
| finishAndAppendContour(end); |
| } |
| assert(contourIdx == contourCount); |
| assert(contourCount > 0); |
| assert(curveIdx <= maxPaddedCurves); |
| assert(rotationIdx <= maxPaddedRotations); |
| assert(curveIdx % 4 == 0); // Because we write parametric segment counts in batches of 4. |
| assert(rotationIdx % 4 == 0); // Because we write polar segment counts in batches of 4. |
| assert(isStroked() || maxPaddedRotations == 0); |
| assert(isStroked() || rotationIdx == 0); |
| |
| // Return any data we conservatively allocated but did not use. |
| if (isStroked()) |
| { |
| m_numChops.shrinkToFit(context->numChopsAllocator(), maxChops); |
| m_chopVertices.shrinkToFit(context->chopVerticesAllocator(), maxChopVertices); |
| context->tangentPairsAllocator().rewindLastAllocation(maxPaddedRotations - rotationIdx); |
| context->polarSegmentCountsAllocator().rewindLastAllocation(maxPaddedRotations - |
| rotationIdx); |
| } |
| context->parametricSegmentCountsAllocator().rewindLastAllocation(maxPaddedCurves - curveIdx); |
| |
| // Iteration pass 2: Finish calculating the numbers of tessellation segments in each contour, |
| // using SIMD. |
| size_t contourFirstLineIdx = 0; |
| size_t tessVertexCount = 0; |
| for (size_t i = 0; i < contourCount; ++i) |
| { |
| ContourInfo* contour = &m_contours[i]; |
| size_t contourLineCount = contour->endLineIdx - contourFirstLineIdx; |
| uint32_t contourVertexCount = contourLineCount * 2; // Each line tessellates to 2 vertices. |
| uint4 mergedTessVertexSums4 = 0; |
| |
| // Finish calculating and counting parametric segments for each curve. |
| size_t j; |
| for (j = contour->firstCurveIdx; j < contour->endCurveIdx; j += 4) |
| { |
| // Curves recorded their segment counts raised to the 4th power. Now find their |
| // roots and convert to integers in batches of 4. |
| assert(j + 4 <= curveIdx); |
| float4 n = simd::load4f(m_parametricSegmentCounts + j); |
| n = simd::ceil(simd::sqrt(simd::sqrt(n))); |
| n = simd::clamp(n, float4(1), float4(kMaxParametricSegments)); |
| uint4 n_ = simd::cast<uint32_t>(n); |
| assert(j + 4 <= curveIdx); |
| simd::store(m_parametricSegmentCounts + j, n_); |
| mergedTessVertexSums4 += n_; |
| } |
| // We counted in batches of 4. Undo the values we counted from beyond the end of the |
| // path. |
| while (j-- > contour->endCurveIdx) |
| { |
| contourVertexCount -= m_parametricSegmentCounts[j]; |
| } |
| |
| if (isStroked()) |
| { |
| // Finish calculating and counting polar segments for each stroked curve and |
| // round join. |
| const float r_ = m_strokeRadius * m_strokeMatrixMaxScale; |
| const float polarSegmentsPerRad = |
| pathutils::CalcPolarSegmentsPerRadian<kPolarPrecision>(r_); |
| for (j = contour->firstRotationIdx; j < contour->endRotationIdx; j += 4) |
| { |
| // Measure the rotations of curves in batches of 4. |
| assert(j + 4 <= rotationIdx); |
| auto [tx0, ty0, tx1, ty1] = simd::load4x4f(&m_tangentPairs[j][0].x); |
| float4 numer = tx0 * tx1 + ty0 * ty1; |
| float4 denom_pow2 = (tx0 * tx0 + ty0 * ty0) * (tx1 * tx1 + ty1 * ty1); |
| float4 cosTheta = numer / simd::sqrt(denom_pow2); |
| cosTheta = simd::clamp(cosTheta, float4(-1), float4(1)); |
| float4 theta = simd::fast_acos(cosTheta); |
| // Find polar segment counts from the rotation angles. |
| float4 n = simd::ceil(theta * polarSegmentsPerRad); |
| n = simd::clamp(n, float4(1), float4(kMaxPolarSegments)); |
| uint4 n_ = simd::cast<uint32_t>(n); |
| assert(j + 4 <= rotationIdx); |
| simd::store(m_polarSegmentCounts + j, n_); |
| // Polar and parametric segments share the first and final vertices. |
| // Therefore: |
| // |
| // parametricVertexCount = parametricSegmentCount + 1 |
| // |
| // polarVertexCount = polarVertexCount + 1 |
| // |
| // mergedVertexCount = parametricVertexCount + polarVertexCount - 2 |
| // = parametricSegmentCount + 1 + polarSegmentCount + 1 |
| // - 2 = parametricSegmentCount + polarSegmentCount |
| // |
| mergedTessVertexSums4 += n_; |
| } |
| |
| // We counted in batches of 4. Undo the values we counted from beyond the end of |
| // the path. |
| while (j-- > contour->endRotationIdx) |
| { |
| contourVertexCount -= m_polarSegmentCounts[j]; |
| } |
| |
| // Count joins. |
| if (m_strokeJoin == StrokeJoin::round) |
| { |
| // Round joins share their beginning and ending vertices with the curve on |
| // either side. Therefore, the number of vertices we need to allocate for a |
| // round join is "joinSegmentCount - 1". Do all the -1's here. |
| contourVertexCount -= contour->strokeJoinCount; |
| } |
| else |
| { |
| // The shader needs 3 segments for each miter and bevel join (which |
| // translates to two interior vertices, since joins share their beginning |
| // and ending vertices with the curve on either side). |
| contourVertexCount += |
| contour->strokeJoinCount * (kNumSegmentsInMiterOrBevelJoin - 1); |
| } |
| |
| // Count stroke caps, if any. |
| bool empty = contour->endLineIdx == contourFirstLineIdx && |
| contour->endCurveIdx == contour->firstCurveIdx; |
| StrokeCap cap; |
| bool needsCaps; |
| if (!empty) |
| { |
| cap = m_strokeCap; |
| needsCaps = !contour->closed; |
| } |
| else |
| { |
| cap = empty_stroke_cap(contour->closed, m_strokeJoin, m_strokeCap); |
| needsCaps = cap != StrokeCap::butt; // Ignore butt caps when the contour is empty. |
| } |
| if (needsCaps) |
| { |
| // We emulate stroke caps as 180-degree joins. |
| if (cap == StrokeCap::round) |
| { |
| // Round caps rotate 180 degrees. |
| contour->strokeCapSegmentCount = ceilf(polarSegmentsPerRad * math::PI); |
| // +2 because round caps emulated as joins need to emit vertices at T=0 |
| // and T=1, unlike normal round joins. |
| contour->strokeCapSegmentCount += 2; |
| // Make sure not to exceed kMaxPolarSegments. |
| contour->strokeCapSegmentCount = |
| std::min(contour->strokeCapSegmentCount, kMaxPolarSegments); |
| } |
| else |
| { |
| contour->strokeCapSegmentCount = kNumSegmentsInMiterOrBevelJoin; |
| } |
| // pushContourToRenderContext() uses "strokeCapSegmentCount != 0" to tell if it |
| // needs stroke caps. |
| assert(contour->strokeCapSegmentCount != 0); |
| // As long as a contour isn't empty, we can tack the end cap onto the join |
| // section of the final curve in the stroke. Otherwise, we need to introduce |
| // 0-tessellation-segment curves with non-empty joins to carry the caps. |
| emptyStrokeCountForCaps += empty ? 2 : 1; |
| contourVertexCount += (contour->strokeCapSegmentCount - 1) * 2; |
| } |
| } |
| else |
| { |
| // Fills don't have polar segments: |
| // |
| // mergedVertexCount = parametricVertexCount = parametricSegmentCount + 1 |
| // |
| // Just collect the +1 for each non-stroked curve. |
| size_t contourCurveCount = contour->endCurveIdx - contour->firstCurveIdx; |
| contourVertexCount += contourCurveCount; |
| } |
| contourVertexCount += simd::reduce_add(mergedTessVertexSums4); |
| |
| // Add padding vertices until the number of tessellation vertices in the contour is |
| // an exact multiple of kMidpointFanPatchSegmentSpan. This ensures that patch |
| // boundaries align with contour boundaries. |
| contour->paddingVertexCount = |
| PaddingToAlignUp<kMidpointFanPatchSegmentSpan>(contourVertexCount); |
| contourVertexCount += contour->paddingVertexCount; |
| assert(contourVertexCount % kMidpointFanPatchSegmentSpan == 0); |
| RIVE_DEBUG_CODE(contour->tessVertexCount = contourVertexCount;) |
| |
| tessVertexCount += contourVertexCount; |
| contourFirstLineIdx = contour->endLineIdx; |
| } |
| |
| assert(contourFirstLineIdx == lineCount); |
| RIVE_DEBUG_CODE(m_pendingLineCount = lineCount); |
| RIVE_DEBUG_CODE(m_pendingCurveCount = unpaddedCurveCount); |
| RIVE_DEBUG_CODE(m_pendingRotationCount = unpaddedRotationCount); |
| RIVE_DEBUG_CODE(m_pendingEmptyStrokeCountForCaps = emptyStrokeCountForCaps); |
| |
| if (tessVertexCount > 0) |
| { |
| m_resourceCounts.pathCount = 1; |
| m_resourceCounts.contourCount = contourCount; |
| // maxTessellatedSegmentCount does not get doubled when we emit both forward and mirrored |
| // contours because the forward and mirrored pair both get packed into a single |
| // pls::TessVertexSpan. |
| m_resourceCounts.maxTessellatedSegmentCount = |
| lineCount + unpaddedCurveCount + emptyStrokeCountForCaps; |
| m_resourceCounts.midpointFanTessVertexCount = |
| m_contourDirections == pls::ContourDirections::reverseAndForward ? tessVertexCount * 2 |
| : tessVertexCount; |
| } |
| } |
| |
| void MidpointFanPathDraw::onPushToRenderContext(PLSRenderContext::LogicalFlush* flush) |
| { |
| const RawPath& rawPath = m_pathRef->getRawPath(); |
| RawPath::Iter startOfContour = rawPath.begin(); |
| for (size_t i = 0; i < m_resourceCounts.contourCount; ++i) |
| { |
| // Push a contour and curve records. |
| const ContourInfo& contour = m_contours[i]; |
| assert(startOfContour.verb() == PathVerb::move); |
| assert(isStroked() || contour.closed); // Fills are always closed. |
| RIVE_DEBUG_CODE(m_pendingStrokeJoinCount = isStroked() ? contour.strokeJoinCount : 0;) |
| RIVE_DEBUG_CODE(m_pendingStrokeCapCount = contour.strokeCapSegmentCount != 0 ? 2 : 0;) |
| |
| const Vec2D* pts = startOfContour.rawPtsPtr(); |
| size_t curveIdx = contour.firstCurveIdx; |
| size_t rotationIdx = contour.firstRotationIdx; |
| const RawPath::Iter end = contour.endOfContour; |
| uint32_t joinTypeFlags = 0; |
| bool roundJoinStroked = false; |
| bool needsFirstEmulatedCapAsJoin = false; // Emit a starting cap before the next cubic? |
| uint32_t emulatedCapAsJoinFlags = 0; |
| if (isStroked()) |
| { |
| joinTypeFlags = join_type_flags(m_strokeJoin); |
| roundJoinStroked = joinTypeFlags == 0; |
| if (contour.strokeCapSegmentCount != 0) |
| { |
| StrokeCap cap = !contour.closed ? m_strokeCap |
| : empty_stroke_cap(true, m_strokeJoin, m_strokeCap); |
| emulatedCapAsJoinFlags = EMULATED_STROKE_CAP_CONTOUR_FLAG; |
| if (cap == StrokeCap::square) |
| { |
| emulatedCapAsJoinFlags |= MITER_CLIP_JOIN_CONTOUR_FLAG; |
| } |
| else if (cap == StrokeCap::butt) |
| { |
| emulatedCapAsJoinFlags |= BEVEL_JOIN_CONTOUR_FLAG; |
| } |
| needsFirstEmulatedCapAsJoin = true; |
| } |
| } |
| |
| // Make a data record for this current contour on the GPU. |
| flush->pushContour(contour.midpoint, contour.closed, contour.paddingVertexCount); |
| |
| // Convert all curves in the contour to cubics and push them to the GPU. |
| const int styleFlags = style_flags(isStroked(), 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 = kNumSegmentsInMiterOrBevelJoin; |
| 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(flush, |
| cubic.data(), |
| emulatedCapAsJoinFlags, |
| contour.strokeCapSegmentCount); |
| needsFirstEmulatedCapAsJoin = false; |
| } |
| flush->pushCubic(cubic.data(), |
| joinTangent, |
| joinTypeFlags, |
| 1, |
| 1, |
| joinSegmentCount); |
| RIVE_DEBUG_CODE(--m_pendingLineCount;) |
| break; |
| } |
| case StyledVerb::roundJoinStrokedQuad: |
| case StyledVerb::strokedQuad: |
| case StyledVerb::filledQuad: |
| RIVE_UNREACHABLE(); |
| break; |
| case StyledVerb::roundJoinStrokedCubic: |
| case StyledVerb::strokedCubic: |
| { |
| const Vec2D* p = iter.cubicPts(); |
| uint8_t chopKey = m_numChops.pop_front(); |
| uint8_t numChops = 0; |
| Vec2D localChopBuffer[16]; |
| switch (chopKey) |
| { |
| case cusp_chop_key(2): // 2 cusps |
| case cusp_chop_key(1): // 1 cusp |
| // We have to chop carefully around stroked cusps in order to avoid |
| // rendering artifacts. Luckily, cusps are extremely rare in real-world |
| // content. |
| chop_cubic_around_cusps(p, |
| localChopBuffer, |
| &m_chopVertices.pop_front().x, |
| chopKey >> 1, |
| m_strokeMatrixMaxScale); |
| p = localChopBuffer; |
| // The bottom bit of chopKey is 1, meaning "areCusps". Clearing the |
| // bottom bit leaves "numChops * 2", which is the number of chops a cusp |
| // needs! |
| numChops = chopKey ^ 1; |
| break; |
| |
| case simple_chop_key(2): // 2 non-cusp chops |
| { |
| // Curves that need 2 chops are rare in real-world content. Just re-chop |
| // the curve this time around as well. |
| auto [t0, t1] = m_chopVertices.pop_front(); |
| pathutils::ChopCubicAt(p, localChopBuffer, t0, t1); |
| p = localChopBuffer; |
| numChops = 2; |
| break; |
| } |
| case simple_chop_key(1): // 1 non-cusp chop |
| // Single-chop curves were saved in the m_chopVertices queue. |
| localChopBuffer[0] = p[0]; |
| memcpy(localChopBuffer + 1, |
| m_chopVertices.pop_front_n(5), |
| sizeof(Vec2D) * 5); |
| localChopBuffer[6] = p[3]; |
| p = localChopBuffer; |
| numChops = 1; |
| break; |
| } |
| if (needsFirstEmulatedCapAsJoin) |
| { |
| // Emulate the start cap as a 180-degree join before the first stroke. |
| pushEmulatedStrokeCapAsJoinBeforeCubic(flush, |
| p, |
| emulatedCapAsJoinFlags, |
| contour.strokeCapSegmentCount); |
| needsFirstEmulatedCapAsJoin = false; |
| } |
| // Push chops before the final one. |
| for (size_t end = curveIdx + numChops; curveIdx != end; |
| ++curveIdx, ++rotationIdx, p += 3) |
| { |
| uint32_t parametricSegmentCount = m_parametricSegmentCounts[curveIdx]; |
| uint32_t polarSegmentCount = m_polarSegmentCounts[rotationIdx]; |
| flush->pushCubic(p, |
| joinTangent, |
| joinTypeFlags, |
| parametricSegmentCount, |
| polarSegmentCount, |
| 1); |
| RIVE_DEBUG_CODE(--m_pendingCurveCount;) |
| RIVE_DEBUG_CODE(--m_pendingRotationCount;) |
| } |
| // Push the final chop, with a join. |
| uint32_t parametricSegmentCount = m_parametricSegmentCounts[curveIdx++]; |
| uint32_t polarSegmentCount = m_polarSegmentCounts[rotationIdx++]; |
| RIVE_DEBUG_CODE(--m_pendingRotationCount;) |
| if (contour.closed || !is_final_verb_of_contour(iter, end)) |
| { |
| if (styledVerb == StyledVerb::roundJoinStrokedCubic) |
| { |
| joinTangent = m_tangentPairs[rotationIdx][1]; |
| joinSegmentCount = m_polarSegmentCounts[rotationIdx]; |
| ++rotationIdx; |
| RIVE_DEBUG_CODE(--m_pendingRotationCount;) |
| } |
| else |
| { |
| joinTangent = find_join_tangent(iter.cubicPts() + 3, |
| end.rawPtsPtr(), |
| contour.closed, |
| pts); |
| joinSegmentCount = kNumSegmentsInMiterOrBevelJoin; |
| } |
| 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;) |
| } |
| flush->pushCubic(p, |
| joinTangent, |
| joinTypeFlags, |
| parametricSegmentCount, |
| polarSegmentCount, |
| joinSegmentCount); |
| RIVE_DEBUG_CODE(--m_pendingCurveCount;) |
| break; |
| } |
| case StyledVerb::filledCubic: |
| { |
| uint32_t parametricSegmentCount = m_parametricSegmentCounts[curveIdx++]; |
| flush->pushCubic(iter.cubicPts(), Vec2D{}, 0, parametricSegmentCount, 1, 1); |
| 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(flush, |
| std::array{p0, right, right, right}.data(), |
| emulatedCapAsJoinFlags, |
| contour.strokeCapSegmentCount); |
| pushEmulatedStrokeCapAsJoinBeforeCubic(flush, |
| std::array{p0, left, left, left}.data(), |
| emulatedCapAsJoinFlags, |
| contour.strokeCapSegmentCount); |
| } |
| else if (contour.closed) |
| { |
| implicitClose[0] = end.rawPtsPtr()[-1]; |
| if (implicitClose[0] != implicitClose[1]) |
| { |
| // Draw a line back to the beginning of the contour. |
| std::array<Vec2D, 4> cubic = convert_line_to_cubic(implicitClose); |
| // Closing join back to the beginning of the contour. |
| if (roundJoinStroked) |
| { |
| joinTangent = m_tangentPairs[rotationIdx][1]; |
| joinSegmentCount = m_polarSegmentCounts[rotationIdx]; |
| ++rotationIdx; |
| RIVE_DEBUG_CODE(--m_pendingRotationCount;) |
| RIVE_DEBUG_CODE(--m_pendingStrokeJoinCount;) |
| } |
| else if (isStroked()) |
| { |
| joinTangent = find_starting_tangent(pts, end.rawPtsPtr()); |
| joinSegmentCount = kNumSegmentsInMiterOrBevelJoin; |
| RIVE_DEBUG_CODE(--m_pendingStrokeJoinCount;) |
| } |
| flush->pushCubic(cubic.data(), joinTangent, joinTypeFlags, 1, 1, joinSegmentCount); |
| RIVE_DEBUG_CODE(--m_pendingLineCount;) |
| } |
| } |
| |
| assert(curveIdx == contour.endCurveIdx); |
| assert(rotationIdx == contour.endRotationIdx); |
| |
| assert(m_pendingStrokeJoinCount == 0); |
| assert(m_pendingStrokeCapCount == 0); |
| startOfContour = contour.endOfContour; |
| } |
| |
| // Make sure we only pushed the amount of data we reserved. |
| assert(m_pendingLineCount == 0); |
| assert(m_pendingCurveCount == 0); |
| assert(m_pendingRotationCount == 0); |
| assert(m_pendingEmptyStrokeCountForCaps == 0); |
| } |
| |
| void MidpointFanPathDraw::pushEmulatedStrokeCapAsJoinBeforeCubic( |
| PLSRenderContext::LogicalFlush* flush, |
| const Vec2D cubic[], |
| uint32_t emulatedCapAsJoinFlags, |
| uint32_t strokeCapSegmentCount) |
| { |
| // Reverse the cubic and push it with zero parametric and polar segments, and a 180-degree join |
| // tangent. This results in a solitary join, positioned immediately before the provided cubic, |
| // that looks like the desired stroke cap. |
| flush->pushCubic(std::array{cubic[3], cubic[2], cubic[1], cubic[0]}.data(), |
| find_cubic_tan0(cubic), |
| emulatedCapAsJoinFlags, |
| 0, |
| 0, |
| strokeCapSegmentCount); |
| RIVE_DEBUG_CODE(--m_pendingStrokeCapCount;) |
| RIVE_DEBUG_CODE(--m_pendingEmptyStrokeCountForCaps;) |
| } |
| |
| InteriorTriangulationDraw::InteriorTriangulationDraw(PLSRenderContext* context, |
| IAABB pixelBounds, |
| const Mat2D& matrix, |
| rcp<const PLSPath> path, |
| FillRule fillRule, |
| const PLSPaint* paint, |
| RawPath* scratchPath, |
| TriangulatorAxis triangulatorAxis) : |
| PLSPathDraw(pixelBounds, |
| matrix, |
| std::move(path), |
| fillRule, |
| paint, |
| Type::interiorTriangulationPath, |
| context->frameInterlockMode()) |
| { |
| assert(!isStroked()); |
| assert(m_strokeRadius == 0); |
| processPath(PathOp::countDataAndTriangulate, |
| &context->perFrameAllocator(), |
| scratchPath, |
| triangulatorAxis, |
| nullptr); |
| } |
| |
| void InteriorTriangulationDraw::onPushToRenderContext(PLSRenderContext::LogicalFlush* flush) |
| { |
| processPath(PathOp::submitOuterCubics, nullptr, nullptr, TriangulatorAxis::dontCare, flush); |
| if (flush->desc().interlockMode == pls::InterlockMode::atomics) |
| { |
| // We need a barrier between the outer cubics and interior triangles in atomic mode. |
| flush->pushBarrier(); |
| } |
| flush->pushInteriorTriangulation(this); |
| } |
| |
| void InteriorTriangulationDraw::processPath(PathOp op, |
| TrivialBlockAllocator* allocator, |
| RawPath* scratchPath, |
| TriangulatorAxis triangulatorAxis, |
| PLSRenderContext::LogicalFlush* flush) |
| { |
| Vec2D chops[kMaxCurveSubdivisions * 3 + 1]; |
| const RawPath& rawPath = m_pathRef->getRawPath(); |
| assert(!rawPath.empty()); |
| wangs_formula::VectorXform vectorXform(m_matrix); |
| size_t patchCount = 0; |
| size_t contourCount = 0; |
| Vec2D p0 = {0, 0}; |
| if (op == PathOp::countDataAndTriangulate) |
| { |
| scratchPath->rewind(); |
| } |
| for (const auto [verb, pts] : rawPath) |
| { |
| switch (verb) |
| { |
| case PathVerb::move: |
| if (contourCount != 0 && pts[-1] != p0) |
| { |
| if (op == PathOp::submitOuterCubics) |
| { |
| flush->pushCubic(convert_line_to_cubic(pts[-1], p0).data(), |
| {0, 0}, |
| CULL_EXCESS_TESSELLATION_SEGMENTS_CONTOUR_FLAG, |
| kPatchSegmentCountExcludingJoin, |
| 1, |
| kJoinSegmentCount); |
| } |
| ++patchCount; |
| } |
| if (op == PathOp::countDataAndTriangulate) |
| { |
| scratchPath->move(pts[0]); |
| } |
| else |
| { |
| flush->pushContour({0, 0}, true, 0); |
| } |
| p0 = pts[0]; |
| ++contourCount; |
| break; |
| case PathVerb::line: |
| if (op == PathOp::countDataAndTriangulate) |
| { |
| scratchPath->line(pts[1]); |
| } |
| else |
| { |
| flush->pushCubic(convert_line_to_cubic(pts).data(), |
| {0, 0}, |
| CULL_EXCESS_TESSELLATION_SEGMENTS_CONTOUR_FLAG, |
| kPatchSegmentCountExcludingJoin, |
| 1, |
| kJoinSegmentCount); |
| } |
| ++patchCount; |
| break; |
| case PathVerb::quad: |
| RIVE_UNREACHABLE(); |
| case PathVerb::cubic: |
| { |
| size_t numSubdivisions = FindSubdivisionCount(pts, vectorXform); |
| if (numSubdivisions == 1) |
| { |
| if (op == PathOp::countDataAndTriangulate) |
| { |
| scratchPath->line(pts[3]); |
| } |
| else |
| { |
| flush->pushCubic(pts, |
| {0, 0}, |
| CULL_EXCESS_TESSELLATION_SEGMENTS_CONTOUR_FLAG, |
| kPatchSegmentCountExcludingJoin, |
| 1, |
| kJoinSegmentCount); |
| } |
| } |
| else |
| { |
| // Passing nullptr for the 'tValues' causes it to chop the cubic uniformly |
| // in T. |
| pathutils::ChopCubicAt(pts, chops, nullptr, numSubdivisions - 1); |
| const Vec2D* chop = chops; |
| for (size_t i = 0; i < numSubdivisions; ++i) |
| { |
| if (op == PathOp::countDataAndTriangulate) |
| { |
| scratchPath->line(chop[3]); |
| } |
| else |
| { |
| flush->pushCubic(chop, |
| {0, 0}, |
| CULL_EXCESS_TESSELLATION_SEGMENTS_CONTOUR_FLAG, |
| kPatchSegmentCountExcludingJoin, |
| 1, |
| kJoinSegmentCount); |
| } |
| chop += 3; |
| } |
| } |
| patchCount += numSubdivisions; |
| break; |
| } |
| case PathVerb::close: |
| break; |
| } |
| } |
| Vec2D lastPt = rawPath.points().back(); |
| if (contourCount != 0 && lastPt != p0) |
| { |
| if (op == PathOp::submitOuterCubics) |
| { |
| flush->pushCubic(convert_line_to_cubic(lastPt, p0).data(), |
| {0, 0}, |
| CULL_EXCESS_TESSELLATION_SEGMENTS_CONTOUR_FLAG, |
| kPatchSegmentCountExcludingJoin, |
| 1, |
| kJoinSegmentCount); |
| } |
| ++patchCount; |
| } |
| |
| if (op == PathOp::countDataAndTriangulate) |
| { |
| assert(m_triangulator == nullptr); |
| assert(triangulatorAxis != TriangulatorAxis::dontCare); |
| m_triangulator = allocator->make<GrInnerFanTriangulator>( |
| *scratchPath, |
| m_matrix, |
| triangulatorAxis == TriangulatorAxis::horizontal |
| ? GrTriangulator::Comparator::Direction::kHorizontal |
| : GrTriangulator::Comparator::Direction::kVertical, |
| m_fillRule, |
| allocator); |
| // We also draw each "grout" triangle using an outerCubic patch. |
| patchCount += m_triangulator->groutList().count(); |
| |
| 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 |
| // pls::TessVertexSpan. |
| m_resourceCounts.maxTessellatedSegmentCount = patchCount; |
| // outerCubic patches emit their tessellated geometry twice: once forward and once mirrored. |
| m_resourceCounts.outerCubicTessVertexCount = |
| m_contourDirections == pls::ContourDirections::reverseAndForward |
| ? patchCount * kOuterCurvePatchSegmentSpan * 2 |
| : patchCount * kOuterCurvePatchSegmentSpan; |
| m_resourceCounts.maxTriangleVertexCount = m_triangulator->maxVertexCount(); |
| } |
| else |
| { |
| assert(m_triangulator != nullptr); |
| // Submit grout triangles, retrofitted into outerCubic patches. |
| for (auto* node = m_triangulator->groutList().head(); node; node = node->fNext) |
| { |
| Vec2D triangleAsCubic[4] = {node->fPts[0], node->fPts[1], {0, 0}, node->fPts[2]}; |
| flush->pushCubic(triangleAsCubic, |
| {0, 0}, |
| RETROFITTED_TRIANGLE_CONTOUR_FLAG, |
| kPatchSegmentCountExcludingJoin, |
| 1, |
| kJoinSegmentCount); |
| ++patchCount; |
| } |
| assert(contourCount == m_resourceCounts.contourCount); |
| assert(patchCount == m_resourceCounts.maxTessellatedSegmentCount); |
| assert(patchCount * kOuterCurvePatchSegmentSpan * 2 == |
| m_resourceCounts.outerCubicTessVertexCount || |
| patchCount * kOuterCurvePatchSegmentSpan == |
| m_resourceCounts.outerCubicTessVertexCount); |
| } |
| } |
| |
| ImageRectDraw::ImageRectDraw(PLSRenderContext* context, |
| IAABB pixelBounds, |
| const Mat2D& matrix, |
| BlendMode blendMode, |
| rcp<const PLSTexture> imageTexture, |
| float opacity) : |
| PLSDraw(pixelBounds, matrix, blendMode, std::move(imageTexture), Type::imageRect), |
| m_opacity(opacity) |
| { |
| // If we support image paints for paths, the client should draw a rectangular path with an |
| // image paint instead of using this draw. |
| assert(!context->frameSupportsImagePaintForPaths()); |
| m_resourceCounts.imageDrawCount = 1; |
| } |
| |
| void ImageRectDraw::pushToRenderContext(PLSRenderContext::LogicalFlush* flush) |
| { |
| flush->pushImageRect(this); |
| } |
| |
| ImageMeshDraw::ImageMeshDraw(IAABB pixelBounds, |
| const Mat2D& matrix, |
| BlendMode blendMode, |
| rcp<const PLSTexture> imageTexture, |
| rcp<const RenderBuffer> vertexBuffer, |
| rcp<const RenderBuffer> uvBuffer, |
| rcp<const RenderBuffer> indexBuffer, |
| uint32_t indexCount, |
| float opacity) : |
| PLSDraw(pixelBounds, matrix, blendMode, std::move(imageTexture), Type::imageMesh), |
| m_vertexBufferRef(vertexBuffer.release()), |
| m_uvBufferRef(uvBuffer.release()), |
| m_indexBufferRef(indexBuffer.release()), |
| m_indexCount(indexCount), |
| m_opacity(opacity) |
| { |
| assert(m_vertexBufferRef != nullptr); |
| assert(m_uvBufferRef != nullptr); |
| assert(m_indexBufferRef != nullptr); |
| m_resourceCounts.imageDrawCount = 1; |
| } |
| |
| void ImageMeshDraw::pushToRenderContext(PLSRenderContext::LogicalFlush* flush) |
| { |
| flush->pushImageMesh(this); |
| } |
| |
| void ImageMeshDraw::releaseRefs() |
| { |
| PLSDraw::releaseRefs(); |
| m_vertexBufferRef->unref(); |
| m_uvBufferRef->unref(); |
| m_indexBufferRef->unref(); |
| } |
| |
| StencilClipReset::StencilClipReset(PLSRenderContext* context, |
| uint32_t previousClipID, |
| ResetAction resetAction) : |
| PLSDraw(context->getClipContentBounds(previousClipID), |
| Mat2D(), |
| BlendMode::srcOver, |
| nullptr, |
| Type::stencilClipReset), |
| m_previousClipID(previousClipID) |
| { |
| switch (resetAction) |
| { |
| case ResetAction::intersectPreviousClip: |
| m_drawContents |= pls::DrawContents::activeClip; |
| [[fallthrough]]; |
| case ResetAction::clearPreviousClip: |
| m_drawContents |= pls::DrawContents::clipUpdate; |
| break; |
| } |
| m_resourceCounts.maxTriangleVertexCount = 6; |
| } |
| |
| void StencilClipReset::pushToRenderContext(PLSRenderContext::LogicalFlush* flush) |
| { |
| flush->pushStencilClipReset(this); |
| } |
| } // namespace rive::pls |