/* * Copyright 2021 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/GrStrokeFixedCountTessellator.h" #include "src/core/SkGeometry.h" #include "src/gpu/geometry/GrPathUtils.h" #include "src/gpu/geometry/GrWangsFormula.h" #include "src/gpu/tessellate/GrStrokeIterator.h" namespace { constexpr static float kMaxParametricSegments_pow4 = 48*48*48*48; // 48^4 // Writes out strokes to the given instance chunk array, chopping if necessary so that all instances // require 48 parametric segments or less. (We don't consider radial segments here. The tessellator // will just add enough additional segments to handle a worst-case 180 degree stroke.) class InstanceWriter { public: using ShaderFlags = GrStrokeTessellator::ShaderFlags; InstanceWriter(ShaderFlags shaderFlags, GrMeshDrawOp::Target* target, float matrixMaxScale, GrVertexChunkArray* patchChunks, size_t instanceStride, int minInstancesPerChunk) : fShaderFlags(shaderFlags) , fChunkBuilder(target, patchChunks, instanceStride, minInstancesPerChunk) , fParametricPrecision(GrStrokeTolerances::CalcParametricPrecision(matrixMaxScale)) { } float parametricPrecision() const { return fParametricPrecision; } // maxParametricSegments^4, or the number of parametric segments, raised to the 4th power, // that are required by the single instance we've written that requires the most segments. float maxParametricSegments_pow4() const { return fMaxParametricSegments_pow4; } // Updates the dynamic stroke state that we will write out with each instance. void updateDynamicStroke(const SkStrokeRec& stroke) { SkASSERT(!fHasDeferredFirstStroke); SkASSERT(fShaderFlags & ShaderFlags::kDynamicStroke); fDynamicStroke.set(stroke); } // Updates the dynamic color state that we will write out with each instance. void updateDynamicColor(const SkPMColor4f& color) { SkASSERT(!fHasDeferredFirstStroke); SkASSERT(fShaderFlags & ShaderFlags::kDynamicColor); bool wideColor = fShaderFlags & ShaderFlags::kWideColor; SkASSERT(wideColor || color.fitsInBytes()); fDynamicColor.set(color, wideColor); } void lineTo(SkPoint start, SkPoint end) { SkPoint cubic[] = {start, start, end, end}; SkPoint endControlPoint = start; this->writeStroke(cubic, endControlPoint); } void quadraticTo(const SkPoint p[3]) { float numParametricSegments_pow4 = GrWangsFormula::quadratic_pow4(fParametricPrecision, p); if (numParametricSegments_pow4 > kMaxParametricSegments_pow4) { SkPoint chops[5]; SkChopQuadAtHalf(p, chops); this->quadraticTo(chops); this->quadraticTo(chops + 2); return; } SkPoint cubic[4]; GrPathUtils::convertQuadToCubic(p, cubic); SkPoint endControlPoint = cubic[2]; this->writeStroke(cubic, endControlPoint); fMaxParametricSegments_pow4 = std::max(numParametricSegments_pow4, fMaxParametricSegments_pow4); } void conicTo(const SkPoint p[3], float w) { float numParametricSegments_pow2 = GrWangsFormula::conic_pow2(1/fParametricPrecision, p, w); float numParametricSegments_pow4 = numParametricSegments_pow2 * numParametricSegments_pow2; if (numParametricSegments_pow4 > kMaxParametricSegments_pow4) { SkConic chops[2]; if (SkConic(p, w).chopAt(.5f, chops)) { this->conicTo(chops[0].fPts, chops[0].fW); this->conicTo(chops[1].fPts, chops[1].fW); return; } } SkPoint conic[4]; GrPathShader::WriteConicPatch(p, w, conic); SkPoint endControlPoint = conic[1]; this->writeStroke(conic, endControlPoint); fMaxParametricSegments_pow4 = std::max(numParametricSegments_pow4, fMaxParametricSegments_pow4); } void cubicConvex180To(const SkPoint p[4]) { float numParametricSegments_pow4 = GrWangsFormula::cubic_pow4(fParametricPrecision, p); if (numParametricSegments_pow4 > kMaxParametricSegments_pow4) { SkPoint chops[7]; SkChopCubicAtHalf(p, chops); this->cubicConvex180To(chops); this->cubicConvex180To(chops + 3); return; } SkPoint endControlPoint = (p[3] != p[2]) ? p[2] : (p[2] != p[1]) ? p[1] : p[0]; this->writeStroke(p, endControlPoint); fMaxParametricSegments_pow4 = std::max(numParametricSegments_pow4, fMaxParametricSegments_pow4); } // Called when we encounter the verb "kMoveWithinContour". Moves invalidate the previous control // point. The stroke iterator tells us the new value to use for the previous control point. void setLastControlPoint(SkPoint newLastControlPoint) { fLastControlPoint = newLastControlPoint; fHasLastControlPoint = true; } // Draws a circle whose diameter is equal to the stroke width. We emit circles at cusp points // round caps, and empty strokes that are specified to be drawn as circles. void writeCircle(SkPoint location) { if (GrVertexWriter writer = fChunkBuilder.appendVertex()) { // The shader interprets an empty stroke + empty join as a special case that denotes a // circle, or 180-degree point stroke. writer.fill(location, 5); this->writeDynamicAttribs(&writer); } } void finishContour() { if (fHasDeferredFirstStroke) { // We deferred the first stroke because we didn't know the previous control point to use // for its join. We write it out now. SkASSERT(fHasLastControlPoint); this->writeStroke(fDeferredFirstStroke, SkPoint()); fHasDeferredFirstStroke = false; } fHasLastControlPoint = false; } private: void writeStroke(const SkPoint p[4], SkPoint endControlPoint) { if (!fHasLastControlPoint) { // We don't know the previous control point yet to use for the join. Defer writing out // this stroke until the end. memcpy(fDeferredFirstStroke, p, sizeof(fDeferredFirstStroke)); fHasDeferredFirstStroke = true; fHasLastControlPoint = true; } else if (GrVertexWriter writer = fChunkBuilder.appendVertex()) { writer.writeArray(p, 4); writer.write(fLastControlPoint); this->writeDynamicAttribs(&writer); } fLastControlPoint = endControlPoint; } void writeDynamicAttribs(GrVertexWriter* writer) { if (fShaderFlags & ShaderFlags::kDynamicStroke) { writer->write(fDynamicStroke); } if (fShaderFlags & ShaderFlags::kDynamicColor) { writer->write(fDynamicColor); } } const ShaderFlags fShaderFlags; GrVertexChunkBuilder fChunkBuilder; const float fParametricPrecision; float fMaxParametricSegments_pow4 = 1; // We can't write out the first stroke until we know the previous control point for its join. SkPoint fDeferredFirstStroke[4]; SkPoint fLastControlPoint; // Used to configure the joins in the instance data. bool fHasDeferredFirstStroke = false; bool fHasLastControlPoint = false; // Values for the current dynamic state (if any) that will get written out with each instance. GrStrokeTessellateShader::DynamicStroke fDynamicStroke; GrVertexColor fDynamicColor; }; // Returns the worst-case number of edges we will need in order to draw a join of the given type. static int worst_case_edges_in_join(SkPaint::Join joinType, float numRadialSegmentsPerRadian) { int numEdges = GrStrokeTessellateShader::NumFixedEdgesInJoin(joinType); if (joinType == SkPaint::kRound_Join) { // For round joins we need to count the radial edges on our own. Account for a worst-case // join of 180 degrees (SK_ScalarPI radians). numEdges += std::max(SkScalarCeilToInt(numRadialSegmentsPerRadian * SK_ScalarPI) - 1, 0); } return numEdges; } } // namespace void GrStrokeFixedCountTessellator::prepare(GrMeshDrawOp::Target* target, int totalCombinedVerbCnt) { int maxEdgesInJoin = 0; float maxRadialSegmentsPerRadian = 0; std::array matrixMinMaxScales; if (!fShader.viewMatrix().getMinMaxScales(matrixMinMaxScales.data())) { matrixMinMaxScales.fill(1); } // Over-allocate enough patches for each stroke to chop once, and for 8 extra caps. Since we // have to chop at inflections, points of 180 degree rotation, and anywhere a stroke requires // too many parametric segments, many strokes will end up getting choppped. int strokePreallocCount = totalCombinedVerbCnt * 2; int capPreallocCount = 8; int minInstancesPerChunk = strokePreallocCount + capPreallocCount; InstanceWriter instanceWriter(fShaderFlags, target, matrixMinMaxScales[1], &fInstanceChunks, fShader.instanceStride(), minInstancesPerChunk); if (!(fShaderFlags & ShaderFlags::kDynamicStroke)) { // Strokes are static. Calculate tolerances once. const SkStrokeRec& stroke = fPathStrokeList->fStroke; float localStrokeWidth = GrStrokeTolerances::GetLocalStrokeWidth(matrixMinMaxScales.data(), stroke.getWidth()); float numRadialSegmentsPerRadian = GrStrokeTolerances::CalcNumRadialSegmentsPerRadian( instanceWriter.parametricPrecision(), localStrokeWidth); maxEdgesInJoin = worst_case_edges_in_join(stroke.getJoin(), numRadialSegmentsPerRadian); maxRadialSegmentsPerRadian = numRadialSegmentsPerRadian; } // Fast SIMD queue that buffers up values for "numRadialSegmentsPerRadian". Only used when we // have dynamic stroke. GrStrokeToleranceBuffer toleranceBuffer(instanceWriter.parametricPrecision()); for (PathStrokeList* pathStroke = fPathStrokeList; pathStroke; pathStroke = pathStroke->fNext) { const SkStrokeRec& stroke = pathStroke->fStroke; if (fShaderFlags & ShaderFlags::kDynamicStroke) { // Strokes are dynamic. Calculate tolerances every time. float numRadialSegmentsPerRadian = toleranceBuffer.fetchRadialSegmentsPerRadian(pathStroke); maxEdgesInJoin = std::max( worst_case_edges_in_join(stroke.getJoin(), numRadialSegmentsPerRadian), maxEdgesInJoin); maxRadialSegmentsPerRadian = std::max(numRadialSegmentsPerRadian, maxRadialSegmentsPerRadian); instanceWriter.updateDynamicStroke(stroke); } if (fShaderFlags & ShaderFlags::kDynamicColor) { instanceWriter.updateDynamicColor(pathStroke->fColor); } GrStrokeIterator strokeIter(pathStroke->fPath, &pathStroke->fStroke, &fShader.viewMatrix()); while (strokeIter.next()) { const SkPoint* p = strokeIter.pts(); switch (strokeIter.verb()) { using Verb = GrStrokeIterator::Verb; int numChops; case Verb::kContourFinished: instanceWriter.finishContour(); break; case Verb::kCircle: // Round cap or else an empty stroke that is specified to be drawn as a circle. instanceWriter.writeCircle(p[0]); [[fallthrough]]; case Verb::kMoveWithinContour: instanceWriter.setLastControlPoint(p[0]); break; case Verb::kLine: instanceWriter.lineTo(p[0], p[1]); break; case Verb::kQuad: if (GrPathUtils::conicHasCusp(p)) { // The cusp is always at the midtandent. SkPoint cusp = SkEvalQuadAt(p, SkFindQuadMidTangent(p)); instanceWriter.writeCircle(cusp); // A quad can only have a cusp if it's flat with a 180-degree turnaround. instanceWriter.lineTo(p[0], cusp); instanceWriter.lineTo(cusp, p[2]); } else { instanceWriter.quadraticTo(p); } break; case Verb::kConic: if (GrPathUtils::conicHasCusp(p)) { // The cusp is always at the midtandent. SkConic conic(p, strokeIter.w()); SkPoint cusp = conic.evalAt(conic.findMidTangent()); instanceWriter.writeCircle(cusp); // A conic can only have a cusp if it's flat with a 180-degree turnaround. instanceWriter.lineTo(p[0], cusp); instanceWriter.lineTo(cusp, p[2]); } else { instanceWriter.conicTo(p, strokeIter.w()); } break; case Verb::kCubic: SkPoint chops[10]; float T[2]; bool areCusps; numChops = GrPathUtils::findCubicConvex180Chops(p, T, &areCusps); if (numChops == 0) { instanceWriter.cubicConvex180To(p); } else if (numChops == 1) { SkChopCubicAt(p, chops, T[0]); if (areCusps) { instanceWriter.writeCircle(chops[3]); // In a perfect world, these 3 points would be be equal after chopping // on a cusp. chops[2] = chops[4] = chops[3]; } instanceWriter.cubicConvex180To(chops); instanceWriter.cubicConvex180To(chops + 3); } else { SkASSERT(numChops == 2); SkChopCubicAt(p, chops, T[0], T[1]); if (areCusps) { instanceWriter.writeCircle(chops[3]); instanceWriter.writeCircle(chops[6]); // Two cusps are only possible if it's a flat line with two 180-degree // turnarounds. instanceWriter.lineTo(chops[0], chops[3]); instanceWriter.lineTo(chops[3], chops[6]); instanceWriter.lineTo(chops[6], chops[9]); } else { instanceWriter.cubicConvex180To(chops); instanceWriter.cubicConvex180To(chops + 3); instanceWriter.cubicConvex180To(chops + 6); } } break; } } } // The maximum rotation we can have in a stroke is 180 degrees (SK_ScalarPI radians). int maxRadialSegmentsInStroke = std::max(SkScalarCeilToInt(maxRadialSegmentsPerRadian * SK_ScalarPI), 1); int maxParametricSegmentsInStroke = SkScalarCeilToInt(sqrtf(sqrtf( instanceWriter.maxParametricSegments_pow4()))); SkASSERT(maxParametricSegmentsInStroke >= 1); // maxParametricSegments_pow4 is always >= 1. // Now calculate the maximum number of edges we will need in the stroke portion of the instance. // The first and last edges in a stroke 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: // // numSegments = numEdges - 1 // // So the total number of combined edges in the stroke is: // // numEdgesInStroke = numParametricSegments + 1 + numRadialSegments + 1 - 2 // = numParametricSegments + numRadialSegments // int maxEdgesInStroke = maxRadialSegmentsInStroke + maxParametricSegmentsInStroke; // Each triangle strip has two sections: It starts with a join then transitions to a stroke. The // number of edges in an instance is the sum of edges from the join and stroke sections both. // NOTE: The final join edge and the first stroke edge are co-located, however we still need to // emit both because the join's edge is half-width and the stroke's is full-width. int fixedEdgeCount = maxEdgesInJoin + maxEdgesInStroke; fShader.setFixedCountNumTotalEdges(fixedEdgeCount); fFixedVertexCount = fixedEdgeCount * 2; } void GrStrokeFixedCountTessellator::draw(GrOpFlushState* flushState) const { if (fInstanceChunks.empty() || fFixedVertexCount <= 0) { return; } for (const auto& instanceChunk : fInstanceChunks) { flushState->bindBuffers(nullptr, instanceChunk.fBuffer, nullptr); flushState->drawInstanced(instanceChunk.fCount, instanceChunk.fBase, fFixedVertexCount, 0); } }