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