| /* | |
| * 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] = fmaxf(cuspT[i] - math::EPSILON, minT); | |
| t[i * 2 + 1] = fminf(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 && | |
| path->getRawPath().verbs().count() < 1000 && | |
| 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(!m_pathRef->getRawPath().empty()); | |
| 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 = fmaxf(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()); | |
| assert(!m_pathRef->getRawPath().empty()); | |
| size_t tessVertexCount = m_type == Type::midpointFanPath | |
| ? m_resourceCounts.midpointFanTessVertexCount | |
| : m_resourceCounts.outerCubicTessVertexCount; | |
| if (tessVertexCount > 0) | |
| { | |
| // 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(); | |
| assert(contourCount != 0); | |
| m_contours = reinterpret_cast<ContourInfo*>( | |
| context->perFrameAllocator().alloc(sizeof(ContourInfo) * contourCount)); | |
| size_t maxStrokedCurvesBeforeChops = 0; | |
| size_t maxCurves = 0; | |
| size_t maxRotations = 0; | |
| // Reserve enough space to record all the info we might need for this path. | |
| assert(rawPath.verbs()[0] == PathVerb::move); | |
| // Every path has at least 1 (non-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. 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. | |
| float 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. | |
| strokeCapSegmentCount += 2; | |
| // Make sure not to exceed kMaxPolarSegments. | |
| strokeCapSegmentCount = fminf(strokeCapSegmentCount, kMaxPolarSegments); | |
| contour->strokeCapSegmentCount = static_cast<uint32_t>(strokeCapSegmentCount); | |
| } | |
| else | |
| { | |
| contour->strokeCapSegmentCount = kNumSegmentsInMiterOrBevelJoin; | |
| } | |
| // PLS expects all patches to have >0 tessellation vertices, so for the case of an | |
| // empty patch with a stroke cap, contour->strokeCapSegmentCount can't be zero. | |
| // Also, pushContourToRenderContext() uses "strokeCapSegmentCount != 0" to tell if | |
| // it needs stroke caps. | |
| assert(contour->strokeCapSegmentCount >= 2); | |
| // As long as a contour isn't empty, we can tack the end cap onto the join | |
| // section of the final curve in the stroke. Otherwise, we need to introduce | |
| // 0-tessellation-segment curves with non-empty joins to carry the caps. | |
| emptyStrokeCountForCaps += empty ? 2 : 1; | |
| contourVertexCount += (contour->strokeCapSegmentCount - 1) * 2; | |
| } | |
| } | |
| else | |
| { | |
| // Fills don't have polar segments: | |
| // | |
| // mergedVertexCount = parametricVertexCount = parametricSegmentCount + 1 | |
| // | |
| // Just collect the +1 for each non-stroked curve. | |
| size_t contourCurveCount = contour->endCurveIdx - contour->firstCurveIdx; | |
| contourVertexCount += contourCurveCount; | |
| } | |
| contourVertexCount += simd::reduce_add(mergedTessVertexSums4); | |
| // Add padding vertices until the number of tessellation vertices in the contour is | |
| // an exact multiple of kMidpointFanPatchSegmentSpan. This ensures that patch | |
| // boundaries align with contour boundaries. | |
| contour->paddingVertexCount = | |
| 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]; | |
| // 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 (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. | |
| assert(strokeCapSegmentCount >= 2); | |
| 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(); | |
| 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 | |
| // 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 |