HW Acceleration support for stroked arcs with BUTT caps

bug:4419017

Change-Id: I7371bfb36cef460da861a47d4d945218c6d0c3d0
diff --git a/libs/hwui/OpenGLRenderer.cpp b/libs/hwui/OpenGLRenderer.cpp
index cc536f2..2b50091 100644
--- a/libs/hwui/OpenGLRenderer.cpp
+++ b/libs/hwui/OpenGLRenderer.cpp
@@ -2427,17 +2427,39 @@
 }
 
 status_t OpenGLRenderer::drawArc(float left, float top, float right, float bottom,
-        float startAngle, float sweepAngle, bool useCenter, SkPaint* paint) {
-    if (mSnapshot->isIgnored()) return DrawGlInfo::kStatusDone;
-
-    if (fabs(sweepAngle) >= 360.0f) {
-        return drawOval(left, top, right, bottom, paint);
+        float startAngle, float sweepAngle, bool useCenter, SkPaint* p) {
+    if (mSnapshot->isIgnored() || quickRejectPreStroke(left, top, right, bottom, p)) {
+        return DrawGlInfo::kStatusDone;
     }
 
-    mCaches.activeTexture(0);
-    const PathTexture* texture = mCaches.arcShapeCache.getArc(right - left, bottom - top,
-            startAngle, sweepAngle, useCenter, paint);
-    return drawShape(left, top, texture, paint);
+    if (fabs(sweepAngle) >= 360.0f) {
+        return drawOval(left, top, right, bottom, p);
+    }
+
+    // TODO: support fills (accounting for concavity if useCenter && sweepAngle > 180)
+    if (p->getStyle() != SkPaint::kStroke_Style || p->getPathEffect() != 0 || p->getStrokeCap() != SkPaint::kButt_Cap) {
+        mCaches.activeTexture(0);
+        const PathTexture* texture = mCaches.arcShapeCache.getArc(right - left, bottom - top,
+                startAngle, sweepAngle, useCenter, p);
+        return drawShape(left, top, texture, p);
+    }
+
+    SkRect rect = SkRect::MakeLTRB(left, top, right, bottom);
+    if (p->getStyle() == SkPaint::kStrokeAndFill_Style) {
+        rect.outset(p->getStrokeWidth() / 2, p->getStrokeWidth() / 2);
+    }
+
+    SkPath path;
+    if (useCenter) {
+        path.moveTo(rect.centerX(), rect.centerY());
+    }
+    path.arcTo(rect, startAngle, sweepAngle, !useCenter);
+    if (useCenter) {
+        path.close();
+    }
+    drawConvexPath(path, p);
+
+    return DrawGlInfo::kStatusDrew;
 }
 
 // See SkPaintDefaults.h
diff --git a/libs/hwui/PathRenderer.cpp b/libs/hwui/PathRenderer.cpp
index 58d6cb8..dd13d79 100644
--- a/libs/hwui/PathRenderer.cpp
+++ b/libs/hwui/PathRenderer.cpp
@@ -80,11 +80,24 @@
  *
  * Note that we can't add and normalize the two vectors, that would result in a rectangle having an
  * offset of (sqrt(2)/2, sqrt(2)/2) at each corner, instead of (1, 1)
+ *
+ * NOTE: assumes angles between normals 90 degrees or less
  */
 inline vec2 totalOffsetFromNormals(const vec2& normalA, const vec2& normalB) {
     return (normalA + normalB) / (1 + fabs(normalA.dot(normalB)));
 }
 
+inline void scaleOffsetForStrokeWidth(vec2& offset, float halfStrokeWidth,
+        float inverseScaleX, float inverseScaleY) {
+    if (halfStrokeWidth == 0.0f) {
+        // hairline - compensate for scale
+        offset.x *= 0.5f * inverseScaleX;
+        offset.y *= 0.5f * inverseScaleY;
+    } else {
+        offset *= halfStrokeWidth;
+    }
+}
+
 void getFillVerticesFromPerimeter(const Vector<Vertex>& perimeter, VertexBuffer& vertexBuffer) {
     Vertex* buffer = vertexBuffer.alloc<Vertex>(perimeter.size());
 
@@ -119,13 +132,7 @@
         nextNormal.normalize();
 
         vec2 totalOffset = totalOffsetFromNormals(lastNormal, nextNormal);
-        if (halfStrokeWidth == 0.0f) {
-            // hairline - compensate for scale
-            totalOffset.x *= 0.5f * inverseScaleX;
-            totalOffset.y *= 0.5f * inverseScaleY;
-        } else {
-            totalOffset *= halfStrokeWidth;
-        }
+        scaleOffsetForStrokeWidth(totalOffset, halfStrokeWidth, inverseScaleX, inverseScaleY);
 
         Vertex::set(&buffer[currentIndex++],
                 current->position[0] + totalOffset.x,
@@ -145,6 +152,55 @@
     copyVertex(&buffer[currentIndex++], &buffer[1]);
 }
 
+void getStrokeVerticesFromUnclosedVertices(const Vector<Vertex>& vertices, float halfStrokeWidth,
+        VertexBuffer& vertexBuffer, float inverseScaleX, float inverseScaleY) {
+    Vertex* buffer = vertexBuffer.alloc<Vertex>(vertices.size() * 2);
+
+    int currentIndex = 0;
+    const Vertex* current = &(vertices[0]);
+    vec2 lastNormal;
+    for (unsigned int i = 0; i < vertices.size() - 1; i++) {
+        const Vertex* next = &(vertices[i + 1]);
+        vec2 nextNormal(next->position[1] - current->position[1],
+                current->position[0] - next->position[0]);
+        nextNormal.normalize();
+
+        vec2 totalOffset;
+        if (i == 0) {
+            totalOffset = nextNormal;
+        } else {
+            totalOffset = totalOffsetFromNormals(lastNormal, nextNormal);
+        }
+        scaleOffsetForStrokeWidth(totalOffset, halfStrokeWidth, inverseScaleX, inverseScaleY);
+
+        Vertex::set(&buffer[currentIndex++],
+                current->position[0] + totalOffset.x,
+                current->position[1] + totalOffset.y);
+
+        Vertex::set(&buffer[currentIndex++],
+                current->position[0] - totalOffset.x,
+                current->position[1] - totalOffset.y);
+
+        current = next;
+        lastNormal = nextNormal;
+    }
+
+    vec2 totalOffset = lastNormal;
+    scaleOffsetForStrokeWidth(totalOffset, halfStrokeWidth, inverseScaleX, inverseScaleY);
+
+    Vertex::set(&buffer[currentIndex++],
+            current->position[0] + totalOffset.x,
+            current->position[1] + totalOffset.y);
+    Vertex::set(&buffer[currentIndex++],
+            current->position[0] - totalOffset.x,
+            current->position[1] - totalOffset.y);
+#if VERTEX_DEBUG
+    for (unsigned int i = 0; i < vertexBuffer.getSize(); i++) {
+        ALOGD("point at %f %f", buffer[i].position[0], buffer[i].position[1]);
+    }
+#endif
+}
+
 void getFillVerticesFromPerimeterAA(const Vector<Vertex>& perimeter, VertexBuffer& vertexBuffer,
          float inverseScaleX, float inverseScaleY) {
     AlphaVertex* buffer = vertexBuffer.alloc<AlphaVertex>(perimeter.size() * 3 + 2);
@@ -202,11 +258,167 @@
 
 #if VERTEX_DEBUG
     for (unsigned int i = 0; i < vertexBuffer.getSize(); i++) {
-        ALOGD("point at %f %f", buffer[i].position[0], buffer[i].position[1]);
+        ALOGD("point at %f %f, alpha %f", buffer[i].position[0], buffer[i].position[1], buffer[i].alpha);
     }
 #endif
 }
 
+
+void getStrokeVerticesFromUnclosedVerticesAA(const Vector<Vertex>& vertices, float halfStrokeWidth,
+        VertexBuffer& vertexBuffer, float inverseScaleX, float inverseScaleY) {
+    AlphaVertex* buffer = vertexBuffer.alloc<AlphaVertex>(6 * vertices.size() + 2);
+
+    // avoid lines smaller than hairline since they break triangle based sampling. instead reducing
+    // alpha value (TODO: support different X/Y scale)
+    float maxAlpha = 1.0f;
+    if (halfStrokeWidth != 0 && inverseScaleX == inverseScaleY &&
+            halfStrokeWidth * inverseScaleX < 0.5f) {
+        maxAlpha *= (2 * halfStrokeWidth) / inverseScaleX;
+        halfStrokeWidth = 0.0f;
+    }
+
+    // there is no outer/inner here, using them for consistency with below approach
+    int offset = 2 * (vertices.size() - 2);
+    int currentAAOuterIndex = 2;
+    int currentAAInnerIndex = 2 * offset + 5; // reversed
+    int currentStrokeIndex = currentAAInnerIndex + 7;
+
+    const Vertex* last = &(vertices[0]);
+    const Vertex* current = &(vertices[1]);
+    vec2 lastNormal(current->position[1] - last->position[1],
+            last->position[0] - current->position[0]);
+    lastNormal.normalize();
+
+    {
+        // start cap
+        vec2 totalOffset = lastNormal;
+        vec2 AAOffset = totalOffset;
+        AAOffset.x *= 0.5f * inverseScaleX;
+        AAOffset.y *= 0.5f * inverseScaleY;
+
+        vec2 innerOffset = totalOffset;
+        scaleOffsetForStrokeWidth(innerOffset, halfStrokeWidth, inverseScaleX, inverseScaleY);
+        vec2 outerOffset = innerOffset + AAOffset;
+        innerOffset -= AAOffset;
+
+        // TODO: support square cap by changing this offset to incorporate halfStrokeWidth
+        vec2 capAAOffset(AAOffset.y, -AAOffset.x);
+        AlphaVertex::set(&buffer[0],
+                last->position[0] + outerOffset.x + capAAOffset.x,
+                last->position[1] + outerOffset.y + capAAOffset.y,
+                0.0f);
+        AlphaVertex::set(&buffer[1],
+                last->position[0] + innerOffset.x - capAAOffset.x,
+                last->position[1] + innerOffset.y - capAAOffset.y,
+                maxAlpha);
+
+        AlphaVertex::set(&buffer[2 * offset + 6],
+                last->position[0] - outerOffset.x + capAAOffset.x,
+                last->position[1] - outerOffset.y + capAAOffset.y,
+                0.0f);
+        AlphaVertex::set(&buffer[2 * offset + 7],
+                last->position[0] - innerOffset.x - capAAOffset.x,
+                last->position[1] - innerOffset.y - capAAOffset.y,
+                maxAlpha);
+        copyAlphaVertex(&buffer[2 * offset + 8], &buffer[0]);
+        copyAlphaVertex(&buffer[2 * offset + 9], &buffer[1]);
+        copyAlphaVertex(&buffer[2 * offset + 10], &buffer[1]); // degenerate tris (the only two!)
+        copyAlphaVertex(&buffer[2 * offset + 11], &buffer[2 * offset + 7]);
+    }
+
+    for (unsigned int i = 1; i < vertices.size() - 1; i++) {
+        const Vertex* next = &(vertices[i + 1]);
+        vec2 nextNormal(next->position[1] - current->position[1],
+                current->position[0] - next->position[0]);
+        nextNormal.normalize();
+
+        vec2 totalOffset = totalOffsetFromNormals(lastNormal, nextNormal);
+        vec2 AAOffset = totalOffset;
+        AAOffset.x *= 0.5f * inverseScaleX;
+        AAOffset.y *= 0.5f * inverseScaleY;
+
+        vec2 innerOffset = totalOffset;
+        scaleOffsetForStrokeWidth(innerOffset, halfStrokeWidth, inverseScaleX, inverseScaleY);
+        vec2 outerOffset = innerOffset + AAOffset;
+        innerOffset -= AAOffset;
+
+        AlphaVertex::set(&buffer[currentAAOuterIndex++],
+                current->position[0] + outerOffset.x,
+                current->position[1] + outerOffset.y,
+                0.0f);
+        AlphaVertex::set(&buffer[currentAAOuterIndex++],
+                current->position[0] + innerOffset.x,
+                current->position[1] + innerOffset.y,
+                maxAlpha);
+
+        AlphaVertex::set(&buffer[currentStrokeIndex++],
+                current->position[0] + innerOffset.x,
+                current->position[1] + innerOffset.y,
+                maxAlpha);
+        AlphaVertex::set(&buffer[currentStrokeIndex++],
+                current->position[0] - innerOffset.x,
+                current->position[1] - innerOffset.y,
+                maxAlpha);
+
+        AlphaVertex::set(&buffer[currentAAInnerIndex--],
+                current->position[0] - innerOffset.x,
+                current->position[1] - innerOffset.y,
+                maxAlpha);
+        AlphaVertex::set(&buffer[currentAAInnerIndex--],
+                current->position[0] - outerOffset.x,
+                current->position[1] - outerOffset.y,
+                0.0f);
+
+        last = current;
+        current = next;
+        lastNormal = nextNormal;
+    }
+
+    {
+        // end cap
+        vec2 totalOffset = lastNormal;
+        vec2 AAOffset = totalOffset;
+        AAOffset.x *= 0.5f * inverseScaleX;
+        AAOffset.y *= 0.5f * inverseScaleY;
+
+        vec2 innerOffset = totalOffset;
+        scaleOffsetForStrokeWidth(innerOffset, halfStrokeWidth, inverseScaleX, inverseScaleY);
+        vec2 outerOffset = innerOffset + AAOffset;
+        innerOffset -= AAOffset;
+
+        // TODO: support square cap by changing this offset to incorporate halfStrokeWidth
+        vec2 capAAOffset(-AAOffset.y, AAOffset.x);
+
+        AlphaVertex::set(&buffer[offset + 2],
+                current->position[0] + outerOffset.x + capAAOffset.x,
+                current->position[1] + outerOffset.y + capAAOffset.y,
+                0.0f);
+        AlphaVertex::set(&buffer[offset + 3],
+                current->position[0] + innerOffset.x - capAAOffset.x,
+                current->position[1] + innerOffset.y - capAAOffset.y,
+                maxAlpha);
+
+        AlphaVertex::set(&buffer[offset + 4],
+                current->position[0] - outerOffset.x + capAAOffset.x,
+                current->position[1] - outerOffset.y + capAAOffset.y,
+                0.0f);
+        AlphaVertex::set(&buffer[offset + 5],
+                current->position[0] - innerOffset.x - capAAOffset.x,
+                current->position[1] - innerOffset.y - capAAOffset.y,
+                maxAlpha);
+
+        copyAlphaVertex(&buffer[vertexBuffer.getSize() - 2], &buffer[offset + 3]);
+        copyAlphaVertex(&buffer[vertexBuffer.getSize() - 1], &buffer[offset + 5]);
+    }
+
+#if VERTEX_DEBUG
+    for (unsigned int i = 0; i < vertexBuffer.getSize(); i++) {
+        ALOGD("point at %f %f, alpha %f", buffer[i].position[0], buffer[i].position[1], buffer[i].alpha);
+    }
+#endif
+}
+
+
 void getStrokeVerticesFromPerimeterAA(const Vector<Vertex>& perimeter, float halfStrokeWidth,
         VertexBuffer& vertexBuffer, float inverseScaleX, float inverseScaleY) {
     AlphaVertex* buffer = vertexBuffer.alloc<AlphaVertex>(6 * perimeter.size() + 8);
@@ -242,13 +454,7 @@
         AAOffset.y *= 0.5f * inverseScaleY;
 
         vec2 innerOffset = totalOffset;
-        if (halfStrokeWidth == 0.0f) {
-            // hairline! - compensate for scale
-            innerOffset.x *= 0.5f * inverseScaleX;
-            innerOffset.y *= 0.5f * inverseScaleY;
-        } else {
-            innerOffset *= halfStrokeWidth;
-        }
+        scaleOffsetForStrokeWidth(innerOffset, halfStrokeWidth, inverseScaleX, inverseScaleY);
         vec2 outerOffset = innerOffset + AAOffset;
         innerOffset -= AAOffset;
 
@@ -296,6 +502,12 @@
     copyAlphaVertex(&buffer[currentAAInnerIndex++], &buffer[2 * offset]);
     copyAlphaVertex(&buffer[currentAAInnerIndex++], &buffer[2 * offset + 1]);
     // don't need to create last degenerate tri
+
+#if VERTEX_DEBUG
+    for (unsigned int i = 0; i < vertexBuffer.getSize(); i++) {
+        ALOGD("point at %f %f, alpha %f", buffer[i].position[0], buffer[i].position[1], buffer[i].alpha);
+    }
+#endif
 }
 
 void PathRenderer::convexPathVertices(const SkPath &path, const SkPaint* paint,
@@ -320,7 +532,10 @@
             threshInvScaleY *= bounds.height() / (bounds.height() + paint->getStrokeWidth());
         }
     }
-    convexPathPerimeterVertices(path, threshInvScaleX * threshInvScaleX,
+
+    // force close if we're filling the path, since fill path expects closed perimeter.
+    bool forceClose = style != SkPaint::kStroke_Style;
+    bool wasClosed = convexPathPerimeterVertices(path, forceClose, threshInvScaleX * threshInvScaleX,
             threshInvScaleY * threshInvScaleY, tempVertices);
 
     if (!tempVertices.size()) {
@@ -337,11 +552,22 @@
     if (style == SkPaint::kStroke_Style) {
         float halfStrokeWidth = paint->getStrokeWidth() * 0.5f;
         if (!isAA) {
-            getStrokeVerticesFromPerimeter(tempVertices, halfStrokeWidth, vertexBuffer,
-                    inverseScaleX, inverseScaleY);
+            if (wasClosed) {
+                getStrokeVerticesFromPerimeter(tempVertices, halfStrokeWidth, vertexBuffer,
+                        inverseScaleX, inverseScaleY);
+            } else {
+                getStrokeVerticesFromUnclosedVertices(tempVertices, halfStrokeWidth, vertexBuffer,
+                        inverseScaleX, inverseScaleY);
+            }
+
         } else {
-            getStrokeVerticesFromPerimeterAA(tempVertices, halfStrokeWidth, vertexBuffer,
-                    inverseScaleX, inverseScaleY);
+            if (wasClosed) {
+                getStrokeVerticesFromPerimeterAA(tempVertices, halfStrokeWidth, vertexBuffer,
+                        inverseScaleX, inverseScaleY);
+            } else {
+                getStrokeVerticesFromUnclosedVerticesAA(tempVertices, halfStrokeWidth, vertexBuffer,
+                        inverseScaleX, inverseScaleY);
+            }
         }
     } else {
         // For kStrokeAndFill style, the path should be adjusted externally, as it will be treated as a fill here.
@@ -354,19 +580,27 @@
 }
 
 
-void PathRenderer::convexPathPerimeterVertices(const SkPath& path,
+void pushToVector(Vector<Vertex>& vertices, float x, float y) {
+    // TODO: make this not yuck
+    vertices.push();
+    Vertex* newVertex = &(vertices.editArray()[vertices.size() - 1]);
+    Vertex::set(newVertex, x, y);
+}
+
+bool PathRenderer::convexPathPerimeterVertices(const SkPath& path, bool forceClose,
         float sqrInvScaleX, float sqrInvScaleY, Vector<Vertex>& outputVertices) {
     ATRACE_CALL();
 
-    SkPath::Iter iter(path, true);
-    SkPoint pos;
+    // TODO: to support joins other than sharp miter, join vertices should be labelled in the
+    // perimeter, or resolved into more vertices. Reconsider forceClose-ing in that case.
+    SkPath::Iter iter(path, forceClose);
     SkPoint pts[4];
     SkPath::Verb v;
     Vertex* newVertex = 0;
     while (SkPath::kDone_Verb != (v = iter.next(pts))) {
             switch (v) {
                 case SkPath::kMove_Verb:
-                    pos = pts[0];
+                    pushToVector(outputVertices, pts[0].x(), pts[0].y());
                     ALOGV("Move to pos %f %f", pts[0].x(), pts[0].y());
                     break;
                 case SkPath::kClose_Verb:
@@ -377,10 +611,7 @@
                             pts[0].x(), pts[0].y(),
                             pts[1].x(), pts[1].y());
 
-                    // TODO: make this not yuck
-                    outputVertices.push();
-                    newVertex = &(outputVertices.editArray()[outputVertices.size() - 1]);
-                    Vertex::set(newVertex, pts[1].x(), pts[1].y());
+                    pushToVector(outputVertices, pts[1].x(), pts[1].y());
                     break;
                 case SkPath::kQuad_Verb:
                     ALOGV("kQuad_Verb");
@@ -403,6 +634,14 @@
                     break;
             }
     }
+
+    int size = outputVertices.size();
+    if (size >= 2 && outputVertices[0].position[0] == outputVertices[size - 1].position[0] &&
+            outputVertices[0].position[1] == outputVertices[size - 1].position[1]) {
+        outputVertices.pop();
+        return true;
+    }
+    return false;
 }
 
 void PathRenderer::recursiveCubicBezierVertices(
@@ -419,10 +658,7 @@
 
     if (d * d < THRESHOLD * THRESHOLD * (dx * dx * sqrInvScaleY + dy * dy * sqrInvScaleX)) {
         // below thresh, draw line by adding endpoint
-        // TODO: make this not yuck
-        outputVertices.push();
-        Vertex* newVertex = &(outputVertices.editArray()[outputVertices.size() - 1]);
-        Vertex::set(newVertex, p2x, p2y);
+        pushToVector(outputVertices, p2x, p2y);
     } else {
         float p1c1x = (p1x + c1x) * 0.5f;
         float p1c1y = (p1y + c1y) * 0.5f;
@@ -463,10 +699,7 @@
 
     if (d * d < THRESHOLD * THRESHOLD * (dx * dx * sqrInvScaleY + dy * dy * sqrInvScaleX)) {
         // below thresh, draw line by adding endpoint
-        // TODO: make this not yuck
-        outputVertices.push();
-        Vertex* newVertex = &(outputVertices.editArray()[outputVertices.size() - 1]);
-        Vertex::set(newVertex, bx, by);
+        pushToVector(outputVertices, bx, by);
     } else {
         float acx = (ax + cx) * 0.5f;
         float bcx = (bx + cx) * 0.5f;
diff --git a/libs/hwui/PathRenderer.h b/libs/hwui/PathRenderer.h
index 28a5b90..e9f347b 100644
--- a/libs/hwui/PathRenderer.h
+++ b/libs/hwui/PathRenderer.h
@@ -71,10 +71,8 @@
             const mat4 *transform, VertexBuffer& vertexBuffer);
 
 private:
-    static void convexPathPerimeterVertices(
-        const SkPath &path,
-        float sqrInvScaleX, float sqrInvScaleY,
-        Vector<Vertex> &outputVertices);
+    static bool convexPathPerimeterVertices(const SkPath &path, bool forceClose,
+        float sqrInvScaleX, float sqrInvScaleY, Vector<Vertex> &outputVertices);
 
 /*
   endpoints a & b,