Merged op dispatch in OpReorderer

bug:22480459

Also switches std::functions to function pointers on OpReorderer, and
switches AssetAtlas' entry getter methods to using pixelRef pointers,
so it's clear they're the keys.

Change-Id: I3040ce5ff4e178a8364e0fd7ab0876ada7d4de05
diff --git a/libs/hwui/AssetAtlas.cpp b/libs/hwui/AssetAtlas.cpp
index 7e09699..41411a9 100644
--- a/libs/hwui/AssetAtlas.cpp
+++ b/libs/hwui/AssetAtlas.cpp
@@ -79,13 +79,13 @@
 // Entries
 ///////////////////////////////////////////////////////////////////////////////
 
-AssetAtlas::Entry* AssetAtlas::getEntry(const SkBitmap* bitmap) const {
-    ssize_t index = mEntries.indexOfKey(bitmap->pixelRef());
+AssetAtlas::Entry* AssetAtlas::getEntry(const SkPixelRef* pixelRef) const {
+    ssize_t index = mEntries.indexOfKey(pixelRef);
     return index >= 0 ? mEntries.valueAt(index) : nullptr;
 }
 
-Texture* AssetAtlas::getEntryTexture(const SkBitmap* bitmap) const {
-    ssize_t index = mEntries.indexOfKey(bitmap->pixelRef());
+Texture* AssetAtlas::getEntryTexture(const SkPixelRef* pixelRef) const {
+    ssize_t index = mEntries.indexOfKey(pixelRef);
     return index >= 0 ? mEntries.valueAt(index)->texture : nullptr;
 }
 
diff --git a/libs/hwui/AssetAtlas.h b/libs/hwui/AssetAtlas.h
index f1cd0b4..a037725 100644
--- a/libs/hwui/AssetAtlas.h
+++ b/libs/hwui/AssetAtlas.h
@@ -148,15 +148,15 @@
 
     /**
      * Returns the entry in the atlas associated with the specified
-     * bitmap. If the bitmap is not in the atlas, return NULL.
+     * pixelRef. If the pixelRef is not in the atlas, return NULL.
      */
-    Entry* getEntry(const SkBitmap* bitmap) const;
+    Entry* getEntry(const SkPixelRef* pixelRef) const;
 
     /**
      * Returns the texture for the atlas entry associated with the
-     * specified bitmap. If the bitmap is not in the atlas, return NULL.
+     * specified pixelRef. If the pixelRef is not in the atlas, return NULL.
      */
-    Texture* getEntryTexture(const SkBitmap* bitmap) const;
+    Texture* getEntryTexture(const SkPixelRef* pixelRef) const;
 
 private:
     void createEntries(Caches& caches, int64_t* map, int count);
diff --git a/libs/hwui/BakedOpDispatcher.cpp b/libs/hwui/BakedOpDispatcher.cpp
index b56b1e4..fde12dd 100644
--- a/libs/hwui/BakedOpDispatcher.cpp
+++ b/libs/hwui/BakedOpDispatcher.cpp
@@ -31,20 +31,182 @@
 namespace android {
 namespace uirenderer {
 
+static void storeTexturedRect(TextureVertex* vertices, const Rect& bounds, const Rect& texCoord) {
+    vertices[0] = { bounds.left, bounds.top, texCoord.left, texCoord.top };
+    vertices[1] = { bounds.right, bounds.top, texCoord.right, texCoord.top };
+    vertices[2] = { bounds.left, bounds.bottom, texCoord.left, texCoord.bottom };
+    vertices[3] = { bounds.right, bounds.bottom, texCoord.right, texCoord.bottom };
+}
+
+void BakedOpDispatcher::onMergedBitmapOps(BakedOpRenderer& renderer,
+        const MergedBakedOpList& opList) {
+
+    const BakedOpState& firstState = *(opList.states[0]);
+    const SkBitmap* bitmap = (static_cast<const BitmapOp*>(opList.states[0]->op))->bitmap;
+
+    AssetAtlas::Entry* entry = renderer.renderState().assetAtlas().getEntry(bitmap->pixelRef());
+    Texture* texture = entry ? entry->texture : renderer.caches().textureCache.get(bitmap);
+    if (!texture) return;
+    const AutoTexture autoCleanup(texture);
+
+    TextureVertex vertices[opList.count * 4];
+    Rect texCoords(0, 0, 1, 1);
+    if (entry) {
+        entry->uvMapper.map(texCoords);
+    }
+    // init to non-empty, so we can safely expandtoCoverRect
+    Rect totalBounds = firstState.computedState.clippedBounds;
+    for (size_t i = 0; i < opList.count; i++) {
+        const BakedOpState& state = *(opList.states[i]);
+        TextureVertex* rectVerts = &vertices[i * 4];
+        Rect opBounds = state.computedState.clippedBounds;
+        if (CC_LIKELY(state.computedState.transform.isPureTranslate())) {
+            // pure translate, so snap (same behavior as onBitmapOp)
+            opBounds.snapToPixelBoundaries();
+        }
+        storeTexturedRect(rectVerts, opBounds, texCoords);
+        renderer.dirtyRenderTarget(opBounds);
+
+        totalBounds.expandToCover(opBounds);
+    }
+
+    const int textureFillFlags = (bitmap->colorType() == kAlpha_8_SkColorType)
+            ? TextureFillFlags::IsAlphaMaskTexture : TextureFillFlags::None;
+    Glop glop;
+    GlopBuilder(renderer.renderState(), renderer.caches(), &glop)
+            .setRoundRectClipState(firstState.roundRectClipState)
+            .setMeshTexturedIndexedQuads(vertices, opList.count * 6)
+            .setFillTexturePaint(*texture, textureFillFlags, firstState.op->paint, firstState.alpha)
+            .setTransform(Matrix4::identity(), TransformFlags::None)
+            .setModelViewOffsetRect(0, 0, totalBounds) // don't snap here, we snap per-quad above
+            .build();
+    renderer.renderGlop(nullptr, opList.clipSideFlags ? &opList.clip : nullptr, glop);
+}
+
+static void renderTextShadow(BakedOpRenderer& renderer, FontRenderer& fontRenderer,
+        const TextOp& op, const BakedOpState& state) {
+    renderer.caches().textureState().activateTexture(0);
+
+    PaintUtils::TextShadow textShadow;
+    if (!PaintUtils::getTextShadow(op.paint, &textShadow)) {
+        LOG_ALWAYS_FATAL("failed to query shadow attributes");
+    }
+
+    renderer.caches().dropShadowCache.setFontRenderer(fontRenderer);
+    ShadowTexture* texture = renderer.caches().dropShadowCache.get(
+            op.paint, (const char*) op.glyphs,
+            op.glyphCount, textShadow.radius, op.positions);
+    // If the drop shadow exceeds the max texture size or couldn't be
+    // allocated, skip drawing
+    if (!texture) return;
+    const AutoTexture autoCleanup(texture);
+
+    const float sx = op.x - texture->left + textShadow.dx;
+    const float sy = op.y - texture->top + textShadow.dy;
+
+    Glop glop;
+    GlopBuilder(renderer.renderState(), renderer.caches(), &glop)
+            .setRoundRectClipState(state.roundRectClipState)
+            .setMeshTexturedUnitQuad(nullptr)
+            .setFillShadowTexturePaint(*texture, textShadow.color, *op.paint, state.alpha)
+            .setTransform(state.computedState.transform, TransformFlags::None)
+            .setModelViewMapUnitToRect(Rect(sx, sy, sx + texture->width, sy + texture->height))
+            .build();
+    renderer.renderGlop(state, glop);
+}
+
+enum class TextRenderType {
+    Defer,
+    Flush
+};
+
+static void renderTextOp(BakedOpRenderer& renderer, const TextOp& op, const BakedOpState& state,
+        const Rect* renderClip, TextRenderType renderType) {
+    FontRenderer& fontRenderer = renderer.caches().fontRenderer.getFontRenderer();
+
+    if (CC_UNLIKELY(PaintUtils::hasTextShadow(op.paint))) {
+        fontRenderer.setFont(op.paint, SkMatrix::I());
+        renderTextShadow(renderer, fontRenderer, op, state);
+    }
+
+    float x = op.x;
+    float y = op.y;
+    const Matrix4& transform = state.computedState.transform;
+    const bool pureTranslate = transform.isPureTranslate();
+    if (CC_LIKELY(pureTranslate)) {
+        x = floorf(x + transform.getTranslateX() + 0.5f);
+        y = floorf(y + transform.getTranslateY() + 0.5f);
+        fontRenderer.setFont(op.paint, SkMatrix::I());
+        fontRenderer.setTextureFiltering(false);
+    } else if (CC_UNLIKELY(transform.isPerspective())) {
+        fontRenderer.setFont(op.paint, SkMatrix::I());
+        fontRenderer.setTextureFiltering(true);
+    } else {
+        // We only pass a partial transform to the font renderer. That partial
+        // matrix defines how glyphs are rasterized. Typically we want glyphs
+        // to be rasterized at their final size on screen, which means the partial
+        // matrix needs to take the scale factor into account.
+        // When a partial matrix is used to transform glyphs during rasterization,
+        // the mesh is generated with the inverse transform (in the case of scale,
+        // the mesh is generated at 1.0 / scale for instance.) This allows us to
+        // apply the full transform matrix at draw time in the vertex shader.
+        // Applying the full matrix in the shader is the easiest way to handle
+        // rotation and perspective and allows us to always generated quads in the
+        // font renderer which greatly simplifies the code, clipping in particular.
+        float sx, sy;
+        transform.decomposeScale(sx, sy);
+        fontRenderer.setFont(op.paint, SkMatrix::MakeScale(
+                roundf(std::max(1.0f, sx)),
+                roundf(std::max(1.0f, sy))));
+        fontRenderer.setTextureFiltering(true);
+    }
+    Rect layerBounds(FLT_MAX / 2.0f, FLT_MAX / 2.0f, FLT_MIN / 2.0f, FLT_MIN / 2.0f);
+
+    int alpha = PaintUtils::getAlphaDirect(op.paint) * state.alpha;
+    SkXfermode::Mode mode = PaintUtils::getXfermodeDirect(op.paint);
+    TextDrawFunctor functor(&renderer, &state, renderClip,
+            x, y, pureTranslate, alpha, mode, op.paint);
+
+    bool forceFinish = (renderType == TextRenderType::Flush);
+    bool mustDirtyRenderTarget = renderer.offscreenRenderTarget();
+    const Rect* localOpClip = pureTranslate ? &state.computedState.clipRect : nullptr;
+    fontRenderer.renderPosText(op.paint, localOpClip,
+            (const char*) op.glyphs, op.glyphCount, x, y,
+            op.positions, mustDirtyRenderTarget ? &layerBounds : nullptr, &functor, forceFinish);
+
+    if (mustDirtyRenderTarget) {
+        if (!pureTranslate) {
+            transform.mapRect(layerBounds);
+        }
+        renderer.dirtyRenderTarget(layerBounds);
+    }
+}
+
+void BakedOpDispatcher::onMergedTextOps(BakedOpRenderer& renderer,
+        const MergedBakedOpList& opList) {
+    const Rect* clip = opList.clipSideFlags ? &opList.clip : nullptr;
+    for (size_t i = 0; i < opList.count; i++) {
+        const BakedOpState& state = *(opList.states[i]);
+        const TextOp& op = *(static_cast<const TextOp*>(state.op));
+        TextRenderType renderType = (i + 1 == opList.count)
+                ? TextRenderType::Flush : TextRenderType::Defer;
+        renderTextOp(renderer, op, state, clip, renderType);
+    }
+}
+
 void BakedOpDispatcher::onRenderNodeOp(BakedOpRenderer&, const RenderNodeOp&, const BakedOpState&) {
     LOG_ALWAYS_FATAL("unsupported operation");
 }
 
-void BakedOpDispatcher::onBeginLayerOp(BakedOpRenderer& renderer, const BeginLayerOp& op, const BakedOpState& state) {
+void BakedOpDispatcher::onBeginLayerOp(BakedOpRenderer&, const BeginLayerOp&, const BakedOpState&) {
     LOG_ALWAYS_FATAL("unsupported operation");
 }
 
-void BakedOpDispatcher::onEndLayerOp(BakedOpRenderer& renderer, const EndLayerOp& op, const BakedOpState& state) {
+void BakedOpDispatcher::onEndLayerOp(BakedOpRenderer&, const EndLayerOp&, const BakedOpState&) {
     LOG_ALWAYS_FATAL("unsupported operation");
 }
 
 void BakedOpDispatcher::onBitmapOp(BakedOpRenderer& renderer, const BitmapOp& op, const BakedOpState& state) {
-    renderer.caches().textureState().activateTexture(0); // TODO: should this be automatic, and/or elsewhere?
     Texture* texture = renderer.getTexture(op.bitmap);
     if (!texture) return;
     const AutoTexture autoCleanup(texture);
@@ -153,89 +315,9 @@
     renderer.renderGlop(state, glop);
 }
 
-static void renderTextShadow(BakedOpRenderer& renderer, FontRenderer& fontRenderer,
-        const TextOp& op, const BakedOpState& state) {
-    renderer.caches().textureState().activateTexture(0);
-
-    PaintUtils::TextShadow textShadow;
-    if (!PaintUtils::getTextShadow(op.paint, &textShadow)) {
-        LOG_ALWAYS_FATAL("failed to query shadow attributes");
-    }
-
-    renderer.caches().dropShadowCache.setFontRenderer(fontRenderer);
-    ShadowTexture* texture = renderer.caches().dropShadowCache.get(
-            op.paint, (const char*) op.glyphs,
-            op.glyphCount, textShadow.radius, op.positions);
-    // If the drop shadow exceeds the max texture size or couldn't be
-    // allocated, skip drawing
-    if (!texture) return;
-    const AutoTexture autoCleanup(texture);
-
-    const float sx = op.x - texture->left + textShadow.dx;
-    const float sy = op.y - texture->top + textShadow.dy;
-
-    Glop glop;
-    GlopBuilder(renderer.renderState(), renderer.caches(), &glop)
-            .setRoundRectClipState(state.roundRectClipState)
-            .setMeshTexturedUnitQuad(nullptr)
-            .setFillShadowTexturePaint(*texture, textShadow.color, *op.paint, state.alpha)
-            .setTransform(state.computedState.transform, TransformFlags::None)
-            .setModelViewMapUnitToRect(Rect(sx, sy, sx + texture->width, sy + texture->height))
-            .build();
-    renderer.renderGlop(state, glop);
-}
-
 void BakedOpDispatcher::onTextOp(BakedOpRenderer& renderer, const TextOp& op, const BakedOpState& state) {
-    FontRenderer& fontRenderer = renderer.caches().fontRenderer.getFontRenderer();
-
-    if (CC_UNLIKELY(PaintUtils::hasTextShadow(op.paint))) {
-        fontRenderer.setFont(op.paint, SkMatrix::I());
-        renderTextShadow(renderer, fontRenderer, op, state);
-    }
-
-    float x = op.x;
-    float y = op.y;
-    const Matrix4& transform = state.computedState.transform;
-    const bool pureTranslate = transform.isPureTranslate();
-    if (CC_LIKELY(pureTranslate)) {
-        x = floorf(x + transform.getTranslateX() + 0.5f);
-        y = floorf(y + transform.getTranslateY() + 0.5f);
-        fontRenderer.setFont(op.paint, SkMatrix::I());
-        fontRenderer.setTextureFiltering(false);
-    } else if (CC_UNLIKELY(transform.isPerspective())) {
-        fontRenderer.setFont(op.paint, SkMatrix::I());
-        fontRenderer.setTextureFiltering(true);
-    } else {
-        // We only pass a partial transform to the font renderer. That partial
-        // matrix defines how glyphs are rasterized. Typically we want glyphs
-        // to be rasterized at their final size on screen, which means the partial
-        // matrix needs to take the scale factor into account.
-        // When a partial matrix is used to transform glyphs during rasterization,
-        // the mesh is generated with the inverse transform (in the case of scale,
-        // the mesh is generated at 1.0 / scale for instance.) This allows us to
-        // apply the full transform matrix at draw time in the vertex shader.
-        // Applying the full matrix in the shader is the easiest way to handle
-        // rotation and perspective and allows us to always generated quads in the
-        // font renderer which greatly simplifies the code, clipping in particular.
-        float sx, sy;
-        transform.decomposeScale(sx, sy);
-        fontRenderer.setFont(op.paint, SkMatrix::MakeScale(
-                roundf(std::max(1.0f, sx)),
-                roundf(std::max(1.0f, sy))));
-        fontRenderer.setTextureFiltering(true);
-    }
-
-    // TODO: Implement better clipping for scaled/rotated text
-    const Rect* clip = !pureTranslate ? nullptr : &state.computedState.clipRect;
-    Rect layerBounds(FLT_MAX / 2.0f, FLT_MAX / 2.0f, FLT_MIN / 2.0f, FLT_MIN / 2.0f);
-
-    int alpha = PaintUtils::getAlphaDirect(op.paint) * state.alpha;
-    SkXfermode::Mode mode = PaintUtils::getXfermodeDirect(op.paint);
-    TextDrawFunctor functor(&renderer, &state, x, y, pureTranslate, alpha, mode, op.paint);
-
-    bool hasActiveLayer = false; // TODO
-    fontRenderer.renderPosText(op.paint, clip, (const char*) op.glyphs, op.glyphCount, x, y,
-            op.positions, hasActiveLayer ? &layerBounds : nullptr, &functor, true); // TODO: merging
+    const Rect* clip = state.computedState.clipSideFlags ? &state.computedState.clipRect : nullptr;
+    renderTextOp(renderer, op, state, clip, TextRenderType::Flush);
 }
 
 void BakedOpDispatcher::onLayerOp(BakedOpRenderer& renderer, const LayerOp& op, const BakedOpState& state) {
diff --git a/libs/hwui/BakedOpDispatcher.h b/libs/hwui/BakedOpDispatcher.h
index caf14bf..0e763d9 100644
--- a/libs/hwui/BakedOpDispatcher.h
+++ b/libs/hwui/BakedOpDispatcher.h
@@ -26,16 +26,21 @@
 /**
  * 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.
- *
- * This dispatcher is separate from the renderer so that the dispatcher / renderer interaction is
- * minimal through public BakedOpRenderer APIs.
  */
 class BakedOpDispatcher {
 public:
+    // Declares all "onMergedBitmapOps(...)" style methods for mergeable op types
+#define X(Type) \
+        static void onMerged##Type##s(BakedOpRenderer& renderer, const MergedBakedOpList& opList);
+    MAP_MERGED_OPS(X)
+#undef X
+
     // Declares all "onBitmapOp(...)" style methods for every op type
-#define DISPATCH_METHOD(Type) \
+#define X(Type) \
         static void on##Type(BakedOpRenderer& renderer, const Type& op, const BakedOpState& state);
-    MAP_OPS(DISPATCH_METHOD);
+    MAP_OPS(X)
+#undef X
+
 };
 
 }; // namespace uirenderer
diff --git a/libs/hwui/BakedOpRenderer.cpp b/libs/hwui/BakedOpRenderer.cpp
index 6cdc320..93a9406 100644
--- a/libs/hwui/BakedOpRenderer.cpp
+++ b/libs/hwui/BakedOpRenderer.cpp
@@ -121,30 +121,35 @@
 }
 
 Texture* BakedOpRenderer::getTexture(const SkBitmap* bitmap) {
-    Texture* texture = mRenderState.assetAtlas().getEntryTexture(bitmap);
+    Texture* texture = mRenderState.assetAtlas().getEntryTexture(bitmap->pixelRef());
     if (!texture) {
         return mCaches.textureCache.get(bitmap);
     }
     return texture;
 }
 
-void BakedOpRenderer::renderGlop(const BakedOpState& state, const Glop& glop) {
-    bool useScissor = state.computedState.clipSideFlags != OpClipSideFlags::None;
-    mRenderState.scissor().setEnabled(useScissor);
-    if (useScissor) {
-        const Rect& clip = state.computedState.clipRect;
-        mRenderState.scissor().set(clip.left, mRenderTarget.viewportHeight - clip.bottom,
-            clip.getWidth(), clip.getHeight());
+void BakedOpRenderer::renderGlop(const Rect* dirtyBounds, const Rect* clip, const Glop& glop) {
+    mRenderState.scissor().setEnabled(clip != nullptr);
+    if (clip) {
+        mRenderState.scissor().set(clip->left, mRenderTarget.viewportHeight - clip->bottom,
+            clip->getWidth(), clip->getHeight());
     }
-    if (mRenderTarget.offscreenBuffer) { // TODO: not with multi-draw
+    if (dirtyBounds && mRenderTarget.offscreenBuffer) {
         // register layer damage to draw-back region
-        const Rect& uiDirty = state.computedState.clippedBounds;
-        android::Rect dirty(uiDirty.left, uiDirty.top, uiDirty.right, uiDirty.bottom);
+        android::Rect dirty(dirtyBounds->left, dirtyBounds->top,
+                dirtyBounds->right, dirtyBounds->bottom);
         mRenderTarget.offscreenBuffer->region.orSelf(dirty);
     }
     mRenderState.render(glop, mRenderTarget.orthoMatrix);
     if (!mRenderTarget.frameBufferId) mHasDrawn = true;
 }
 
+void BakedOpRenderer::dirtyRenderTarget(const Rect& uiDirty) {
+    if (mRenderTarget.offscreenBuffer) {
+        android::Rect dirty(uiDirty.left, uiDirty.top, uiDirty.right, uiDirty.bottom);
+        mRenderTarget.offscreenBuffer->region.orSelf(dirty);
+    }
+}
+
 } // namespace uirenderer
 } // namespace android
diff --git a/libs/hwui/BakedOpRenderer.h b/libs/hwui/BakedOpRenderer.h
index 62d1838..d7600db 100644
--- a/libs/hwui/BakedOpRenderer.h
+++ b/libs/hwui/BakedOpRenderer.h
@@ -67,7 +67,16 @@
     Texture* getTexture(const SkBitmap* bitmap);
     const LightInfo& getLightInfo() { return mLightInfo; }
 
-    void renderGlop(const BakedOpState& state, const Glop& glop);
+    void renderGlop(const BakedOpState& state, const Glop& glop) {
+        bool useScissor = state.computedState.clipSideFlags != OpClipSideFlags::None;
+        renderGlop(&state.computedState.clippedBounds,
+                useScissor ? &state.computedState.clipRect : nullptr,
+                glop);
+    }
+
+    void renderGlop(const Rect* dirtyBounds, const Rect* clip, const Glop& glop);
+    bool offscreenRenderTarget() { return mRenderTarget.offscreenBuffer != nullptr; }
+    void dirtyRenderTarget(const Rect& dirtyRect);
     bool didDraw() { return mHasDrawn; }
 private:
     void setViewport(uint32_t width, uint32_t height);
diff --git a/libs/hwui/BakedOpState.h b/libs/hwui/BakedOpState.h
index 9a40c3b..983c27b 100644
--- a/libs/hwui/BakedOpState.h
+++ b/libs/hwui/BakedOpState.h
@@ -38,6 +38,16 @@
 }
 
 /**
+ * Holds a list of BakedOpStates of ops that can be drawn together
+ */
+struct MergedBakedOpList {
+    const BakedOpState*const* states;
+    size_t count;
+    int clipSideFlags;
+    Rect clip;
+};
+
+/**
  * Holds the resolved clip, transform, and bounds of a recordedOp, when replayed with a snapshot
  */
 class ResolvedRenderState {
diff --git a/libs/hwui/ClipArea.cpp b/libs/hwui/ClipArea.cpp
index a9d1e42..fd6f0b5 100644
--- a/libs/hwui/ClipArea.cpp
+++ b/libs/hwui/ClipArea.cpp
@@ -26,7 +26,7 @@
 static void handlePoint(Rect& transformedBounds, const Matrix4& transform, float x, float y) {
     Vertex v = {x, y};
     transform.mapPoint(v.x, v.y);
-    transformedBounds.expandToCoverVertex(v.x, v.y);
+    transformedBounds.expandToCover(v.x, v.y);
 }
 
 Rect transformAndCalculateBounds(const Rect& r, const Matrix4& transform) {
diff --git a/libs/hwui/DisplayListOp.h b/libs/hwui/DisplayListOp.h
index e7cc464..92217edc 100644
--- a/libs/hwui/DisplayListOp.h
+++ b/libs/hwui/DisplayListOp.h
@@ -612,7 +612,7 @@
     AssetAtlas::Entry* getAtlasEntry(OpenGLRenderer& renderer) {
         if (!mEntryValid) {
             mEntryValid = true;
-            mEntry = renderer.renderState().assetAtlas().getEntry(mBitmap);
+            mEntry = renderer.renderState().assetAtlas().getEntry(mBitmap->pixelRef());
         }
         return mEntry;
     }
@@ -777,7 +777,7 @@
     AssetAtlas::Entry* getAtlasEntry(OpenGLRenderer& renderer) {
         if (!mEntryValid) {
             mEntryValid = true;
-            mEntry = renderer.renderState().assetAtlas().getEntry(mBitmap);
+            mEntry = renderer.renderState().assetAtlas().getEntry(mBitmap->pixelRef());
         }
         return mEntry;
     }
diff --git a/libs/hwui/FontRenderer.cpp b/libs/hwui/FontRenderer.cpp
index 47654f4..9c8649f 100644
--- a/libs/hwui/FontRenderer.cpp
+++ b/libs/hwui/FontRenderer.cpp
@@ -75,7 +75,8 @@
             .setTransform(bakedState->computedState.transform, transformFlags)
             .setModelViewOffsetRect(0, 0, Rect(0, 0, 0, 0))
             .build();
-    renderer->renderGlop(*bakedState, glop);
+    // Note: don't pass dirty bounds here, so user must manage passing dirty bounds to renderer
+    renderer->renderGlop(nullptr, clip, glop);
 #else
     GlopBuilder(renderer->mRenderState, renderer->mCaches, &glop)
             .setRoundRectClipState(renderer->currentSnapshot()->roundRectClipState)
diff --git a/libs/hwui/FontRenderer.h b/libs/hwui/FontRenderer.h
index 87cfe7f..ff4dc4a 100644
--- a/libs/hwui/FontRenderer.h
+++ b/libs/hwui/FontRenderer.h
@@ -57,6 +57,7 @@
 #if HWUI_NEW_OPS
             BakedOpRenderer* renderer,
             const BakedOpState* bakedState,
+            const Rect* clip,
 #else
             OpenGLRenderer* renderer,
 #endif
@@ -65,6 +66,7 @@
         : renderer(renderer)
 #if HWUI_NEW_OPS
         , bakedState(bakedState)
+        , clip(clip)
 #endif
         , x(x)
         , y(y)
@@ -79,6 +81,7 @@
 #if HWUI_NEW_OPS
     BakedOpRenderer* renderer;
     const BakedOpState* bakedState;
+    const Rect* clip;
 #else
     OpenGLRenderer* renderer;
 #endif
diff --git a/libs/hwui/GlopBuilder.h b/libs/hwui/GlopBuilder.h
index 6270dcb..b647b90 100644
--- a/libs/hwui/GlopBuilder.h
+++ b/libs/hwui/GlopBuilder.h
@@ -53,7 +53,7 @@
     GlopBuilder& setMeshTexturedUvQuad(const UvMapper* uvMapper, const Rect uvs);
     GlopBuilder& setMeshVertexBuffer(const VertexBuffer& vertexBuffer, bool shadowInterp);
     GlopBuilder& setMeshIndexedQuads(Vertex* vertexData, int quadCount);
-    GlopBuilder& setMeshTexturedMesh(TextureVertex* vertexData, int elementCount); // TODO: use indexed quads
+    GlopBuilder& setMeshTexturedMesh(TextureVertex* vertexData, int elementCount); // TODO: delete
     GlopBuilder& setMeshColoredTexturedMesh(ColorTextureVertex* vertexData, int elementCount); // TODO: use indexed quads
     GlopBuilder& setMeshTexturedIndexedQuads(TextureVertex* vertexData, int elementCount); // TODO: take quadCount
     GlopBuilder& setMeshPatchQuads(const Patch& patch);
diff --git a/libs/hwui/OpReorderer.cpp b/libs/hwui/OpReorderer.cpp
index 9cbd9c2d..9460361 100644
--- a/libs/hwui/OpReorderer.cpp
+++ b/libs/hwui/OpReorderer.cpp
@@ -202,6 +202,9 @@
         if (newClipSideFlags & OpClipSideFlags::Bottom) mClipRect.bottom = opClip.bottom;
     }
 
+    bool getClipSideFlags() const { return mClipSideFlags; }
+    const Rect& getClipRect() const { return mClipRect; }
+
 private:
     int mClipSideFlags = 0;
     Rect mClipRect;
@@ -291,12 +294,31 @@
     }
 }
 
-void OpReorderer::LayerReorderer::replayBakedOpsImpl(void* arg, BakedOpDispatcher* receivers) const {
+void OpReorderer::LayerReorderer::replayBakedOpsImpl(void* arg,
+        BakedOpReceiver* unmergedReceivers, MergedOpReceiver* mergedReceivers) const {
     ATRACE_NAME("flush drawing commands");
     for (const BatchBase* batch : mBatches) {
-        // TODO: different behavior based on batch->isMerging()
-        for (const BakedOpState* op : batch->getOps()) {
-            receivers[op->op->opId](arg, *op->op, *op);
+        size_t size = batch->getOps().size();
+        if (size > 1 && batch->isMerging()) {
+            int opId = batch->getOps()[0]->op->opId;
+            const MergingOpBatch* mergingBatch = static_cast<const MergingOpBatch*>(batch);
+            MergedBakedOpList data = {
+                    batch->getOps().data(),
+                    size,
+                    mergingBatch->getClipSideFlags(),
+                    mergingBatch->getClipRect()
+            };
+            if (data.clipSideFlags) {
+                // if right or bottom sides aren't used to clip, init them to viewport bounds
+                // in the clip rect, so it can be used to scissor
+                if (!(data.clipSideFlags & OpClipSideFlags::Right)) data.clip.right = width;
+                if (!(data.clipSideFlags & OpClipSideFlags::Bottom)) data.clip.bottom = height;
+            }
+            mergedReceivers[opId](arg, data);
+        } else {
+            for (const BakedOpState* op : batch->getOps()) {
+                unmergedReceivers[op->op->opId](arg, *op);
+            }
         }
     }
 }
@@ -639,7 +661,8 @@
 #define OP_RECEIVER(Type) \
         [](OpReorderer& reorderer, const RecordedOp& op) { reorderer.on##Type(static_cast<const Type&>(op)); },
 void OpReorderer::deferNodeOps(const RenderNode& renderNode) {
-    static std::function<void(OpReorderer& reorderer, const RecordedOp&)> receivers[] = {
+    typedef void (*OpDispatcher) (OpReorderer& reorderer, const RecordedOp& op);
+    static OpDispatcher receivers[] = {
         MAP_OPS(OP_RECEIVER)
     };
 
@@ -692,42 +715,57 @@
 }
 
 void OpReorderer::onBitmapOp(const BitmapOp& op) {
-    BakedOpState* bakedStateOp = tryBakeOpState(op);
-    if (!bakedStateOp) return; // quick rejected
+    BakedOpState* bakedState = tryBakeOpState(op);
+    if (!bakedState) return; // quick rejected
 
-    mergeid_t mergeId = (mergeid_t) op.bitmap->getGenerationID();
-    // TODO: AssetAtlas
-    currentLayer().deferMergeableOp(mAllocator, bakedStateOp, OpBatchType::Bitmap, mergeId);
+    // Don't merge non-simply transformed or neg scale ops, SET_TEXTURE doesn't handle rotation
+    // Don't merge A8 bitmaps - the paint's color isn't compared by mergeId, or in
+    // MergingDrawBatch::canMergeWith()
+    if (bakedState->computedState.transform.isSimple()
+            && bakedState->computedState.transform.positiveScale()
+            && PaintUtils::getXfermodeDirect(op.paint) == SkXfermode::kSrcOver_Mode
+            && op.bitmap->colorType() != kAlpha_8_SkColorType) {
+        mergeid_t mergeId = (mergeid_t) op.bitmap->getGenerationID();
+        // TODO: AssetAtlas in mergeId
+        currentLayer().deferMergeableOp(mAllocator, bakedState, OpBatchType::Bitmap, mergeId);
+    } else {
+        currentLayer().deferUnmergeableOp(mAllocator, bakedState, OpBatchType::Bitmap);
+    }
 }
 
 void OpReorderer::onLinesOp(const LinesOp& op) {
-    BakedOpState* bakedStateOp = tryBakeOpState(op);
-    if (!bakedStateOp) return; // quick rejected
-    currentLayer().deferUnmergeableOp(mAllocator, bakedStateOp, OpBatchType::Vertices);
-
+    BakedOpState* bakedState = tryBakeOpState(op);
+    if (!bakedState) return; // quick rejected
+    currentLayer().deferUnmergeableOp(mAllocator, bakedState, tessellatedBatchId(*op.paint));
 }
 
 void OpReorderer::onRectOp(const RectOp& op) {
-    BakedOpState* bakedStateOp = tryBakeOpState(op);
-    if (!bakedStateOp) return; // quick rejected
-    currentLayer().deferUnmergeableOp(mAllocator, bakedStateOp, tessellatedBatchId(*op.paint));
+    BakedOpState* bakedState = tryBakeOpState(op);
+    if (!bakedState) return; // quick rejected
+    currentLayer().deferUnmergeableOp(mAllocator, bakedState, tessellatedBatchId(*op.paint));
 }
 
 void OpReorderer::onSimpleRectsOp(const SimpleRectsOp& op) {
-    BakedOpState* bakedStateOp = tryBakeOpState(op);
-    if (!bakedStateOp) return; // quick rejected
-    currentLayer().deferUnmergeableOp(mAllocator, bakedStateOp, OpBatchType::Vertices);
+    BakedOpState* bakedState = tryBakeOpState(op);
+    if (!bakedState) return; // quick rejected
+    currentLayer().deferUnmergeableOp(mAllocator, bakedState, OpBatchType::Vertices);
 }
 
 void OpReorderer::onTextOp(const TextOp& op) {
-    BakedOpState* bakedStateOp = tryBakeOpState(op);
-    if (!bakedStateOp) return; // quick rejected
+    BakedOpState* bakedState = tryBakeOpState(op);
+    if (!bakedState) return; // quick rejected
 
     // TODO: better handling of shader (since we won't care about color then)
     batchid_t batchId = op.paint->getColor() == SK_ColorBLACK
             ? OpBatchType::Text : OpBatchType::ColorText;
-    mergeid_t mergeId = reinterpret_cast<mergeid_t>(op.paint->getColor());
-    currentLayer().deferMergeableOp(mAllocator, bakedStateOp, batchId, mergeId);
+
+    if (bakedState->computedState.transform.isPureTranslate()
+            && PaintUtils::getXfermodeDirect(op.paint) == SkXfermode::kSrcOver_Mode) {
+        mergeid_t mergeId = reinterpret_cast<mergeid_t>(op.paint->getColor());
+        currentLayer().deferMergeableOp(mAllocator, bakedState, batchId, mergeId);
+    } else {
+        currentLayer().deferUnmergeableOp(mAllocator, bakedState, batchId);
+    }
 }
 
 void OpReorderer::saveForLayer(uint32_t layerWidth, uint32_t layerHeight,
diff --git a/libs/hwui/OpReorderer.h b/libs/hwui/OpReorderer.h
index 00df8b0..fc77c61 100644
--- a/libs/hwui/OpReorderer.h
+++ b/libs/hwui/OpReorderer.h
@@ -58,7 +58,8 @@
 }
 
 class OpReorderer : public CanvasStateClient {
-    typedef std::function<void(void*, const RecordedOp&, const BakedOpState&)> BakedOpDispatcher;
+    typedef void (*BakedOpReceiver)(void*, const BakedOpState&);
+    typedef void (*MergedOpReceiver)(void*, const MergedBakedOpList& opList);
 
     /**
      * Stores the deferred render operations and state used to compute ordering
@@ -87,7 +88,7 @@
         void deferMergeableOp(LinearAllocator& allocator,
                 BakedOpState* op, batchid_t batchId, mergeid_t mergeId);
 
-        void replayBakedOpsImpl(void* arg, BakedOpDispatcher* receivers) const;
+        void replayBakedOpsImpl(void* arg, BakedOpReceiver* receivers, MergedOpReceiver*) const;
 
         bool empty() const {
             return mBatches.empty();
@@ -132,19 +133,44 @@
      * It constructs a lookup array of lambdas, which allows a recorded BakeOpState to use
      * state->op->opId to lookup a receiver that will be called when the op is replayed.
      *
-     * For example a BitmapOp would resolve, via the lambda lookup, to calling:
-     *
-     * StaticDispatcher::onBitmapOp(Renderer& renderer, const BitmapOp& op, const BakedOpState& state);
      */
-#define BAKED_OP_RECEIVER(Type) \
-    [](void* internalRenderer, const RecordedOp& op, const BakedOpState& state) { \
-        StaticDispatcher::on##Type(*(static_cast<Renderer*>(internalRenderer)), static_cast<const Type&>(op), state); \
-    },
     template <typename StaticDispatcher, typename Renderer>
     void replayBakedOps(Renderer& renderer) {
-        static BakedOpDispatcher receivers[] = {
-            MAP_OPS(BAKED_OP_RECEIVER)
+        /**
+         * defines a LUT of lambdas which allow a recorded BakedOpState to use state->op->opId to
+         * dispatch the op via a method on a static dispatcher when the op is replayed.
+         *
+         * For example a BitmapOp would resolve, via the lambda lookup, to calling:
+         *
+         * StaticDispatcher::onBitmapOp(Renderer& renderer, const BitmapOp& op, const BakedOpState& state);
+         */
+        #define X(Type) \
+                [](void* renderer, const BakedOpState& state) { \
+                    StaticDispatcher::on##Type(*(static_cast<Renderer*>(renderer)), static_cast<const Type&>(*(state.op)), state); \
+                },
+        static BakedOpReceiver unmergedReceivers[] = {
+            MAP_OPS(X)
         };
+        #undef X
+
+        /**
+         * defines a LUT of lambdas which allow merged arrays of BakedOpState* to be passed to a
+         * static dispatcher when the group of merged ops is replayed. Unmergeable ops trigger
+         * a LOG_ALWAYS_FATAL().
+         */
+        #define X(Type) \
+                [](void* renderer, const MergedBakedOpList& opList) { \
+                    LOG_ALWAYS_FATAL("op type %d does not support merging", opList.states[0]->op->opId); \
+                },
+        #define Y(Type) \
+                [](void* renderer, const MergedBakedOpList& opList) { \
+                    StaticDispatcher::onMerged##Type##s(*(static_cast<Renderer*>(renderer)), opList); \
+                },
+        static MergedOpReceiver mergedReceivers[] = {
+            MAP_OPS_BASED_ON_MERGEABILITY(X, Y)
+        };
+        #undef X
+        #undef Y
 
         // Relay through layers in reverse order, since layers
         // later in the list will be drawn by earlier ones
@@ -153,18 +179,18 @@
             if (layer.renderNode) {
                 // cached HW layer - can't skip layer if empty
                 renderer.startRepaintLayer(layer.offscreenBuffer, layer.repaintRect);
-                layer.replayBakedOpsImpl((void*)&renderer, receivers);
+                layer.replayBakedOpsImpl((void*)&renderer, unmergedReceivers, mergedReceivers);
                 renderer.endLayer();
             } else if (!layer.empty()) { // save layer - skip entire layer if empty
                 layer.offscreenBuffer = renderer.startTemporaryLayer(layer.width, layer.height);
-                layer.replayBakedOpsImpl((void*)&renderer, receivers);
+                layer.replayBakedOpsImpl((void*)&renderer, unmergedReceivers, mergedReceivers);
                 renderer.endLayer();
             }
         }
 
         const LayerReorderer& fbo0 = mLayerReorderers[0];
         renderer.startFrame(fbo0.width, fbo0.height, fbo0.repaintRect);
-        fbo0.replayBakedOpsImpl((void*)&renderer, receivers);
+        fbo0.replayBakedOpsImpl((void*)&renderer, unmergedReceivers, mergedReceivers);
         renderer.endFrame();
     }
 
@@ -213,7 +239,7 @@
 
     void deferRenderNodeOp(const RenderNodeOp& op);
 
-    void replayBakedOpsImpl(void* arg, BakedOpDispatcher* receivers);
+    void replayBakedOpsImpl(void* arg, BakedOpReceiver* receivers);
 
     SkPath* createFrameAllocatedPath() {
         mFrameAllocatedPaths.emplace_back(new SkPath);
diff --git a/libs/hwui/OpenGLRenderer.cpp b/libs/hwui/OpenGLRenderer.cpp
index e386b1c..2cb32c4 100644
--- a/libs/hwui/OpenGLRenderer.cpp
+++ b/libs/hwui/OpenGLRenderer.cpp
@@ -1525,7 +1525,7 @@
         colors = tempColors.get();
     }
 
-    Texture* texture = mRenderState.assetAtlas().getEntryTexture(bitmap);
+    Texture* texture = mRenderState.assetAtlas().getEntryTexture(bitmap->pixelRef());
     const UvMapper& mapper(getMapper(texture));
 
     for (int32_t y = 0; y < meshHeight; y++) {
@@ -2146,7 +2146,7 @@
     bool status;
 #if HWUI_NEW_OPS
     LOG_ALWAYS_FATAL("unsupported");
-    TextDrawFunctor functor(nullptr, nullptr, x, y, pureTranslate, alpha, mode, paint);
+    TextDrawFunctor functor(nullptr, nullptr, nullptr, x, y, pureTranslate, alpha, mode, paint);
 #else
     TextDrawFunctor functor(this, x, y, pureTranslate, alpha, mode, paint);
 #endif
@@ -2190,7 +2190,7 @@
     SkXfermode::Mode mode = PaintUtils::getXfermodeDirect(paint);
 #if HWUI_NEW_OPS
     LOG_ALWAYS_FATAL("unsupported");
-    TextDrawFunctor functor(nullptr, nullptr, 0.0f, 0.0f, false, alpha, mode, paint);
+    TextDrawFunctor functor(nullptr, nullptr, nullptr, 0.0f, 0.0f, false, alpha, mode, paint);
 #else
     TextDrawFunctor functor(this, 0.0f, 0.0f, false, alpha, mode, paint);
 #endif
@@ -2308,7 +2308,7 @@
 ///////////////////////////////////////////////////////////////////////////////
 
 Texture* OpenGLRenderer::getTexture(const SkBitmap* bitmap) {
-    Texture* texture = mRenderState.assetAtlas().getEntryTexture(bitmap);
+    Texture* texture = mRenderState.assetAtlas().getEntryTexture(bitmap->pixelRef());
     if (!texture) {
         return mCaches.textureCache.get(bitmap);
     }
diff --git a/libs/hwui/PathTessellator.cpp b/libs/hwui/PathTessellator.cpp
index b57b8f0..9246237 100644
--- a/libs/hwui/PathTessellator.cpp
+++ b/libs/hwui/PathTessellator.cpp
@@ -799,7 +799,7 @@
     dstBuffer.alloc<TYPE>(numPoints * verticesPerPoint + (numPoints - 1) * 2);
 
     for (int i = 0; i < count; i += 2) {
-        bounds.expandToCoverVertex(points[i + 0], points[i + 1]);
+        bounds.expandToCover(points[i + 0], points[i + 1]);
         dstBuffer.copyInto<TYPE>(srcBuffer, points[i + 0], points[i + 1]);
     }
     dstBuffer.createDegenerateSeparators<TYPE>(verticesPerPoint);
@@ -878,8 +878,8 @@
         }
 
         // calculate bounds
-        bounds.expandToCoverVertex(tempVerticesData[0].x, tempVerticesData[0].y);
-        bounds.expandToCoverVertex(tempVerticesData[1].x, tempVerticesData[1].y);
+        bounds.expandToCover(tempVerticesData[0].x, tempVerticesData[0].y);
+        bounds.expandToCover(tempVerticesData[1].x, tempVerticesData[1].y);
     }
 
     // since multiple objects tessellated into buffer, separate them with degen tris
diff --git a/libs/hwui/RecordedOp.h b/libs/hwui/RecordedOp.h
index b4a201e..b966401 100644
--- a/libs/hwui/RecordedOp.h
+++ b/libs/hwui/RecordedOp.h
@@ -37,21 +37,34 @@
 struct Vertex;
 
 /**
- * The provided macro is executed for each op type in order, with the results separated by commas.
+ * On of the provided macros is executed for each op type in order. The first will be used for ops
+ * that cannot merge, and the second for those that can.
  *
  * 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) \
+        M_OP_FN(BitmapOp) \
+        U_OP_FN(LinesOp) \
+        U_OP_FN(RectOp) \
+        U_OP_FN(RenderNodeOp) \
+        U_OP_FN(ShadowOp) \
+        U_OP_FN(SimpleRectsOp) \
+        M_OP_FN(TextOp) \
+        U_OP_FN(BeginLayerOp) \
+        U_OP_FN(EndLayerOp) \
+        U_OP_FN(LayerOp)
+
+/**
+ * The provided macro is executed for each op type in order. This is used in cases where
+ * merge-ability of ops doesn't matter.
+ */
 #define MAP_OPS(OP_FN) \
-        OP_FN(BitmapOp) \
-        OP_FN(LinesOp) \
-        OP_FN(RectOp) \
-        OP_FN(RenderNodeOp) \
-        OP_FN(ShadowOp) \
-        OP_FN(SimpleRectsOp) \
-        OP_FN(TextOp) \
-        OP_FN(BeginLayerOp) \
-        OP_FN(EndLayerOp) \
-        OP_FN(LayerOp)
+        MAP_OPS_BASED_ON_MERGEABILITY(OP_FN, OP_FN)
+
+#define NULL_OP_FN(Type)
+
+#define MAP_MERGED_OPS(OP_FN) \
+        MAP_OPS_BASED_ON_MERGEABILITY(NULL_OP_FN, OP_FN)
 
 // Generate OpId enum
 #define IDENTITY_FN(Type) Type,
diff --git a/libs/hwui/RecordingCanvas.cpp b/libs/hwui/RecordingCanvas.cpp
index 69c686e..e6020cd 100644
--- a/libs/hwui/RecordingCanvas.cpp
+++ b/libs/hwui/RecordingCanvas.cpp
@@ -248,10 +248,7 @@
 
     Rect unmappedBounds(points[0], points[1], points[0], points[1]);
     for (int i = 2; i < floatCount; i += 2) {
-        unmappedBounds.left = std::min(unmappedBounds.left, points[i]);
-        unmappedBounds.right = std::max(unmappedBounds.right, points[i]);
-        unmappedBounds.top = std::min(unmappedBounds.top, points[i + 1]);
-        unmappedBounds.bottom = std::max(unmappedBounds.bottom, points[i + 1]);
+        unmappedBounds.expandToCover(points[i], points[i + 1]);
     }
 
     // since anything AA stroke with less than 1.0 pixel width is drawn with an alpha-reduced
@@ -413,6 +410,7 @@
     glyphs = refBuffer<glyph_t>(glyphs, glyphCount);
     positions = refBuffer<float>(positions, glyphCount * 2);
 
+    // TODO: either must account for text shadow in bounds, or record separate ops for text shadows
     addOp(new (alloc()) TextOp(
             Rect(boundsLeft, boundsTop, boundsRight, boundsBottom),
             *(mState.currentSnapshot()->transform),
diff --git a/libs/hwui/Rect.h b/libs/hwui/Rect.h
index 472aad7..30c925c 100644
--- a/libs/hwui/Rect.h
+++ b/libs/hwui/Rect.h
@@ -253,7 +253,18 @@
         bottom = ceilf(bottom);
     }
 
-    void expandToCoverVertex(float x, float y) {
+    /*
+     * Similar to unionWith, except this makes the assumption that both rects are non-empty
+     * to avoid both emptiness checks.
+     */
+    void expandToCover(const Rect& other) {
+        left = std::min(left, other.left);
+        top = std::min(top, other.top);
+        right = std::max(right, other.right);
+        bottom = std::max(bottom, other.bottom);
+    }
+
+    void expandToCover(float x, float y) {
         left = std::min(left, x);
         top = std::min(top, y);
         right = std::max(right, x);
diff --git a/libs/hwui/TextureCache.cpp b/libs/hwui/TextureCache.cpp
index a6c72a3..21901cf 100644
--- a/libs/hwui/TextureCache.cpp
+++ b/libs/hwui/TextureCache.cpp
@@ -138,7 +138,7 @@
 // in the cache (and is thus added to the cache)
 Texture* TextureCache::getCachedTexture(const SkBitmap* bitmap, AtlasUsageType atlasUsageType) {
     if (CC_LIKELY(mAssetAtlas != nullptr) && atlasUsageType == AtlasUsageType::Use) {
-        AssetAtlas::Entry* entry = mAssetAtlas->getEntry(bitmap);
+        AssetAtlas::Entry* entry = mAssetAtlas->getEntry(bitmap->pixelRef());
         if (CC_UNLIKELY(entry)) {
             return entry->texture;
         }
diff --git a/libs/hwui/VertexBuffer.h b/libs/hwui/VertexBuffer.h
index c0373ac..bdb5b7b 100644
--- a/libs/hwui/VertexBuffer.h
+++ b/libs/hwui/VertexBuffer.h
@@ -118,7 +118,7 @@
         TYPE* end = current + vertexCount;
         mBounds.set(current->x, current->y, current->x, current->y);
         for (; current < end; current++) {
-            mBounds.expandToCoverVertex(current->x, current->y);
+            mBounds.expandToCover(current->x, current->y);
         }
     }
 
diff --git a/libs/hwui/tests/common/TestUtils.h b/libs/hwui/tests/common/TestUtils.h
index 9c1c0b9..0af9939 100644
--- a/libs/hwui/tests/common/TestUtils.h
+++ b/libs/hwui/tests/common/TestUtils.h
@@ -103,10 +103,11 @@
         return snapshot;
     }
 
-    static SkBitmap createSkBitmap(int width, int height) {
+    static SkBitmap createSkBitmap(int width, int height,
+            SkColorType colorType = kN32_SkColorType) {
         SkBitmap bitmap;
         SkImageInfo info = SkImageInfo::Make(width, height,
-                kN32_SkColorType, kPremul_SkAlphaType);
+                colorType, kPremul_SkAlphaType);
         bitmap.setInfo(info);
         bitmap.allocPixels(info);
         return bitmap;
diff --git a/libs/hwui/tests/common/scenes/ListViewAnimation.cpp b/libs/hwui/tests/common/scenes/ListViewAnimation.cpp
index 27adb12..6c64a32 100644
--- a/libs/hwui/tests/common/scenes/ListViewAnimation.cpp
+++ b/libs/hwui/tests/common/scenes/ListViewAnimation.cpp
@@ -62,7 +62,9 @@
         int cardIndexOffset = scrollPx / (cardSpacing + cardHeight);
         int pxOffset = -(scrollPx % (cardSpacing + cardHeight));
 
-        TestCanvas canvas(cardWidth, cardHeight);
+        TestCanvas canvas(
+                listView->stagingProperties().getWidth(),
+                listView->stagingProperties().getHeight());
         for (size_t ci = 0; ci < cards.size(); ci++) {
             // update card position
             auto card = cards[(ci + cardIndexOffset) % cards.size()];
@@ -121,9 +123,11 @@
             static SkBitmap filledBox = createBoxBitmap(true);
             static SkBitmap strokedBox = createBoxBitmap(false);
 
-            props.mutableOutline().setRoundRect(0, 0, cardWidth, cardHeight, dp(6), 1);
-            props.mutableOutline().setShouldClip(true);
-            canvas.drawColor(Color::White, SkXfermode::kSrcOver_Mode);
+            // TODO: switch to using round rect clipping, once merging correctly handles that
+            SkPaint roundRectPaint;
+            roundRectPaint.setAntiAlias(true);
+            roundRectPaint.setColor(Color::White);
+            canvas.drawRoundRect(0, 0, cardWidth, cardHeight, dp(6), dp(6), roundRectPaint);
 
             SkPaint textPaint;
             textPaint.setTextEncoding(SkPaint::kGlyphID_TextEncoding);
diff --git a/libs/hwui/tests/microbench/OpReordererBench.cpp b/libs/hwui/tests/microbench/OpReordererBench.cpp
index 406bfcc..ac2b15c 100644
--- a/libs/hwui/tests/microbench/OpReordererBench.cpp
+++ b/libs/hwui/tests/microbench/OpReordererBench.cpp
@@ -25,7 +25,7 @@
 #include "RecordingCanvas.h"
 #include "tests/common/TestUtils.h"
 #include "Vector.h"
-#include "microbench/MicroBench.h"
+#include "tests/microbench/MicroBench.h"
 
 #include <vector>
 
diff --git a/libs/hwui/tests/unit/OpReordererTests.cpp b/libs/hwui/tests/unit/OpReordererTests.cpp
index 98a430a..068e832 100644
--- a/libs/hwui/tests/unit/OpReordererTests.cpp
+++ b/libs/hwui/tests/unit/OpReordererTests.cpp
@@ -65,12 +65,22 @@
     virtual void startFrame(uint32_t width, uint32_t height, const Rect& repaintRect) {}
     virtual void endFrame() {}
 
-    // define virtual defaults for direct
-#define BASE_OP_METHOD(Type) \
+    // define virtual defaults for single draw methods
+#define X(Type) \
     virtual void on##Type(const Type&, const BakedOpState&) { \
         ADD_FAILURE() << #Type " not expected in this test"; \
     }
-    MAP_OPS(BASE_OP_METHOD)
+    MAP_OPS(X)
+#undef X
+
+    // define virtual defaults for merged draw methods
+#define X(Type) \
+    virtual void onMerged##Type##s(const MergedBakedOpList& opList) { \
+        ADD_FAILURE() << "Merged " #Type "s not expected in this test"; \
+    }
+    MAP_MERGED_OPS(X)
+#undef X
+
     int getIndex() { return mIndex; }
 
 protected:
@@ -83,11 +93,21 @@
  */
 class TestDispatcher {
 public:
-#define DISPATCHER_METHOD(Type) \
+    // define single op methods, which redirect to TestRendererBase
+#define X(Type) \
     static void on##Type(TestRendererBase& renderer, const Type& op, const BakedOpState& state) { \
         renderer.on##Type(op, state); \
     }
-    MAP_OPS(DISPATCHER_METHOD);
+    MAP_OPS(X);
+#undef X
+
+    // define merged op methods, which redirect to TestRendererBase
+#define X(Type) \
+    static void onMerged##Type##s(TestRendererBase& renderer, const MergedBakedOpList& opList) { \
+        renderer.onMerged##Type##s(opList); \
+    }
+    MAP_MERGED_OPS(X);
+#undef X
 };
 
 class FailRenderer : public TestRendererBase {};
@@ -153,7 +173,8 @@
 
     auto node = TestUtils::createNode(0, 0, 200, 200,
             [](RenderProperties& props, RecordingCanvas& canvas) {
-        SkBitmap bitmap = TestUtils::createSkBitmap(10, 10);
+        SkBitmap bitmap = TestUtils::createSkBitmap(10, 10,
+                kAlpha_8_SkColorType); // Disable merging by using alpha 8 bitmap
 
         // Alternate between drawing rects and bitmaps, with bitmaps overlapping rects.
         // Rects don't overlap bitmaps, so bitmaps should be brought to front as a group.
@@ -171,7 +192,7 @@
     SimpleBatchingTestRenderer renderer;
     reorderer.replayBakedOps<TestDispatcher>(renderer);
     EXPECT_EQ(2 * LOOPS, renderer.getIndex())
-            << "Expect number of ops = 2 * loop count"; // TODO: force no merging
+            << "Expect number of ops = 2 * loop count";
 }
 
 TEST(OpReorderer, textStrikethroughBatching) {
@@ -181,8 +202,10 @@
         void onRectOp(const RectOp& op, const BakedOpState& state) override {
             EXPECT_TRUE(mIndex++ >= LOOPS) << "Strikethrough rects should be above all text";
         }
-        void onTextOp(const TextOp& op, const BakedOpState& state) override {
-            EXPECT_TRUE(mIndex++ < LOOPS) << "Text should be beneath all strikethrough rects";
+        void onMergedTextOps(const MergedBakedOpList& opList) override {
+            EXPECT_EQ(0, mIndex);
+            mIndex += opList.count;
+            EXPECT_EQ(5u, opList.count);
         }
     };
     auto node = TestUtils::createNode(0, 0, 200, 2000,