/* * Copyright 2022 Google LLC * * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. */ #ifndef skgpu_tessellate_LinearTolerances_DEFINED #define skgpu_tessellate_LinearTolerances_DEFINED #include "include/core/SkScalar.h" #include "include/private/base/SkAssert.h" #include "src/gpu/tessellate/Tessellation.h" #include "src/gpu/tessellate/WangsFormula.h" #include namespace skgpu::tess { /** * LinearTolerances stores state to approximate the final device-space transform applied * to curves, and uses that to calculate segmentation levels for both the parametric curves and * radial components (when stroking, where you have to represent the offset of a curve). * These tolerances determine the worst-case number of parametric and radial segments required to * accurately linearize curves. * - segments = a linear subsection on the curve, either defined as parametric (linear in t) or * radial (linear in curve's internal rotation). * - edges = orthogonal geometry to segments, used in stroking to offset from the central curve by * half the stroke width, or to construct the join geometry. * * The tolerance values and decisions are estimated in the local path space, although PatchWriter * uses a 2x2 vector transform that approximates the scale/skew (as-best-as-possible) of the full * local-to-device transform applied in the vertex shader. * * The properties tracked in LinearTolerances can be used to compute the final segmentation factor * for filled paths (the resolve level) or stroked paths (the number of edges). */ class LinearTolerances { public: float numParametricSegments_p4() const { return fNumParametricSegments_p4; } float numRadialSegmentsPerRadian() const { return fNumRadialSegmentsPerRadian; } int numEdgesInJoins() const { return fEdgesInJoins; } // Fast log2 of minimum required # of segments per tracked Wang's formula calculations. int requiredResolveLevel() const { // log16(n^4) == log2(n) return wangs_formula::nextlog16(fNumParametricSegments_p4); } int requiredStrokeEdges() const { // The maximum rotation we can have in a stroke is 180 degrees (SK_ScalarPI radians). int maxRadialSegmentsInStroke = std::max(SkScalarCeilToInt(fNumRadialSegmentsPerRadian * SK_ScalarPI), 1); int maxParametricSegmentsInStroke = SkScalarCeilToInt(wangs_formula::root4(fNumParametricSegments_p4)); SkASSERT(maxParametricSegmentsInStroke >= 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 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 is full-width. return fEdgesInJoins + maxEdgesInStroke; } void setParametricSegments(float n4) { SkASSERT(n4 >= 0.f); fNumParametricSegments_p4 = n4; } void setStroke(const StrokeParams& strokeParams, float maxScale) { float approxDeviceStrokeRadius; if (strokeParams.fRadius == 0.f) { // Hairlines are always 1 px wide approxDeviceStrokeRadius = 0.5f; } else { // Approximate max scale * local stroke width / 2 approxDeviceStrokeRadius = strokeParams.fRadius * maxScale; } fNumRadialSegmentsPerRadian = CalcNumRadialSegmentsPerRadian(approxDeviceStrokeRadius); fEdgesInJoins = NumFixedEdgesInJoin(strokeParams); if (strokeParams.fJoinType < 0.f && fNumRadialSegmentsPerRadian > 0.f) { // 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). fEdgesInJoins += SkScalarCeilToInt(fNumRadialSegmentsPerRadian * SK_ScalarPI) - 1; } } void accumulate(const LinearTolerances& tolerances) { if (tolerances.fNumParametricSegments_p4 > fNumParametricSegments_p4) { fNumParametricSegments_p4 = tolerances.fNumParametricSegments_p4; } if (tolerances.fNumRadialSegmentsPerRadian > fNumRadialSegmentsPerRadian) { fNumRadialSegmentsPerRadian = tolerances.fNumRadialSegmentsPerRadian; } if (tolerances.fEdgesInJoins > fEdgesInJoins) { fEdgesInJoins = tolerances.fEdgesInJoins; } } private: // Used for both fills and strokes, always at least one parametric segment float fNumParametricSegments_p4 = 1.f; // Used for strokes, adding additional segments along the curve to account for its rotation // TODO: Currently we assume the worst case 180 degree rotation for any curve, but tracking // max(radialSegments * patch curvature) would be tighter. This would require computing // rotation per patch, which could be approximated by tracking min of the tangent dot // products, but then we'd be left with the slightly less accurate // "max(radialSegments) * acos(min(tan dot product))". It is also unknown if requesting // tighter bounds pays off with less GPU work for more CPU work float fNumRadialSegmentsPerRadian = 0.f; // Used for strokes, tracking the number of additional vertices required to handle joins // based on the join type and stroke width. // TODO: For round joins, we could also track the rotation angle of the join, instead of // assuming 180 degrees. PatchWriter has all necessary control points to do so, but runs // into similar trade offs between CPU vs GPU work, and accuracy vs. reducing calls to acos. int fEdgesInJoins = 0; }; } // namespace skgpu::tess #endif // skgpu_tessellate_LinearTolerances_DEFINED