Add more shape drawing to new reorderer/renderer

bug:22480459

Add support for outsetting final bounds based on stroke.

Change-Id: I659318ccec51882bba1906ce3c7042288ce35c30
diff --git a/libs/hwui/BakedOpDispatcher.cpp b/libs/hwui/BakedOpDispatcher.cpp
index fde12dd..3d35dd5 100644
--- a/libs/hwui/BakedOpDispatcher.cpp
+++ b/libs/hwui/BakedOpDispatcher.cpp
@@ -20,6 +20,7 @@
 #include "Caches.h"
 #include "Glop.h"
 #include "GlopBuilder.h"
+#include "PathTessellator.h"
 #include "renderstate/OffscreenBufferPool.h"
 #include "renderstate/RenderState.h"
 #include "utils/GLUtils.h"
@@ -27,6 +28,7 @@
 
 #include <algorithm>
 #include <math.h>
+#include <SkPaintDefaults.h>
 
 namespace android {
 namespace uirenderer {
@@ -206,6 +208,90 @@
     LOG_ALWAYS_FATAL("unsupported operation");
 }
 
+namespace VertexBufferRenderFlags {
+    enum {
+        Offset = 0x1,
+        ShadowInterp = 0x2,
+    };
+}
+
+static void renderVertexBuffer(BakedOpRenderer& renderer, const BakedOpState& state,
+        const VertexBuffer& vertexBuffer, float translateX, float translateY,
+        const SkPaint& paint, int vertexBufferRenderFlags) {
+    if (CC_LIKELY(vertexBuffer.getVertexCount())) {
+        bool shadowInterp = vertexBufferRenderFlags & VertexBufferRenderFlags::ShadowInterp;
+        const int transformFlags = TransformFlags::OffsetByFudgeFactor;
+        Glop glop;
+        GlopBuilder(renderer.renderState(), renderer.caches(), &glop)
+                .setRoundRectClipState(state.roundRectClipState)
+                .setMeshVertexBuffer(vertexBuffer, shadowInterp)
+                .setFillPaint(paint, state.alpha)
+                .setTransform(state.computedState.transform, transformFlags)
+                .setModelViewOffsetRect(translateX, translateY, vertexBuffer.getBounds())
+                .build();
+        renderer.renderGlop(state, glop);
+    }
+}
+
+static void renderConvexPath(BakedOpRenderer& renderer, const BakedOpState& state,
+        const SkPath& path, const SkPaint& paint) {
+    VertexBuffer vertexBuffer;
+    // TODO: try clipping large paths to viewport
+    PathTessellator::tessellatePath(path, &paint, state.computedState.transform, vertexBuffer);
+    renderVertexBuffer(renderer, state, vertexBuffer, 0.0f, 0.0f, paint, 0);
+}
+
+static void renderPathTexture(BakedOpRenderer& renderer, const BakedOpState& state,
+        PathTexture& texture, const RecordedOp& op) {
+    Rect dest(texture.width, texture.height);
+    dest.translate(texture.left + op.unmappedBounds.left - texture.offset,
+            texture.top + op.unmappedBounds.top - texture.offset);
+    Glop glop;
+    GlopBuilder(renderer.renderState(), renderer.caches(), &glop)
+            .setRoundRectClipState(state.roundRectClipState)
+            .setMeshTexturedUnitQuad(nullptr)
+            .setFillPathTexturePaint(texture, *(op.paint), state.alpha)
+            .setTransform(state.computedState.transform,  TransformFlags::None)
+            .setModelViewMapUnitToRect(dest)
+            .build();
+    renderer.renderGlop(state, glop);
+}
+
+SkRect getBoundsOfFill(const RecordedOp& op) {
+    SkRect bounds = op.unmappedBounds.toSkRect();
+    if (op.paint->getStyle() == SkPaint::kStrokeAndFill_Style) {
+        float outsetDistance = op.paint->getStrokeWidth() / 2;
+        bounds.outset(outsetDistance, outsetDistance);
+    }
+    return bounds;
+}
+
+void BakedOpDispatcher::onArcOp(BakedOpRenderer& renderer, const ArcOp& op, const BakedOpState& state) {
+    // TODO: support fills (accounting for concavity if useCenter && sweepAngle > 180)
+    if (op.paint->getStyle() != SkPaint::kStroke_Style
+            || op.paint->getPathEffect() != nullptr
+            || op.useCenter) {
+        PathTexture* texture = renderer.caches().pathCache.getArc(
+                op.unmappedBounds.getWidth(), op.unmappedBounds.getHeight(),
+                op.startAngle, op.sweepAngle, op.useCenter, op.paint);
+        const AutoTexture holder(texture);
+        if (CC_LIKELY(holder.texture)) {
+            renderPathTexture(renderer, state, *texture, op);
+        }
+    } else {
+        SkRect rect = getBoundsOfFill(op);
+        SkPath path;
+        if (op.useCenter) {
+            path.moveTo(rect.centerX(), rect.centerY());
+        }
+        path.arcTo(rect, op.startAngle, op.sweepAngle, !op.useCenter);
+        if (op.useCenter) {
+            path.close();
+        }
+        renderConvexPath(renderer, state, path, *(op.paint));
+    }
+}
+
 void BakedOpDispatcher::onBitmapOp(BakedOpRenderer& renderer, const BitmapOp& op, const BakedOpState& state) {
     Texture* texture = renderer.getTexture(op.bitmap);
     if (!texture) return;
@@ -225,43 +311,101 @@
 }
 
 void BakedOpDispatcher::onLinesOp(BakedOpRenderer& renderer, const LinesOp& op, const BakedOpState& state) {
-    LOG_ALWAYS_FATAL("todo");
+    VertexBuffer buffer;
+    PathTessellator::tessellateLines(op.points, op.floatCount, op.paint,
+            state.computedState.transform, buffer);
+    int displayFlags = op.paint->isAntiAlias() ? 0 : VertexBufferRenderFlags::Offset;
+    renderVertexBuffer(renderer, state, buffer, 0, 0, *(op.paint), displayFlags);
 }
 
+void BakedOpDispatcher::onOvalOp(BakedOpRenderer& renderer, const OvalOp& op, const BakedOpState& state) {
+    if (op.paint->getPathEffect() != nullptr) {
+        PathTexture* texture = renderer.caches().pathCache.getOval(
+                op.unmappedBounds.getWidth(), op.unmappedBounds.getHeight(), op.paint);
+        const AutoTexture holder(texture);
+        if (CC_LIKELY(holder.texture)) {
+            renderPathTexture(renderer, state, *texture, op);
+        }
+    } else {
+        SkPath path;
+        SkRect rect = getBoundsOfFill(op);
+        path.addOval(rect);
+        renderConvexPath(renderer, state, path, *(op.paint));
+    }
+}
+
+void BakedOpDispatcher::onPathOp(BakedOpRenderer& renderer, const PathOp& op, const BakedOpState& state) {
+    PathTexture* texture = renderer.caches().pathCache.get(op.path, op.paint);
+    const AutoTexture holder(texture);
+    if (CC_LIKELY(holder.texture)) {
+        renderPathTexture(renderer, state, *texture, op);
+    }
+}
+
+void BakedOpDispatcher::onPointsOp(BakedOpRenderer& renderer, const PointsOp& op, const BakedOpState& state) {
+    VertexBuffer buffer;
+    PathTessellator::tessellatePoints(op.points, op.floatCount, op.paint,
+            state.computedState.transform, buffer);
+    int displayFlags = op.paint->isAntiAlias() ? 0 : VertexBufferRenderFlags::Offset;
+    renderVertexBuffer(renderer, state, buffer, 0, 0, *(op.paint), displayFlags);
+}
+
+// See SkPaintDefaults.h
+#define SkPaintDefaults_MiterLimit SkIntToScalar(4)
+
 void BakedOpDispatcher::onRectOp(BakedOpRenderer& renderer, const RectOp& op, const BakedOpState& state) {
-    Glop glop;
-    GlopBuilder(renderer.renderState(), renderer.caches(), &glop)
-            .setRoundRectClipState(state.roundRectClipState)
-            .setMeshUnitQuad()
-            .setFillPaint(*op.paint, state.alpha)
-            .setTransform(state.computedState.transform, TransformFlags::None)
-            .setModelViewMapUnitToRect(op.unmappedBounds)
-            .build();
-    renderer.renderGlop(state, glop);
+    if (op.paint->getStyle() != SkPaint::kFill_Style) {
+        // only fill + default miter is supported by drawConvexPath, since others must handle joins
+        static_assert(SkPaintDefaults_MiterLimit == 4.0f, "Miter limit has changed");
+        if (CC_UNLIKELY(op.paint->getPathEffect() != nullptr
+                || op.paint->getStrokeJoin() != SkPaint::kMiter_Join
+                || op.paint->getStrokeMiter() != SkPaintDefaults_MiterLimit)) {
+             PathTexture* texture = renderer.caches().pathCache.getRect(
+                     op.unmappedBounds.getWidth(), op.unmappedBounds.getHeight(), op.paint);
+             const AutoTexture holder(texture);
+             if (CC_LIKELY(holder.texture)) {
+                 renderPathTexture(renderer, state, *texture, op);
+             }
+        } else {
+            SkPath path;
+            path.addRect(getBoundsOfFill(op));
+            renderConvexPath(renderer, state, path, *(op.paint));
+        }
+    } else {
+        if (op.paint->isAntiAlias() && !state.computedState.transform.isSimple()) {
+            SkPath path;
+            path.addRect(op.unmappedBounds.toSkRect());
+            renderConvexPath(renderer, state, path, *(op.paint));
+        } else {
+            // render simple unit quad, no tessellation required
+            Glop glop;
+            GlopBuilder(renderer.renderState(), renderer.caches(), &glop)
+                    .setRoundRectClipState(state.roundRectClipState)
+                    .setMeshUnitQuad()
+                    .setFillPaint(*op.paint, state.alpha)
+                    .setTransform(state.computedState.transform, TransformFlags::None)
+                    .setModelViewMapUnitToRect(op.unmappedBounds)
+                    .build();
+            renderer.renderGlop(state, glop);
+        }
+    }
 }
 
-namespace VertexBufferRenderFlags {
-    enum {
-        Offset = 0x1,
-        ShadowInterp = 0x2,
-    };
-}
-
-static void renderVertexBuffer(BakedOpRenderer& renderer, const BakedOpState& state,
-        const VertexBuffer& vertexBuffer, float translateX, float translateY,
-        SkPaint& paint, int vertexBufferRenderFlags) {
-    if (CC_LIKELY(vertexBuffer.getVertexCount())) {
-        bool shadowInterp = vertexBufferRenderFlags & VertexBufferRenderFlags::ShadowInterp;
-        const int transformFlags = TransformFlags::OffsetByFudgeFactor;
-        Glop glop;
-        GlopBuilder(renderer.renderState(), renderer.caches(), &glop)
-                .setRoundRectClipState(state.roundRectClipState)
-                .setMeshVertexBuffer(vertexBuffer, shadowInterp)
-                .setFillPaint(paint, state.alpha)
-                .setTransform(state.computedState.transform, transformFlags)
-                .setModelViewOffsetRect(translateX, translateY, vertexBuffer.getBounds())
-                .build();
-        renderer.renderGlop(state, glop);
+void BakedOpDispatcher::onRoundRectOp(BakedOpRenderer& renderer, const RoundRectOp& op, const BakedOpState& state) {
+    if (op.paint->getPathEffect() != nullptr) {
+        PathTexture* texture = renderer.caches().pathCache.getRoundRect(
+                op.unmappedBounds.getWidth(), op.unmappedBounds.getHeight(),
+                op.rx, op.ry, op.paint);
+        const AutoTexture holder(texture);
+        if (CC_LIKELY(holder.texture)) {
+            renderPathTexture(renderer, state, *texture, op);
+        }
+    } else {
+        const VertexBuffer* buffer = renderer.caches().tessellationCache.getRoundRect(
+                state.computedState.transform, *(op.paint),
+                op.unmappedBounds.getWidth(), op.unmappedBounds.getHeight(), op.rx, op.ry);
+        renderVertexBuffer(renderer, state, *buffer,
+                op.unmappedBounds.left, op.unmappedBounds.top, *(op.paint), 0);
     }
 }
 
@@ -323,8 +467,6 @@
 void BakedOpDispatcher::onLayerOp(BakedOpRenderer& renderer, const LayerOp& op, const BakedOpState& state) {
     OffscreenBuffer* buffer = *op.layerHandle;
 
-    // TODO: extend this to handle HW layers & paint properties which
-    // reside in node.properties().layerProperties()
     float layerAlpha = op.alpha * state.alpha;
     Glop glop;
     GlopBuilder(renderer.renderState(), renderer.caches(), &glop)
diff --git a/libs/hwui/BakedOpDispatcher.h b/libs/hwui/BakedOpDispatcher.h
index 0e763d9..ed34ada 100644
--- a/libs/hwui/BakedOpDispatcher.h
+++ b/libs/hwui/BakedOpDispatcher.h
@@ -26,6 +26,10 @@
 /**
  * Provides all "onBitmapOp(...)" style static methods for every op type, which convert the
  * RecordedOps and their state to Glops, and renders them with the provided BakedOpRenderer.
+ *
+ * onXXXOp methods must either render directly with the renderer, or call a static renderYYY
+ * method to render content. There should never be draw content rejection in BakedOpDispatcher -
+ * it must happen at a higher level (except in error-ish cases, like texture-too-big).
  */
 class BakedOpDispatcher {
 public:
diff --git a/libs/hwui/BakedOpState.h b/libs/hwui/BakedOpState.h
index 983c27b..9c836a0 100644
--- a/libs/hwui/BakedOpState.h
+++ b/libs/hwui/BakedOpState.h
@@ -53,7 +53,7 @@
 class ResolvedRenderState {
 public:
     // TODO: remove the mapRects/matrix multiply when snapshot & recorded transforms are translates
-    ResolvedRenderState(const Snapshot& snapshot, const RecordedOp& recordedOp) {
+    ResolvedRenderState(const Snapshot& snapshot, const RecordedOp& recordedOp, bool expandForStroke) {
         /* TODO: benchmark a fast path for translate-only matrices, such as:
         if (CC_LIKELY(snapshot.transform->getType() == Matrix4::kTypeTranslate
                 && recordedOp.localMatrix.getType() == Matrix4::kTypeTranslate)) {
@@ -83,7 +83,17 @@
 
         // resolvedClippedBounds = intersect(resolvedMatrix * opBounds, resolvedClipRect)
         clippedBounds = recordedOp.unmappedBounds;
+        if (CC_UNLIKELY(expandForStroke)) {
+            // account for non-hairline stroke
+            clippedBounds.outset(recordedOp.paint->getStrokeWidth() * 0.5f);
+        }
         transform.mapRect(clippedBounds);
+        if (CC_UNLIKELY(expandForStroke
+                && (!transform.isPureTranslate() || recordedOp.paint->getStrokeWidth() < 1.0f))) {
+            // account for hairline stroke when stroke may be < 1 scaled pixel
+            // Non translate || strokeWidth < 1 is conservative, but will cover all cases
+            clippedBounds.outset(0.5f);
+        }
 
         if (clipRect.left > clippedBounds.left) clipSideFlags |= OpClipSideFlags::Left;
         if (clipRect.top > clippedBounds.top) clipSideFlags |= OpClipSideFlags::Top;
@@ -129,13 +139,36 @@
 public:
     static BakedOpState* tryConstruct(LinearAllocator& allocator,
             const Snapshot& snapshot, const RecordedOp& recordedOp) {
-        BakedOpState* bakedOp = new (allocator) BakedOpState(snapshot, recordedOp);
-        if (bakedOp->computedState.clippedBounds.isEmpty()) {
+        BakedOpState* bakedState = new (allocator) BakedOpState(snapshot, recordedOp, false);
+        if (bakedState->computedState.clippedBounds.isEmpty()) {
             // bounds are empty, so op is rejected
-            allocator.rewindIfLastAlloc(bakedOp);
+            allocator.rewindIfLastAlloc(bakedState);
             return nullptr;
         }
-        return bakedOp;
+        return bakedState;
+    }
+
+    enum class StrokeBehavior {
+        // stroking is forced, regardless of style on paint
+        Forced,
+        // stroking is defined by style on paint
+        StyleDefined,
+    };
+
+    static BakedOpState* tryStrokeableOpConstruct(LinearAllocator& allocator,
+            const Snapshot& snapshot, const RecordedOp& recordedOp, StrokeBehavior strokeBehavior) {
+        bool expandForStroke = (strokeBehavior == StrokeBehavior::StyleDefined)
+                ? (recordedOp.paint && recordedOp.paint->getStyle() != SkPaint::kFill_Style)
+                : true;
+
+        BakedOpState* bakedState = new (allocator) BakedOpState(
+                snapshot, recordedOp, expandForStroke);
+        if (bakedState->computedState.clippedBounds.isEmpty()) {
+            // bounds are empty, so op is rejected
+            allocator.rewindIfLastAlloc(bakedState);
+            return nullptr;
+        }
+        return bakedState;
     }
 
     static BakedOpState* tryShadowOpConstruct(LinearAllocator& allocator,
@@ -160,8 +193,8 @@
     const RecordedOp* op;
 
 private:
-    BakedOpState(const Snapshot& snapshot, const RecordedOp& recordedOp)
-            : computedState(snapshot, recordedOp)
+    BakedOpState(const Snapshot& snapshot, const RecordedOp& recordedOp, bool expandForStroke)
+            : computedState(snapshot, recordedOp, expandForStroke)
             , alpha(snapshot.alpha)
             , roundRectClipState(snapshot.roundRectClipState)
             , projectionPathMask(snapshot.projectionPathMask)
diff --git a/libs/hwui/Canvas.h b/libs/hwui/Canvas.h
index b585a27..0643a54 100644
--- a/libs/hwui/Canvas.h
+++ b/libs/hwui/Canvas.h
@@ -113,10 +113,10 @@
 
     // Geometry
     virtual void drawPoint(float x, float y, const SkPaint& paint) = 0;
-    virtual void drawPoints(const float* points, int count, const SkPaint& paint) = 0;
+    virtual void drawPoints(const float* points, int floatCount, const SkPaint& paint) = 0;
     virtual void drawLine(float startX, float startY, float stopX, float stopY,
                 const SkPaint& paint) = 0;
-    virtual void drawLines(const float* points, int count, const SkPaint& paint) = 0;
+    virtual void drawLines(const float* points, int floatCount, const SkPaint& paint) = 0;
     virtual void drawRect(float left, float top, float right, float bottom,
             const SkPaint& paint) = 0;
     virtual void drawRegion(const SkRegion& region, const SkPaint& paint) = 0;
diff --git a/libs/hwui/OpReorderer.cpp b/libs/hwui/OpReorderer.cpp
index 9460361..f948f18 100644
--- a/libs/hwui/OpReorderer.cpp
+++ b/libs/hwui/OpReorderer.cpp
@@ -708,12 +708,36 @@
     }
 }
 
-static batchid_t tessellatedBatchId(const SkPaint& paint) {
+/**
+ * Defers an unmergeable, strokeable op, accounting correctly
+ * for paint's style on the bounds being computed.
+ */
+void OpReorderer::onStrokeableOp(const RecordedOp& op, batchid_t batchId,
+        BakedOpState::StrokeBehavior strokeBehavior) {
+    // Note: here we account for stroke when baking the op
+    BakedOpState* bakedState = BakedOpState::tryStrokeableOpConstruct(
+            mAllocator, *mCanvasState.currentSnapshot(), op, strokeBehavior);
+    if (!bakedState) return; // quick rejected
+    currentLayer().deferUnmergeableOp(mAllocator, bakedState, batchId);
+}
+
+/**
+ * Returns batch id for tessellatable shapes, based on paint. Checks to see if path effect/AA will
+ * be used, since they trigger significantly different rendering paths.
+ *
+ * Note: not used for lines/points, since they don't currently support path effects.
+ */
+static batchid_t tessBatchId(const RecordedOp& op) {
+    const SkPaint& paint = *(op.paint);
     return paint.getPathEffect()
             ? OpBatchType::AlphaMaskTexture
             : (paint.isAntiAlias() ? OpBatchType::AlphaVertices : OpBatchType::Vertices);
 }
 
+void OpReorderer::onArcOp(const ArcOp& op) {
+    onStrokeableOp(op, tessBatchId(op));
+}
+
 void OpReorderer::onBitmapOp(const BitmapOp& op) {
     BakedOpState* bakedState = tryBakeOpState(op);
     if (!bakedState) return; // quick rejected
@@ -734,15 +758,29 @@
 }
 
 void OpReorderer::onLinesOp(const LinesOp& op) {
-    BakedOpState* bakedState = tryBakeOpState(op);
-    if (!bakedState) return; // quick rejected
-    currentLayer().deferUnmergeableOp(mAllocator, bakedState, tessellatedBatchId(*op.paint));
+    batchid_t batch = op.paint->isAntiAlias() ? OpBatchType::AlphaVertices : OpBatchType::Vertices;
+    onStrokeableOp(op, batch, BakedOpState::StrokeBehavior::Forced);
+}
+
+void OpReorderer::onOvalOp(const OvalOp& op) {
+    onStrokeableOp(op, tessBatchId(op));
+}
+
+void OpReorderer::onPathOp(const PathOp& op) {
+    onStrokeableOp(op, OpBatchType::Bitmap);
+}
+
+void OpReorderer::onPointsOp(const PointsOp& op) {
+    batchid_t batch = op.paint->isAntiAlias() ? OpBatchType::AlphaVertices : OpBatchType::Vertices;
+    onStrokeableOp(op, batch, BakedOpState::StrokeBehavior::Forced);
 }
 
 void OpReorderer::onRectOp(const RectOp& op) {
-    BakedOpState* bakedState = tryBakeOpState(op);
-    if (!bakedState) return; // quick rejected
-    currentLayer().deferUnmergeableOp(mAllocator, bakedState, tessellatedBatchId(*op.paint));
+    onStrokeableOp(op, tessBatchId(op));
+}
+
+void OpReorderer::onRoundRectOp(const RoundRectOp& op) {
+    onStrokeableOp(op, tessBatchId(op));
 }
 
 void OpReorderer::onSimpleRectsOp(const SimpleRectsOp& op) {
diff --git a/libs/hwui/OpReorderer.h b/libs/hwui/OpReorderer.h
index fc77c61..58b607c 100644
--- a/libs/hwui/OpReorderer.h
+++ b/libs/hwui/OpReorderer.h
@@ -245,6 +245,10 @@
         mFrameAllocatedPaths.emplace_back(new SkPath);
         return mFrameAllocatedPaths.back().get();
     }
+
+    void onStrokeableOp(const RecordedOp& op, batchid_t batchId,
+            BakedOpState::StrokeBehavior strokeBehavior = BakedOpState::StrokeBehavior::StyleDefined);
+
     /**
      * Declares all OpReorderer::onXXXXOp() methods for every RecordedOp type.
      *
diff --git a/libs/hwui/OpenGLRenderer.cpp b/libs/hwui/OpenGLRenderer.cpp
index 2cb32c4..f49237c 100644
--- a/libs/hwui/OpenGLRenderer.cpp
+++ b/libs/hwui/OpenGLRenderer.cpp
@@ -40,6 +40,7 @@
 
 #include <SkCanvas.h>
 #include <SkColor.h>
+#include <SkPaintDefaults.h>
 #include <SkPathOps.h>
 #include <SkShader.h>
 #include <SkTypeface.h>
@@ -1908,9 +1909,6 @@
     drawConvexPath(path, p);
 }
 
-// See SkPaintDefaults.h
-#define SkPaintDefaults_MiterLimit SkIntToScalar(4)
-
 void OpenGLRenderer::drawRect(float left, float top, float right, float bottom,
         const SkPaint* p) {
     if (mState.currentlyIgnored()
@@ -1921,6 +1919,7 @@
 
     if (p->getStyle() != SkPaint::kFill_Style) {
         // only fill style is supported by drawConvexPath, since others have to handle joins
+        static_assert(SkPaintDefaults_MiterLimit == 4.0f, "Miter limit has changed");
         if (p->getPathEffect() != nullptr || p->getStrokeJoin() != SkPaint::kMiter_Join ||
                 p->getStrokeMiter() != SkPaintDefaults_MiterLimit) {
             mCaches.textureState().activateTexture(0);
diff --git a/libs/hwui/RecordedOp.h b/libs/hwui/RecordedOp.h
index b966401..8ce5473 100644
--- a/libs/hwui/RecordedOp.h
+++ b/libs/hwui/RecordedOp.h
@@ -43,10 +43,15 @@
  * This serves as the authoritative list of ops, used for generating ID enum, and ID based LUTs.
  */
 #define MAP_OPS_BASED_ON_MERGEABILITY(U_OP_FN, M_OP_FN) \
+        U_OP_FN(ArcOp) \
         M_OP_FN(BitmapOp) \
         U_OP_FN(LinesOp) \
+        U_OP_FN(OvalOp) \
+        U_OP_FN(PathOp) \
+        U_OP_FN(PointsOp) \
         U_OP_FN(RectOp) \
         U_OP_FN(RenderNodeOp) \
+        U_OP_FN(RoundRectOp) \
         U_OP_FN(ShadowOp) \
         U_OP_FN(SimpleRectsOp) \
         M_OP_FN(TextOp) \
@@ -74,7 +79,7 @@
         Count,
     };
 }
-static_assert(RecordedOpId::BitmapOp == 0,
+static_assert(RecordedOpId::ArcOp == 0,
         "First index must be zero for LUTs to work");
 
 #define BASE_PARAMS const Rect& unmappedBounds, const Matrix4& localMatrix, const Rect& localClipRect, const SkPaint* paint
@@ -86,7 +91,7 @@
     /* ID from RecordedOpId - generally used for jumping into function tables */
     const int opId;
 
-    /* bounds in *local* space, without accounting for DisplayList transformation */
+    /* bounds in *local* space, without accounting for DisplayList transformation, or stroke */
     const Rect unmappedBounds;
 
     /* transform in recording space (vs DisplayList origin) */
@@ -128,6 +133,17 @@
 // Standard Ops
 ////////////////////////////////////////////////////////////////////////////////////////////////////
 
+struct ArcOp : RecordedOp {
+    ArcOp(BASE_PARAMS, float startAngle, float sweepAngle, bool useCenter)
+            : SUPER(ArcOp)
+            , startAngle(startAngle)
+            , sweepAngle(sweepAngle)
+            , useCenter(useCenter) {}
+    const float startAngle;
+    const float sweepAngle;
+    const bool useCenter;
+};
+
 struct BitmapOp : RecordedOp {
     BitmapOp(BASE_PARAMS, const SkBitmap* bitmap)
             : SUPER(BitmapOp)
@@ -145,11 +161,41 @@
     const int floatCount;
 };
 
+struct OvalOp : RecordedOp {
+    OvalOp(BASE_PARAMS)
+            : SUPER(OvalOp) {}
+};
+
+struct PathOp : RecordedOp {
+    PathOp(BASE_PARAMS, const SkPath* path)
+            : SUPER(PathOp)
+            , path(path) {}
+    const SkPath* path;
+};
+
+struct PointsOp : RecordedOp {
+    PointsOp(BASE_PARAMS, const float* points, const int floatCount)
+            : SUPER(PointsOp)
+            , points(points)
+            , floatCount(floatCount) {}
+    const float* points;
+    const int floatCount;
+};
+
 struct RectOp : RecordedOp {
     RectOp(BASE_PARAMS)
             : SUPER(RectOp) {}
 };
 
+struct RoundRectOp : RecordedOp {
+    RoundRectOp(BASE_PARAMS, float rx, float ry)
+            : SUPER(RoundRectOp)
+            , rx(rx)
+            , ry(ry) {}
+    const float rx;
+    const float ry;
+};
+
 /**
  * Real-time, dynamic-lit shadow.
  *
diff --git a/libs/hwui/RecordingCanvas.cpp b/libs/hwui/RecordingCanvas.cpp
index e6020cd..148c940 100644
--- a/libs/hwui/RecordingCanvas.cpp
+++ b/libs/hwui/RecordingCanvas.cpp
@@ -237,26 +237,32 @@
             refPaint(&paint)));
 }
 
+static Rect calcBoundsOfPoints(const float* points, int floatCount) {
+    Rect unmappedBounds(points[0], points[1], points[0], points[1]);
+    for (int i = 2; i < floatCount; i += 2) {
+        unmappedBounds.expandToCover(points[i], points[i + 1]);
+    }
+    return unmappedBounds;
+}
+
 // Geometry
-void RecordingCanvas::drawPoints(const float* points, int count, const SkPaint& paint) {
-    LOG_ALWAYS_FATAL("TODO!");
+void RecordingCanvas::drawPoints(const float* points, int floatCount, const SkPaint& paint) {
+    if (floatCount < 2) return;
+    floatCount &= ~0x1; // round down to nearest two
+
+    addOp(new (alloc()) PointsOp(
+            calcBoundsOfPoints(points, floatCount),
+            *mState.currentSnapshot()->transform,
+            mState.getRenderTargetClipBounds(),
+            refPaint(&paint), refBuffer<float>(points, floatCount), floatCount));
 }
 
 void RecordingCanvas::drawLines(const float* points, int floatCount, const SkPaint& paint) {
     if (floatCount < 4) return;
     floatCount &= ~0x3; // round down to nearest four
 
-    Rect unmappedBounds(points[0], points[1], points[0], points[1]);
-    for (int i = 2; i < floatCount; i += 2) {
-        unmappedBounds.expandToCover(points[i], points[i + 1]);
-    }
-
-    // since anything AA stroke with less than 1.0 pixel width is drawn with an alpha-reduced
-    // 1.0 stroke, treat 1.0 as minimum.
-    unmappedBounds.outset(std::max(paint.getStrokeWidth(), 1.0f) * 0.5f);
-
     addOp(new (alloc()) LinesOp(
-            unmappedBounds,
+            calcBoundsOfPoints(points, floatCount),
             *mState.currentSnapshot()->transform,
             mState.getRenderTargetClipBounds(),
             refPaint(&paint), refBuffer<float>(points, floatCount), floatCount));
@@ -330,20 +336,42 @@
 }
 void RecordingCanvas::drawRoundRect(float left, float top, float right, float bottom,
             float rx, float ry, const SkPaint& paint) {
-    LOG_ALWAYS_FATAL("TODO!");
+    addOp(new (alloc()) RoundRectOp(
+            Rect(left, top, right, bottom),
+            *(mState.currentSnapshot()->transform),
+            mState.getRenderTargetClipBounds(),
+            refPaint(&paint), rx, ry));
 }
+
 void RecordingCanvas::drawCircle(float x, float y, float radius, const SkPaint& paint) {
-    LOG_ALWAYS_FATAL("TODO!");
+    if (radius <= 0) return;
+    drawOval(x - radius, y - radius, x + radius, y + radius, paint);
 }
+
 void RecordingCanvas::drawOval(float left, float top, float right, float bottom, const SkPaint& paint) {
-    LOG_ALWAYS_FATAL("TODO!");
+    addOp(new (alloc()) OvalOp(
+            Rect(left, top, right, bottom),
+            *(mState.currentSnapshot()->transform),
+            mState.getRenderTargetClipBounds(),
+            refPaint(&paint)));
 }
+
 void RecordingCanvas::drawArc(float left, float top, float right, float bottom,
-            float startAngle, float sweepAngle, bool useCenter, const SkPaint& paint) {
-    LOG_ALWAYS_FATAL("TODO!");
+        float startAngle, float sweepAngle, bool useCenter, const SkPaint& paint) {
+    addOp(new (alloc()) ArcOp(
+            Rect(left, top, right, bottom),
+            *(mState.currentSnapshot()->transform),
+            mState.getRenderTargetClipBounds(),
+            refPaint(&paint),
+            startAngle, sweepAngle, useCenter));
 }
+
 void RecordingCanvas::drawPath(const SkPath& path, const SkPaint& paint) {
-    LOG_ALWAYS_FATAL("TODO!");
+    addOp(new (alloc()) PathOp(
+            Rect(path.getBounds()),
+            *(mState.currentSnapshot()->transform),
+            mState.getRenderTargetClipBounds(),
+            refPaint(&paint), refPath(&path)));
 }
 
 // Bitmap-based
@@ -375,6 +403,7 @@
         restore();
     }
 }
+
 void RecordingCanvas::drawBitmap(const SkBitmap& bitmap, float srcLeft, float srcTop,
             float srcRight, float srcBottom, float dstLeft, float dstTop,
             float dstRight, float dstBottom, const SkPaint* paint) {
@@ -392,10 +421,12 @@
         LOG_ALWAYS_FATAL("TODO!");
     }
 }
+
 void RecordingCanvas::drawBitmapMesh(const SkBitmap& bitmap, int meshWidth, int meshHeight,
             const float* vertices, const int* colors, const SkPaint* paint) {
     LOG_ALWAYS_FATAL("TODO!");
 }
+
 void RecordingCanvas::drawNinePatch(const SkBitmap& bitmap, const android::Res_png_9patch& chunk,
             float dstLeft, float dstTop, float dstRight, float dstBottom,
             const SkPaint* paint) {
diff --git a/libs/hwui/RecordingCanvas.h b/libs/hwui/RecordingCanvas.h
index 6d0e9e0..16a2771 100644
--- a/libs/hwui/RecordingCanvas.h
+++ b/libs/hwui/RecordingCanvas.h
@@ -140,13 +140,13 @@
         float points[2] = { x, y };
         drawPoints(points, 2, paint);
     }
-    virtual void drawPoints(const float* points, int count, const SkPaint& paint) override;
+    virtual void drawPoints(const float* points, int floatCount, const SkPaint& paint) override;
     virtual void drawLine(float startX, float startY, float stopX, float stopY,
             const SkPaint& paint) override {
         float points[4] = { startX, startY, stopX, stopY };
         drawLines(points, 4, paint);
     }
-    virtual void drawLines(const float* points, int count, const SkPaint& paint) override;
+    virtual void drawLines(const float* points, int floatCount, const SkPaint& paint) override;
     virtual void drawRect(float left, float top, float right, float bottom, const SkPaint& paint) override;
     virtual void drawRegion(const SkRegion& region, const SkPaint& paint) override;
     virtual void drawRoundRect(float left, float top, float right, float bottom,
diff --git a/libs/hwui/Texture.h b/libs/hwui/Texture.h
index 4bcd96d..1c544b9 100644
--- a/libs/hwui/Texture.h
+++ b/libs/hwui/Texture.h
@@ -120,16 +120,16 @@
 
 class AutoTexture {
 public:
-    AutoTexture(const Texture* texture): mTexture(texture) { }
+    AutoTexture(const Texture* texture)
+            : texture(texture) {}
     ~AutoTexture() {
-        if (mTexture && mTexture->cleanup) {
-            mTexture->deleteTexture();
-            delete mTexture;
+        if (texture && texture->cleanup) {
+            texture->deleteTexture();
+            delete texture;
         }
     }
 
-private:
-    const Texture* mTexture;
+    const Texture *const texture;
 }; // class AutoTexture
 
 }; // namespace uirenderer
diff --git a/libs/hwui/tests/unit/BakedOpStateTests.cpp b/libs/hwui/tests/unit/BakedOpStateTests.cpp
index de14abf..8321ff9 100644
--- a/libs/hwui/tests/unit/BakedOpStateTests.cpp
+++ b/libs/hwui/tests/unit/BakedOpStateTests.cpp
@@ -23,41 +23,130 @@
 namespace android {
 namespace uirenderer {
 
-TEST(ResolvedRenderState, resolution) {
-    Matrix4 identity;
-    identity.loadIdentity();
-
+TEST(ResolvedRenderState, construct) {
     Matrix4 translate10x20;
     translate10x20.loadTranslate(10, 20, 0);
 
     SkPaint paint;
-    RectOp recordedOp(Rect(30, 40, 100, 200), translate10x20, Rect(0, 0, 100, 200), &paint);
+    RectOp recordedOp(Rect(30, 40, 100, 200), translate10x20, Rect(100, 200), &paint);
     {
         // recorded with transform, no parent transform
-        auto parentSnapshot = TestUtils::makeSnapshot(identity, Rect(0, 0, 100, 200));
-        ResolvedRenderState state(*parentSnapshot, recordedOp);
+        auto parentSnapshot = TestUtils::makeSnapshot(Matrix4::identity(), Rect(100, 200));
+        ResolvedRenderState state(*parentSnapshot, recordedOp, false);
         EXPECT_MATRIX_APPROX_EQ(state.transform, translate10x20);
-        EXPECT_EQ(state.clipRect, Rect(0, 0, 100, 200));
-        EXPECT_EQ(state.clippedBounds, Rect(40, 60, 100, 200)); // translated and also clipped
+        EXPECT_EQ(Rect(0, 0, 100, 200), state.clipRect);
+        EXPECT_EQ(Rect(40, 60, 100, 200), state.clippedBounds); // translated and also clipped
+        EXPECT_EQ(OpClipSideFlags::Right | OpClipSideFlags::Bottom, state.clipSideFlags);
     }
     {
         // recorded with transform and parent transform
-        auto parentSnapshot = TestUtils::makeSnapshot(translate10x20, Rect(0, 0, 100, 200));
-        ResolvedRenderState state(*parentSnapshot, recordedOp);
+        auto parentSnapshot = TestUtils::makeSnapshot(translate10x20, Rect(100, 200));
+        ResolvedRenderState state(*parentSnapshot, recordedOp, false);
 
         Matrix4 expectedTranslate;
         expectedTranslate.loadTranslate(20, 40, 0);
-        EXPECT_MATRIX_APPROX_EQ(state.transform, expectedTranslate);
+        EXPECT_MATRIX_APPROX_EQ(expectedTranslate, state.transform);
 
         // intersection of parent & transformed child clip
-        EXPECT_EQ(state.clipRect, Rect(10, 20, 100, 200));
+        EXPECT_EQ(Rect(10, 20, 100, 200), state.clipRect);
 
         // translated and also clipped
-        EXPECT_EQ(state.clippedBounds, Rect(50, 80, 100, 200));
+        EXPECT_EQ(Rect(50, 80, 100, 200), state.clippedBounds);
+        EXPECT_EQ(OpClipSideFlags::Right | OpClipSideFlags::Bottom, state.clipSideFlags);
     }
 }
 
-TEST(BakedOpState, constructAndReject) {
+const float HAIRLINE = 0.0f;
+
+// Note: bounds will be conservative, but not precise for non-hairline
+// - use approx bounds checks for these
+const float SEMI_HAIRLINE = 0.3f;
+
+struct StrokeTestCase {
+    float scale;
+    float strokeWidth;
+    const std::function<void(const ResolvedRenderState&)> validator;
+};
+
+const static StrokeTestCase sStrokeTestCases[] = {
+    {
+        1, HAIRLINE, [](const ResolvedRenderState& state) {
+            EXPECT_EQ(Rect(49.5f, 49.5f, 150.5f, 150.5f), state.clippedBounds);
+        }
+    },
+    {
+        1, SEMI_HAIRLINE, [](const ResolvedRenderState& state) {
+            EXPECT_TRUE(state.clippedBounds.contains(49.5f, 49.5f, 150.5f, 150.5f));
+            EXPECT_TRUE(Rect(49, 49, 151, 151).contains(state.clippedBounds));
+        }
+    },
+    {
+        1, 20, [](const ResolvedRenderState& state) {
+            EXPECT_EQ(Rect(40, 40, 160, 160), state.clippedBounds);
+        }
+    },
+
+    // 3x3 scale:
+    {
+        3, HAIRLINE, [](const ResolvedRenderState& state) {
+            EXPECT_EQ(Rect(149.5f, 149.5f, 200, 200), state.clippedBounds);
+            EXPECT_EQ(OpClipSideFlags::Right | OpClipSideFlags::Bottom, state.clipSideFlags);
+        }
+    },
+    {
+        3, SEMI_HAIRLINE, [](const ResolvedRenderState& state) {
+            EXPECT_TRUE(state.clippedBounds.contains(149.5f, 149.5f, 200, 200));
+            EXPECT_TRUE(Rect(149, 149, 200, 200).contains(state.clippedBounds));
+        }
+    },
+    {
+        3, 20, [](const ResolvedRenderState& state) {
+            EXPECT_TRUE(state.clippedBounds.contains(120, 120, 200, 200));
+            EXPECT_TRUE(Rect(119, 119, 200, 200).contains(state.clippedBounds));
+        }
+    },
+
+    // 0.5f x 0.5f scale
+    {
+        0.5f, HAIRLINE, [](const ResolvedRenderState& state) {
+            EXPECT_EQ(Rect(24.5f, 24.5f, 75.5f, 75.5f), state.clippedBounds);
+        }
+    },
+    {
+        0.5f, SEMI_HAIRLINE, [](const ResolvedRenderState& state) {
+            EXPECT_TRUE(state.clippedBounds.contains(24.5f, 24.5f, 75.5f, 75.5f));
+            EXPECT_TRUE(Rect(24, 24, 76, 76).contains(state.clippedBounds));
+        }
+    },
+    {
+        0.5f, 20, [](const ResolvedRenderState& state) {
+            EXPECT_TRUE(state.clippedBounds.contains(19.5f, 19.5f, 80.5f, 80.5f));
+            EXPECT_TRUE(Rect(19, 19, 81, 81).contains(state.clippedBounds));
+        }
+    }
+};
+
+TEST(ResolvedRenderState, construct_expandForStroke) {
+    // Loop over table of test cases and verify different combinations of stroke width and transform
+    for (auto&& testCase : sStrokeTestCases) {
+        SkPaint strokedPaint;
+        strokedPaint.setAntiAlias(true);
+        strokedPaint.setStyle(SkPaint::kStroke_Style);
+        strokedPaint.setStrokeWidth(testCase.strokeWidth);
+
+        RectOp recordedOp(Rect(50, 50, 150, 150),
+                Matrix4::identity(), Rect(200, 200), &strokedPaint);
+
+        Matrix4 snapshotMatrix;
+        snapshotMatrix.loadScale(testCase.scale, testCase.scale, 1);
+        auto parentSnapshot = TestUtils::makeSnapshot(snapshotMatrix, Rect(200, 200));
+
+        ResolvedRenderState state(*parentSnapshot, recordedOp, true);
+        testCase.validator(state);
+    }
+}
+
+TEST(BakedOpState, tryConstruct) {
     LinearAllocator allocator;
 
     Matrix4 translate100x0;
@@ -65,41 +154,85 @@
 
     SkPaint paint;
     {
-        RectOp rejectOp(Rect(30, 40, 100, 200), translate100x0, Rect(0, 0, 100, 200), &paint);
-        auto snapshot = TestUtils::makeSnapshot(Matrix4::identity(), Rect(0, 0, 100, 200));
-        BakedOpState* bakedOp = BakedOpState::tryConstruct(allocator, *snapshot, rejectOp);
+        RectOp rejectOp(Rect(30, 40, 100, 200), translate100x0, Rect(100, 200), &paint);
+        auto snapshot = TestUtils::makeSnapshot(Matrix4::identity(), Rect(100, 200));
+        BakedOpState* bakedState = BakedOpState::tryConstruct(allocator, *snapshot, rejectOp);
 
-        EXPECT_EQ(bakedOp, nullptr); // rejected by clip, so not constructed
-        EXPECT_LE(allocator.usedSize(), 8u); // no significant allocation space used for rejected op
+        EXPECT_EQ(nullptr, bakedState); // rejected by clip, so not constructed
+        EXPECT_GT(8u, allocator.usedSize()); // no significant allocation space used for rejected op
     }
     {
-        RectOp successOp(Rect(30, 40, 100, 200), Matrix4::identity(), Rect(0, 0, 100, 200), &paint);
-        auto snapshot = TestUtils::makeSnapshot(Matrix4::identity(), Rect(0, 0, 100, 200));
-        BakedOpState* bakedOp = BakedOpState::tryConstruct(allocator, *snapshot, successOp);
+        RectOp successOp(Rect(30, 40, 100, 200), Matrix4::identity(), Rect(100, 200), &paint);
+        auto snapshot = TestUtils::makeSnapshot(Matrix4::identity(), Rect(100, 200));
+        BakedOpState* bakedState = BakedOpState::tryConstruct(allocator, *snapshot, successOp);
 
-        EXPECT_NE(bakedOp, nullptr); // NOT rejected by clip, so will be constructed
-        EXPECT_GT(allocator.usedSize(), 64u); // relatively large alloc for non-rejected op
+        EXPECT_NE(nullptr, bakedState); // NOT rejected by clip, so will be constructed
+        EXPECT_LE(64u, allocator.usedSize()); // relatively large alloc for non-rejected op
     }
 }
 
-TEST(BakedOpState, oplessConstructAndReject) {
+TEST(BakedOpState, tryShadowOpConstruct) {
     LinearAllocator allocator;
     {
-        auto snapshot = TestUtils::makeSnapshot(Matrix4::identity(), Rect(0, 0, 0, 0)); // empty
-        BakedOpState* bakedOp = BakedOpState::tryShadowOpConstruct(allocator, *snapshot, (ShadowOp*)0x1234);
+        auto snapshot = TestUtils::makeSnapshot(Matrix4::identity(), Rect()); // Note: empty clip
+        BakedOpState* bakedState = BakedOpState::tryShadowOpConstruct(allocator, *snapshot, (ShadowOp*)0x1234);
 
-        EXPECT_EQ(bakedOp, nullptr); // rejected by clip, so not constructed
-        EXPECT_LE(allocator.usedSize(), 8u); // no significant allocation space used for rejected op
+        EXPECT_EQ(nullptr, bakedState); // rejected by clip, so not constructed
+        EXPECT_GT(8u, allocator.usedSize()); // no significant allocation space used for rejected op
     }
     {
-        auto snapshot = TestUtils::makeSnapshot(Matrix4::identity(), Rect(0, 0, 100, 200));
-        BakedOpState* bakedOp = BakedOpState::tryShadowOpConstruct(allocator, *snapshot, (ShadowOp*)0x1234);
+        auto snapshot = TestUtils::makeSnapshot(Matrix4::identity(), Rect(100, 200));
+        BakedOpState* bakedState = BakedOpState::tryShadowOpConstruct(allocator, *snapshot, (ShadowOp*)0x1234);
 
-        EXPECT_NE(bakedOp, nullptr); // NOT rejected by clip, so will be constructed
-        EXPECT_GT(allocator.usedSize(), 64u); // relatively large alloc for non-rejected op
-        EXPECT_EQ((ShadowOp*)0x1234, bakedOp->op);
+        ASSERT_NE(nullptr, bakedState); // NOT rejected by clip, so will be constructed
+        EXPECT_LE(64u, allocator.usedSize()); // relatively large alloc for non-rejected op
     }
 }
 
+TEST(BakedOpState, tryStrokeableOpConstruct) {
+    LinearAllocator allocator;
+    {
+        // check regular rejection
+        SkPaint paint;
+        paint.setStyle(SkPaint::kStrokeAndFill_Style);
+        paint.setStrokeWidth(0.0f);
+        RectOp rejectOp(Rect(0, 0, 100, 200), Matrix4::identity(), Rect(100, 200), &paint);
+        auto snapshot = TestUtils::makeSnapshot(Matrix4::identity(), Rect()); // Note: empty clip
+        auto bakedState = BakedOpState::tryStrokeableOpConstruct(allocator, *snapshot, rejectOp,
+                BakedOpState::StrokeBehavior::StyleDefined);
+
+        EXPECT_EQ(nullptr, bakedState);
+        EXPECT_GT(8u, allocator.usedSize()); // no significant allocation space used for rejected op
+    }
+    {
+        // check simple unscaled expansion
+        SkPaint paint;
+        paint.setStyle(SkPaint::kStrokeAndFill_Style);
+        paint.setStrokeWidth(10.0f);
+        RectOp rejectOp(Rect(50, 50, 150, 150), Matrix4::identity(), Rect(200, 200), &paint);
+        auto snapshot = TestUtils::makeSnapshot(Matrix4::identity(), Rect(200, 200));
+        auto bakedState = BakedOpState::tryStrokeableOpConstruct(allocator, *snapshot, rejectOp,
+                BakedOpState::StrokeBehavior::StyleDefined);
+
+        ASSERT_NE(nullptr, bakedState);
+        EXPECT_EQ(Rect(45, 45, 155, 155), bakedState->computedState.clippedBounds);
+        EXPECT_EQ(0, bakedState->computedState.clipSideFlags);
+    }
+    {
+        // check simple unscaled expansion, and fill style with stroke forced
+        SkPaint paint;
+        paint.setStyle(SkPaint::kFill_Style);
+        paint.setStrokeWidth(10.0f);
+        RectOp rejectOp(Rect(50, 50, 150, 150), Matrix4::identity(), Rect(200, 200), &paint);
+        auto snapshot = TestUtils::makeSnapshot(Matrix4::identity(), Rect(200, 200));
+        auto bakedState = BakedOpState::tryStrokeableOpConstruct(allocator, *snapshot, rejectOp,
+                BakedOpState::StrokeBehavior::Forced);
+
+        ASSERT_NE(nullptr, bakedState);
+        EXPECT_EQ(Rect(45, 45, 155, 155), bakedState->computedState.clippedBounds);
+        EXPECT_EQ(0, bakedState->computedState.clipSideFlags);
+    }
 }
-}
+
+} // namespace uirenderer
+} // namespace android
diff --git a/libs/hwui/tests/unit/ClipAreaTests.cpp b/libs/hwui/tests/unit/ClipAreaTests.cpp
index d6192df..c4d305e 100644
--- a/libs/hwui/tests/unit/ClipAreaTests.cpp
+++ b/libs/hwui/tests/unit/ClipAreaTests.cpp
@@ -92,12 +92,10 @@
 
 TEST(ClipArea, paths) {
     ClipArea area(createClipArea());
-    Matrix4 transform;
-    transform.loadIdentity();
     SkPath path;
     SkScalar r = 100;
     path.addCircle(r, r, r);
-    area.clipPathWithTransform(path, &transform, SkRegion::kIntersect_Op);
+    area.clipPathWithTransform(path, &Matrix4::identity(), SkRegion::kIntersect_Op);
     EXPECT_FALSE(area.isEmpty());
     EXPECT_FALSE(area.isSimple());
     EXPECT_FALSE(area.isRectangleList());
@@ -116,11 +114,10 @@
     ClipArea area(createClipArea());
     area.setClip(0, 0, 100, 100);
 
-    Matrix4 transform;
-    transform.loadIdentity();
     Rect expected(-50, -50, 50, 50);
-    area.clipRectWithTransform(expected, &transform, SkRegion::kReplace_Op);
+    area.clipRectWithTransform(expected, &Matrix4::identity(), SkRegion::kReplace_Op);
     EXPECT_EQ(expected, area.getClipRect());
 }
+
 }
 }
diff --git a/libs/hwui/tests/unit/DamageAccumulatorTests.cpp b/libs/hwui/tests/unit/DamageAccumulatorTests.cpp
index 29354a7..7700138 100644
--- a/libs/hwui/tests/unit/DamageAccumulatorTests.cpp
+++ b/libs/hwui/tests/unit/DamageAccumulatorTests.cpp
@@ -31,13 +31,11 @@
 // as the output.
 TEST(DamageAccumulator, identity) {
     DamageAccumulator da;
-    Matrix4 identity;
     SkRect curDirty;
-    identity.loadIdentity();
-    da.pushTransform(&identity);
+    da.pushTransform(&Matrix4::identity());
     da.dirty(50, 50, 100, 100);
     {
-        da.pushTransform(&identity);
+        da.pushTransform(&Matrix4::identity());
         da.peekAtDirty(&curDirty);
         ASSERT_EQ(SkRect(), curDirty);
         da.popTransform();
@@ -68,15 +66,13 @@
 // Test that dirty rectangles are being unioned across "siblings
 TEST(DamageAccumulator, union) {
     DamageAccumulator da;
-    Matrix4 identity;
     SkRect curDirty;
-    identity.loadIdentity();
-    da.pushTransform(&identity);
+    da.pushTransform(&Matrix4::identity());
     {
-        da.pushTransform(&identity);
+        da.pushTransform(&Matrix4::identity());
         da.dirty(50, 50, 100, 100);
         da.popTransform();
-        da.pushTransform(&identity);
+        da.pushTransform(&Matrix4::identity());
         da.dirty(150, 50, 200, 125);
         da.popTransform();
     }
diff --git a/libs/hwui/tests/unit/OpReordererTests.cpp b/libs/hwui/tests/unit/OpReordererTests.cpp
index 068e832..5eac498 100644
--- a/libs/hwui/tests/unit/OpReordererTests.cpp
+++ b/libs/hwui/tests/unit/OpReordererTests.cpp
@@ -144,6 +144,32 @@
     EXPECT_EQ(4, renderer.getIndex()); // 2 ops + start + end
 }
 
+TEST(OpReorderer, simpleStroke) {
+    class SimpleStrokeTestRenderer : public TestRendererBase {
+    public:
+        void onPointsOp(const PointsOp& op, const BakedOpState& state) override {
+            EXPECT_EQ(0, mIndex++);
+            // even though initial bounds are empty...
+            EXPECT_TRUE(op.unmappedBounds.isEmpty())
+                    << "initial bounds should be empty, since they're unstroked";
+            EXPECT_EQ(Rect(45, 45, 55, 55), state.computedState.clippedBounds)
+                    << "final bounds should account for stroke";
+        }
+    };
+
+    auto node = TestUtils::createNode(0, 0, 100, 200,
+            [](RenderProperties& props, RecordingCanvas& canvas) {
+        SkPaint strokedPaint;
+        strokedPaint.setStrokeWidth(10);
+        canvas.drawPoint(50, 50, strokedPaint);
+    });
+    OpReorderer reorderer(sEmptyLayerUpdateQueue, SkRect::MakeWH(100, 200), 100, 200,
+            createSyncedNodeList(node), sLightCenter);
+    SimpleStrokeTestRenderer renderer;
+    reorderer.replayBakedOps<TestDispatcher>(renderer);
+    EXPECT_EQ(1, renderer.getIndex());
+}
+
 TEST(OpReorderer, simpleRejection) {
     auto node = TestUtils::createNode(0, 0, 200, 200,
             [](RenderProperties& props, RecordingCanvas& canvas) {
diff --git a/libs/hwui/tests/unit/RecordingCanvasTests.cpp b/libs/hwui/tests/unit/RecordingCanvasTests.cpp
index 2449ce8..ba9d185 100644
--- a/libs/hwui/tests/unit/RecordingCanvasTests.cpp
+++ b/libs/hwui/tests/unit/RecordingCanvasTests.cpp
@@ -44,7 +44,7 @@
 TEST(RecordingCanvas, drawLines) {
     auto dl = TestUtils::createDisplayList<RecordingCanvas>(100, 200, [](RecordingCanvas& canvas) {
         SkPaint paint;
-        paint.setStrokeWidth(20);
+        paint.setStrokeWidth(20); // doesn't affect recorded bounds - would be resolved at bake time
         float points[] = { 0, 0, 20, 10, 30, 40, 90 }; // NB: only 1 valid line
         canvas.drawLines(&points[0], 7, paint);
     });
@@ -54,8 +54,8 @@
     ASSERT_EQ(RecordedOpId::LinesOp, op->opId);
     EXPECT_EQ(4, ((LinesOp*)op)->floatCount)
             << "float count must be rounded down to closest multiple of 4";
-    EXPECT_EQ(Rect(-10, -10, 30, 20), op->unmappedBounds)
-            << "unmapped bounds must be size of line, outset by 1/2 stroke width";
+    EXPECT_EQ(Rect(0, 0, 20, 10), op->unmappedBounds)
+            << "unmapped bounds must be size of line, and not outset for stroke width";
 }
 
 TEST(RecordingCanvas, drawRect) {