1/*
2 * Copyright (C) 2012 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16#define LOG_NDEBUG 1
17
18#define VERTEX_DEBUG 0
19
20#if VERTEX_DEBUG
21#define DEBUG_DUMP_ALPHA_BUFFER()                                                           \
22    for (unsigned int i = 0; i < vertexBuffer.getSize(); i++) {                             \
23        ALOGD("point %d at %f %f, alpha %f", i, buffer[i].x, buffer[i].y, buffer[i].alpha); \
24    }
25#define DEBUG_DUMP_BUFFER()                                      \
26    for (unsigned int i = 0; i < vertexBuffer.getSize(); i++) {  \
27        ALOGD("point %d at %f %f", i, buffer[i].x, buffer[i].y); \
28    }
29#else
30#define DEBUG_DUMP_ALPHA_BUFFER()
31#define DEBUG_DUMP_BUFFER()
32#endif
33
34#include "PathTessellator.h"
35
36#include "Matrix.h"
37#include "Vector.h"
38#include "Vertex.h"
39#include "utils/MathUtils.h"
40
41#include <algorithm>
42
43#include <SkGeometry.h>  // WARNING: Internal Skia Header
44#include <SkPaint.h>
45#include <SkPath.h>
46#include <SkPoint.h>
47
48#include <stdint.h>
49#include <stdlib.h>
50#include <sys/types.h>
51
52#include <utils/Log.h>
53#include <utils/Trace.h>
54
55namespace android {
56namespace uirenderer {
57
58#define OUTLINE_REFINE_THRESHOLD 0.5f
59#define ROUND_CAP_THRESH 0.25f
60#define PI 3.1415926535897932f
61#define MAX_DEPTH 15
62
63/**
64 * Extracts the x and y scale from the transform as positive values, and clamps them
65 */
66void PathTessellator::extractTessellationScales(const Matrix4& transform, float* scaleX,
67                                                float* scaleY) {
68    if (CC_LIKELY(transform.isPureTranslate())) {
69        *scaleX = 1.0f;
70        *scaleY = 1.0f;
71    } else {
72        float m00 = transform.data[Matrix4::kScaleX];
73        float m01 = transform.data[Matrix4::kSkewY];
74        float m10 = transform.data[Matrix4::kSkewX];
75        float m11 = transform.data[Matrix4::kScaleY];
76        *scaleX = MathUtils::clampTessellationScale(sqrt(m00 * m00 + m01 * m01));
77        *scaleY = MathUtils::clampTessellationScale(sqrt(m10 * m10 + m11 * m11));
78    }
79}
80
81/**
82 * Produces a pseudo-normal for a vertex, given the normals of the two incoming lines. If the offset
83 * from each vertex in a perimeter is calculated, the resultant lines connecting the offset vertices
84 * will be offset by 1.0
85 *
86 * Note that we can't add and normalize the two vectors, that would result in a rectangle having an
87 * offset of (sqrt(2)/2, sqrt(2)/2) at each corner, instead of (1, 1)
88 *
89 * NOTE: assumes angles between normals 90 degrees or less
90 */
91inline static Vector2 totalOffsetFromNormals(const Vector2& normalA, const Vector2& normalB) {
92    return (normalA + normalB) / (1 + fabs(normalA.dot(normalB)));
93}
94
95/**
96 * Structure used for storing useful information about the SkPaint and scale used for tessellating
97 */
98struct PaintInfo {
99public:
100    PaintInfo(const SkPaint* paint, const mat4& transform)
101            : style(paint->getStyle())
102            , cap(paint->getStrokeCap())
103            , isAA(paint->isAntiAlias())
104            , halfStrokeWidth(paint->getStrokeWidth() * 0.5f)
105            , maxAlpha(1.0f) {
106        // compute inverse scales
107        if (CC_LIKELY(transform.isPureTranslate())) {
108            inverseScaleX = 1.0f;
109            inverseScaleY = 1.0f;
110        } else {
111            float scaleX, scaleY;
112            PathTessellator::extractTessellationScales(transform, &scaleX, &scaleY);
113            inverseScaleX = 1.0f / scaleX;
114            inverseScaleY = 1.0f / scaleY;
115        }
116
117        if (isAA && halfStrokeWidth != 0 && inverseScaleX == inverseScaleY &&
118            2 * halfStrokeWidth < inverseScaleX) {
119            // AA, with non-hairline stroke, width < 1 pixel. Scale alpha and treat as hairline.
120            maxAlpha *= (2 * halfStrokeWidth) / inverseScaleX;
121            halfStrokeWidth = 0.0f;
122        }
123    }
124
125    SkPaint::Style style;
126    SkPaint::Cap cap;
127    bool isAA;
128    float inverseScaleX;
129    float inverseScaleY;
130    float halfStrokeWidth;
131    float maxAlpha;
132
133    inline void scaleOffsetForStrokeWidth(Vector2& offset) const {
134        if (halfStrokeWidth == 0.0f) {
135            // hairline - compensate for scale
136            offset.x *= 0.5f * inverseScaleX;
137            offset.y *= 0.5f * inverseScaleY;
138        } else {
139            offset *= halfStrokeWidth;
140        }
141    }
142
143    /**
144     * NOTE: the input will not always be a normal, especially for sharp edges - it should be the
145     * result of totalOffsetFromNormals (see documentation there)
146     */
147    inline Vector2 deriveAAOffset(const Vector2& offset) const {
148        return (Vector2){offset.x * 0.5f * inverseScaleX, offset.y * 0.5f * inverseScaleY};
149    }
150
151    /**
152     * Returns the number of cap divisions beyond the minimum 2 (kButt_Cap/kSquareCap will return 0)
153     * Should only be used when stroking and drawing caps
154     */
155    inline int capExtraDivisions() const {
156        if (cap == SkPaint::kRound_Cap) {
157            // always use 2 points for hairline
158            if (halfStrokeWidth == 0.0f) return 2;
159
160            float threshold = std::min(inverseScaleX, inverseScaleY) * ROUND_CAP_THRESH;
161            return MathUtils::divisionsNeededToApproximateArc(halfStrokeWidth, PI, threshold);
162        }
163        return 0;
164    }
165
166    /**
167     * Outset the bounds of point data (for line endpoints or points) to account for stroke
168     * geometry.
169     *
170     * bounds are in pre-scaled space.
171     */
172    void expandBoundsForStroke(Rect* bounds) const {
173        if (halfStrokeWidth == 0) {
174            // hairline, outset by (0.5f + fudge factor) in post-scaling space
175            bounds->outset(fabs(inverseScaleX) * (0.5f + Vertex::GeometryFudgeFactor()),
176                           fabs(inverseScaleY) * (0.5f + Vertex::GeometryFudgeFactor()));
177        } else {
178            // non hairline, outset by half stroke width pre-scaled, and fudge factor post scaled
179            bounds->outset(halfStrokeWidth + fabs(inverseScaleX) * Vertex::GeometryFudgeFactor(),
180                           halfStrokeWidth + fabs(inverseScaleY) * Vertex::GeometryFudgeFactor());
181        }
182    }
183};
184
185void getFillVerticesFromPerimeter(const std::vector<Vertex>& perimeter,
186                                  VertexBuffer& vertexBuffer) {
187    Vertex* buffer = vertexBuffer.alloc<Vertex>(perimeter.size());
188
189    int currentIndex = 0;
190    // zig zag between all previous points on the inside of the hull to create a
191    // triangle strip that fills the hull
192    int srcAindex = 0;
193    int srcBindex = perimeter.size() - 1;
194    while (srcAindex <= srcBindex) {
195        buffer[currentIndex++] = perimeter[srcAindex];
196        if (srcAindex == srcBindex) break;
197        buffer[currentIndex++] = perimeter[srcBindex];
198        srcAindex++;
199        srcBindex--;
200    }
201}
202
203/*
204 * Fills a vertexBuffer with non-alpha vertices, zig-zagging at each perimeter point to create a
205 * tri-strip as wide as the stroke.
206 *
207 * Uses an additional 2 vertices at the end to wrap around, closing the tri-strip
208 * (for a total of perimeter.size() * 2 + 2 vertices)
209 */
210void getStrokeVerticesFromPerimeter(const PaintInfo& paintInfo,
211                                    const std::vector<Vertex>& perimeter,
212                                    VertexBuffer& vertexBuffer) {
213    Vertex* buffer = vertexBuffer.alloc<Vertex>(perimeter.size() * 2 + 2);
214
215    int currentIndex = 0;
216    const Vertex* last = &(perimeter[perimeter.size() - 1]);
217    const Vertex* current = &(perimeter[0]);
218    Vector2 lastNormal = {current->y - last->y, last->x - current->x};
219    lastNormal.normalize();
220    for (unsigned int i = 0; i < perimeter.size(); i++) {
221        const Vertex* next = &(perimeter[i + 1 >= perimeter.size() ? 0 : i + 1]);
222        Vector2 nextNormal = {next->y - current->y, current->x - next->x};
223        nextNormal.normalize();
224
225        Vector2 totalOffset = totalOffsetFromNormals(lastNormal, nextNormal);
226        paintInfo.scaleOffsetForStrokeWidth(totalOffset);
227
228        Vertex::set(&buffer[currentIndex++], current->x + totalOffset.x,
229                    current->y + totalOffset.y);
230
231        Vertex::set(&buffer[currentIndex++], current->x - totalOffset.x,
232                    current->y - totalOffset.y);
233
234        current = next;
235        lastNormal = nextNormal;
236    }
237
238    // wrap around to beginning
239    buffer[currentIndex++] = buffer[0];
240    buffer[currentIndex++] = buffer[1];
241
242    DEBUG_DUMP_BUFFER();
243}
244
245static inline void storeBeginEnd(const PaintInfo& paintInfo, const Vertex& center,
246                                 const Vector2& normal, Vertex* buffer, int& currentIndex,
247                                 bool begin) {
248    Vector2 strokeOffset = normal;
249    paintInfo.scaleOffsetForStrokeWidth(strokeOffset);
250
251    Vector2 referencePoint = {center.x, center.y};
252    if (paintInfo.cap == SkPaint::kSquare_Cap) {
253        Vector2 rotated = {-strokeOffset.y, strokeOffset.x};
254        referencePoint += rotated * (begin ? -1 : 1);
255    }
256
257    Vertex::set(&buffer[currentIndex++], referencePoint + strokeOffset);
258    Vertex::set(&buffer[currentIndex++], referencePoint - strokeOffset);
259}
260
261/**
262 * Fills a vertexBuffer with non-alpha vertices similar to getStrokeVerticesFromPerimeter, except:
263 *
264 * 1 - Doesn't need to wrap around, since the input vertices are unclosed
265 *
266 * 2 - can zig-zag across 'extra' vertices at either end, to create round caps
267 */
268void getStrokeVerticesFromUnclosedVertices(const PaintInfo& paintInfo,
269                                           const std::vector<Vertex>& vertices,
270                                           VertexBuffer& vertexBuffer) {
271    const int extra = paintInfo.capExtraDivisions();
272    const int allocSize = (vertices.size() + extra) * 2;
273    Vertex* buffer = vertexBuffer.alloc<Vertex>(allocSize);
274
275    const int lastIndex = vertices.size() - 1;
276    if (extra > 0) {
277        // tessellate both round caps
278        float beginTheta = atan2(-(vertices[0].x - vertices[1].x), vertices[0].y - vertices[1].y);
279        float endTheta = atan2(-(vertices[lastIndex].x - vertices[lastIndex - 1].x),
280                               vertices[lastIndex].y - vertices[lastIndex - 1].y);
281        const float dTheta = PI / (extra + 1);
282
283        int capOffset;
284        for (int i = 0; i < extra; i++) {
285            if (i < extra / 2) {
286                capOffset = extra - 2 * i - 1;
287            } else {
288                capOffset = 2 * i - extra;
289            }
290
291            beginTheta += dTheta;
292            Vector2 beginRadialOffset = {cosf(beginTheta), sinf(beginTheta)};
293            paintInfo.scaleOffsetForStrokeWidth(beginRadialOffset);
294            Vertex::set(&buffer[capOffset], vertices[0].x + beginRadialOffset.x,
295                        vertices[0].y + beginRadialOffset.y);
296
297            endTheta += dTheta;
298            Vector2 endRadialOffset = {cosf(endTheta), sinf(endTheta)};
299            paintInfo.scaleOffsetForStrokeWidth(endRadialOffset);
300            Vertex::set(&buffer[allocSize - 1 - capOffset],
301                        vertices[lastIndex].x + endRadialOffset.x,
302                        vertices[lastIndex].y + endRadialOffset.y);
303        }
304    }
305
306    int currentIndex = extra;
307    const Vertex* last = &(vertices[0]);
308    const Vertex* current = &(vertices[1]);
309    Vector2 lastNormal = {current->y - last->y, last->x - current->x};
310    lastNormal.normalize();
311
312    storeBeginEnd(paintInfo, vertices[0], lastNormal, buffer, currentIndex, true);
313
314    for (unsigned int i = 1; i < vertices.size() - 1; i++) {
315        const Vertex* next = &(vertices[i + 1]);
316        Vector2 nextNormal = {next->y - current->y, current->x - next->x};
317        nextNormal.normalize();
318
319        Vector2 strokeOffset = totalOffsetFromNormals(lastNormal, nextNormal);
320        paintInfo.scaleOffsetForStrokeWidth(strokeOffset);
321
322        Vector2 center = {current->x, current->y};
323        Vertex::set(&buffer[currentIndex++], center + strokeOffset);
324        Vertex::set(&buffer[currentIndex++], center - strokeOffset);
325
326        current = next;
327        lastNormal = nextNormal;
328    }
329
330    storeBeginEnd(paintInfo, vertices[lastIndex], lastNormal, buffer, currentIndex, false);
331
332    DEBUG_DUMP_BUFFER();
333}
334
335/**
336 * Populates a vertexBuffer with AlphaVertices to create an anti-aliased fill shape tessellation
337 *
338 * 1 - create the AA perimeter of unit width, by zig-zagging at each point around the perimeter of
339 * the shape (using 2 * perimeter.size() vertices)
340 *
341 * 2 - wrap around to the beginning to complete the perimeter (2 vertices)
342 *
343 * 3 - zig zag back and forth inside the shape to fill it (using perimeter.size() vertices)
344 */
345void getFillVerticesFromPerimeterAA(const PaintInfo& paintInfo,
346                                    const std::vector<Vertex>& perimeter,
347                                    VertexBuffer& vertexBuffer, float maxAlpha = 1.0f) {
348    AlphaVertex* buffer = vertexBuffer.alloc<AlphaVertex>(perimeter.size() * 3 + 2);
349
350    // generate alpha points - fill Alpha vertex gaps in between each point with
351    // alpha 0 vertex, offset by a scaled normal.
352    int currentIndex = 0;
353    const Vertex* last = &(perimeter[perimeter.size() - 1]);
354    const Vertex* current = &(perimeter[0]);
355    Vector2 lastNormal = {current->y - last->y, last->x - current->x};
356    lastNormal.normalize();
357    for (unsigned int i = 0; i < perimeter.size(); i++) {
358        const Vertex* next = &(perimeter[i + 1 >= perimeter.size() ? 0 : i + 1]);
359        Vector2 nextNormal = {next->y - current->y, current->x - next->x};
360        nextNormal.normalize();
361
362        // AA point offset from original point is that point's normal, such that each side is offset
363        // by .5 pixels
364        Vector2 totalOffset =
365                paintInfo.deriveAAOffset(totalOffsetFromNormals(lastNormal, nextNormal));
366
367        AlphaVertex::set(&buffer[currentIndex++], current->x + totalOffset.x,
368                         current->y + totalOffset.y, 0.0f);
369        AlphaVertex::set(&buffer[currentIndex++], current->x - totalOffset.x,
370                         current->y - totalOffset.y, maxAlpha);
371
372        current = next;
373        lastNormal = nextNormal;
374    }
375
376    // wrap around to beginning
377    buffer[currentIndex++] = buffer[0];
378    buffer[currentIndex++] = buffer[1];
379
380    // zig zag between all previous points on the inside of the hull to create a
381    // triangle strip that fills the hull, repeating the first inner point to
382    // create degenerate tris to start inside path
383    int srcAindex = 0;
384    int srcBindex = perimeter.size() - 1;
385    while (srcAindex <= srcBindex) {
386        buffer[currentIndex++] = buffer[srcAindex * 2 + 1];
387        if (srcAindex == srcBindex) break;
388        buffer[currentIndex++] = buffer[srcBindex * 2 + 1];
389        srcAindex++;
390        srcBindex--;
391    }
392
393    DEBUG_DUMP_BUFFER();
394}
395
396/**
397 * Stores geometry for a single, AA-perimeter (potentially rounded) cap
398 *
399 * For explanation of constants and general methodoloyg, see comments for
400 * getStrokeVerticesFromUnclosedVerticesAA() below.
401 */
402inline static void storeCapAA(const PaintInfo& paintInfo, const std::vector<Vertex>& vertices,
403                              AlphaVertex* buffer, bool isFirst, Vector2 normal, int offset) {
404    const int extra = paintInfo.capExtraDivisions();
405    const int extraOffset = (extra + 1) / 2;
406    const int capIndex =
407            isFirst ? 2 * offset + 6 + 2 * (extra + extraOffset) : offset + 2 + 2 * extraOffset;
408    if (isFirst) normal *= -1;
409
410    // TODO: this normal should be scaled by radialScale if extra != 0, see totalOffsetFromNormals()
411    Vector2 AAOffset = paintInfo.deriveAAOffset(normal);
412
413    Vector2 strokeOffset = normal;
414    paintInfo.scaleOffsetForStrokeWidth(strokeOffset);
415    Vector2 outerOffset = strokeOffset + AAOffset;
416    Vector2 innerOffset = strokeOffset - AAOffset;
417
418    Vector2 capAAOffset = {0, 0};
419    if (paintInfo.cap != SkPaint::kRound_Cap) {
420        // if the cap is square or butt, the inside primary cap vertices will be inset in two
421        // directions - both normal to the stroke, and parallel to it.
422        capAAOffset = (Vector2){-AAOffset.y, AAOffset.x};
423    }
424
425    // determine referencePoint, the center point for the 4 primary cap vertices
426    const Vertex& point = isFirst ? vertices.front() : vertices.back();
427    Vector2 referencePoint = {point.x, point.y};
428    if (paintInfo.cap == SkPaint::kSquare_Cap) {
429        // To account for square cap, move the primary cap vertices (that create the AA edge) by the
430        // stroke offset vector (rotated to be parallel to the stroke)
431        Vector2 rotated = {-strokeOffset.y, strokeOffset.x};
432        referencePoint += rotated;
433    }
434
435    AlphaVertex::set(&buffer[capIndex + 0], referencePoint.x + outerOffset.x + capAAOffset.x,
436                     referencePoint.y + outerOffset.y + capAAOffset.y, 0.0f);
437    AlphaVertex::set(&buffer[capIndex + 1], referencePoint.x + innerOffset.x - capAAOffset.x,
438                     referencePoint.y + innerOffset.y - capAAOffset.y, paintInfo.maxAlpha);
439
440    bool isRound = paintInfo.cap == SkPaint::kRound_Cap;
441
442    const int postCapIndex = (isRound && isFirst) ? (2 * extraOffset - 2) : capIndex + (2 * extra);
443    AlphaVertex::set(&buffer[postCapIndex + 2], referencePoint.x - outerOffset.x + capAAOffset.x,
444                     referencePoint.y - outerOffset.y + capAAOffset.y, 0.0f);
445    AlphaVertex::set(&buffer[postCapIndex + 3], referencePoint.x - innerOffset.x - capAAOffset.x,
446                     referencePoint.y - innerOffset.y - capAAOffset.y, paintInfo.maxAlpha);
447
448    if (isRound) {
449        const float dTheta = PI / (extra + 1);
450        const float radialScale = 2.0f / (1 + cos(dTheta));
451        float theta = atan2(normal.y, normal.x);
452        int capPerimIndex = capIndex + 2;
453
454        for (int i = 0; i < extra; i++) {
455            theta += dTheta;
456
457            Vector2 radialOffset = {cosf(theta), sinf(theta)};
458
459            // scale to compensate for pinching at sharp angles, see totalOffsetFromNormals()
460            radialOffset *= radialScale;
461
462            AAOffset = paintInfo.deriveAAOffset(radialOffset);
463            paintInfo.scaleOffsetForStrokeWidth(radialOffset);
464            AlphaVertex::set(&buffer[capPerimIndex++],
465                             referencePoint.x + radialOffset.x + AAOffset.x,
466                             referencePoint.y + radialOffset.y + AAOffset.y, 0.0f);
467            AlphaVertex::set(&buffer[capPerimIndex++],
468                             referencePoint.x + radialOffset.x - AAOffset.x,
469                             referencePoint.y + radialOffset.y - AAOffset.y, paintInfo.maxAlpha);
470
471            if (isFirst && i == extra - extraOffset) {
472                // copy most recent two points to first two points
473                buffer[0] = buffer[capPerimIndex - 2];
474                buffer[1] = buffer[capPerimIndex - 1];
475
476                capPerimIndex = 2;  // start writing the rest of the round cap at index 2
477            }
478        }
479
480        if (isFirst) {
481            const int startCapFillIndex = capIndex + 2 * (extra - extraOffset) + 4;
482            int capFillIndex = startCapFillIndex;
483            for (int i = 0; i < extra + 2; i += 2) {
484                buffer[capFillIndex++] = buffer[1 + i];
485                // TODO: to support odd numbers of divisions, break here on the last iteration
486                buffer[capFillIndex++] = buffer[startCapFillIndex - 3 - i];
487            }
488        } else {
489            int capFillIndex = 6 * vertices.size() + 2 + 6 * extra - (extra + 2);
490            for (int i = 0; i < extra + 2; i += 2) {
491                buffer[capFillIndex++] = buffer[capIndex + 1 + i];
492                // TODO: to support odd numbers of divisions, break here on the last iteration
493                buffer[capFillIndex++] = buffer[capIndex + 3 + 2 * extra - i];
494            }
495        }
496        return;
497    }
498    if (isFirst) {
499        buffer[0] = buffer[postCapIndex + 2];
500        buffer[1] = buffer[postCapIndex + 3];
501        buffer[postCapIndex + 4] = buffer[1];  // degenerate tris (the only two!)
502        buffer[postCapIndex + 5] = buffer[postCapIndex + 1];
503    } else {
504        buffer[6 * vertices.size()] = buffer[postCapIndex + 1];
505        buffer[6 * vertices.size() + 1] = buffer[postCapIndex + 3];
506    }
507}
508
509/*
510the geometry for an aa, capped stroke consists of the following:
511
512       # vertices       |    function
513----------------------------------------------------------------------
514a) 2                    | Start AA perimeter
515b) 2, 2 * roundDivOff   | First half of begin cap's perimeter
516                        |
517   2 * middlePts        | 'Outer' or 'Top' AA perimeter half (between caps)
518                        |
519a) 4                    | End cap's
520b) 2, 2 * roundDivs, 2  |    AA perimeter
521                        |
522   2 * middlePts        | 'Inner' or 'bottom' AA perimeter half
523                        |
524a) 6                    | Begin cap's perimeter
525b) 2, 2*(rD - rDO + 1), | Last half of begin cap's perimeter
526       roundDivs, 2     |
527                        |
528   2 * middlePts        | Stroke's full opacity center strip
529                        |
530a) 2                    | end stroke
531b) 2, roundDivs         |    (and end cap fill, for round)
532
533Notes:
534* rows starting with 'a)' denote the Butt or Square cap vertex use, 'b)' denote Round
535
536* 'middlePts' is (number of points in the unclosed input vertex list, minus 2) times two
537
538* 'roundDivs' or 'rD' is the number of extra vertices (beyond the minimum of 2) that define the
539        round cap's shape, and is at least two. This will increase with cap size to sufficiently
540        define the cap's level of tessellation.
541
542* 'roundDivOffset' or 'rDO' is the point about halfway along the start cap's round perimeter, where
543        the stream of vertices for the AA perimeter starts. By starting and ending the perimeter at
544        this offset, the fill of the stroke is drawn from this point with minimal extra vertices.
545
546This means the outer perimeter starts at:
547    outerIndex = (2) OR (2 + 2 * roundDivOff)
548the inner perimeter (since it is filled in reverse) starts at:
549    innerIndex = outerIndex + (4 * middlePts) + ((4) OR (4 + 2 * roundDivs)) - 1
550the stroke starts at:
551    strokeIndex = innerIndex + 1 + ((6) OR (6 + 3 * roundDivs - 2 * roundDivOffset))
552
553The total needed allocated space is either:
554    2 + 4 + 6 + 2 + 3 * (2 * middlePts) = 14 + 6 * middlePts = 2 + 6 * pts
555or, for rounded caps:
556    (2 + 2 * rDO) + (4 + 2 * rD) + (2 * (rD - rDO + 1)
557            + roundDivs + 4) + (2 + roundDivs) + 3 * (2 * middlePts)
558    = 14 + 6 * middlePts + 6 * roundDivs
559    = 2 + 6 * pts + 6 * roundDivs
560 */
561void getStrokeVerticesFromUnclosedVerticesAA(const PaintInfo& paintInfo,
562                                             const std::vector<Vertex>& vertices,
563                                             VertexBuffer& vertexBuffer) {
564    const int extra = paintInfo.capExtraDivisions();
565    const int allocSize = 6 * vertices.size() + 2 + 6 * extra;
566
567    AlphaVertex* buffer = vertexBuffer.alloc<AlphaVertex>(allocSize);
568
569    const int extraOffset = (extra + 1) / 2;
570    int offset = 2 * (vertices.size() - 2);
571    // there is no outer/inner here, using them for consistency with below approach
572    int currentAAOuterIndex = 2 + 2 * extraOffset;
573    int currentAAInnerIndex = currentAAOuterIndex + (2 * offset) + 3 + (2 * extra);
574    int currentStrokeIndex = currentAAInnerIndex + 7 + (3 * extra - 2 * extraOffset);
575
576    const Vertex* last = &(vertices[0]);
577    const Vertex* current = &(vertices[1]);
578    Vector2 lastNormal = {current->y - last->y, last->x - current->x};
579    lastNormal.normalize();
580
581    // TODO: use normal from bezier traversal for cap, instead of from vertices
582    storeCapAA(paintInfo, vertices, buffer, true, lastNormal, offset);
583
584    for (unsigned int i = 1; i < vertices.size() - 1; i++) {
585        const Vertex* next = &(vertices[i + 1]);
586        Vector2 nextNormal = {next->y - current->y, current->x - next->x};
587        nextNormal.normalize();
588
589        Vector2 totalOffset = totalOffsetFromNormals(lastNormal, nextNormal);
590        Vector2 AAOffset = paintInfo.deriveAAOffset(totalOffset);
591
592        Vector2 innerOffset = totalOffset;
593        paintInfo.scaleOffsetForStrokeWidth(innerOffset);
594        Vector2 outerOffset = innerOffset + AAOffset;
595        innerOffset -= AAOffset;
596
597        AlphaVertex::set(&buffer[currentAAOuterIndex++], current->x + outerOffset.x,
598                         current->y + outerOffset.y, 0.0f);
599        AlphaVertex::set(&buffer[currentAAOuterIndex++], current->x + innerOffset.x,
600                         current->y + innerOffset.y, paintInfo.maxAlpha);
601
602        AlphaVertex::set(&buffer[currentStrokeIndex++], current->x + innerOffset.x,
603                         current->y + innerOffset.y, paintInfo.maxAlpha);
604        AlphaVertex::set(&buffer[currentStrokeIndex++], current->x - innerOffset.x,
605                         current->y - innerOffset.y, paintInfo.maxAlpha);
606
607        AlphaVertex::set(&buffer[currentAAInnerIndex--], current->x - innerOffset.x,
608                         current->y - innerOffset.y, paintInfo.maxAlpha);
609        AlphaVertex::set(&buffer[currentAAInnerIndex--], current->x - outerOffset.x,
610                         current->y - outerOffset.y, 0.0f);
611
612        current = next;
613        lastNormal = nextNormal;
614    }
615
616    // TODO: use normal from bezier traversal for cap, instead of from vertices
617    storeCapAA(paintInfo, vertices, buffer, false, lastNormal, offset);
618
619    DEBUG_DUMP_ALPHA_BUFFER();
620}
621
622void getStrokeVerticesFromPerimeterAA(const PaintInfo& paintInfo,
623                                      const std::vector<Vertex>& perimeter,
624                                      VertexBuffer& vertexBuffer) {
625    AlphaVertex* buffer = vertexBuffer.alloc<AlphaVertex>(6 * perimeter.size() + 8);
626
627    int offset = 2 * perimeter.size() + 3;
628    int currentAAOuterIndex = 0;
629    int currentStrokeIndex = offset;
630    int currentAAInnerIndex = offset * 2;
631
632    const Vertex* last = &(perimeter[perimeter.size() - 1]);
633    const Vertex* current = &(perimeter[0]);
634    Vector2 lastNormal = {current->y - last->y, last->x - current->x};
635    lastNormal.normalize();
636    for (unsigned int i = 0; i < perimeter.size(); i++) {
637        const Vertex* next = &(perimeter[i + 1 >= perimeter.size() ? 0 : i + 1]);
638        Vector2 nextNormal = {next->y - current->y, current->x - next->x};
639        nextNormal.normalize();
640
641        Vector2 totalOffset = totalOffsetFromNormals(lastNormal, nextNormal);
642        Vector2 AAOffset = paintInfo.deriveAAOffset(totalOffset);
643
644        Vector2 innerOffset = totalOffset;
645        paintInfo.scaleOffsetForStrokeWidth(innerOffset);
646        Vector2 outerOffset = innerOffset + AAOffset;
647        innerOffset -= AAOffset;
648
649        AlphaVertex::set(&buffer[currentAAOuterIndex++], current->x + outerOffset.x,
650                         current->y + outerOffset.y, 0.0f);
651        AlphaVertex::set(&buffer[currentAAOuterIndex++], current->x + innerOffset.x,
652                         current->y + innerOffset.y, paintInfo.maxAlpha);
653
654        AlphaVertex::set(&buffer[currentStrokeIndex++], current->x + innerOffset.x,
655                         current->y + innerOffset.y, paintInfo.maxAlpha);
656        AlphaVertex::set(&buffer[currentStrokeIndex++], current->x - innerOffset.x,
657                         current->y - innerOffset.y, paintInfo.maxAlpha);
658
659        AlphaVertex::set(&buffer[currentAAInnerIndex++], current->x - innerOffset.x,
660                         current->y - innerOffset.y, paintInfo.maxAlpha);
661        AlphaVertex::set(&buffer[currentAAInnerIndex++], current->x - outerOffset.x,
662                         current->y - outerOffset.y, 0.0f);
663
664        current = next;
665        lastNormal = nextNormal;
666    }
667
668    // wrap each strip around to beginning, creating degenerate tris to bridge strips
669    buffer[currentAAOuterIndex++] = buffer[0];
670    buffer[currentAAOuterIndex++] = buffer[1];
671    buffer[currentAAOuterIndex++] = buffer[1];
672
673    buffer[currentStrokeIndex++] = buffer[offset];
674    buffer[currentStrokeIndex++] = buffer[offset + 1];
675    buffer[currentStrokeIndex++] = buffer[offset + 1];
676
677    buffer[currentAAInnerIndex++] = buffer[2 * offset];
678    buffer[currentAAInnerIndex++] = buffer[2 * offset + 1];
679    // don't need to create last degenerate tri
680
681    DEBUG_DUMP_ALPHA_BUFFER();
682}
683
684void PathTessellator::tessellatePath(const SkPath& path, const SkPaint* paint,
685                                     const mat4& transform, VertexBuffer& vertexBuffer) {
686    ATRACE_CALL();
687
688    const PaintInfo paintInfo(paint, transform);
689
690    std::vector<Vertex> tempVertices;
691    float threshInvScaleX = paintInfo.inverseScaleX;
692    float threshInvScaleY = paintInfo.inverseScaleY;
693    if (paintInfo.style == SkPaint::kStroke_Style) {
694        // alter the bezier recursion threshold values we calculate in order to compensate for
695        // expansion done after the path vertices are found
696        SkRect bounds = path.getBounds();
697        if (!bounds.isEmpty()) {
698            threshInvScaleX *= bounds.width() / (bounds.width() + paint->getStrokeWidth());
699            threshInvScaleY *= bounds.height() / (bounds.height() + paint->getStrokeWidth());
700        }
701    }
702
703    // force close if we're filling the path, since fill path expects closed perimeter.
704    bool forceClose = paintInfo.style != SkPaint::kStroke_Style;
705    PathApproximationInfo approximationInfo(threshInvScaleX, threshInvScaleY,
706                                            OUTLINE_REFINE_THRESHOLD);
707    bool wasClosed =
708            approximatePathOutlineVertices(path, forceClose, approximationInfo, tempVertices);
709
710    if (!tempVertices.size()) {
711        // path was empty, return without allocating vertex buffer
712        return;
713    }
714
715#if VERTEX_DEBUG
716    for (unsigned int i = 0; i < tempVertices.size(); i++) {
717        ALOGD("orig path: point at %f %f", tempVertices[i].x, tempVertices[i].y);
718    }
719#endif
720
721    if (paintInfo.style == SkPaint::kStroke_Style) {
722        if (!paintInfo.isAA) {
723            if (wasClosed) {
724                getStrokeVerticesFromPerimeter(paintInfo, tempVertices, vertexBuffer);
725            } else {
726                getStrokeVerticesFromUnclosedVertices(paintInfo, tempVertices, vertexBuffer);
727            }
728
729        } else {
730            if (wasClosed) {
731                getStrokeVerticesFromPerimeterAA(paintInfo, tempVertices, vertexBuffer);
732            } else {
733                getStrokeVerticesFromUnclosedVerticesAA(paintInfo, tempVertices, vertexBuffer);
734            }
735        }
736    } else {
737        // For kStrokeAndFill style, the path should be adjusted externally.
738        // It will be treated as a fill here.
739        if (!paintInfo.isAA) {
740            getFillVerticesFromPerimeter(tempVertices, vertexBuffer);
741        } else {
742            getFillVerticesFromPerimeterAA(paintInfo, tempVertices, vertexBuffer);
743        }
744    }
745
746    Rect bounds(path.getBounds());
747    paintInfo.expandBoundsForStroke(&bounds);
748    vertexBuffer.setBounds(bounds);
749    vertexBuffer.setMeshFeatureFlags(paintInfo.isAA ? VertexBuffer::kAlpha : VertexBuffer::kNone);
750}
751
752template <class TYPE>
753static void instanceVertices(VertexBuffer& srcBuffer, VertexBuffer& dstBuffer, const float* points,
754                             int count, Rect& bounds) {
755    bounds.set(points[0], points[1], points[0], points[1]);
756
757    int numPoints = count / 2;
758    int verticesPerPoint = srcBuffer.getVertexCount();
759    dstBuffer.alloc<TYPE>(numPoints * verticesPerPoint + (numPoints - 1) * 2);
760
761    for (int i = 0; i < count; i += 2) {
762        bounds.expandToCover(points[i + 0], points[i + 1]);
763        dstBuffer.copyInto<TYPE>(srcBuffer, points[i + 0], points[i + 1]);
764    }
765    dstBuffer.createDegenerateSeparators<TYPE>(verticesPerPoint);
766}
767
768void PathTessellator::tessellatePoints(const float* points, int count, const SkPaint* paint,
769                                       const mat4& transform, VertexBuffer& vertexBuffer) {
770    const PaintInfo paintInfo(paint, transform);
771
772    // determine point shape
773    SkPath path;
774    float radius = paintInfo.halfStrokeWidth;
775    if (radius == 0.0f) radius = 0.5f;
776
777    if (paintInfo.cap == SkPaint::kRound_Cap) {
778        path.addCircle(0, 0, radius);
779    } else {
780        path.addRect(-radius, -radius, radius, radius);
781    }
782
783    // calculate outline
784    std::vector<Vertex> outlineVertices;
785    PathApproximationInfo approximationInfo(paintInfo.inverseScaleX, paintInfo.inverseScaleY,
786                                            OUTLINE_REFINE_THRESHOLD);
787    approximatePathOutlineVertices(path, true, approximationInfo, outlineVertices);
788
789    if (!outlineVertices.size()) return;
790
791    Rect bounds;
792    // tessellate, then duplicate outline across points
793    VertexBuffer tempBuffer;
794    if (!paintInfo.isAA) {
795        getFillVerticesFromPerimeter(outlineVertices, tempBuffer);
796        instanceVertices<Vertex>(tempBuffer, vertexBuffer, points, count, bounds);
797    } else {
798        // note: pass maxAlpha directly, since we want fill to be alpha modulated
799        getFillVerticesFromPerimeterAA(paintInfo, outlineVertices, tempBuffer, paintInfo.maxAlpha);
800        instanceVertices<AlphaVertex>(tempBuffer, vertexBuffer, points, count, bounds);
801    }
802
803    // expand bounds from vertex coords to pixel data
804    paintInfo.expandBoundsForStroke(&bounds);
805    vertexBuffer.setBounds(bounds);
806    vertexBuffer.setMeshFeatureFlags(paintInfo.isAA ? VertexBuffer::kAlpha : VertexBuffer::kNone);
807}
808
809void PathTessellator::tessellateLines(const float* points, int count, const SkPaint* paint,
810                                      const mat4& transform, VertexBuffer& vertexBuffer) {
811    ATRACE_CALL();
812    const PaintInfo paintInfo(paint, transform);
813
814    const int extra = paintInfo.capExtraDivisions();
815    int numLines = count / 4;
816    int lineAllocSize;
817    // pre-allocate space for lines in the buffer, and degenerate tris in between
818    if (paintInfo.isAA) {
819        lineAllocSize = 6 * (2) + 2 + 6 * extra;
820        vertexBuffer.alloc<AlphaVertex>(numLines * lineAllocSize + (numLines - 1) * 2);
821    } else {
822        lineAllocSize = 2 * ((2) + extra);
823        vertexBuffer.alloc<Vertex>(numLines * lineAllocSize + (numLines - 1) * 2);
824    }
825
826    std::vector<Vertex> tempVertices(2);
827    Vertex* tempVerticesData = &tempVertices.front();
828    Rect bounds;
829    bounds.set(points[0], points[1], points[0], points[1]);
830    for (int i = 0; i < count; i += 4) {
831        Vertex::set(&(tempVerticesData[0]), points[i + 0], points[i + 1]);
832        Vertex::set(&(tempVerticesData[1]), points[i + 2], points[i + 3]);
833
834        if (paintInfo.isAA) {
835            getStrokeVerticesFromUnclosedVerticesAA(paintInfo, tempVertices, vertexBuffer);
836        } else {
837            getStrokeVerticesFromUnclosedVertices(paintInfo, tempVertices, vertexBuffer);
838        }
839
840        // calculate bounds
841        bounds.expandToCover(tempVerticesData[0].x, tempVerticesData[0].y);
842        bounds.expandToCover(tempVerticesData[1].x, tempVerticesData[1].y);
843    }
844
845    // since multiple objects tessellated into buffer, separate them with degen tris
846    if (paintInfo.isAA) {
847        vertexBuffer.createDegenerateSeparators<AlphaVertex>(lineAllocSize);
848    } else {
849        vertexBuffer.createDegenerateSeparators<Vertex>(lineAllocSize);
850    }
851
852    // expand bounds from vertex coords to pixel data
853    paintInfo.expandBoundsForStroke(&bounds);
854    vertexBuffer.setBounds(bounds);
855    vertexBuffer.setMeshFeatureFlags(paintInfo.isAA ? VertexBuffer::kAlpha : VertexBuffer::kNone);
856}
857
858///////////////////////////////////////////////////////////////////////////////
859// Simple path line approximation
860///////////////////////////////////////////////////////////////////////////////
861
862bool PathTessellator::approximatePathOutlineVertices(const SkPath& path, float threshold,
863                                                     std::vector<Vertex>& outputVertices) {
864    PathApproximationInfo approximationInfo(1.0f, 1.0f, threshold);
865    return approximatePathOutlineVertices(path, true, approximationInfo, outputVertices);
866}
867
868class ClockwiseEnforcer {
869public:
870    void addPoint(const SkPoint& point) {
871        double x = point.x();
872        double y = point.y();
873
874        if (initialized) {
875            sum += (x + lastX) * (y - lastY);
876        } else {
877            initialized = true;
878        }
879
880        lastX = x;
881        lastY = y;
882    }
883    void reverseVectorIfNotClockwise(std::vector<Vertex>& vertices) {
884        if (sum < 0) {
885            // negative sum implies CounterClockwise
886            const int size = vertices.size();
887            for (int i = 0; i < size / 2; i++) {
888                Vertex tmp = vertices[i];
889                int k = size - 1 - i;
890                vertices[i] = vertices[k];
891                vertices[k] = tmp;
892            }
893        }
894    }
895
896private:
897    bool initialized = false;
898    double lastX = 0;
899    double lastY = 0;
900    double sum = 0;
901};
902
903bool PathTessellator::approximatePathOutlineVertices(const SkPath& path, bool forceClose,
904                                                     const PathApproximationInfo& approximationInfo,
905                                                     std::vector<Vertex>& outputVertices) {
906    ATRACE_CALL();
907
908    // TODO: to support joins other than sharp miter, join vertices should be labelled in the
909    // perimeter, or resolved into more vertices. Reconsider forceClose-ing in that case.
910    SkPath::Iter iter(path, forceClose);
911    SkPoint pts[4];
912    SkPath::Verb v;
913    ClockwiseEnforcer clockwiseEnforcer;
914    while (SkPath::kDone_Verb != (v = iter.next(pts))) {
915        switch (v) {
916            case SkPath::kMove_Verb:
917                outputVertices.push_back(Vertex{pts[0].x(), pts[0].y()});
918                ALOGV("Move to pos %f %f", pts[0].x(), pts[0].y());
919                clockwiseEnforcer.addPoint(pts[0]);
920                break;
921            case SkPath::kClose_Verb:
922                ALOGV("Close at pos %f %f", pts[0].x(), pts[0].y());
923                clockwiseEnforcer.addPoint(pts[0]);
924                break;
925            case SkPath::kLine_Verb:
926                ALOGV("kLine_Verb %f %f -> %f %f", pts[0].x(), pts[0].y(), pts[1].x(), pts[1].y());
927                outputVertices.push_back(Vertex{pts[1].x(), pts[1].y()});
928                clockwiseEnforcer.addPoint(pts[1]);
929                break;
930            case SkPath::kQuad_Verb:
931                ALOGV("kQuad_Verb");
932                recursiveQuadraticBezierVertices(pts[0].x(), pts[0].y(), pts[2].x(), pts[2].y(),
933                                                 pts[1].x(), pts[1].y(), approximationInfo,
934                                                 outputVertices);
935                clockwiseEnforcer.addPoint(pts[1]);
936                clockwiseEnforcer.addPoint(pts[2]);
937                break;
938            case SkPath::kCubic_Verb:
939                ALOGV("kCubic_Verb");
940                recursiveCubicBezierVertices(pts[0].x(), pts[0].y(), pts[1].x(), pts[1].y(),
941                                             pts[3].x(), pts[3].y(), pts[2].x(), pts[2].y(),
942                                             approximationInfo, outputVertices);
943                clockwiseEnforcer.addPoint(pts[1]);
944                clockwiseEnforcer.addPoint(pts[2]);
945                clockwiseEnforcer.addPoint(pts[3]);
946                break;
947            case SkPath::kConic_Verb: {
948                ALOGV("kConic_Verb");
949                SkAutoConicToQuads converter;
950                const SkPoint* quads = converter.computeQuads(
951                        pts, iter.conicWeight(), approximationInfo.thresholdForConicQuads);
952                for (int i = 0; i < converter.countQuads(); ++i) {
953                    const int offset = 2 * i;
954                    recursiveQuadraticBezierVertices(quads[offset].x(), quads[offset].y(),
955                                                     quads[offset + 2].x(), quads[offset + 2].y(),
956                                                     quads[offset + 1].x(), quads[offset + 1].y(),
957                                                     approximationInfo, outputVertices);
958                }
959                clockwiseEnforcer.addPoint(pts[1]);
960                clockwiseEnforcer.addPoint(pts[2]);
961                break;
962            }
963            default:
964                static_assert(SkPath::kMove_Verb == 0 && SkPath::kLine_Verb == 1 &&
965                                      SkPath::kQuad_Verb == 2 && SkPath::kConic_Verb == 3 &&
966                                      SkPath::kCubic_Verb == 4 && SkPath::kClose_Verb == 5 &&
967                                      SkPath::kDone_Verb == 6,
968                              "Path enum changed, new types may have been added");
969                break;
970        }
971    }
972
973    bool wasClosed = false;
974    int size = outputVertices.size();
975    if (size >= 2 && outputVertices[0].x == outputVertices[size - 1].x &&
976        outputVertices[0].y == outputVertices[size - 1].y) {
977        outputVertices.pop_back();
978        wasClosed = true;
979    }
980
981    // ensure output vector is clockwise
982    clockwiseEnforcer.reverseVectorIfNotClockwise(outputVertices);
983    return wasClosed;
984}
985
986///////////////////////////////////////////////////////////////////////////////
987// Bezier approximation
988//
989// All the inputs and outputs here are in path coordinates.
990// We convert the error threshold from screen coordinates into path coordinates.
991///////////////////////////////////////////////////////////////////////////////
992
993// Get a threshold in path coordinates, by scaling the thresholdSquared from screen coordinates.
994// TODO: Document the math behind this algorithm.
995static inline float getThreshold(const PathApproximationInfo& info, float dx, float dy) {
996    // multiplying by sqrInvScaleY/X equivalent to multiplying in dimensional scale factors
997    float scale = (dx * dx * info.sqrInvScaleY + dy * dy * info.sqrInvScaleX);
998    return info.thresholdSquared * scale;
999}
1000
1001void PathTessellator::recursiveCubicBezierVertices(float p1x, float p1y, float c1x, float c1y,
1002                                                   float p2x, float p2y, float c2x, float c2y,
1003                                                   const PathApproximationInfo& approximationInfo,
1004                                                   std::vector<Vertex>& outputVertices, int depth) {
1005    float dx = p2x - p1x;
1006    float dy = p2y - p1y;
1007    float d1 = fabs((c1x - p2x) * dy - (c1y - p2y) * dx);
1008    float d2 = fabs((c2x - p2x) * dy - (c2y - p2y) * dx);
1009    float d = d1 + d2;
1010
1011    if (depth >= MAX_DEPTH || d * d <= getThreshold(approximationInfo, dx, dy)) {
1012        // below thresh, draw line by adding endpoint
1013        outputVertices.push_back(Vertex{p2x, p2y});
1014    } else {
1015        float p1c1x = (p1x + c1x) * 0.5f;
1016        float p1c1y = (p1y + c1y) * 0.5f;
1017        float p2c2x = (p2x + c2x) * 0.5f;
1018        float p2c2y = (p2y + c2y) * 0.5f;
1019
1020        float c1c2x = (c1x + c2x) * 0.5f;
1021        float c1c2y = (c1y + c2y) * 0.5f;
1022
1023        float p1c1c2x = (p1c1x + c1c2x) * 0.5f;
1024        float p1c1c2y = (p1c1y + c1c2y) * 0.5f;
1025
1026        float p2c1c2x = (p2c2x + c1c2x) * 0.5f;
1027        float p2c1c2y = (p2c2y + c1c2y) * 0.5f;
1028
1029        float mx = (p1c1c2x + p2c1c2x) * 0.5f;
1030        float my = (p1c1c2y + p2c1c2y) * 0.5f;
1031
1032        recursiveCubicBezierVertices(p1x, p1y, p1c1x, p1c1y, mx, my, p1c1c2x, p1c1c2y,
1033                                     approximationInfo, outputVertices, depth + 1);
1034        recursiveCubicBezierVertices(mx, my, p2c1c2x, p2c1c2y, p2x, p2y, p2c2x, p2c2y,
1035                                     approximationInfo, outputVertices, depth + 1);
1036    }
1037}
1038
1039void PathTessellator::recursiveQuadraticBezierVertices(
1040        float ax, float ay, float bx, float by, float cx, float cy,
1041        const PathApproximationInfo& approximationInfo, std::vector<Vertex>& outputVertices,
1042        int depth) {
1043    float dx = bx - ax;
1044    float dy = by - ay;
1045    // d is the cross product of vector (B-A) and (C-B).
1046    float d = (cx - bx) * dy - (cy - by) * dx;
1047
1048    if (depth >= MAX_DEPTH || d * d <= getThreshold(approximationInfo, dx, dy)) {
1049        // below thresh, draw line by adding endpoint
1050        outputVertices.push_back(Vertex{bx, by});
1051    } else {
1052        float acx = (ax + cx) * 0.5f;
1053        float bcx = (bx + cx) * 0.5f;
1054        float acy = (ay + cy) * 0.5f;
1055        float bcy = (by + cy) * 0.5f;
1056
1057        // midpoint
1058        float mx = (acx + bcx) * 0.5f;
1059        float my = (acy + bcy) * 0.5f;
1060
1061        recursiveQuadraticBezierVertices(ax, ay, mx, my, acx, acy, approximationInfo,
1062                                         outputVertices, depth + 1);
1063        recursiveQuadraticBezierVertices(mx, my, bx, by, bcx, bcy, approximationInfo,
1064                                         outputVertices, depth + 1);
1065    }
1066}
1067
1068};  // namespace uirenderer
1069};  // namespace android
1070