| /* |
| * Copyright 2020 Google LLC. |
| * |
| * Use of this source code is governed by a BSD-style license that can be |
| * found in the LICENSE file. |
| */ |
| |
| #include "src/gpu/tessellate/GrStrokeHardwareTessellator.h" |
| |
| #include "src/core/SkPathPriv.h" |
| #include "src/gpu/GrRecordingContextPriv.h" |
| #include "src/gpu/geometry/GrPathUtils.h" |
| #include "src/gpu/tessellate/GrWangsFormula.h" |
| |
| static float num_combined_segments(float numParametricSegments, float numRadialSegments) { |
| // The first and last edges are shared by both the parametric and radial sets of edges, so |
| // the total number of edges is: |
| // |
| // numCombinedEdges = numParametricEdges + numRadialEdges - 2 |
| // |
| // It's also important to differentiate between the number of edges and segments in a strip: |
| // |
| // numCombinedSegments = numCombinedEdges - 1 |
| // |
| // So the total number of segments in the combined strip is: |
| // |
| // numCombinedSegments = numParametricEdges + numRadialEdges - 2 - 1 |
| // = numParametricSegments + 1 + numRadialSegments + 1 - 2 - 1 |
| // = numParametricSegments + numRadialSegments - 1 |
| // |
| return numParametricSegments + numRadialSegments - 1; |
| } |
| |
| static float num_parametric_segments(float numCombinedSegments, float numRadialSegments) { |
| // numCombinedSegments = numParametricSegments + numRadialSegments - 1. |
| // (See num_combined_segments()). |
| return std::max(numCombinedSegments + 1 - numRadialSegments, 0.f); |
| } |
| |
| static float pow4(float x) { |
| float xx = x*x; |
| return xx*xx; |
| } |
| |
| void GrStrokeHardwareTessellator::updateTolerances(Tolerances tolerances, SkPaint::Join joinType) { |
| // Calculate the worst-case numbers of parametric segments our hardware can support for the |
| // current stroke radius, in the event that there are also enough radial segments to rotate |
| // 180 and 360 degrees respectively. These are used for "quick accepts" that allow us to |
| // send almost all curves directly to the hardware without having to chop. |
| float numRadialSegments180 = std::max(std::ceil( |
| SK_ScalarPI * tolerances.fNumRadialSegmentsPerRadian), 1.f); |
| float maxParametricSegments180 = num_parametric_segments(fMaxTessellationSegments, |
| numRadialSegments180); |
| fMaxParametricSegments180_pow4 = pow4(maxParametricSegments180); |
| |
| float numRadialSegments360 = std::max(std::ceil( |
| 2*SK_ScalarPI * tolerances.fNumRadialSegmentsPerRadian), 1.f); |
| float maxParametricSegments360 = num_parametric_segments(fMaxTessellationSegments, |
| numRadialSegments360); |
| fMaxParametricSegments360_pow4 = pow4(maxParametricSegments360); |
| |
| // Now calculate the worst-case numbers of parametric segments if we are to integrate a join |
| // into the same patch as the curve. |
| float maxNumSegmentsInJoin; |
| switch (joinType) { |
| case SkPaint::kBevel_Join: |
| maxNumSegmentsInJoin = 1; |
| break; |
| case SkPaint::kMiter_Join: |
| maxNumSegmentsInJoin = 2; |
| break; |
| case SkPaint::kRound_Join: |
| // 180-degree round join. |
| maxNumSegmentsInJoin = numRadialSegments180; |
| break; |
| } |
| // Subtract an extra 1 off the end because when we integrate a join, the tessellator has to add |
| // a redundant edge between the join and curve. |
| fMaxParametricSegments180_pow4_withJoin = pow4(std::max( |
| maxParametricSegments180 - maxNumSegmentsInJoin - 1, 0.f)); |
| fMaxParametricSegments360_pow4_withJoin = pow4(std::max( |
| maxParametricSegments360 - maxNumSegmentsInJoin - 1, 0.f)); |
| fMaxCombinedSegments_withJoin = fMaxTessellationSegments - maxNumSegmentsInJoin - 1; |
| fSoloRoundJoinAlwaysFitsInPatch = (numRadialSegments180 <= fMaxTessellationSegments); |
| fTolerances = tolerances; |
| } |
| |
| static bool conic_has_cusp(const SkPoint p[3]) { |
| SkVector a = p[1] - p[0]; |
| SkVector b = p[2] - p[1]; |
| // A conic of any class can only have a cusp if it is a degenerate flat line with a 180 degree |
| // turnarund. To detect this, the beginning and ending tangents must be parallel |
| // (a.cross(b) == 0) and pointing in opposite directions (a.dot(b) < 0). |
| return a.cross(b) == 0 && a.dot(b) < 0; |
| } |
| |
| void GrStrokeHardwareTessellator::prepare(GrMeshDrawOp::Target* target, |
| const SkMatrix& viewMatrix) { |
| SkASSERT(!fTarget); |
| fTarget = target; |
| |
| std::array<float, 2> matrixScales; |
| if (!viewMatrix.getMinMaxScales(matrixScales.data())) { |
| matrixScales.fill(1); |
| } |
| |
| // Pre-allocate at least enough vertex space for 1 in 4 strokes to chop, and for 8 caps. |
| int strokePreallocCount = fTotalCombinedVerbCnt * 5/4; |
| int capPreallocCount = 8; |
| this->allocPatchChunkAtLeast(strokePreallocCount + capPreallocCount); |
| |
| const SkStrokeRec* strokeForTolerances = nullptr; |
| |
| for (const auto& pathStroke : fPathStrokeList) { |
| const SkStrokeRec& stroke = pathStroke.fStroke; |
| if (!strokeForTolerances || strokeForTolerances->getWidth() != stroke.getWidth() || |
| strokeForTolerances->getJoin() != stroke.getJoin()) { |
| auto tolerances = Tolerances::MakePreTransform(matrixScales.data(), stroke.getWidth()); |
| this->updateTolerances(tolerances, stroke.getJoin()); |
| strokeForTolerances = &stroke; |
| } |
| auto strokeJoinType = JoinType(stroke.getJoin()); |
| |
| if (fShaderFlags & ShaderFlags::kDynamicStroke) { |
| fDynamicStroke.set(stroke); |
| } |
| if (fShaderFlags & ShaderFlags::kDynamicColor) { |
| bool wideColor = fShaderFlags & ShaderFlags::kWideColor; |
| SkASSERT(wideColor || pathStroke.fColor.fitsInBytes()); |
| fDynamicColor.set(pathStroke.fColor, wideColor); |
| } |
| |
| const SkPath& path = pathStroke.fPath; |
| fHasLastControlPoint = false; |
| SkPathVerb previousVerb = SkPathVerb::kClose; |
| for (auto [verb, p, w] : SkPathPriv::Iterate(path)) { |
| switch (verb) { |
| case SkPathVerb::kMove: |
| // "A subpath ... consisting of a single moveto shall not be stroked." |
| // https://www.w3.org/TR/SVG11/painting.html#StrokeProperties |
| if (previousVerb != SkPathVerb::kMove && previousVerb != SkPathVerb::kClose) { |
| this->cap(p[-1], viewMatrix, stroke); |
| } |
| this->moveTo(p[0]); |
| break; |
| case SkPathVerb::kLine: |
| this->lineTo(strokeJoinType, p[0], p[1]); |
| break; |
| case SkPathVerb::kQuad: |
| if (conic_has_cusp(p)) { |
| SkPoint cusp = SkEvalQuadAt(p, SkFindQuadMidTangent(p)); |
| this->lineTo(strokeJoinType, p[0], cusp); |
| this->lineTo(JoinType::kBowtie, cusp, p[2]); |
| } else { |
| this->conicTo(strokeJoinType, p, 1); |
| } |
| break; |
| case SkPathVerb::kConic: |
| if (conic_has_cusp(p)) { |
| SkConic conic(p, *w); |
| SkPoint cusp = conic.evalAt(conic.findMidTangent()); |
| this->lineTo(strokeJoinType, p[0], cusp); |
| this->lineTo(JoinType::kBowtie, cusp, p[2]); |
| } else { |
| this->conicTo(strokeJoinType, p, *w); |
| } |
| break; |
| case SkPathVerb::kCubic: |
| bool areCusps; |
| GrPathUtils::findCubicConvex180Chops(p, nullptr, &areCusps); |
| if (areCusps) { |
| this->cubicConvex180SegmentsTo(strokeJoinType, p); |
| } else { |
| this->cubicTo(strokeJoinType, p); |
| } |
| break; |
| case SkPathVerb::kClose: |
| this->close(p[0], viewMatrix, stroke); |
| break; |
| } |
| previousVerb = verb; |
| } |
| if (previousVerb != SkPathVerb::kMove && previousVerb != SkPathVerb::kClose) { |
| const SkPoint* p = SkPathPriv::PointData(path); |
| this->cap(p[path.countPoints() - 1], viewMatrix, stroke); |
| } |
| } |
| |
| fTarget->putBackVertices(fCurrChunkPatchCapacity - fPatchChunks.back().fPatchCount, |
| fPatchStride); |
| |
| fTarget = nullptr; |
| } |
| |
| void GrStrokeHardwareTessellator::moveTo(SkPoint pt) { |
| fCurrContourStartPoint = pt; |
| fHasLastControlPoint = false; |
| } |
| |
| void GrStrokeHardwareTessellator::moveTo(SkPoint pt, SkPoint lastControlPoint) { |
| fCurrContourStartPoint = pt; |
| fCurrContourFirstControlPoint = fLastControlPoint = lastControlPoint; |
| fHasLastControlPoint = true; |
| } |
| |
| void GrStrokeHardwareTessellator::lineTo(JoinType prevJoinType, SkPoint p0, SkPoint p1) { |
| // Zero-length paths need special treatment because they are spec'd to behave differently. |
| if (p0 == p1) { |
| return; |
| } |
| |
| if (fMaxCombinedSegments_withJoin < 1) { |
| // The stroke has extremely thick round joins and there aren't enough guaranteed segments to |
| // always combine a join with a line patch. Emit the join in its own separate patch. |
| this->joinTo(prevJoinType, p0, p1); |
| prevJoinType = JoinType::kNone; |
| } |
| |
| SkPoint asPatch[4] = {p0, p0, p1, p1}; |
| this->emitPatch(prevJoinType, asPatch, p1); |
| } |
| |
| void GrStrokeHardwareTessellator::conicTo(JoinType prevJoinType, const SkPoint p[3], float w, |
| int maxDepth) { |
| // Zero-length paths need special treatment because they are spec'd to behave differently. If |
| // the control point is colocated on an endpoint then this might end up being the case. Fall |
| // back on a lineTo and let it make the final check. |
| if (p[1] == p[0] || p[1] == p[2] || w == 0) { |
| this->lineTo(prevJoinType, p[0], p[2]); |
| return; |
| } |
| |
| // Convert to a patch. |
| SkPoint asPatch[4]; |
| if (w == 1) { |
| GrPathUtils::convertQuadToCubic(p, asPatch); |
| } else { |
| GrPathShader::WriteConicPatch(p, w, asPatch); |
| } |
| |
| // Ensure our hardware supports enough tessellation segments to render the curve. This early out |
| // assumes a worst-case quadratic rotation of 180 degrees and a worst-case number of segments in |
| // the join. |
| // |
| // An informal survey of skottie animations and gms revealed that even with a bare minimum of 64 |
| // tessellation segments, 99.9%+ of quadratics take this early out. |
| float numParametricSegments_pow4 = |
| GrWangsFormula::quadratic_pow4(fTolerances.fParametricIntolerance, p); |
| if (numParametricSegments_pow4 <= fMaxParametricSegments180_pow4_withJoin) { |
| this->emitPatch(prevJoinType, asPatch, p[2]); |
| return; |
| } |
| |
| if (numParametricSegments_pow4 <= fMaxParametricSegments180_pow4 || maxDepth == 0) { |
| if (numParametricSegments_pow4 > fMaxParametricSegments180_pow4_withJoin) { |
| // There aren't enough guaranteed segments to include the join. Emit a standalone patch |
| // for the join. |
| this->joinTo(prevJoinType, asPatch); |
| prevJoinType = JoinType::kNone; |
| } |
| this->emitPatch(prevJoinType, asPatch, p[2]); |
| return; |
| } |
| |
| // We still might have enough tessellation segments to render the curve. Check again with the |
| // actual rotation. |
| float numRadialSegments = SkMeasureQuadRotation(p) * fTolerances.fNumRadialSegmentsPerRadian; |
| numRadialSegments = std::max(std::ceil(numRadialSegments), 1.f); |
| float numParametricSegments = GrWangsFormula::root4(numParametricSegments_pow4); |
| numParametricSegments = std::max(std::ceil(numParametricSegments), 1.f); |
| float numCombinedSegments = num_combined_segments(numParametricSegments, numRadialSegments); |
| if (numCombinedSegments > fMaxTessellationSegments) { |
| // The hardware doesn't support enough segments for this curve. Chop and recurse. |
| if (maxDepth < 0) { |
| // Decide on an extremely conservative upper bound for when to quit chopping. This |
| // is solely to protect us from infinite recursion in instances where FP error |
| // prevents us from chopping at the correct midtangent. |
| maxDepth = sk_float_nextlog2(numParametricSegments) + |
| sk_float_nextlog2(numRadialSegments) + 1; |
| maxDepth = std::max(maxDepth, 1); |
| } |
| if (w == 1) { |
| SkPoint chops[5]; |
| if (numParametricSegments >= numRadialSegments) { |
| SkChopQuadAtHalf(p, chops); |
| } else { |
| SkChopQuadAtMidTangent(p, chops); |
| } |
| this->conicTo(prevJoinType, chops, 1, maxDepth - 1); |
| this->conicTo(JoinType::kBowtie, chops + 2, 1, maxDepth - 1); |
| } else { |
| SkConic conic(p, w); |
| float chopT = (numParametricSegments >= numRadialSegments) ? .5f |
| : conic.findMidTangent(); |
| SkConic chops[2]; |
| if (conic.chopAt(chopT, chops)) { |
| this->conicTo(prevJoinType, chops[0].fPts, chops[0].fW, maxDepth - 1); |
| this->conicTo(JoinType::kBowtie, chops[1].fPts, chops[1].fW, maxDepth - 1); |
| } |
| } |
| return; |
| } |
| |
| if (numCombinedSegments > fMaxCombinedSegments_withJoin) { |
| // There aren't enough guaranteed segments to include the join. Emit a standalone patch for |
| // the join. |
| this->joinTo(prevJoinType, asPatch); |
| prevJoinType = JoinType::kNone; |
| } |
| this->emitPatch(prevJoinType, asPatch, p[2]); |
| } |
| |
| void GrStrokeHardwareTessellator::cubicTo(JoinType prevJoinType, const SkPoint p[4], |
| Convex180Status convex180Status, int maxDepth) { |
| // The stroke tessellation shader assigns special meaning to p0==p1==p2 and p1==p2==p3. If this |
| // is the case then we need to rewrite the cubic. |
| if (p[1] == p[2] && (p[1] == p[0] || p[1] == p[3])) { |
| this->lineTo(prevJoinType, p[0], p[3]); |
| return; |
| } |
| |
| // Ensure our hardware supports enough tessellation segments to render the curve. This early out |
| // assumes a worst-case cubic rotation of 360 degrees and a worst-case number of segments in the |
| // join. |
| // |
| // An informal survey of skottie animations revealed that with a bare minimum of 64 tessellation |
| // segments, 95% of cubics take this early out. |
| float numParametricSegments_pow4 = |
| GrWangsFormula::cubic_pow4(fTolerances.fParametricIntolerance, p); |
| if (numParametricSegments_pow4 <= fMaxParametricSegments360_pow4_withJoin) { |
| this->emitPatch(prevJoinType, p, p[3]); |
| return; |
| } |
| |
| float maxParametricSegments_pow4 = (convex180Status == Convex180Status::kYes) ? |
| fMaxParametricSegments180_pow4 : fMaxParametricSegments360_pow4; |
| if (numParametricSegments_pow4 <= maxParametricSegments_pow4 || maxDepth == 0) { |
| float maxParametricSegments_pow4_withJoin = (convex180Status == Convex180Status::kYes) ? |
| fMaxParametricSegments180_pow4_withJoin : fMaxParametricSegments360_pow4_withJoin; |
| if (numParametricSegments_pow4 > maxParametricSegments_pow4_withJoin) { |
| // There aren't enough guaranteed segments to include the join. Emit a standalone patch |
| // for the join. |
| this->joinTo(prevJoinType, p); |
| prevJoinType = JoinType::kNone; |
| } |
| this->emitPatch(prevJoinType, p, p[3]); |
| return; |
| } |
| |
| // Ensure the curve does not inflect or rotate >180 degrees before we start subdividing and |
| // measuring rotation. |
| if (convex180Status == Convex180Status::kUnknown) { |
| this->cubicConvex180SegmentsTo(prevJoinType, p); |
| return; |
| } |
| |
| // We still might have enough tessellation segments to render the curve. Check again with |
| // its actual rotation. |
| float numRadialSegments = |
| SkMeasureNonInflectCubicRotation(p) * fTolerances.fNumRadialSegmentsPerRadian; |
| numRadialSegments = std::max(std::ceil(numRadialSegments), 1.f); |
| float numParametricSegments = GrWangsFormula::root4(numParametricSegments_pow4); |
| numParametricSegments = std::max(std::ceil(numParametricSegments), 1.f); |
| float numCombinedSegments = num_combined_segments(numParametricSegments, numRadialSegments); |
| if (numCombinedSegments > fMaxTessellationSegments) { |
| // The hardware doesn't support enough segments for this curve. Chop and recurse. |
| SkPoint chops[7]; |
| if (maxDepth < 0) { |
| // Decide on an extremely conservative upper bound for when to quit chopping. This |
| // is solely to protect us from infinite recursion in instances where FP error |
| // prevents us from chopping at the correct midtangent. |
| maxDepth = sk_float_nextlog2(numParametricSegments) + |
| sk_float_nextlog2(numRadialSegments) + 1; |
| maxDepth = std::max(maxDepth, 1); |
| } |
| if (numParametricSegments >= numRadialSegments) { |
| SkChopCubicAtHalf(p, chops); |
| } else { |
| SkChopCubicAtMidTangent(p, chops); |
| } |
| this->cubicTo(prevJoinType, chops, Convex180Status::kYes, maxDepth - 1); |
| this->cubicTo(JoinType::kBowtie, chops + 3, Convex180Status::kYes, maxDepth - 1); |
| return; |
| } |
| |
| if (numCombinedSegments > fMaxCombinedSegments_withJoin) { |
| // There aren't enough guaranteed segments to include the join. Emit a standalone patch for |
| // the join. |
| this->joinTo(prevJoinType, p); |
| prevJoinType = JoinType::kNone; |
| } |
| this->emitPatch(prevJoinType, p, p[3]); |
| } |
| |
| void GrStrokeHardwareTessellator::cubicConvex180SegmentsTo(JoinType prevJoinType, |
| const SkPoint p[4]) { |
| SkPoint chops[10]; |
| float chopT[2]; |
| bool areCusps = false; |
| int numChops = GrPathUtils::findCubicConvex180Chops(p, chopT, &areCusps); |
| if (numChops == 0) { |
| // The curve is already convex and rotates no more than 180 degrees. |
| this->cubicTo(prevJoinType, p, Convex180Status::kYes); |
| } else if (numChops == 1) { |
| SkChopCubicAt(p, chops, chopT[0]); |
| if (areCusps) { |
| // When chopping on a perfect cusp, these 3 points will be equal. |
| chops[2] = chops[4] = chops[3]; |
| } |
| this->cubicTo(prevJoinType, chops, Convex180Status::kYes); |
| this->cubicTo(JoinType::kBowtie, chops + 3, Convex180Status::kYes); |
| } else { |
| SkASSERT(numChops == 2); |
| SkChopCubicAt(p, chops, chopT[0], chopT[1]); |
| // Two cusps are only possible on a flat line with two 180-degree turnarounds. |
| if (areCusps) { |
| this->lineTo(prevJoinType, chops[0], chops[3]); |
| this->lineTo(JoinType::kBowtie, chops[3], chops[6]); |
| this->lineTo(JoinType::kBowtie, chops[6], chops[9]); |
| return; |
| } |
| this->cubicTo(prevJoinType, chops, Convex180Status::kYes); |
| this->cubicTo(JoinType::kBowtie, chops + 3, Convex180Status::kYes); |
| this->cubicTo(JoinType::kBowtie, chops + 6, Convex180Status::kYes); |
| } |
| } |
| |
| void GrStrokeHardwareTessellator::joinTo(JoinType joinType, SkPoint junctionPoint, |
| SkPoint nextControlPoint, int maxDepth) { |
| if (!fHasLastControlPoint) { |
| // The first stroke doesn't have a previous join. |
| return; |
| } |
| |
| if (!fSoloRoundJoinAlwaysFitsInPatch && maxDepth != 0 && |
| (joinType == JoinType::kRound || joinType == JoinType::kBowtie)) { |
| SkVector tan0 = junctionPoint - fLastControlPoint; |
| SkVector tan1 = nextControlPoint - junctionPoint; |
| float rotation = SkMeasureAngleBetweenVectors(tan0, tan1); |
| float numRadialSegments = rotation * fTolerances.fNumRadialSegmentsPerRadian; |
| if (numRadialSegments > fMaxTessellationSegments) { |
| // This is a round join that requires more segments than the tessellator supports. |
| // Split it and recurse. |
| if (maxDepth < 0) { |
| // Decide on an upper bound for when to quit chopping. This is solely to protect |
| // us from infinite recursion due to FP precision issues. |
| maxDepth = sk_float_nextlog2(numRadialSegments / fMaxTessellationSegments); |
| maxDepth = std::max(maxDepth, 1); |
| } |
| // Find the bisector so we can split the join in half. |
| SkPoint bisector = SkFindBisector(tan0, tan1); |
| // c0 will be the "next" control point for the first join half, and c1 will be the |
| // "previous" control point for the second join half. |
| SkPoint c0, c1; |
| // FIXME(skia:11347): This hack ensures "c0 - junctionPoint" gives the exact same ieee |
| // fp32 vector as "-(c1 - junctionPoint)". Tessellated stroking is becoming less |
| // experimental, so t's time to think of a cleaner method to avoid T-junctions when we |
| // chop joins. |
| int maxAttempts = 10; |
| do { |
| bisector = (junctionPoint + bisector) - (junctionPoint - bisector); |
| c0 = junctionPoint + bisector; |
| c1 = junctionPoint - bisector; |
| } while (c0 - junctionPoint != -(c1 - junctionPoint) && --maxAttempts); |
| this->joinTo(joinType, junctionPoint, c0, maxDepth - 1); // First join half. |
| fLastControlPoint = c1; |
| // Second join half. |
| this->joinTo(joinType, junctionPoint, nextControlPoint, maxDepth - 1); |
| return; |
| } |
| } |
| |
| this->emitJoinPatch(joinType, junctionPoint, nextControlPoint); |
| } |
| |
| void GrStrokeHardwareTessellator::close(SkPoint contourEndpoint, const SkMatrix& viewMatrix, |
| const SkStrokeRec& stroke) { |
| if (!fHasLastControlPoint) { |
| // Draw caps instead of closing if the subpath is zero length: |
| // |
| // "Any zero length subpath ... shall be stroked if the 'stroke-linecap' property has a |
| // value of round or square producing respectively a circle or a square." |
| // |
| // (https://www.w3.org/TR/SVG11/painting.html#StrokeProperties) |
| // |
| this->cap(contourEndpoint, viewMatrix, stroke); |
| return; |
| } |
| |
| // Draw a line back to the beginning. (This will be discarded if |
| // contourEndpoint == fCurrContourStartPoint.) |
| auto strokeJoinType = JoinType(stroke.getJoin()); |
| this->lineTo(strokeJoinType, contourEndpoint, fCurrContourStartPoint); |
| this->joinTo(strokeJoinType, fCurrContourStartPoint, fCurrContourFirstControlPoint); |
| |
| fHasLastControlPoint = false; |
| } |
| |
| void GrStrokeHardwareTessellator::cap(SkPoint contourEndpoint, const SkMatrix& viewMatrix, |
| const SkStrokeRec& stroke) { |
| if (!fHasLastControlPoint) { |
| // We don't have any control points to orient the caps. In this case, square and round caps |
| // are specified to be drawn as an axis-aligned square or circle respectively. Assign |
| // default control points that achieve this. |
| SkVector outset; |
| if (!stroke.isHairlineStyle()) { |
| outset = {1, 0}; |
| } else { |
| // If the stroke is hairline, orient the square on the post-transform x-axis instead. |
| // We don't need to worry about the vector length since it will be normalized later. |
| // Since the matrix cannot have perspective, the below is equivalent to: |
| // |
| // outset = inverse(|a b|) * |1| * arbitrary_scale |
| // |c d| |0| |
| // |
| // == 1/det * | d -b| * |1| * arbitrary_scale |
| // |-c a| |0| |
| // |
| // == 1/det * | d| * arbitrary_scale |
| // |-c| |
| // |
| // == | d| |
| // |-c| |
| // |
| SkASSERT(!viewMatrix.hasPerspective()); |
| float c=viewMatrix.getSkewY(), d=viewMatrix.getScaleY(); |
| outset = {d, -c}; |
| } |
| fCurrContourFirstControlPoint = fCurrContourStartPoint - outset; |
| fLastControlPoint = fCurrContourStartPoint + outset; |
| fHasLastControlPoint = true; |
| contourEndpoint = fCurrContourStartPoint; |
| } |
| |
| switch (stroke.getCap()) { |
| case SkPaint::kButt_Cap: |
| break; |
| case SkPaint::kRound_Cap: { |
| // A round cap is the same thing as a 180-degree round join. |
| // If our join type isn't round we can alternatively use a bowtie. |
| JoinType roundCapJoinType = (stroke.getJoin() == SkPaint::kRound_Join) |
| ? JoinType::kRound : JoinType::kBowtie; |
| this->joinTo(roundCapJoinType, contourEndpoint, fLastControlPoint); |
| this->moveTo(fCurrContourStartPoint, fCurrContourFirstControlPoint); |
| this->joinTo(roundCapJoinType, fCurrContourStartPoint, |
| fCurrContourFirstControlPoint); |
| break; |
| } |
| case SkPaint::kSquare_Cap: { |
| // A square cap is the same as appending lineTos. |
| auto strokeJoinType = JoinType(stroke.getJoin()); |
| SkVector lastTangent = contourEndpoint - fLastControlPoint; |
| if (!stroke.isHairlineStyle()) { |
| // Extend the cap by 1/2 stroke width. |
| lastTangent *= (.5f * stroke.getWidth()) / lastTangent.length(); |
| } else { |
| // Extend the cap by what will be 1/2 pixel after transformation. |
| lastTangent *= .5f / viewMatrix.mapVector(lastTangent.fX, lastTangent.fY).length(); |
| } |
| this->lineTo(strokeJoinType, contourEndpoint, contourEndpoint + lastTangent); |
| this->moveTo(fCurrContourStartPoint, fCurrContourFirstControlPoint); |
| SkVector firstTangent = fCurrContourFirstControlPoint - fCurrContourStartPoint; |
| if (!stroke.isHairlineStyle()) { |
| // Set the the cap back by 1/2 stroke width. |
| firstTangent *= (-.5f * stroke.getWidth()) / firstTangent.length(); |
| } else { |
| // Set the cap back by what will be 1/2 pixel after transformation. |
| firstTangent *= |
| -.5f / viewMatrix.mapVector(firstTangent.fX, firstTangent.fY).length(); |
| } |
| this->lineTo(strokeJoinType, fCurrContourStartPoint, |
| fCurrContourStartPoint + firstTangent); |
| break; |
| } |
| } |
| |
| fHasLastControlPoint = false; |
| } |
| |
| void GrStrokeHardwareTessellator::emitPatch(JoinType prevJoinType, const SkPoint p[4], |
| SkPoint endPt) { |
| SkPoint c1 = (p[1] == p[0]) ? p[2] : p[1]; |
| SkPoint c2 = (p[2] == endPt) ? p[1] : p[2]; |
| |
| if (prevJoinType == JoinType::kBowtie) { |
| // Bowties need to go in their own patch if they will have >1 segment. |
| // TODO: Investigate if an optimization like "x < fCosRadiansPerSegment" would be worth it. |
| float rotation = SkMeasureAngleBetweenVectors(p[0] - fLastControlPoint, c1 - p[0]); |
| if (rotation * fTolerances.fNumRadialSegmentsPerRadian > 1) { |
| this->joinTo(prevJoinType, p[0], c1); |
| prevJoinType = JoinType::kNone; |
| } |
| } |
| |
| if (!fHasLastControlPoint) { |
| // The first stroke doesn't have a previous join (yet). If the current contour ends up |
| // closing itself, we will add that join as its own patch. |
| // TODO: Consider deferring the first stroke until we know whether the contour will close. |
| // This will allow us to use the closing join as the first patch's previous join. |
| prevJoinType = JoinType::kNone; |
| fCurrContourFirstControlPoint = c1; |
| fHasLastControlPoint = true; |
| } else { |
| // By using JoinType::kNone, the caller promises to have written out their own join that |
| // seams exactly with this curve. |
| SkASSERT((prevJoinType != JoinType::kNone) || fLastControlPoint == c1); |
| } |
| |
| if (this->reservePatch()) { |
| // Disable the join section of this patch if prevJoinType is kNone by setting the previous |
| // control point equal to p0. |
| fPatchWriter.write((prevJoinType == JoinType::kNone) ? p[0] : fLastControlPoint); |
| fPatchWriter.writeArray(p, 4); |
| this->emitDynamicAttribs(); |
| } |
| |
| fLastControlPoint = c2; |
| } |
| |
| void GrStrokeHardwareTessellator::emitJoinPatch(JoinType joinType, SkPoint junctionPoint, |
| SkPoint nextControlPoint) { |
| // We should never write out joins before the first curve. |
| SkASSERT(fHasLastControlPoint); |
| |
| if (this->reservePatch()) { |
| fPatchWriter.write(fLastControlPoint, junctionPoint); |
| if (joinType == JoinType::kBowtie) { |
| // {prevControlPoint, [p0, p0, p0, p3]} is a reserved patch pattern that means this |
| // patch is a bowtie. The bowtie is anchored on p0 and its tangent angles go from |
| // (p0 - prevControlPoint) to (p3 - p0). |
| fPatchWriter.write(junctionPoint, junctionPoint); |
| } else { |
| SkASSERT(joinType != JoinType::kNone); |
| // {prevControlPoint, [p0, p3, p3, p3]} is a reserved patch pattern that means this |
| // patch is a join only (no curve sections in the patch). The join is anchored on p0 and |
| // its tangent angles go from (p0 - prevControlPoint) to (p3 - p0). |
| fPatchWriter.write(nextControlPoint, nextControlPoint); |
| } |
| fPatchWriter.write(nextControlPoint); |
| this->emitDynamicAttribs(); |
| } |
| |
| fLastControlPoint = nextControlPoint; |
| } |
| |
| void GrStrokeHardwareTessellator::emitDynamicAttribs() { |
| if (fShaderFlags & ShaderFlags::kDynamicStroke) { |
| fPatchWriter.write(fDynamicStroke); |
| } |
| if (fShaderFlags & ShaderFlags::kDynamicColor) { |
| fPatchWriter.write(fDynamicColor); |
| } |
| } |
| |
| bool GrStrokeHardwareTessellator::reservePatch() { |
| if (fPatchChunks.back().fPatchCount >= fCurrChunkPatchCapacity) { |
| // The current chunk is full. Time to allocate a new one. (And no need to put back vertices; |
| // the buffer is full.) |
| this->allocPatchChunkAtLeast(fCurrChunkMinPatchAllocCount * 2); |
| } |
| if (!fPatchWriter.isValid()) { |
| SkDebugf("WARNING: Failed to allocate vertex buffer for tessellated stroke."); |
| return false; |
| } |
| SkASSERT(fPatchChunks.back().fPatchCount <= fCurrChunkPatchCapacity); |
| ++fPatchChunks.back().fPatchCount; |
| return true; |
| } |
| |
| void GrStrokeHardwareTessellator::allocPatchChunkAtLeast(int minPatchAllocCount) { |
| SkASSERT(fTarget); |
| PatchChunk* chunk = &fPatchChunks.push_back(); |
| fPatchWriter = {fTarget->makeVertexSpaceAtLeast(fPatchStride, minPatchAllocCount, |
| minPatchAllocCount, &chunk->fPatchBuffer, |
| &chunk->fBasePatch, &fCurrChunkPatchCapacity)}; |
| fCurrChunkMinPatchAllocCount = minPatchAllocCount; |
| } |
| |
| void GrStrokeHardwareTessellator::draw(GrOpFlushState* flushState) const { |
| for (const auto& chunk : fPatchChunks) { |
| if (chunk.fPatchBuffer) { |
| flushState->bindBuffers(nullptr, nullptr, std::move(chunk.fPatchBuffer)); |
| flushState->draw(chunk.fPatchCount, chunk.fBasePatch); |
| } |
| } |
| } |