Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


Quelle  sksl_graphite_vert.sksl   Sprache: unbekannt

 
// Graphite-specific vertex shader code

const float $PI = 3.141592653589793238;

///////////////////////////////////////////////////////////////////////////////////////////////////
// Support functions for tessellating path renderers

const float $kCubicCurveType = 0;            // skgpu::tess::kCubicCurveType
const float $kConicCurveType = 1;            // skgpu::tess::kConicCurveType
const float $kTriangularConicCurveType = 2;  // skgpu::tess::kTriangularConicCurveType

// This function can be used on GPUs with infinity support to infer the curve type from the specific
// path control-point encoding used by tessellating path renderers. Calling this function on a
// platform that lacks infinity support may result in a shader compilation error.
$pure float curve_type_using_inf_support(float4 p23) {
    return isinf(p23.z) ? $kTriangularConicCurveType :
           isinf(p23.w) ? $kConicCurveType :
                          $kCubicCurveType;
}

$pure bool $is_conic_curve(float curveType) {
    return curveType != $kCubicCurveType;
}

$pure bool $is_triangular_conic_curve(float curveType) {
    return curveType == $kTriangularConicCurveType;
}

// Wang's formula gives the minimum number of evenly spaced (in the parametric sense) line segments
// that a bezier curve must be chopped into in order to guarantee all lines stay within a distance
// of "1/precision" pixels from the true curve. Its definition for a bezier curve of degree "n" is
// as follows:
//
//     maxLength = max([length(p[i+2] - 2p[i+1] + p[i]) for (0 <= i <= n-2)])
//     numParametricSegments = sqrt(maxLength * precision * n*(n - 1)/8)
//
// (Goldman, Ron. (2003). 5.6.3 Wang's Formula. "Pyramid Algorithms: A Dynamic Programming Approach
// to Curves and Surfaces for Geometric Modeling". Morgan Kaufmann Publishers.)

const float $kDegree = 3;
const float $kPrecision = 4; // Must match skgpu::tess::kPrecision
const float $kLengthTerm     = ($kDegree * ($kDegree - 1) / 8.0) * $kPrecision;
const float $kLengthTermPow2 = (($kDegree * $kDegree) * (($kDegree - 1) * ($kDegree - 1)) / 64.0) *
                               ($kPrecision * $kPrecision);

// Returns the length squared of the largest forward difference from Wang's cubic formula.
$pure float $wangs_formula_max_fdiff_p2(float2 p0, float2 p1, float2 p2, float2 p3,
                                        float2x2 matrix) {
    float2 d0 = matrix * (fma(float2(-2), p1, p2) + p0);
    float2 d1 = matrix * (fma(float2(-2), p2, p3) + p1);
    return max(dot(d0,d0), dot(d1,d1));
}

$pure float $wangs_formula_cubic(float2 p0, float2 p1, float2 p2, float2 p3,
                                 float2x2 matrix) {
    float m = $wangs_formula_max_fdiff_p2(p0, p1, p2, p3, matrix);
    return max(ceil(sqrt($kLengthTerm * sqrt(m))), 1.0);
}

$pure float $wangs_formula_cubic_log2(float2 p0, float2 p1, float2 p2, float2 p3,
                                      float2x2 matrix) {
    float m = $wangs_formula_max_fdiff_p2(p0, p1, p2, p3, matrix);
    return ceil(log2(max($kLengthTermPow2 * m, 1.0)) * .25);
}

$pure float $wangs_formula_conic_p2(float2 p0, float2 p1, float2 p2, float w) {
    // Translate the bounding box center to the origin.
    float2 C = (min(min(p0, p1), p2) + max(max(p0, p1), p2)) * 0.5;
    p0 -= C;
    p1 -= C;
    p2 -= C;

    // Compute max length.
    float m = sqrt(max(max(dot(p0,p0), dot(p1,p1)), dot(p2,p2)));

    // Compute forward differences.
    float2 dp = fma(float2(-2.0 * w), p1, p0) + p2;
    float dw = abs(fma(-2.0, w, 2.0));

    // Compute numerator and denominator for parametric step size of linearization. Here, the
    // epsilon referenced from the cited paper is 1/precision.
    float rp_minus_1 = max(0.0, fma(m, $kPrecision, -1.0));
    float numer = length(dp) * $kPrecision + rp_minus_1 * dw;
    float denom = 4 * min(w, 1.0);

    return numer/denom;
}

$pure float $wangs_formula_conic(float2 p0, float2 p1, float2 p2, float w) {
    float n2 = $wangs_formula_conic_p2(p0, p1, p2, w);
    return max(ceil(sqrt(n2)), 1.0);
}

$pure float $wangs_formula_conic_log2(float2 p0, float2 p1, float2 p2, float w) {
    float n2 = $wangs_formula_conic_p2(p0, p1, p2, w);
    return ceil(log2(max(n2, 1.0)) * .5);
}

// Returns the normalized difference between a and b, i.e. normalize(a - b), with care taken for
// if 'a' and/or 'b' have large coordinates.
$pure float2 $robust_normalize_diff(float2 a, float2 b) {
    float2 diff = a - b;
    if (diff == float2(0.0)) {
        return float2(0.0);
    } else {
        float invMag = 1.0 / max(abs(diff.x), abs(diff.y));
        return normalize(invMag * diff);
    }
}

// Returns the cosine of the angle between a and b, assuming a and b are unit vectors already.
// Guaranteed to be between [-1, 1].
$pure float $cosine_between_unit_vectors(float2 a, float2 b) {
    // Since a and b are assumed to be normalized, the cosine is equal to the dot product, although
    // we clamp that to ensure it falls within the expected range of [-1, 1].
    return clamp(dot(a, b), -1.0, 1.0);
}

// Extends the middle radius to either the miter point, or the bevel edge if we surpassed the
// miter limit and need to revert to a bevel join.
$pure float $miter_extent(float cosTheta, float miterLimit) {
    float x = fma(cosTheta, .5, .5);
    return (x * miterLimit * miterLimit >= 1.0) ? inversesqrt(x) : sqrt(x);
}

// Returns the number of radial segments required for each radian of rotation, in order for the
// curve to appear "smooth" as defined by the approximate device-space stroke radius.
$pure float $num_radial_segments_per_radian(float approxDevStrokeRadius) {
    return .5 / acos(max(1.0 - (1.0 / $kPrecision) / approxDevStrokeRadius, -1.0));
}

// Unlike mix(), this does not return b when t==1. But it otherwise seems to get better
// precision than "a*(1 - t) + b*t" for things like chopping cubics on exact cusp points.
// We override this result anyway when t==1 so it shouldn't be a problem.
$pure float $unchecked_mix(float a, float b, float T) {
    return fma(b - a, T, a);
}
$pure float2 $unchecked_mix(float2 a, float2 b, float T) {
    return fma(b - a, float2(T), a);
}
$pure float4 $unchecked_mix(float4 a, float4 b, float4 T) {
    return fma(b - a, T, a);
}

// Compute a vertex position for the curve described by p01 and p23 packed control points,
// tessellated to the given resolve level, and assuming it will be drawn as a filled curve.
$pure float2 tessellate_filled_curve(float2x2 vectorXform,
                                     float resolveLevel, float idxInResolveLevel,
                                     float4 p01, float4 p23,
                                     float curveType) {
    float2 localcoord;
    if ($is_triangular_conic_curve(curveType)) {
        // This patch is an exact triangle.
        localcoord = (resolveLevel != 0)      ? p01.zw
                   : (idxInResolveLevel != 0) ? p23.xy
                                              : p01.xy;
    } else {
        float2 p0=p01.xy, p1=p01.zw, p2=p23.xy, p3=p23.zw;
        float w = -1;  // w < 0 tells us to treat the instance as an integral cubic.
        float maxResolveLevel;
        if ($is_conic_curve(curveType)) {
            // Conics are 3 points, with the weight in p3.
            w = p3.x;
            maxResolveLevel = $wangs_formula_conic_log2(vectorXform*p0,
                                                        vectorXform*p1,
                                                        vectorXform*p2, w);
            p1 *= w;  // Unproject p1.
            p3 = p2;  // Duplicate the endpoint for shared code that also runs on cubics.
        } else {
            // The patch is an integral cubic.
            maxResolveLevel = $wangs_formula_cubic_log2(p0, p1, p2, p3, vectorXform);
        }
        if (resolveLevel > maxResolveLevel) {
            // This vertex is at a higher resolve level than we need. Demote to a lower
            // resolveLevel, which will produce a degenerate triangle.
            idxInResolveLevel = floor(ldexp(idxInResolveLevel,
                                            int(maxResolveLevel - resolveLevel)));
            resolveLevel = maxResolveLevel;
        }
        // Promote our location to a discrete position in the maximum fixed resolve level.
        // This is extra paranoia to ensure we get the exact same fp32 coordinates for
        // colocated points from different resolve levels (e.g., the vertices T=3/4 and
        // T=6/8 should be exactly colocated).
        float fixedVertexID = floor(.5 + ldexp(idxInResolveLevel, int(5 - resolveLevel)));
        if (0 < fixedVertexID && fixedVertexID < 32) {
            float T = fixedVertexID * (1 / 32.0);

            // Evaluate at T. Use De Casteljau's for its accuracy and stability.
            float2 ab = mix(p0, p1, T);
            float2 bc = mix(p1, p2, T);
            float2 cd = mix(p2, p3, T);
            float2 abc = mix(ab, bc, T);
            float2 bcd = mix(bc, cd, T);
            float2 abcd = mix(abc, bcd, T);

            // Evaluate the conic weight at T.
            float u = mix(1.0, w, T);
            float v = w + 1 - u;  // == mix(w, 1, T)
            float uv = mix(u, v, T);

            localcoord = (w < 0) ? /*cubic*/ abcd : /*conic*/ abc/uv;
        } else {
            localcoord = (fixedVertexID == 0) ? p0.xy : p3.xy;
        }
    }
    return localcoord;
}

// Device coords are in xy, local coords are in zw, since for now perspective isn't supported.
$pure float4 tessellate_stroked_curve(float edgeID, float maxEdges,
                                      float2x2 affineMatrix,
                                      float2 translate,
                                      float maxScale /* derived from affineMatrix */,
                                      float4 p01, float4 p23,
                                      float2 lastControlPoint,
                                      float2 strokeParams,
                                      float curveType) {
    float2 p0=p01.xy, p1=p01.zw, p2=p23.xy, p3=p23.zw;
    float w = -1;  // w<0 means the curve is an integral cubic.
    if ($is_conic_curve(curveType)) {
        // Conics are 3 points, with the weight in p3.
        w = p3.x;
        p3 = p2;  // Setting p3 equal to p2 works for the remaining rotational logic.
    }

    // Call Wang's formula to determine parametric segments before transform points for hairlines
    // so that it is consistent with how the CPU tested the control points for chopping.
    float numParametricSegments;
    if (w < 0) {
        if (p0 == p1 && p2 == p3) {
            numParametricSegments = 1; // a line
        } else {
            numParametricSegments = $wangs_formula_cubic(p0, p1, p2, p3, affineMatrix);
        }
    } else {
        numParametricSegments = $wangs_formula_conic(affineMatrix * p0,
                                                     affineMatrix * p1,
                                                     affineMatrix * p2, w);
    }

    // Matches skgpu::tess::StrokeParams
    float strokeRadius = strokeParams.x;
    float joinType = strokeParams.y; // <0 = round join, ==0 = bevel join, >0 encodes miter limit
    bool isHairline = strokeParams.x == 0.0;
    float numRadialSegmentsPerRadian;
    if (isHairline) {
        numRadialSegmentsPerRadian = $num_radial_segments_per_radian(1.0);
        strokeRadius = 0.5;
    } else {
        numRadialSegmentsPerRadian = $num_radial_segments_per_radian(maxScale * strokeParams.x);
    }

    if (isHairline) {
        // Hairline case. Transform the points before tessellation. We can still hold off on the
        // translate until the end; we just need to perform the scale and skew right now.
        p0 = affineMatrix * p0;
        p1 = affineMatrix * p1;
        p2 = affineMatrix * p2;
        p3 = affineMatrix * p3;
        lastControlPoint = affineMatrix * lastControlPoint;
    }

    // Find the starting and ending tangents.
    float2 tan0 = $robust_normalize_diff((p0 == p1) ? ((p1 == p2) ? p3 : p2) : p1, p0);
    float2 tan1 = $robust_normalize_diff(p3, (p3 == p2) ? ((p2 == p1) ? p0 : p1) : p2);
    if (tan0 == float2(0)) {
        // The stroke is a point. This special case tells us to draw a stroke-width circle as a
        // 180 degree point stroke instead.
        tan0 = float2(1,0);
        tan1 = float2(-1,0);
    }

    // Determine how many edges to give to the join. We emit the first and final edges
    // of the join twice: once full width and once restricted to half width. This guarantees
    // perfect seaming by matching the vertices from the join as well as from the strokes on
    // either side.
    float numEdgesInJoin;
    if (joinType >= 0 /*Is the join not a round type?*/) {
        // Bevel(0) and miter(+) joins get 1 and 2 segments respectively.
        // +2 because we emit the beginning and ending edges twice (see above comments).
        numEdgesInJoin = sign(joinType) + (1 + 2);
    } else {
        float2 prevTan = $robust_normalize_diff(p0, lastControlPoint);
        float joinRads = acos($cosine_between_unit_vectors(prevTan, tan0));
        float numRadialSegmentsInJoin = max(ceil(joinRads * numRadialSegmentsPerRadian), 1);
        // +2 because we emit the beginning and ending edges twice (see above comment).
        numEdgesInJoin = numRadialSegmentsInJoin + 2;
        // The stroke section needs at least two edges. Don't assign more to the join than
        // "maxEdges - 2". (This is only relevant when the ideal max edge count calculated
        // on the CPU had to be limited to maxEdges in the draw call).
        numEdgesInJoin = min(numEdgesInJoin, maxEdges - 2);
    }

    // Find which direction the curve turns.
    // NOTE: Since the curve is not allowed to inflect, we can just check F'(.5) x F''(.5).
    // NOTE: F'(.5) x F''(.5) has the same sign as (P2 - P0) x (P3 - P1)
    float turn = cross_length_2d(p2 - p0, p3 - p1);
    float combinedEdgeID = abs(edgeID) - numEdgesInJoin;
    if (combinedEdgeID < 0) {
        tan1 = tan0;
        // Don't let tan0 become zero. The code as-is isn't built to handle that case. tan0=0
        // means the join is disabled, and to disable it with the existing code we can leave
        // tan0 equal to tan1.
        if (lastControlPoint != p0) {
            tan0 = $robust_normalize_diff(p0, lastControlPoint);
        }
        turn = cross_length_2d(tan0, tan1);
    }

    // Calculate the curve's starting angle and rotation.
    float cosTheta = $cosine_between_unit_vectors(tan0, tan1);
    float rotation = acos(cosTheta);
    if (turn < 0) {
        // Adjust sign of rotation to match the direction the curve turns.
        rotation = -rotation;
    }

    float numRadialSegments;
    float strokeOutset = sign(edgeID);
    if (combinedEdgeID < 0) {
        // We belong to the preceding join. The first and final edges get duplicated, so we only
        // have "numEdgesInJoin - 2" segments.
        numRadialSegments = numEdgesInJoin - 2;
        numParametricSegments = 1;  // Joins don't have parametric segments.
        p3 = p2 = p1 = p0;  // Colocate all points on the junction point.
        // Shift combinedEdgeID to the range [-1, numRadialSegments]. This duplicates the first
        // edge and lands one edge at the very end of the join. (The duplicated final edge will
        // actually come from the section of our strip that belongs to the stroke.)
        combinedEdgeID += numRadialSegments + 1;
        if (combinedEdgeID < 0) {
            combinedEdgeID = 0;
        } else {
            // We normally restrict the join on one side of the junction, but if the tangents are
            // nearly equivalent this could theoretically result in bad seaming and/or cracks on the
            // side we don't put it on. If the tangents are nearly equivalent then we leave the join
            // double-sided.
            const float sinEpsilon = 1e-2;  // ~= sin(180deg / 3000)
            bool tangentsNearlyParallel =
                    (abs(turn) * inversesqrt(dot(tan0, tan0) * dot(tan1, tan1))) < sinEpsilon;
            if (!tangentsNearlyParallel || dot(tan0, tan1) < 0) {
                // There are two edges colocated at the beginning. Leave the first one double sided
                // for seaming with the previous stroke. (The double sided edge at the end will
                // actually come from the section of our strip that belongs to the stroke.)
                strokeOutset = (turn < 0) ? min(strokeOutset, 0) : max(strokeOutset, 0);
            }
        }
    } else {
        // We belong to the stroke. Unless numRadialSegmentsPerRadian is incredibly high,
        // clamping to maxCombinedSegments will be a no-op because the draw call was invoked with
        // sufficient vertices to cover the worst case scenario of 180 degree rotation.
        float maxCombinedSegments = maxEdges - numEdgesInJoin - 1;
        numRadialSegments = max(ceil(abs(rotation) * numRadialSegmentsPerRadian), 1);
        numRadialSegments = min(numRadialSegments, maxCombinedSegments);
        numParametricSegments = min(numParametricSegments,
                                    maxCombinedSegments - numRadialSegments + 1);
    }

    // Additional parameters for final tessellation evaluation.
    float radsPerSegment = rotation / numRadialSegments;
    float numCombinedSegments = numParametricSegments + numRadialSegments - 1;
    bool isFinalEdge = (combinedEdgeID >= numCombinedSegments);
    if (combinedEdgeID > numCombinedSegments) {
        strokeOutset = 0;  // The strip has more edges than we need. Drop this one.
    }
    // Edge #2 extends to the miter point.
    if (abs(edgeID) == 2 && joinType > 0/*Is the join a miter type?*/) {
        strokeOutset *= $miter_extent(cosTheta, joinType/*miterLimit*/);
    }

    float2 tangent, strokeCoord;
    if (combinedEdgeID != 0 && !isFinalEdge) {
        // Compute the location and tangent direction of the stroke edge with the integral id
        // "combinedEdgeID", where combinedEdgeID is the sorted-order index of parametric and radial
        // edges. Start by finding the tangent function's power basis coefficients. These define a
        // tangent direction (scaled by some uniform value) as:
        //                                                 |T^2|
        //     Tangent_Direction(T) = dx,dy = |A  2B  C| * |T  |
        //                                    |.   .  .|   |1  |
        float2 A, B, C = p1 - p0;
        float2 D = p3 - p0;
        if (w >= 0.0) {
            // P0..P2 represent a conic and P3==P2. The derivative of a conic has a cumbersome
            // order-4 denominator. However, this isn't necessary if we are only interested in a
            // vector in the same *direction* as a given tangent line. Since the denominator scales
            // dx and dy uniformly, we can throw it out completely after evaluating the derivative
            // with the standard quotient rule. This leaves us with a simpler quadratic function
            // that we use to find a tangent.
            C *= w;
            B = .5*D - C;
            A = (w - 1.0) * D;
            p1 *= w;
        } else {
            float2 E = p2 - p1;
            B = E - C;
            A = fma(float2(-3), E, D);
        }
        // FIXME(crbug.com/800804,skbug.com/11268): Consider normalizing the exponents in A,B,C at
        // this point in order to prevent fp32 overflow.

        // Now find the coefficients that give a tangent direction from a parametric edge ID:
        //
        //                                                                 |parametricEdgeID^2|
        //     Tangent_Direction(parametricEdgeID) = dx,dy = |A  B_  C_| * |parametricEdgeID  |
        //                                                   |.   .   .|   |1                 |
        //
        float2 B_ = B * (numParametricSegments * 2.0);
        float2 C_ = C * (numParametricSegments * numParametricSegments);

        // Run a binary search to determine the highest parametric edge that is located on or before
        // the combinedEdgeID. A combined ID is determined by the sum of complete parametric and
        // radial segments behind it. i.e., find the highest parametric edge where:
        //
        //    parametricEdgeID + floor(numRadialSegmentsAtParametricT) <= combinedEdgeID
        //
        float lastParametricEdgeID = 0.0;
        float maxParametricEdgeID = min(numParametricSegments - 1.0, combinedEdgeID);
        float negAbsRadsPerSegment = -abs(radsPerSegment);
        float maxRotation0 = (1.0 + combinedEdgeID) * abs(radsPerSegment);
        for (float exp = 32.0; exp >= 1.0; exp *= 0.5) {
            // Test the parametric edge at lastParametricEdgeID + (32, 16, 8, 4, 2, 1).
            float testParametricID = lastParametricEdgeID + exp;
            if (testParametricID <= maxParametricEdgeID) {
                float2 testTan = fma(float2(testParametricID), A, B_);
                testTan = fma(float2(testParametricID), testTan, C_);
                float cosRotation = dot(normalize(testTan), tan0);
                float maxRotation = fma(testParametricID, negAbsRadsPerSegment, maxRotation0);
                maxRotation = min(maxRotation, $PI);
                // Is rotation <= maxRotation? (i.e., is the number of complete radial segments
                // behind testT, + testParametricID <= combinedEdgeID?)
                if (cosRotation >= cos(maxRotation)) {
                    // testParametricID is on or before the combinedEdgeID. Keep it!
                    lastParametricEdgeID = testParametricID;
                }
            }
        }

        // Find the T value of the parametric edge at lastParametricEdgeID.
        float parametricT = lastParametricEdgeID / numParametricSegments;

        // Now that we've identified the highest parametric edge on or before the
        // combinedEdgeID, the highest radial edge is easy:
        float lastRadialEdgeID = combinedEdgeID - lastParametricEdgeID;

        // Find the angle of tan0, i.e. the angle between tan0 and the positive x axis.
        float angle0 = acos(clamp(tan0.x, -1.0, 1.0));
        angle0 = tan0.y >= 0.0 ? angle0 : -angle0;

        // Find the tangent vector on the edge at lastRadialEdgeID. By construction it is already
        // normalized.
        float radialAngle = fma(lastRadialEdgeID, radsPerSegment, angle0);
        tangent = float2(cos(radialAngle), sin(radialAngle));
        float2 norm = float2(-tangent.y, tangent.x);

        // Find the T value where the tangent is orthogonal to norm. This is a quadratic:
        //
        //     dot(norm, Tangent_Direction(T)) == 0
        //
        //                         |T^2|
        //     norm * |A  2B  C| * |T  | == 0
        //            |.   .  .|   |1  |
        //
        float a=dot(norm,A), b_over_2=dot(norm,B), c=dot(norm,C);
        float discr_over_4 = max(b_over_2*b_over_2 - a*c, 0.0);
        float q = sqrt(discr_over_4);
        if (b_over_2 > 0.0) {
            q = -q;
        }
        q -= b_over_2;

        // Roots are q/a and c/q. Since each curve section does not inflect or rotate more than 180
        // degrees, there can only be one tangent orthogonal to "norm" inside 0..1. Pick the root
        // nearest .5.
        float _5qa = -.5*q*a;
        float2 root = (abs(fma(q,q,_5qa)) < abs(fma(a,c,_5qa))) ? float2(q,a) : float2(c,q);

        // The root finder above can become unstable when lastRadialEdgeID == 0 (e.g., if there are
        // roots at exatly 0 and 1 both). radialT should always equal 0 in this case.
        float radialT = (lastRadialEdgeID != 0.0 && root.t != 0.0)
                            ? saturate(root.s / root.t)
                            : 0.0;

        // Now that we've identified the T values of the last parametric and radial edges, our final
        // T value for combinedEdgeID is whichever is larger.
        float T = max(parametricT, radialT);

        // Evaluate the cubic at T. Use De Casteljau's for its accuracy and stability.
        float2 ab = $unchecked_mix(p0, p1, T);
        float2 bc = $unchecked_mix(p1, p2, T);
        float2 cd = $unchecked_mix(p2, p3, T);
        float2 abc = $unchecked_mix(ab, bc, T);
        float2 bcd = $unchecked_mix(bc, cd, T);
        float2 abcd = $unchecked_mix(abc, bcd, T);

        // Evaluate the conic weight at T.
        float u = $unchecked_mix(1.0, w, T);
        float v = w + 1 - u;  // == mix(w, 1, T)
        float uv = $unchecked_mix(u, v, T);

        // If we went with T=parametricT, then update the tangent. Otherwise leave it at the radial
        // tangent found previously. (In the event that parametricT == radialT, we keep the radial
        // tangent.)
        if (T != radialT) {
            // We must re-normalize here because the tangent is determined by the curve coefficients
            tangent = w >= 0.0 ? $robust_normalize_diff(bc*u, ab*v)
                               : $robust_normalize_diff(bcd, abc);
        }

        strokeCoord = (w >= 0.0) ? abc/uv : abcd;
    } else {
        // Edges at the beginning and end of the strip use exact endpoints and tangents. This
        // ensures crack-free seaming between instances.
        tangent = (combinedEdgeID == 0) ? tan0 : tan1;
        strokeCoord = (combinedEdgeID == 0) ? p0 : p3;
    }

    // At this point 'tangent' is normalized, so the orthogonal vector is also normalized.
    float2 ortho = float2(tangent.y, -tangent.x);
    strokeCoord += ortho * (strokeRadius * strokeOutset);

    if (isHairline) {
        // Hairline case. The scale and skew already happened before tessellation.
        // TODO: There's probably a more efficient way to tessellate the hairline that lets us
        // avoid inverting the affine matrix to get back to local coords, but it's just a 2x2 so
        // this works for now.
        return float4(strokeCoord + translate, inverse(affineMatrix) * strokeCoord);
    } else {
        // Normal case. Do the transform after tessellation.
        return float4(affineMatrix * strokeCoord + translate, strokeCoord);
    }
}

float4 analytic_rrect_vertex_fn(// Vertex Attributes
                                float2 position,
                                float2 normal,
                                float normalScale,
                                float centerWeight,
                                // Instance Attributes
                                float4 xRadiiOrFlags,
                                float4 radiiOrQuadXs,
                                float4 ltrbOrQuadYs,
                                float4 center,
                                float depth,
                                float3x3 localToDevice,
                                // Varyings
                                out float4 jacobian,
                                out float4 edgeDistances,
                                out float4 xRadii,
                                out float4 yRadii,
                                out float2 strokeParams,
                                out float2 perPixelControl,
                                // Render Step
                                out float2 stepLocalCoords) {
    const uint kCornerVertexCount = 9; // KEEP IN SYNC WITH C++'s
                                       // AnalyticRRectRenderStep::kCornerVertexCount
    const float kMiterScale = 1.0;
    const float kBevelScale = 0.0;
    const float kRoundScale = 0.41421356237; // sqrt(2)-1

    const float kEpsilon = 0.00024; // SK_ScalarNearlyZero

    // Default to miter'ed vertex positioning. Corners with sufficiently large corner radii, or
    // bevel'ed strokes will adjust vertex placement on a per corner basis. This will not affect
    // the final coverage calculations in the fragment shader.
    float joinScale = kMiterScale;

    // Unpack instance-level state that determines the vertex placement and style of shape.
    bool bidirectionalCoverage = center.z <= 0.0;
    bool deviceSpaceDistances = false;
    float4 xs, ys; // ordered TL, TR, BR, BL
    float4 edgeAA = float4(1.0); // ordered L,T,R,B. 1 = AA, 0 = no AA
    bool strokedLine = false;
    if (xRadiiOrFlags.x < -1.0) {
        // Stroked [round] rect or line
        // If y > 0, unpack the line end points, otherwise unpack the rect edges
        strokedLine = xRadiiOrFlags.y > 0.0;
        xs = strokedLine ? ltrbOrQuadYs.LLRR : ltrbOrQuadYs.LRRL;
        ys = ltrbOrQuadYs.TTBB;

        if (xRadiiOrFlags.y < 0.0) {
            // A hairline [r]rect so the X radii are encoded as negative values in this field,
            // and Y radii are stored directly in the subsequent float4.
            xRadii = -xRadiiOrFlags - 2.0;
            yRadii = radiiOrQuadXs;

            // All hairlines use miter joins (join style > 0)
            strokeParams = float2(0.0, 1.0);
        } else {
            xRadii = radiiOrQuadXs;
            yRadii = xRadii; // regular strokes are circular
            strokeParams = xRadiiOrFlags.zw;

            // `sign(strokeParams.y)` evaluates to kMiterScale (1.0) when the
            // input is positive, and kBevelScale (0.0) when it is zero.
            // kRoundScale uses the stroke radius to round rectangular corners.
            joinScale = (strokeParams.y < 0.0) ? kRoundScale
                                               : sign(strokeParams.y);
        }
    } else if (any(greaterThan(xRadiiOrFlags, float4(0.0)))) {
        // Filled round rect
        xs = ltrbOrQuadYs.LRRL;
        ys = ltrbOrQuadYs.TTBB;

        xRadii = xRadiiOrFlags;
        yRadii = radiiOrQuadXs;

        strokeParams = float2(0.0, -1.0); // A negative join style is "round"
    } else {
        // Per-edge quadrilateral, so we have to calculate the corner's basis from the
        // quad's edges.
        xs = radiiOrQuadXs;
        ys = ltrbOrQuadYs;
        edgeAA = -xRadiiOrFlags; // AA flags needed to be < 0 on upload, so flip the sign.

        xRadii = float4(0.0);
        yRadii = float4(0.0);

        strokeParams = float2(0.0, 1.0); // Will be ignored, but set to a "miter"
        deviceSpaceDistances = true;
    }

    // Adjust state on a per-corner basis
    uint cornerID = uint(sk_VertexID) / kCornerVertexCount;
    float2 cornerRadii = float2(xRadii[cornerID], yRadii[cornerID]);
    if (cornerID % 2 != 0) {
        // Corner radii are uploaded in the local coordinate frame, but vertex placement happens
        // in a consistent winding before transforming to final local coords, so swap the
        // radii for odd corners.
        cornerRadii = cornerRadii.yx;
    }

    float2 cornerAspectRatio = float2(1.0);
    if (all(greaterThan(cornerRadii, float2(0.0)))) {
        // Position vertices for an elliptical corner; overriding any previous join style since
        // that only applies when radii are 0.
        joinScale = kRoundScale;
        cornerAspectRatio = cornerRadii.yx;
    }

    // Calculate the local edge vectors, ordered L, T, R, B starting from the bottom left point.
    // For quadrilaterals these are not necessarily axis-aligned, but in all cases they orient
    // the +X/+Y normalized vertex template for each corner.
    float4 dx = xs - xs.wxyz;
    float4 dy = ys - ys.wxyz;
    float4 edgeSquaredLen = dx*dx + dy*dy;

    float4 edgeMask = sign(edgeSquaredLen); // 0 for zero-length edge, 1 for non-zero edge.
    float4 edgeBias = float4(0.0); // adjustment to edge distance for butt cap correction
    float2 strokeRadius = float2(strokeParams.x);
    if (any(equal(edgeMask, float4(0.0)))) {
        // Must clean up (dx,dy) depending on the empty edge configuration
        if (all(equal(edgeMask, float4(0.0)))) {
            // A point so use the canonical basis
            dx = float4( 0.0, 1.0, 0.0, -1.0);
            dy = float4(-1.0, 0.0, 1.0,  0.0);
            edgeSquaredLen = float4(1.0);
        } else {
            // Triangles (3 non-zero edges) copy the adjacent edge. Otherwise it's a line so
            // replace empty edges with the left-hand normal vector of the adjacent edge.
            bool triangle = (edgeMask[0] + edgeMask[1] + edgeMask[2] + edgeMask[3]) > 2.5;
            float4 edgeX = triangle ? dx.yzwx :  dy.yzwx;
            float4 edgeY = triangle ? dy.yzwx : -dx.yzwx;

            dx = mix(edgeX, dx, edgeMask);
            dy = mix(edgeY, dy, edgeMask);
            edgeSquaredLen = mix(edgeSquaredLen.yzwx, edgeSquaredLen, edgeMask);
            edgeAA = mix(edgeAA.yzwx, edgeAA, edgeMask);

            if (!triangle && joinScale == kBevelScale) {
                // Don't outset by stroke radius for butt caps on the zero-length edge, but
                // adjust edgeBias and strokeParams to calculate an AA miter'ed shape with the
                // non-uniform stroke outset.
                strokeRadius *= float2(edgeMask[cornerID], edgeMask.yzwx[cornerID]);
                edgeBias = (edgeMask - 1.0) * strokeParams.x;
                strokeParams.y = 1.0;
                joinScale = kMiterScale;
            }
        }
    }

    float4 inverseEdgeLen = inversesqrt(edgeSquaredLen);
    dx *= inverseEdgeLen;
    dy *= inverseEdgeLen;

    // Calculate local coordinate for the vertex (relative to xAxis and yAxis at first).
    float2 xAxis = -float2(dx.yzwx[cornerID], dy.yzwx[cornerID]);
    float2 yAxis =  float2(dx.xyzw[cornerID], dy.xyzw[cornerID]);
    float2 localPos;
    bool snapToCenter = false;
    if (normalScale < 0.0) {
        // Vertex is inset from the base shape, so we scale by (cornerRadii - strokeRadius)
        // and have to check for the possibility of an inner miter. It is always inset by an
        // additional conservative AA amount.
        if (center.w < 0.0 || centerWeight * center.z != 0.0) {
            snapToCenter = true;
        } else {
            float localAARadius = center.w;
            float2 insetRadii =
                    cornerRadii + (bidirectionalCoverage ? -strokeRadius : strokeRadius);
            if (joinScale == kMiterScale ||
                any(lessThanEqual(insetRadii, float2(localAARadius)))) {
                // Miter the inset position
                localPos = (insetRadii - localAARadius);
            } else {
                localPos = insetRadii*position - localAARadius*normal;
            }
        }
    } else {
        // Vertex is outset from the base shape (and possibly with an additional AA outset later
        // in device space).
        localPos = (cornerRadii + strokeRadius) * (position + joinScale*position.yx);
    }

    if (snapToCenter) {
        // Center is already relative to true local coords, not the corner basis.
        localPos = center.xy;
    } else {
        // Transform from corner basis to true local coords.
        localPos -= cornerRadii;
        localPos = float2(xs[cornerID], ys[cornerID]) + xAxis*localPos.x + yAxis*localPos.y;
    }

    // Calculate edge distances and device space coordinate for the vertex
    edgeDistances = dy*(xs - localPos.x) - dx*(ys - localPos.y) + edgeBias;

    // NOTE: This 3x3 inverse is different than just taking the 1st two columns of the 4x4
    // inverse of the original SkM44 local-to-device matrix. We could calculate the 3x3 inverse
    // and upload it, but it does not seem to be a bottleneck and saves on bandwidth to
    // calculate it here instead.
    float3x3 deviceToLocal = inverse(localToDevice);
    float3 devPos = localToDevice * localPos.xy1;
    jacobian = float4(deviceToLocal[0].xy - deviceToLocal[0].z*localPos,
                      deviceToLocal[1].xy - deviceToLocal[1].z*localPos);

    if (deviceSpaceDistances) {
        // Apply the Jacobian in the vertex shader so any quadrilateral normals do not have to
        // be passed to the fragment shader. However, it's important to use the Jacobian at a
        // vertex on the edge, not the current vertex's Jacobian.
        float4 gx = -dy*(deviceToLocal[0].x - deviceToLocal[0].z*xs) +
                     dx*(deviceToLocal[0].y - deviceToLocal[0].z*ys);
        float4 gy = -dy*(deviceToLocal[1].x - deviceToLocal[1].z*xs) +
                     dx*(deviceToLocal[1].y - deviceToLocal[1].z*ys);
        // NOTE: The gradient is missing a W term so edgeDistances must still be multiplied by
        // 1/w in the fragment shader. The same goes for the encoded coverage scale.
        edgeDistances *= inversesqrt(gx*gx + gy*gy);

        // Bias non-AA edge distances by device W so its coverage contribution is >= 1.0
        edgeDistances += (1 - edgeAA)*abs(devPos.z);

        // Mixed edge AA shapes do not use subpixel scale+bias for coverage, since they tile
        // to a large shape of unknown--but likely not subpixel--size. Triangles and quads do
        // not use subpixel coverage since the scale+bias is not constant over the shape, but
        // we can't evaluate per-fragment since we aren't passing down their arbitrary normals.
        bool subpixelCoverage = edgeAA == float4(1.0) &&
                                dot(abs(dx*dx.yzwx + dy*dy.yzwx), float4(1.0)) < kEpsilon;
        if (subpixelCoverage) {
            // Reconstructs the actual device-space width and height for all rectangle vertices.
            float2 dim = edgeDistances.xy + edgeDistances.zw;
            perPixelControl.y = 1.0 + min(min(dim.x, dim.y), abs(devPos.z));
        } else {
            perPixelControl.y = 1.0 + abs(devPos.z); // standard 1px width pre W division.
        }
    }

    // Only outset for a vertex that is in front of the w=0 plane to avoid dealing with outset
    // triangles rasterizing differently from the main triangles as w crosses 0.
    if (normalScale > 0.0 && devPos.z > 0.0) {
        // Note that when there's no perspective, the jacobian is equivalent to the normal
        // matrix (inverse transpose), but produces correct results when there's perspective
        // because it accounts for the position's influence on a line's projected direction.
        float2x2 J = float2x2(jacobian);

        float2 edgeAANormal = float2(edgeAA[cornerID], edgeAA.yzwx[cornerID]) * normal;
        float2 nx = cornerAspectRatio.x * edgeAANormal.x * perp(-yAxis) * J;
        float2 ny = cornerAspectRatio.y * edgeAANormal.y * perp( xAxis) * J;

        bool isMidVertex = all(notEqual(edgeAANormal, float2(0)));
        if (joinScale == kMiterScale && isMidVertex) {
            // Produce a bisecting vector in device space.
            nx = normalize(nx);
            ny = normalize(ny);
            if (dot(nx, ny) < -0.8) {
                // Normals are in nearly opposite directions, so adjust to avoid float error.
                float s = sign(cross_length_2d(nx, ny));
                nx =  s*perp(nx);
                ny = -s*perp(ny);
            }
        }
        // Adding the normal components together directly results in what we'd have
        // calculated if we'd just transformed 'normal' in one go, assuming they weren't
        // normalized in the if-block above. If they were normalized, the sum equals the
        // bisector between the original nx and ny.
        //
        // We multiply by W so that after perspective division the new point is offset by the
        // now-unit normal.
        // NOTE: (nx + ny) can become the zero vector if the device outset is for an edge
        // marked as non-AA. In this case normalize() could produce the zero vector or NaN.
        // Until a counter-example is found, GPUs seem to discard triangles with NaN vertices,
        // which has the same effect as outsetting by the zero vector with this mesh, so we
        // don't bother guarding the normalize() (yet).
        devPos.xy += devPos.z * normalize(nx + ny);

        // By construction these points are 1px away from the outer edge in device space.
        if (deviceSpaceDistances) {
            // Apply directly to edgeDistances to save work per pixel later on.
            edgeDistances -= devPos.z;
        } else {
            // Otherwise store separately so edgeDistances can be used to reconstruct corner pos
            perPixelControl.y = -devPos.z;
        }
    } else if (!deviceSpaceDistances) {
        // Triangles are within the original shape so there's no additional outsetting to
        // take into account for coverage calculations.
        perPixelControl.y = 0.0;
    }

    perPixelControl.x = (centerWeight != 0.0)
            // A positive value signals that a pixel is trivially full coverage.
            ? 1.0
            // A negative value signals bidirectional coverage, and a zero value signals a solid
            // interior with per-pixel coverage.
            : bidirectionalCoverage ? -1.0 : 0.0;

    // The fragment shader operates in a canonical basis (x-axis = (1,0), y-axis = (0,1)). For
    // stroked lines, incorporate their local orientation into the Jacobian to preserve this.
    if (strokedLine) {
        // The updated Jacobian is J' = B^-1 * J, where B is float2x2(xAxis, yAxis) for the
        // top-left corner (so that B^-1 is constant over the whole shape). Since it's a line
        // the basis was constructed to be orthonormal, det(B) = 1 and B^-1 is trivial.
        // NOTE: float2x2 is column-major.
        jacobian = float4(float2x2(dy[0], -dy[1], -dx[0], dx[1]) * float2x2(jacobian));
    }

    // Write out final results
    stepLocalCoords = localPos;
    return float4(devPos.xy, devPos.z*depth, devPos.z);
}

float4 per_edge_aa_quad_vertex_fn(// Vertex Attributes
                                  float2 normal,
                                  // Instance Attributes
                                  float4 edgeAA,
                                  float4 xs, // ordered TL, TR, BR, BL
                                  float4 ys,
                                  float depth,
                                  float3x3 localToDevice,
                                  // Varyings
                                  out float4 edgeDistances,
                                  // Render Step
                                  out float2 stepLocalCoords) {
    const uint kCornerVertexCount = 4; // KEEP IN SYNC WITH C++'s
                                       // PerEdgeAAQuadRenderStep::kCornerVertexCount

    const float kEpsilon = 0.00024; // SK_ScalarNearlyZero

    // Calculate the local edge vectors, ordered L, T, R, B starting from the bottom left point.
    // For quadrilaterals these are not necessarily axis-aligned, but in all cases they orient
    // the +X/+Y normalized vertex template for each corner.
    float4 dx = xs - xs.wxyz;
    float4 dy = ys - ys.wxyz;
    float4 edgeSquaredLen = dx*dx + dy*dy;

    float4 edgeMask = sign(edgeSquaredLen); // 0 for zero-length edge, 1 for non-zero edge.
    if (any(equal(edgeMask, float4(0.0)))) {
        // Must clean up (dx,dy) depending on the empty edge configuration
        if (all(equal(edgeMask, float4(0.0)))) {
            // A point so use the canonical basis
            dx = float4( 0.0, 1.0, 0.0, -1.0);
            dy = float4(-1.0, 0.0, 1.0,  0.0);
            edgeSquaredLen = float4(1.0);
        } else {
            // Triangles (3 non-zero edges) copy the adjacent edge. Otherwise it's a line so
            // replace empty edges with the left-hand normal vector of the adjacent edge.
            bool triangle = (edgeMask[0] + edgeMask[1] + edgeMask[2] + edgeMask[3]) > 2.5;
            float4 edgeX = triangle ? dx.yzwx :  dy.yzwx;
            float4 edgeY = triangle ? dy.yzwx : -dx.yzwx;

            dx = mix(edgeX, dx, edgeMask);
            dy = mix(edgeY, dy, edgeMask);
            edgeSquaredLen = mix(edgeSquaredLen.yzwx, edgeSquaredLen, edgeMask);
            edgeAA = mix(edgeAA.yzwx, edgeAA, edgeMask);
        }
    }

    float4 inverseEdgeLen = inversesqrt(edgeSquaredLen);
    dx *= inverseEdgeLen;
    dy *= inverseEdgeLen;

    // Calculate local coordinate for the vertex (relative to xAxis and yAxis at first).
    uint cornerID = uint(sk_VertexID) / kCornerVertexCount;
    float2 xAxis = -float2(dx.yzwx[cornerID], dy.yzwx[cornerID]);
    float2 yAxis =  float2(dx.xyzw[cornerID], dy.xyzw[cornerID]);

    // Vertex is outset from the base shape (and possibly with an additional AA outset later
    // in device space).
    float2 localPos = float2(xs[cornerID], ys[cornerID]);

    // Calculate edge distances and device space coordinate for the vertex
    edgeDistances = dy*(xs - localPos.x) - dx*(ys - localPos.y);

    // NOTE: This 3x3 inverse is different than just taking the 1st two columns of the 4x4
    // inverse of the original SkM44 local-to-device matrix. We could calculate the 3x3 inverse
    // and upload it, but it does not seem to be a bottleneck and saves on bandwidth to
    // calculate it here instead.
    float3x3 deviceToLocal = inverse(localToDevice);
    float3 devPos = localToDevice * localPos.xy1;

    // Apply the Jacobian in the vertex shader so any quadrilateral normals do not have to
    // be passed to the fragment shader. However, it's important to use the Jacobian at a
    // vertex on the edge, not the current vertex's Jacobian.
    float4 gx = -dy*(deviceToLocal[0].x - deviceToLocal[0].z*xs) +
                 dx*(deviceToLocal[0].y - deviceToLocal[0].z*ys);
    float4 gy = -dy*(deviceToLocal[1].x - deviceToLocal[1].z*xs) +
                 dx*(deviceToLocal[1].y - deviceToLocal[1].z*ys);
    // NOTE: The gradient is missing a W term so edgeDistances must still be multiplied by
    // 1/w in the fragment shader. The same goes for the encoded coverage scale.
    edgeDistances *= inversesqrt(gx*gx + gy*gy);

    // Bias non-AA edge distances by device W so its coverage contribution is >= 1.0
    // Add additional 1/2 bias here so we don't have to do so in the fragment shader.
    edgeDistances += (1.5 - edgeAA)*abs(devPos.z);

    // Only outset for a vertex that is in front of the w=0 plane to avoid dealing with outset
    // triangles rasterizing differently from the main triangles as w crosses 0.
    if (any(notEqual(normal, float2(0.0))) && devPos.z > 0.0) {
        // Note that when there's no perspective, the jacobian is equivalent to the normal
        // matrix (inverse transpose), but produces correct results when there's perspective
        // because it accounts for the position's influence on a line's projected direction.
        float2x2 J = float2x2(deviceToLocal[0].xy - deviceToLocal[0].z*localPos,
                              deviceToLocal[1].xy - deviceToLocal[1].z*localPos);

        float2 edgeAANormal = float2(edgeAA[cornerID], edgeAA.yzwx[cornerID]) * normal;
        float2 nx = edgeAANormal.x * perp(-yAxis) * J;
        float2 ny = edgeAANormal.y * perp( xAxis) * J;

        bool isMidVertex = all(notEqual(edgeAANormal, float2(0)));
        if (isMidVertex) {
            // Produce a bisecting vector in device space.
            nx = normalize(nx);
            ny = normalize(ny);
            if (dot(nx, ny) < -0.8) {
                // Normals are in nearly opposite directions, so adjust to avoid float error.
                float s = sign(cross_length_2d(nx, ny));
                nx =  s*perp(nx);
                ny = -s*perp(ny);
            }
        }
        // Adding the normal components together directly results in what we'd have
        // calculated if we'd just transformed 'normal' in one go, assuming they weren't
        // normalized in the if-block above. If they were normalized, the sum equals the
        // bisector between the original nx and ny.
        //
        // We multiply by W so that after perspective division the new point is offset by the
        // now-unit normal.
        // NOTE: (nx + ny) can become the zero vector if the device outset is for an edge
        // marked as non-AA. In this case normalize() could produce the zero vector or NaN.
        // Until a counter-example is found, GPUs seem to discard triangles with NaN vertices,
        // which has the same effect as outsetting by the zero vector with this mesh, so we
        // don't bother guarding the normalize() (yet).
        devPos.xy += devPos.z * normalize(nx + ny);

        // By construction these points are 1px away from the outer edge in device space.
        // Apply directly to edgeDistances to save work per pixel later on.
        edgeDistances -= devPos.z;
    }

    // Write out final results
    stepLocalCoords = localPos;
    return float4(devPos.xy, devPos.z*depth, devPos.z);
}

float4 circular_arc_vertex_fn(float3 position,
                              // Instance Attributes
                              float4 centerScales,
                              float3 radiiAndFlags,
                              float3 geoClipPlane,
                              float3 fragClipPlane0,
                              float3 fragClipPlane1,
                              float4 inRoundCapPos,
                              float depth,
                              float3x3 localToDevice,
                              // Varyings
                              out float4 circleEdge,
                              out float3 clipPlane,
                              out float3 isectPlane,
                              out float3 unionPlane,
                              out float  roundCapRadius,
                              out float4 roundCapPos,
                              // Render Step
                              out float2 stepLocalCoords) {
    // TODO: clip offset against clip planes
    float2 localCenter = centerScales.xy;
    float2 localPos = localCenter;
    // do geometric clip in normalized space
    float dist = min(dot(position.xy, geoClipPlane.xy) + geoClipPlane.z, 0);
    position.xy -= geoClipPlane.xy * dist;
    // Get the new length to use below for scaling the offset
    // (origLength is the initial length of position.xy).
    float offsetScale = length(position.xy);

    // scale and translate to local space
    if (position.z > 0) {
        localPos += position.xy * centerScales.z;
    } else {
        localPos += position.xy * centerScales.w;
    }

    float3 devPos = localToDevice * localPos.xy1;
    float3 devCenter = localToDevice * localCenter.xy1;
    float2 offset = devPos.xy - devCenter.xy;
    // offset for AA and correct length of offset
    if (offset != float2(0)) {
        offset = normalize(offset);
        devPos.xy += position.z*offset;
        if (position.z > 0) {
            // Scale using distance from center of unit octagon to the vertex
            // Because of geometry clipping we need to scale by 1.0823922*newLength/origLength
            // But the original length is 1.0823922 so the offsetScale is just newLength
            offset *= offsetScale;
        } else {
            // Because of geometry clipping we need to scale by innerRadius*newLength/origLength
            // But the original length is 1 so this is just innerRadius*newLength
            offset *= offsetScale*radiiAndFlags.y;
        }
    }

    circleEdge = float4(offset, radiiAndFlags.xy);
    if (radiiAndFlags.z > 0) {
        clipPlane = fragClipPlane0;
        isectPlane = fragClipPlane1;
        unionPlane = float3(0, 0, 0);
    } else {
        clipPlane = fragClipPlane0;
        isectPlane = float3(0, 0, 1);
        unionPlane = fragClipPlane1;
    }
    if (abs(radiiAndFlags.z) > 1) {
        // This is the cap radius in normalized space where the outer radius is 1 and
        // radii.y is the normalized inner radius.
        roundCapRadius = (1.0 - radiiAndFlags.y) / 2.0;
    } else {
        roundCapRadius = 0;
    }
    roundCapPos = inRoundCapPos;
    stepLocalCoords = localPos;

    // We assume no perspective
    return float4(devPos.xy, depth, 1);
}

float4 text_vertex_fn(float2 baseCoords,
                      // Uniforms
                      float4x4 subRunDeviceMatrix,
                      float4x4 deviceToLocal,
                      float2 atlasSizeInv,
                      // Instance Attributes
                      float2 size,
                      float2 uvPos,
                      float2 xyPos,
                      float strikeToSourceScale,
                      float depth,
                      // Varyings
                      out float2 textureCoords,
                      out float2 unormTexCoords,  // used as varying in SDFText
                      // Render Step
                      out float2 stepLocalCoords) {
    baseCoords.xy *= float2(size);

    // Sub runs have a decomposed transform and are sometimes already transformed into device
    // space, in which `subRunCoords` represents the bounds projected to device space without
    // the local-to-device translation and `subRunDeviceMatrix` contains the translation.
    float2 subRunCoords = strikeToSourceScale * baseCoords + xyPos;
    float4 position = subRunDeviceMatrix * subRunCoords.xy01;

    // Calculate the local coords used for shading.
    // TODO(b/246963258): This is incorrect if the transform has perspective, which would
    // require a division + a valid z coordinate (which is currently set to 0).
    stepLocalCoords = (deviceToLocal * position).xy;

    unormTexCoords = baseCoords + uvPos;
    textureCoords = unormTexCoords * atlasSizeInv;

    return float4(position.xy, depth*position.w, position.w);
}

float4 coverage_mask_vertex_fn(float2 quadCoords,
                               // Uniforms
                               float3x3 maskToDeviceRemainder,
                               // Instance Attributes
                               float4 drawBounds,
                               float4 maskBoundsIn,
                               float2 deviceOrigin,
                               float depth,
                               float3x3 deviceToLocal,
                               // Varyings
                               out float4 maskBounds,
                               out float2 textureCoords,
                               out half invert,
                               // Render Step
                               out float2 stepLocalCoords) {
    // An atlas shape is an axis-aligned rectangle tessellated as a triangle strip.
    //
    // The bounds coordinates are in an intermediate space, pixel-aligned with the mask texture
    // that's sampled in the fragment shader. The coords must be transformed by both
    // maskToDeviceRemainder and translated by deviceOrigin to get device coords.
    textureCoords = mix(drawBounds.xy, drawBounds.zw, quadCoords);
    float3 drawCoords = maskToDeviceRemainder*((textureCoords + deviceOrigin).xy1);

    // Local coordinates used for shading are derived from the final device coords and the inverse
    // of the original local-to-device matrix.
    float3 localCoords = deviceToLocal * drawCoords;
    // TODO: Support float3 local coordinates if the matrix has perspective so that W is
    // interpolated correctly to the fragment shader.
    stepLocalCoords = localCoords.xy / localCoords.z;

    // For an inverse fill, `textureCoords` will get clamped to `maskBounds` and the edge pixels
    // will always land on a 0-coverage border pixel assuming the atlas was prepared with 1px
    // padding around each mask entry. This includes inverse fills where the mask was fully clipped
    // out, since then maskBounds.RBLT == (0,0,-1,-1) and we sample the top-left-most pixel of the
    // atlas, which is guaranteed to be transparent.
    if (all(lessThanEqual(maskBoundsIn.LT, maskBoundsIn.RB))) {
        // Regular fill
        maskBounds = maskBoundsIn;
        invert = 0;
    } else {
        // Re-arrange the mask bounds to sorted order for texture clamping in the fragment shader
        maskBounds = maskBoundsIn.RBLT;
        invert = 1;
    }

    return float4(drawCoords.xy, depth*drawCoords.z, drawCoords.z);
}

float4 cover_bounds_vertex_fn(float2 corner,
                              float4 bounds,
                              float depth,
                              float3x3 matrix,
                              out float2 stepLocalCoords) {
    if (all(lessThanEqual(bounds.LT, bounds.RB))) {
        // A regular fill
        corner = mix(bounds.LT, bounds.RB, corner);
        float3 devCorner = matrix * corner.xy1;
        stepLocalCoords = corner;
        return float4(devCorner.xy, depth*devCorner.z, devCorner.z);
    } else {
        // An inverse fill
        corner = mix(bounds.RB, bounds.LT, corner);
        // TODO(b/351923375): Get the 3x3 inverse  of the local-to-device transform from the CPU
        // if it can be computed fast enough on the CPU from the cached 4x4 inverse.
        float3 localCoords = inverse(matrix) * corner.xy1;
        // Dividing the inverse mapped local coords by its homogenous coordinate reconstructs the
        // original local coords.
        float invW = 1.0 / localCoords.z;
        stepLocalCoords = localCoords.xy * invW;

        // 1/W also happens to be equal to (matrix*stepLocalCoords.xy1).z, which is the device-space
        // homogenous coordinate we want perspective interpolation to respect. We multiply the
        // output position by 1/W and set the output position's homogenous coord to that same 1/W
        // which ensures the projected vertices are still the device-space corners, but
        // stepLocalCoords will be correctly perspective interpolated by HW.
        return float4(corner*invW, depth*invW, invW);
    }
}

[ Dauer der Verarbeitung: 0.8 Sekunden  (vorverarbeitet)  ]

                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....
    

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge